Native, built-in CMake Support in JUCE

I’m really happy to see the native CMake support :slight_smile:

Unfortunately there’s one issue I’m having with the current implementation: setting compiler warning flags.

Having a project like this:

...

juce_add_gui_app(GuiAppExample)

set(GuiAppExample_sources
    Main.cpp
    MainComponent.cpp
)
target_sources(GuiAppExample
    PRIVATE
    ${GuiAppExample_sources}
)
target_link_libraries(GuiAppExample
    PRIVATE
    juce::juce_gui_extra
)

And then setting warning flags using target_compile_options(GuiAppExample PRIVATE ${MY_WARNING_FLAGS}) causes the Juce sources to be compiled with the same flags.
This may trigger warnings in Juce code (e.g. C4266, C4388, -Wold-style-cast, -Wsign-compare, -Wcast-qual) and prevent me from using -Werror and finding issues in my own code.

A workaround is setting the flags only for my sources and not for the whole target:

set_property(
    SOURCE
    ${GuiAppExample_sources}
    PROPERTY
    COMPILE_OPTIONS
    ${MY_WARNING_FLAGS}
)

But I’d still like to set a decent warning level for the Juce sources, so that I can catch potential issues with my (unusual?) platform and compiler combination.

Using modest warnings in target_compile_options and setting more warnings for my sources works to some degree as CMake currently combines the warning flags in this order:
CMAKE_CXX_FLAGS + target-COMPILE_OPTIONS + source-COMPILE_OPTIONS.

This doesn’t work well with MSVC: Once a warning has been disabled (e.g. /wd4388) you cannot simply add /W4 / /Wall to re-enable the warnings. Instead you have to explicitly re-enable all warnings that have been turned off (e.g. /w34388). This approach is error prone and results in unnecessarily long command lines.

A solution that would work for me could be a function/variable for setting warning flags for the Juce sources (${all_module_sources} if I’m not mistaken), so that the two sets of warning flags don’t interfere.

Thanks for your feedback! You can query the sources used to build a specific module by checking its INTERFACE_SOURCES property:

get_target_property(juce_core_sources juce::juce_core INTERFACE_SOURCES)
set_source_files_properties(${juce_core_sources} PROPERTIES COMPILE_OPTIONS "-Wall;-Wextra")

In this way, you could set different warnings on the JUCE sources and your own sources:

set(juce_modules juce::juce_core juce::juce_events)

# set some warnings on JUCE sources
foreach(module IN LISTS juce_modules)
    get_target_property(sources ${module} INTERFACE_SOURCES)
    set_source_files_properties(${sources} PROPERTIES COMPILE_OPTIONS "-Wall;-Wextra")
endforeach()

# set some different warnings on my own sources
set(my_sources lib.cpp)
set_source_files_properties(${my_sources} PROPERTIES COMPILE_OPTIONS "-Wpedantic")

add_library(util OBJECT ${my_sources})
target_link_libraries(util PRIVATE ${juce_modules})

# set warnings-as-errors on every source in the target
target_compile_options(util PRIVATE "-Werror")

The COMPILE_OPTIONS for JUCE module sources should only include flags that you want to apply every time the module is linked, so be aware of this if you’re building multiple JUCE projects in the same tree. Unfortunately the only way to set per-target flags on the JUCE sources is to set those flags on the target that links against the JUCE modules.

Is an approach like this sufficient for your use-case?

Here’s a question. In our project, we have lots of different CMake targets that depend on JUCE. Compiling it the usual way became prohibitive in terms of too long compile times, because JUCE is being compiled over and over again for each one.

We solved this by compiling the JUCE modules that are used by all targets into a static library, and then link against that static library. Extra JUCE modules that are needed by a few targets (like audio_plugin_client being needed by VST/AU/AAX targets) are added separately. This is all hand-written CMake which is a pain to maintain. Obviously, it would be great to migrate instead to a world where we can just use the new JUCE 6 CMake capabilities.

Will the JUCE 6 CMake allow to compile JUCE (or perhaps a certain set of JUCE modules) as a static library and link against it?

1 Like

The short answer is ‘no, but maybe some day’.

At the moment, this model is unsupported by JUCE modules. Some modules need to introspect the set of other modules that are available using JUCE_MODULE_AVAILABLE macros in order to enable/disable certain functionality. This means that it’s ill-advised to convert a selection of JUCE modules into a static library unless you know for a fact that you will never need to link additional modules in any targets. Unfortunately, tacking on extra modules later means that any modules contained in the static lib won’t get a chance to reconfigure themselves, so some cross-module functionality might break. For example, AAX and Unity plugins won’t work quite right unless juce_events and juce_gui_basics know at the point that they’re built that juce_audio_plugin_client is (and will always be) available.

To be clear, this kind of project setup is inherently unsupported by the JUCE modules themselves, and no amount of build-system-level wrangling will make it ‘work’. I strongly advise against using this technique until it is explicitly supported by JUCE.

I’m not particularly happy about the current state of things, and I’d love to fix it, but this is a massive undertaking which we’re not currently planning to tackle in the near future.

