How to use CMake with JUCE

CMake took me a bit of wrestling (especially on Xcode). I wasn’t totally clear on a few high level concepts at first. I’ll explain those here in hopes it’ll help future plugin devs.

Also check out Pamplejuce, a GitHub template I made for JUCE + CMake + Catch2 + GitHub actions.

What role does CMake play?

CMake is the “glue” that lets you configure and build your JUCE project for multiple platforms.

Before the CMake integration was announced the only way to do this was via JUCE’s custom app, the Projucer.

CMake exports “build tool files.” Among other things, it spits out .xcodeproj files and Windows .vcxproj files that your IDE can open.

It can also build executables with the platform toolchain, which makes it useful for CI environments.

All of these things are configured by a CMakeLists.txt file in your root directory. (You can additionally have CMakeList.txt files in sub directories and add those configurations to the top level one).

Visual Studio and CLion both have built-in CMake support which will keep you off the command line.

The coupling can be concerning…

All the different jobs that CMake does are not separated in CMake’s config.

It’s all mashed together in one big happy festival of configuration directives. Also, the CLI commands are mashed together in one tool and you’ll have to get used to what flags you should be passing.

In my opinion, this is the big reason CMake has the reputation for being “hard”. There’s a lot of complexity resulting from implicit coupling between these concerns.

The other reason: Perhaps because it builds on an historical foundation, the documentation implicitly assumes you know the basics (what configuration means, what a target is, etc).

Some Jargon

A Library is a chunk of code, probably in a subdirectory.
A Target is a discrete executable, such as your plugin, or a library
Toolchain is your complier, debugger, and so on.
Modern CMake loosely refers to CMake being not as difficult to use these days (vs. pre 3.0).

So, for example the function target_link_libraries will link a library (such as a testing framework like Catch2) to your plugin target.

JUCE’s CMake API

JUCE provides helper functions such as juce_add_plugin which abstracts away a lot of framework’s build configuration needs and exposes a bunch of high level plugin/executable config options.

This also means you won’t really be using a lot of add_executable or add_library calls, as those will be handled behind the scenes.

It sets up our project much like the Projucer does, but with a lot more flexibility.

Examples of how to use JUCE’s helpers are in the examples directory.

It Configures

The CMakeLists.txt is parsed, processed and CMake spits out everything it’s going to need to build the project.

On the command line, this looks something like cmake -B Builds.

The -B option tells CMake what folder to perform the build in, where to barf all the configured files.

Tip: Some IDEs such as CLion will automatically detect and run the configure step, using the folders cmake-build-debug and cmake-build-release and so on.

It Generates

If you pass -G, CMake outputs project files for system specific build tooling and IDEs. The idea is you generate a project file and then open it and compile from the IDE.

On windows, this will create a solution file such as Projname.vcxproj

cmake -B Builds -G "Visual Studio 17 2022"

On MacOS this will build you an Xcode project such as myProject.xcodeproj

cmake -B Builds -G Xcode

It Builds

Instead of spitting out IDE project files, you can compile the executable directly with the configured tool chain by passing --build.

cmake --build Builds --config Release

Tip: This is the only place you’ll need to specify the build “type” (Debug, Release, etc).

This is how you will build on CI. Locally, you’ll probably just use your IDE.

It tests

CTest is a unit test runner that comes with CMake.

It doesn’t know anything about your unit test implementation (Catch2 or GoogleTest, etc). It doesn’t know anything about the executable that it will run.

If you want to run tests with JUCE, a test executable has to be created. See the Catch2 CMake integration for details or check out the example in Pamplejuce.

Tests are created as another target

It installs things

(But we’re not using this functionality for JUCE.)

JUCE and CMake Tips

Order Matters

Watch out for how things are specified in the config. For example, a target has to have been added before you can target_link_libraries.

Likewise, juce_add_module should be called before juce_add_plugin.

When in doubt, blow away the build folder

CMake is very much a “turn it on and off again” piece of software.

Don’t bother debugging something esoteric and scary looking before you try and rm -rf Build (or whatever your build folder is).

The layers of caching involved make it just a matter of time before your IDE, a dependency, or a CMake update causes an issue somewhere.

Use CLion

As mentioned, using CLion means never running CMake on the command line again. It’ll auto-run CMake when relevant things change like the CMakeLists.txt.

Use the Ninja Generator

You can speed up the whole process by using Ninja as your generator. This is the default in CLion.

It may sound counterintuitive to add yet another layer to this already complex process, however it’s largely transparent.

brew install ninja on MacOS. On Windows, download the latest exe and make sure it’s in your path.

PUBLIC, PRIVATE, INTERFACE?

There are 3 types of keywords for CMake’s target_link_libraries and target_include_directories. This can be confusing…


PUBLIC: can be used by the currenty target and any other target that depends on this one.
PRIVATE: can be used only by the current target
INTERFACE: won’t be used by the current target, but can be used by anything that depends on the current target

Don’t include(CTest)

include(CTest) adds a ton of unnecessary targets. Use enable_testing().

Glob Glob Glob?

CMake historically and famously recommends not using globs (For example ~Documents/* would be all files in your user Documents folder).

As a hard rule, “no globs” means you must explicitly and manually list out every file you want in your project. You’ll be editing CMakeLists.txt and re-configuring and exporting every time you add or rename a file.

This adds some friction to project file management and can be unwieldily and unpleasant.

In many cases, with “modern” CMake you can ignore this advice and use CONFIGURE_DEPENDS. This will tell CMake to check the glob and rebuild if necessary.

file(GLOB_RECURSE SourceFiles CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Source/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Source/*.h")
target_sources("${PROJECT_NAME}" PRIVATE ${SourceFiles})

Check out example JUCE CMake configurations

Here are some examples to look through:

Leave a comment

Your email address will not be published.