Skip to content

CMake Notes

Skye Soss edited this page Sep 24, 2026 · 4 revisions

How CMake Works

Software development involves many different parties with different responsibilities and priorities. CMake is designed to satisfy different groups; to make things concrete I'll use different personas[^1].

Key Roles

  • Joe is a SML/NJ developer. His job is to write code and run tests. This may involve switching between different dependency versions and swapping between Debug and RelWithDebInfo builds to test correctness and speed respectively.

  • Sam is an SML/NJ end-user. He is a university student whose teacher told him that he needs SML/NJ, but needs to build from source. The main ask is that the installation works in a predictable manner.

  • Debbie is a Debian package maintainer. Her responsibility is to maintain the Debian smlnj package. The distribution has some required policies on whether or not to include debug symbols, handling binary stripping, and the use of compiler flags that deal with hardening. Because of this, she may also re-bootstrap the project to not distribute any binary file downloaded from smlnj.org. The build system also needs to be offline, so all files must be downloaded before the build starts.

General rules

Because Joe writes files for himself, Sam, and Debbie, there are some important guidelines to follow to make sure no one steps on each others' toes:

  1. Don't set CMAKE_* variables in CMakeLists.txt

Every variable that begins with CMAKE_ configures a policy, for example CMAKE_C_FLAGS for C compiler flags. This is how Debbie implements rules like "all C programs are compiled with -fstack-protector-strong"

  1. Use CMakePresets.json for presets, not for normal builds

Manually writing out CMake configuration flags is not always fun: Joe is always going to build testing-only binaries, while that's something Sam never wants enabled. And for SML/NJ releases on MacOS, there are a set of flags that need to be passed to the compiler that are usually left disabled.

But Debbie needs to be able to override all of the CMAKE_ variables that Joe and Sam often set. Joe might always enable -Wall -Werror, while Debbie needs to ensure new compiler versions don't break the build system. The preset file may also default the build directory, which may not be okay for some meta-builders (ex. dpkg-buildpackage). Thus CMakePresets.json should be limited to optional but otherwise useful configuration options.

  1. Properly separate independent sections of the build process

Joe may want to run Continuous Integration whenever a feature is proposed. Most changes to SML/NJ do not touch the LLVM subsystem, so it is not necessary to rebuild it every time a change is made in the main repo. By allowing each CMake project to be separate (ie. using find_package()), a single LLVM build can service many different SML/NJ builds.

CMake targets

Modern CMake[^2] is based around targets. These are usually created with add_executable and add_library. These targets can be marked as dependencies of each other, and marked for installation.

# Library
add_library(mylib
  src/mylib.cpp)
# mark the necessary header files
target_sources(mylib
  FILE_SET HEADERS BASE_DIRS include FILES include/mylib/mylib.hpp)
# mylib needs C++ 17 or later
target_compile_features(mylib cxx_std_17)
# mylib depends on ZLIB, but its interface doesn't mention zlib
target_link_libraries(mylib PRIVATE ZLIB::ZLIB)

# mylib should be installed. I'll talk about exports later
install(TARGETS mylib EXPORT mylibTargets)

CMake cross-project dependencies

CMake can import dependencies from the system or elsewhere using find_package. If the other library was built with CMake, it will have a file named otherlib.cmake which is used to define the targets that otherlib defined.

Configure-time Downloads

CMake also supports downloading files at configure-time via FetchContent. Files downloaded this way can also be provided in an offline manner for Debbie.

include(FetchContent)

# These calls define where "${arch}_bootfiles" can be found
FetchContent_Declare(
  amd64_bootfiles
  URL https://smlnj.org/dist/working/...
  URL_HASH sha256=44ad139c886c65ef813a61d0721fdbed9fe854210b69b2bd612fea360beb0cbf
)
FetchContent_Declare(
  arm64_bootfiles
  URL https://smlnj.org/dist/working/...
  URL_HASH sha256=43015642bf8ba3a4939d9b7c1e20283fa83a9c98770ccd3c7b6f1a50235b2203
)
# This actually downloads the files
FetchContent_MakeAvailable(${arch}_bootfiles)

message(STATUS "downloaded ${arch}_bootfiles to ${${arch}_bootfiles_SOURCE_DIR}")

[^1]: This idea was taken from the Kubernetes Gateway API docs

[^2]: CMake 3.0+

Clone this wiki locally