1 Like

Assuming you’re not regularly changing your project’s preprocessor flags, you could investigate installing ccache on your build machines to reuse cached object files from previous builds. This might provide an acceptable build speedup without depending on broken build methods.

Interesting. Could you explain why you think compiling JUCE as a static library is a “broken build method”?

Yes, we have a fixed number of preprocessor flags that don’t change, so that allows us to statically compile JUCE once and link against it. The exception are audio-plug-in specific settings, which is why we compile audio_plugin_client separately per target (but only that one module).

Building all of JUCE as a staticlib is not broken. Building some of JUCE as a staticlib and then building some modules (including the audio_plugin_client module!) directly into the final target is broken, unless (in this case):

  • you can guarantee that juce_audio_plugin_client will always be available, and
  • you supply JUCE_MODULE_AVAILABLE_juce_audio_plugin_client=1 when building your staticlib.

I think a build is only supported if, in every module TU, the set of JUCE_MODULE_AVAILABLE_* preprocessor definitions exactly matches the set of modules that end up in the final binary. When I say ‘broken build method’, I mean building JUCE in such a way that this is not guaranteed, as is highly likely if a subset of JUCE modules are built into a static lib and then further modules are added to the final target.

Thanks for the quick response.

I also thought about using INTERFACE_SOURCES.
The problem with that approach is that I have to know all the Juce modules that are automatically added via the dependencies declarations of the modules that I am using.

A function that gives me all transitively dependent modules would help a lot:

set(juce_modules juce::juce_core juce::juce_gui_extra)
juce_get_dependent_modules(all_juce_modules ${juce_modules})
message(STATUS "${all_juce_modules}") # --> juce::juce_gui_extra;juce::juce_gui_basics;juce::juce_graphics;juce::juce_data_structures;juce::juce_events;juce::juce_core

When adding this line to the function(juce_add_module module_path) to store the dependent modules of each module:

target_link_libraries(${module_name} INTERFACE ${module_dependencies}) # existing line
set_target_properties(${module_name} PROPERTIES "juce::module_dependencies" "${module_dependencies}") # new line

Then this function can calculate all the transitively dependent modules:

function(juce_get_dependent_modules out_var)

    macro(_pop list_name out_var)
        list(GET ${list_name} 0 ${out_var})
        list(REMOVE_AT ${list_name} 0)
    endmacro(_pop)

    set(new_modules ${ARGN})
    set(all_modules)
    while(TRUE)
        list(LENGTH new_modules new_modules_length)
        if(new_modules_length EQUAL 0)
            break()
        endif()
        _pop(new_modules module_name)
        string(REGEX REPLACE "^juce::" "" module_name "${module_name}")
        string(PREPEND module_name "juce::")
        list(FIND all_modules ${module_name} module_index)
        if (${module_index} GREATER_EQUAL 0)
            continue()
        endif()
        list(APPEND all_modules ${module_name})
        get_target_property(module_dependencies ${module_name} "juce::module_dependencies")
        list(APPEND new_modules ${module_dependencies})
    endwhile()

    set(${out_var} ${all_modules} PARENT_SCOPE)

endfunction(juce_get_dependent_modules)

EDIT:
Unfortunately the INTERFACE_SOURCES approach doesn’t work for me:
The flags added using set_property(SOURCE) / set_source_files_properties aren’t being used.
I don’t know if this doesn’t work because the sources contain generator expressions or because I’m setting the property in my CMakeLists.txt file (CMake Documentation: “Note that source file properties are visible only to targets added in the same directory”).

When using the Visual Studio generator in Windows, it seems like the code for JUCE modules (both built in ones and custom modules) only show up as a single header/source file.

The CLion exporter (as well as FRUT) allowed to add all the contents of the modules as sources to the IDE, which was extremely useful.
VSCmake
Would that be possible to add?

That’s strange, I tested modifying INTERFACE_SOURCES and was able to see the flags being applied when building with cmake --build build-folder --verbose. I only tested on Mac/Ninja though, I only set the properties on the source files in a single location, and I only tested with find_package and not add_subdirectory. Perhaps multiple property updates don’t work as expected, or maybe different generators have slightly different behaviours.

Assuming the INTERFACE_SOURCES approach were suitable, I think juce_get_dependent_modules could be made to work without updating any of JUCE’s CMake files, by switching the use of juce::module_dependencies for the built-in INTERFACE_LINK_LIBRARIES property. This would pick up other library dependencies too, so you’d need to add an extra check to see whether a target really exists after adding a juce:: prefix.

If INTERFACE_SOURCES really doesn’t work, I think the most robust solution would be to apply ‘fallback’ flags to your target (and therefore to your sources and the JUCE sources), and then selectively apply stricter flags to your sources. You mentioned that this wasn’t viable in your earlier post, but perhaps there’s some creative shuffling of warning-order that could get it to work…

I’ll investigate making the full set of module sources browse-able in IDEs, it should be possible.

2 Likes

