Transcripts

CMake

Date

22 October, 2025

Speakers

Not Available

pencil icon

Transcript by

Not Available

Overview

This presentation discusses CMake fundamentals and its future direction, brought into the context of Bitcoin Core. The speaker notes not to expect too many best practices, as these have been discussed before. Best practices for Bitcoin are not best practices for many other projects.

The goal is to explain what CMake is and what direction it should go in the future, to help understand the full scope of what it can be used for and to show some things that can change, because best practices evolve over time.


What is CMake?

Both a Language and a Set of Tools

The Language:

  • Quite awkward
  • Turing complete
  • Has variables and commands

What's different about this language:

  • Command invocations are not expressions
  • Command invocations can, as a side effect, set variables
  • The variable that is set by a command could be the first argument, could be the last, could be an argument that is passed by some other tag
  • The input of a command is a list of tokens
  • How that token is interpreted is decided on a command-by-command basis
  • It's a little bit messy, but is a quite stable mess

Stability and Portability:

  • Has a very strong deprecation mechanism called policies
  • Remains stable for decades
  • Therefore can be considered the most portable scripting language

A Set of Tools

CMake contains five different executables. Each of the tools use a file as an entry point that is written in the CMake language.

Entry Points:

  • Can be written by hand โ€” useful if you want CTest to define a CI workflow for a project that is not using CMake as a build system. Could be useful for secp256k1.
  • Can be generated by the CMake build system generator
  • Can be executed by running CTest

Build Targets:

  • CMake will also generate build targets
  • This can be confusing for users
  • They are part of the problem why CMake is not well understood as a set of tools

Common Problem: If the only CMake tool you know is the build system generator, you will end up writing CI scripts in bash that execute:

cmake --build build --target test

If you want to run multiple processes in parallel this is harder.

The problem is that it will lead to a coupling. You will end up setting CI-specific settings in the CMakeLists.txt file.


Goals of the Maintainer of the Build System

Make Customers Happy

The customers in this case are other projects that use that project as a dependency, e.g., secp256k1, and Core in the GUI, etc.

Make Developers Happy

Respect their local preferences. They can set the build type and the generator tool in their dotfiles and environment variables. We should respect those settings.

Recommendation: Use Ninja

  • Faster
  • Default build system on Windows is to use Visual Studio
  • Default on macOS is Xcode
  • Default on Linux is Unix Makefiles
  • But future features of C++, e.g., C++20 modules, will not work with Unix Makefiles
  • So all future development is concentrated on Ninja
  • Should CMake move to default to Ninja?
  • Even Fedora is having this default conversation

Make Your Future Self Happy

Can be you or anyone in a role you currently have.

Missing from This List

Shipping software that actually works. Not npm. Is reproducible.


Trade-offs and Examples

EXPORT_COMPILE_COMMANDS Example

Core has chosen to disable the EXPORT_COMPILE_COMMANDS. This disables code completion in IDEs, but is better for the maintainer. So the maintainer needs to find a better workaround.

Potential solution: You can set the tidy property on a per-target basis, which would not break people's setups. There might be other workarounds.

Going Back to Goals of a Maintainer

  • Simplifying CI scripts may break setups of devs or customers
  • Convenience may also break setups
  • Setting variables may also break setups, even if the motive is convenience for customers

The ABI Compatibility Problem

The goal must be: if you have a metabuild that consists of multiple dependencies, for ABI reasons, that everything is good for the same compile flags. But I want to make sure their compiler doesn't do something that causes a bug for the user. I only, Bitcoin Core, care about this project.

But what about libsecp? What if someone wants to use Core as a dependency?

At the moment Core is the top level. So it dictates all the dependencies. But what if something else is at the top level?

But we want to enforce something for security reasons. Do we actually care if someone is packaging Core?

Are we just adding obstacles? But inconveniences of users outside of this group who may not be experts. Who do we care about more?


The Minimum Set of Commands Needed to Define a Set of Libraries

(See slide)

We define the library without passing it any sources.

Instead we define it without any source files then use the target source files commands to add headers and source files.

We can link dependencies. Where we do this we always use the keyword, public or private.

We install the target. We do not specify any location. Not specify the runtime, archive location, just use the defaults. All we have to say is we want to install the fileset.

We record the information in this export set. And what this will do is create something that can be imported with find_package. The imported one will have a namespace target. How this is bound together, we export the targets into the namespace. We want to make sure the people who do not use find_package, but a subdirectory, can use it in the same way, whether it is imported or not.

If you build the target on the command line this is the target we have to build. So we add this alias and we need to match the export name and the namespace.

If you link against something that has a namespace, that means CMake checks the target exists. If you type something without a namespace it will just assume this is a library that exists on your system. Always use keywords and make sure the dependencies use namespaces.

All customers will use that alias.

CMake requires that the same signature should be used across all targets. You cannot mix and match. At some point someone should open a pull request and fix this in Core.

All these commands can be used conditionally. You should not define a variable depending on the platform then use it in an unconditional way.

What Can Go Wrong

What happens if you have a namespace without a typo (or not) and it links to an incorrect library? This can happen. E.g., experimental secp and Core links against the production one.


