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. Plus the ecosystem is full of jargon, naming disasters, legacy cruft…

However it’s a very useful tool to get up to speed on. You don’t need to be an expert, but it’s worth knowing the basics.

I’ll explain what I can here in hopes it’ll help future plugin devs.

You can also 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.

So one main thing CMake does is exports “build tool files.” 

That means it spits out .xcodeproj files and Windows .vcxproj files that your IDE can open.

It also configures and builds executables. This makes it really useful for running on the command line in CI environments.

All of these things are configured by a CMakeLists.txt file that sits in the root directory.

You might also see CMakeList.txt files in sub directories and oh boy then things start to get really complicated.

The coupling can be concerning…

So, CMake seems to do a lot of different jobs.

Unfortunately, these discrete jobs are not separated in CMake’s config. Instead, it’s all mashed together in one big happy festival of configuration directives.

The CLI commands you’ll issue on the command line are also all mashed together in one tool. You’ll just have to get used to what flags you should be passing, it’s not too tough!

In my opinion, this is the reason CMake has the (deserved) reputation for being “hard”: a lot of complexity results from all the implicit coupling between these different concerns.

CMake also builds on an long historical foundation of Makefiles, etc. The documentation often assumes you know the basics (what “configuration” means, what a “target” is, etc.)

Some Jargon

Ok, so let’s define a few things you’ll need to know.

A Library is a chunk of code. It’s probably in a subdirectory. It could be a juce module or some cool library you found on github. Or your testing library like Google Test or Catch2.

A Target is an executable or library that gets configured and compiled. These can be configured and built discretely. They might have dependencies on each other. You might have your app target and then a test target. If you have a plugin, each plugin version (AU, VST3) is actually its own target. Your IDE might let you setup build configurations for each target.

The Toolchain is your complier, debugger, and so on.

Modern CMake loosely refers to CMake being not quite as shitty to work with any more (vs. pre 3.0).

So, for example the CMake command 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.

These helpers abstract away a lot of framework’s build configuration needs and lets you write JUCE config in the CMakeLists.txt.

For a vanilla JUCE project, for example, you won’t see a lot of add_executable or add_library calls in the CMakeLists.txt, JUCE automagically configures the targets behind the scenes.

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

Examples of how to use JUCE’s helpers can be found in their examples directory.

It Configures

What is “configuring”?

It’s when the CMakeLists.txt is parsed, processed and CMake spits out everything the whole laundry list of things it’s going to need to build the project.

So it’s sorta like the prep work a kitchen will do before cooking.

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

Visual Studio and CLion both have built-in CMake support which keeps you off the command line. You won’t actually need to generate anything, just open the project…

It Builds

One can compile the executable directly with the configured tool chain by passing --build.

cmake --build Builds --config Release

This is how you will build on CI.

Locally, you’ll probably just use your IDE to build.

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

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, so we’ll ignore this for the time being).

JUCE and CMake Tips & Troubleshooting

Order Matters

Watch out for how things are specified in the config.

For example, a target has to have be 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.

On MacOS? Avoid the brew version of CMake

I’ve gotten reports of problems with the brew install cmake version of CMake not being able to find the C++ compiler.

So if you are using Xcode, download the official pre-compiled binaries instead.

Use VS / CLion? You can forget the command line!

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

The IDE’s will 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 and friends.

This can be… confusing at first. But the main thing to remember is that these keywords control visibility.

PUBLIC: can be used by the current target and any other target that depends on this one.
PRIVATE: can be used only by the current target
INTERFACE: will not 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.

I recommend using enable_testing() if you don’t want to be spammed.

Glob Glob Glob?

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

As a hard rule, “no globs” would mean that you 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.

To be frank, it makes for a terrible-bordering-on-hostile developer experience to have to hand-hold your build system with something so simple as “what files are available.”

In most cases, with you can now happily 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})

You might be cautioned by old timers, but I’ve yet to see or even hear of this actually being a problem in 2023.

CMAKE_CURRENT_LIST_DIR and friends

One of the things CMake itself is absolutely terrible at is naming things.

My assumption is that the devs prioritize backwards compatibility at all costs (namely: usability, new user onboarding, feeling sane in any way whatsoever).

There are trivial examples everywhere, such as all macOS stuff being named OSX (Hello, 2016).

This can really bite you once you get rolling with CMake and start juggling different CMakeLists.txt in subdirectories, modules, etc.

So beware: CMAKE_CURRENT_LIST_DIR isn’t actually “the directory containing the current CMakeLists.txt file”. If you are in an included .cmake file, it’s actually the directory of that included file.

CMAKE_CURRENT_SOURCE_DIR is actually the directory of the current CMakeLists.txt.

And CMAKE_SOURCE_DIR is the directory containing the top level CMakeLists.txt!

Source.

Using all your cores

The configure step on a JUCE build is kinda sluggish. JUCE uses that opportunity to secretly compile a few things the project build will actually need.

If you are using an IDE that manages CMake, it likely manages making sure CMake is fast.

But in CI or on the command line, you’ll probably want to set 2 things to ensure speedy builds:

First, ensure juceaide is compiled quickly on a JUCE project by setting CMAKE_BUILD_PARALLEL_LEVEL, for example, exporting it to your environment

export CMAKE_BUILD_PARALLEL_LEVEL=3 # Use up to 3 cpus 

Secondly, pass -j4 or --parallel 4 to cmake --build to specify the number of parallel build jobs (in this case 4).

If you are using Ninja, this should be automatic.

Check out some example JUCE CMake configurations

Here are some examples to look through:

It’s an additive beast with 1000 oscillators
and a ton of fun sound shaping tools

Check it out

Responses

  1. Dirk Avatar
    Dirk

    It is CMakeLists.txt not CMakesList.txt

    1. sudara Avatar

      Ha, I swear I make this typo every other time I type it, thanks!

  2. Dmytro Kiro Avatar
    Dmytro Kiro

    Awesome explanation! This is a great article on how to use CMake with JUCE!

Leave a Reply

Your email address will not be published. Required fields are marked *