OK, that’s interesting. I wasn’t aware that this was a requirement.

Thank you @reuk, I wasn’t aware of that! I guess it means we will need to rewrite our CMake scripts. To help me prioritise this correctly, could you please tell me what “broken” means exactly? Do you expect crashes? Unexpected/wrong behaviour? Failing asserts? (I am asking because we have not observed any of these using this way of building our final targets.) Something else? Or is it more in the “unsupported but kinda works anyway” camp?

It depends on the combination of modules you exclude from the staticlib. If you were to build everything other than juce_audio_plugin_client into a staticlib without defining JUCE_MODULE_AVAILABLE_juce_audio_plugin_client=1, it looks like that staticlib would be essentially ‘valid’, but would lack support for intercepting modifier keypresses in AAX plugins on Windows, and the message queue might function incorrectly in Unity plugins. (Just skimming the code here, I haven’t tried it.) This case is relatively safe (at the moment) because we’re not checking JUCE_MODULE_AVAILABLE_juce_audio_plugin_client in any headers.

Unfortunately, some headers do change their contents depending on which JUCE_MODULE_AVAILABLE_ tokens have been defined, which is much more dangerous, as different TUs may see different class layouts or inline function definitions. In this case the resulting program would be inherently broken due to ODR violations (although it might link correctly and appear to work).

To reiterate: I think a build is only supported if, in every module TU, the set of JUCE_MODULE_AVAILABLE_* preprocessor definitions exactly matches the set of modules that end up in the final binary.

I’ve installed Juce and everything works as expected. So I’d say that this is caused by the generator expressions in INTERFACE_SOURCES:

  • find_package --> C:/The-Install-Dir/include/JUCE-6.0.0/modules/juce_core/juce_core.h
  • add_subdirectory --> $<BUILD_INTERFACE:C:/Checked-out-Juce-Dir/modules>$<INSTALL_INTERFACE:include/JUCE-6.0.0/modules>/juce_core/juce_core.h

Unfortunately (manually) installing Juce doesn’t really fit my workflow. If there’s no other solution I’ll probably have to add_custom_command to build and install Juce in CMAKE_CURRENT_BINARY_DIR. Or would it be possible to add a flag that controls the behavior of JUCEUtils.cmake to disable the install option and use plain paths in INTERFACE_SOURCES?
I also tried building the used Juce modules into a single static lib and then link to that lib. But this causes the INTERFACE_SOURCES to be built twice.

Using INTERFACE_LINK_LIBRARIES for calculating the dependencies works fine.

Are you calling find_package(JUCE) in the same directory that you add the source properties?

I think find_package will create the imported JUCE targets in the directory where the call occurs, whereas add_subdirectory(JUCE) will create ‘real’ non-imported targets for the JUCE modules in the JUCE subdirectory. If this is the case, then the add_subdirectory approach can’t work, because

Source file properties are visible only to targets added in the same directory (CMakeLists.txt).

Yes, I am using a single CMakeLists.txt file that calls either find_package or add_subdirectory and then sets the source properties.
I also tried setting the source properties directly in JUCEUtils.cmake and in the CMakeLists.txt in the Juce root directory.

I just tried something else and now I’m almost certain that this is caused by the generator expressions:
I changed

set(my_sources
    Main.cpp
    MainComponent.cpp
)

to

set(my_sources
    $<INSTALL_INTERFACE:include/JUCE-6.0.0/modules/>Main.cpp
    MainComponent.cpp
)

And then used set_source_files_properties(${my_sources} ...): Now Main.cpp gets compiled without the warnings.

(I’m also not quiet sure how to interpret that sentence from the CMake documentation. My executable and the call to set_source_files_properties are in the same CMakeLists.txt. It’s just the intermediate interface target that is in a different directory.)

You’re right, I think the generator expressions throw it off somehow.

I’m a bit reluctant to add a “don’t use generator expressions” option, as it’s quite a niche use-case (I think) and the potential for confusion is very high. Also, it feels like a bit of a hack and it’d be nice to find a way to do this properly.

In the meantime, it’s possible to use a regex to remove the generator expressions and then to feed the cleaned-up result to set_source_files_properties:

get_target_property(core_sources juce::juce_core INTERFACE_SOURCES)
string(REGEX REPLACE
    "\\$<BUILD_INTERFACE:([^>]+)>\\$<INSTALL_INTERFACE:[^>]+>" "\\1"
    core_sources "${core_sources}")

Out of interest, how were you dealing with this issue when using Projucer-generated builds? It seems like this kind of build setup would be tricky there too.

Projucer allows you to set a “Compiler Flag Scheme” to files to compile. This makes it easy to add warning flags to your own source files.

Sure, but CMake lets you set flags on you own source files too, and it sounded like that functionality wasn’t sufficient for Tobias’ use case. Maybe the Projucer applies the flags in a different order than CMake does.

Hi guys, any word on Android support with CMake? Seems like it should be a fairly low hanging fruit since the Android exporter already uses CMake. But I may well be ignorant of other potential issues, like generating an Android Studio project via CMake…