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:

1 Like

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…

5 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)
1 Like

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.

Please allow me to resurrect this interesting thread instead of creating a new one. I have switched my project to CMake, and have two targets: the Main app and the Test Units.

It works pretty well, but I have one problem: I use friend declaration to allow my test class to access the private members of the class to tests. I don’t do it often, but sometimes it is simply very handy. But with CMake, now that the class to test and the test unit are in two separated targets, the friendship does not go beyond its own target and the compilation fails.

Is there a clean way to make it work “like before”? Thanks.

I don’t have a solution for this. I also wrote tests and created a test folder with a cmake file to build the tests. I have changed the design to make sure that things are testable. Sometimes i’m using interfaces and handwritten mocks to remove dependencies.

I think that you already know that, but i recommend to change the design, so that you don’t need to access private fields. You maybe use the Dependency Inversion Principle.

Tests should never affect the productive code. Accessing private fields and methods lead to strong coupling between tests and productive code. You may also need to change tests if you refactor a class even if you don’t change the behavior.

1 Like

Yes, I know that, but thanks. But in some cases, this is useful. For example, I test a serialization class (model to XML). The node names are private. Yet it is handy for my test class to know them. Now I have to externalize them.

What I’d do in that case if I absolutely have to test the private members is write a helper class that is inside the code compilation unit and is the friend class, and the test would access it.

But really what I would do is not test the private members.

In my mind a test should be a ‘user’ of your class and not a ‘super user’ that can poke into it in a different way than a regular user would. To test a serializing class I would test inputs and outputs only.

2 Likes

I agree with you, but in some case, friend makes it easy.

To test a serializing class I would test inputs and outputs only.

Which is what I do, but how do you share the XML node names? Duplication is to be avoided, so they were private my class, and friendship allowed my test class to see them. Now I have to externalize them.

Hard to say without reading your code, but I don’t see why duplication would have to take place anywhere.

If in your test code you need to share information so that you don’t copy/paste things like strings, you can create helper functions/classes/namespace that you reuse in the test code itself, without exposing internals of the class being tested.

1 Like

This isn’t really JUCE or C++ related as it is unit tests.

Unit testing should test 1 unit of logic, an input to output or similar. In crusty dev talk, testing private members from some outside source is a code smell. In fact, it would be interesting to hear the exact logic for doing this that cannot be done in an encapsulated unit way.

You should really ask yourself, is this the right design? Is this a design I would have come up writing a test first only having access to public API?

@eyalamir is not testing private state, just refactored utility functions, I do this too, extract logic that does not alter internal state and test that with its use in the class internally. By doing this, you created a contract that even some other client could use in the future AND it’s tested.

Sometimes programming is chess and it’s “think moves ahead”, unit tests really help this in the long run because they define 1 step, not -1 or 2 squared steps. :slight_smile:

2cents

2 Likes

The C++ language doesn’t know anything about CMake targets, so the root of the problem is probably elsewhere. Can you share the full text of the compiler error you’re seeing? Without knowing the error, we can only guess at solutions.

It is a very understandable error:
error: ‘Bar::DisplayedValue Bar::calculateBarTopAndHeight() const’ is private within this context
const auto displayedValue = bar.calculateBarTopAndHeight();

My test class used to be able to call the calculateBarTopAndHeight method, having been declared as a friend in the Bar class:
friend class BarTest;

But now that my main project and the test class are in two different CMake targets, there is no ‘link’ anymore between Bar and BarTest.

Are Bar and BarTest definitely in the same namespace? Is it possible that (for example) Bar is in some namespace, but BarTest is declared in the global namespace?

Absolutely, same global namespace for both classes.

Is it possible this is a C++ name mangling issue?

Maybe because the test class is in a different compilation unit, it can’t recognize the friend class declaration because the class name was mangled in a different way between targets…?

No, it won’t be to do with mangling. The error tells us that the compiler can see the private function, so Bar and BarTest must be visible within the same translation unit.

I think the problem is that the fully qualified name given in the friend declaration doesn’t match the fully qualified name of BarTest. This could be as simple as a typo in one of the names. However, if both the class names match, then my next best guess would be differing namespaces. I can’t think of anything else particularly likely. (I’m guessing you probably haven’t stuck a #define BarTest SomeOtherString anywhere.)

You can force the compiler to print the full name of a class by causing a compiler error which includes the classname:

// This class has no definition, trying to instantiate it will produce an error
template <typename T> class Test;
// This line produces an error which tells us the full name of `BarTest`
// aggregate 'blah::Test<blah::BarTest> blah::t' has incomplete type and cannot be defined
Test<BarTest> t;

I’d recommend trying this method with both Bar and BarTest and making a note of the full typenames inside the <> brackets in the error which are emitted.

1 Like

Your advice was of value… I’m deeply sorry, I made a mistake, this should have been working fine… While I was refactoring to use CMake, I also switched my test framework from Juce to Catch2 which… doesn’t need me to declare my “BarTest” class anymore. I’m sorry for the time you wasted because of this. Please delete the latest posts if you think they polluted this thread.

Anyway, thanks to all of you for your support and advices!