Guidelines

  • Always define an alias
  • Set usage requirements
  • No more target_include_directories use case after CMake 3.22
  • Only namespaced targets in target_link_libraries()
  • Export the target (match ALIAS)

Don't set any variable that begins with CMAKE_. Keep defaults. Can be different if you are the top level project.

Don't set any variable or property that is not strictly necessary.

Hardening Flags

You want to ship your binaries with the hardening flags.

We also need to help users who compile to do so correctly with hardened flags.

CI and Toolchains

Write CI scripts that are reusable across projects. Define compile flags as toolchains. Make sure that all dependencies are built with consistent flags. Know the direction of future development.


Modern Approach: target_sources

We use target_include_directories before we make the switch to target_sources, we should limit the use of target_include_directories.

Example: Imagine we have foo and bar, two libraries. Bar depends on foo. Bar contains this include foo line. foo.c would compile without setting the include flag, but when we compile bar.c the include directory needs to be set, otherwise the file cannot be found. In the past we would have used target_include_directories on bar.

New approach is compatible with target_sources. Work towards this.


Questions

Minimum Supported CMake Version

Ongoing discussion in libmultiprocess. Minimum supported CMake. What are benefits of supporting old versions of CMake which are not available in distros, e.g., 3.12?

Brown proposed to use latest available, not a version range. This creates additional maintainer burden. Would like to avoid. Even having a range creates a maintenance burden.

Speaker's opinion: There are two different concepts:

  1. Minimum version - When are you going to show the user an error
  2. Policy version - How is CMake going to behave

For the minimum version, just document. Don't try and be backwards compatible. For policy, you can do many things. Just pick one.

How Policies Actually Work

When a warning is actually printed. When code is affected by policy, it will execute the old path and new path, then compare. If different, a warning is printed. If results match, no warning. If old, only old. If new, only new. If unset, see comparison approach just described.


Warning and Hardening Flags Discussion

Overall Question

Don't include anything that is strictly necessary โ€” the warning flags, hardening flags โ€” CMakeLists.txt file is not the right place for that. What do we do with that advice?

How Should We Go Forward?

We need to agree on what the goals are for people that want to use our components as dependencies.

I want to choose the middle ground. Our current choice seems to be a good default. A middle ground.

Multiprocess doesn't need to have this discussion. We are really focused on warnings and hardening.

For Core, as long as it's a product, it can be kept as it is. Or we move to toolchain files. At some point where Core is more separate, e.g., kernel, then the goal of kernel is to be integrated and used by developers.

Can We Do Both?

Have warning and hardening and still make kernel easy to consume. Is there no good answer?

I don't think we can do both. Hardenings on by default, set in the toolchain file, is this possible, and then provide a preset that provides that toolchain file.

Just write the compile flags into this depends-provided toolchain file.

Question: Can we set something like a default preset? A default default?

If it was only in the depends toolchain, there will be people who miss those flags. We have pushed back against external packaging of Core. By design.

The only supported thing we produce are the Guix builds. Anything else is a best effort. Which uses the generated toolchain file.

But we don't know how people use the source code.

But that's why we set all the hardening flags and try to avoid all the footguns.

ABI Issues Example

Instead of setting 20 as the language standard, because someone else might pull it into their project, that could create ABI issues.

Can we give them this flexibility and use what we want as defaults?

Therefore the best approach is a defensive approach. Let the customer decide. And if they use an incompatible language version, throw an error message.

We want it to be configurable with the defaults we like and are safe.

We don't have a way to turn off the hardening. I don't know if we should.

We can easily update the depends toolchain to include the flags.

There are real problems with hardcoding things, and philosophical problems we can debate about.

Convenience vs security tradeoff. No free lunch.


Future Modularization

Any modularization that might happen in the future. Kernel library. That will be moving upstream. And it won't have as much build system as we have. C++ with possibly no dependencies. Minimal build that Core is consuming and adding its own stuff too. All through the stack, this needs to align.

That's why you set configs at highest level. It's a packaging thing. A distribution.

Interesting idea: Top level CMake.

Depends System

Depends. Now we have a bunch of new repositories. The hardening flags don't exist in the...

If kernel goes to a separate repository, maybe you have the flags in two places. When they are in one place, it's easier to keep tabs on them.


How Do Other Projects Deal With This?

Explicit flag setting. A CMake variable. Is it uncommon? If it's necessary, it's set as a target property. Tied to the target.

Hardening flags don't fall into this category.


Compiler Flags: Strategic Considerations

Conceptual discussion over where we might set compiler flags like hardening etc.

If Bitcoin Core starts being used as a dependency in different places (downstream), then we might not want to be setting these in our CMakeLists.txt (or anywhere other than a toolchain, possibly in a packaging repo).

We have hardening flags and things encapsulated in a single target currently.

Transcripts

Community-maintained archive to unlocking knowledge from technical Bitcoin transcripts

CategoriesAbout

Explore all Products

ChatBTC imageBitcoin searchBitcoin TLDRSaving SatoshiBitcoin Transcripts Review
Built with ๐Ÿงก by the Bitcoin Dev Project
View our public visitor count
We'd love to hear your feedback on this projectGive Feedback