Learning CMake done right with JUCE 6

Hi everyone,

I’m trying to convert an Open Source plugin, currently under development to the new CMake build system – mainly to learn a bit. My CMake knowledge is a very limited, I found my way around using it somehow in the past, but I’d really like to learn how to do it right :wink:

So, my current project, which can be found here has a folder structure like

root
    Source
        [all the source files here]
    Ext
        JUCE --> Juce included as sumodule
        JBPluginBase --> My own repository containing a JUCE module named jb_plugin_base included as submodule
    BinaryRessources
        SVGs
            [a bunch of svg files here]

So here is my initial approach:

  • I placed the CMakeList.txt file in the root folder
  • For the sources folder I created a variable like set (Sources ${PROJECT_SOURCE_DIR}/Source) and then under target_sources I add them like ${Sources}/Foo.cpp, ${Sources}/bar.cpp
  • For the SVGs I did something similar, setting set (SVGAssets ${PROJECT_SOURCE_DIR}/BinaryRessources/SVGs) and then calling juce_add_binary_data(EmbedddedSVGs SOURCES ${SVGAssets}/a.svg ${SVGAssets}/b.svg)

So far, is that a good or bad choice, how would CMake experts structure their files?

Now when it comes to including the modules, I don’t quite manage to get it working right now. I tried the following

juce_add_module (${PROJECT_SOURCE_DIR}/Ext/JBPluginBase/jb_plugin_base)
target_link_libraries(OJD PRIVATE
        EmbedddedSVGs 
        juce::juce_audio_utils
        juce::jb_plugin_base)

But it fails with

CMake Error at Ext/JUCE/extras/Build/CMake/JUCEUtils.cmake:1371 (add_library):
  Target "OJD_VST3" links to target "juce::jb_plugin_base" but the target was
  not found.  Perhaps a find_package() call is missing for an IMPORTED
  target, or an ALIAS target is missing?

Probably due to my wrong understanding of either what juce_add_module does or what target_link_libraries does.

I feel a bit like back when I started with C++ and tried to find hacky solutions just because I had not really understood how to read the language, how compilation works, how a well designed piece of code looks like etc… :smiley:

So a best practice example of a CMake file for my project would be appreciated as well as some good resources to learn CMake done right from the start :slight_smile:

Your approach sounds reasonable on the whole.

Current CMake best-practice suggests to avoid variables for things like folders and sources except where absolutely necessary. You could consider putting a CMakeLists directly at BinaryResources/SVGs/CMakeLists.txt with contents like

# Relative paths should resolve relative to the directory
# of the current CMakeLists
juce_add_binary_data(embedded_svgs SOURCES a.svg b.svg)

And then from your top-level CMakeLists you can add_subdirectory(BinaryResources/SVGs). This also has the nice property of keeping the build definition for the library right next to the code, so re-using the library might be as simple as converting that subfolder into a submodule.

Custom/user modules don’t have the juce:: prefix, so I would expect this to work if you just link jb_plugin_base, without the juce::.

Personally I learn from reading example code, so you could check out the CMakeLists for the projects in the JUCE repo (open-source JUCE projects using CMake are also starting to appear on Github). The official CMake documentation is also an invaluable resource. Finally, I’d recommend Professional CMake as a good “getting started” guide.

Probably the most important rule of modern CMake is to think in terms of targets rather than directories. Rather than setting variables, which will affect all targets defined below the directory where the variable is set, check whether there’s a target property that will achieve the same thing. That means, prefer code like

# Require at least C++14 to build `my_target`
target_compile_features(my_target PRIVATE cxx_std_14)

rather than

# Set the language standard to C++14 for all targets in this directory
set(CMAKE_CXX_STANDARD 14)
4 Likes

I’m not a CMake expert, and try to pick up advice from more knowledgeable people like @reuk mostly.

I did create a template that I’m using to start new projects with JUCE6/CMake here, with some basic examples that use external modules and/or binary data:

Hopefully that will help you on your quest…

2 Likes

Thank you for that detailed answer @reuk and the example repo @eyalamir

I got it working :slight_smile:

Great, I think this is kind of the best practice advice I was looking after and which I wouldn’t have come up with on my own :wink:

Aaah I see, I somehow assumed that the juce:: prefixed marked a juce module. By the way, I was very surprised to find out that all those recommended flags need to be put here as well. I guess my understanding of how all this works under the hood is still very limited, but I’m curious at which point these prefixes get parsed, is this some kind of a CMake way to specify that something is treated in a special way if such a C++ namespace like prefix is added or is that something specific to the JUCE CMake implementation?

By the way, this is how my CMakeList.txt looks now

cmake_minimum_required (VERSION 3.15)

project (SCHRAMMEL_OJD VERSION 0.9.5)

# Adding JUCE
add_subdirectory (Ext/JUCE)

# Adding own modules
juce_add_module (Ext/JBPluginBase/jb_plugin_base)

set (FormatsToBuild AU VST3)

# If a path to the AAX SDK is passed to CMake, an AAX version will be built too
if (AAX_SDK_PATH)
    juce_set_aax_sdk_path (${AAX_SDK_PATH})
    list (APPEND FormatsToBuild AAX)
endif()

juce_add_plugin (OJD
        COMPANY_NAME Schrammel                      # Specify the name of the plugin's author
        IS_SYNTH FALSE                              # Is this a synth or an effect?
        NEEDS_MIDI_INPUT FALSE                      # Does the plugin need midi input?
        NEEDS_MIDI_OUTPUT FALSE                     # Does the plugin need midi output?
        IS_MIDI_EFFECT FALSE                        # Is this plugin a MIDI effect?
        COPY_PLUGIN_AFTER_BUILD TRUE                # Should the plugin be installed to a default location after building?
        PLUGIN_MANUFACTURER_CODE Juce               # A four-character manufacturer id with at least one upper-case character
        PLUGIN_CODE Dem0                            # A unique four-character plugin id with at least one upper-case character
        FORMATS ${FormatsToBuild}                   # The formats to build. Other valid formats are: AAX Unity VST AU AUv3
        PRODUCT_NAME "OJD")                         # The name of the final executable, which can differ from the target name

target_compile_features (OJD PRIVATE cxx_std_14)

juce_generate_juce_header (OJD)

target_sources (OJD PRIVATE
        Source/OJDAudioProcessorEditor.cpp
        Source/OJDProcessor.cpp
        Source/OJDParameters.cpp)

add_subdirectory (BinaryResources/SVGs)

target_compile_definitions (OJD
        PUBLIC
        JUCE_WEB_BROWSER=0
        JUCE_USE_CURL=0
        JUCE_STRICT_REFCOUNTEDPTR=1
        JUCE_VST3_CAN_REPLACE_VST2=0)

target_link_libraries (OJD PRIVATE
        # JUCE Modules
        juce::juce_audio_utils
        juce::juce_dsp

        # Custom modules
        jb_plugin_base

        # Binary Data
        EmbeddedSVGs

        # Recommended flags
        juce::juce_recommended_lto_flags
        juce::juce_recommended_warning_flags
        juce::juce_recommended_config_flags)

While we are at it, CLion experts here? This IDE looks so familiar to me as I’m used to work with AppCode, however I’m stuck launching a DAW for testing from it (I’m using Reaper here) if the Run button is hit. However when clicking Debug Reaper opens up fine and lets me debug my plugin. These are my settings

When clicking Run I get Process finished with exit code 127 and nothing happens :thinking:

I’m getting a similar issue here on CLion - only the “Debug” button works.
Never bothered me, though.