LibUseful is a toy library to showcase various build automation features of CMake, CTest and CPack.
The build definition aims at showcasing as many good practices as reasonably possible. The author knows that the set of best practices is an ever-moving target and the scripts are most certainly even in their current incarnation leave room for improvement.
CMake is a scripting language and writing maintainable, easy to understand build definitions is as much dependent on scripting style as in any other sciprt language. Here are a couple of best practices to keep in mind.
It is tempting to automate more than strictly necessary, but resist that temptation. CMake scripts should do no more than build-test-install-package.
Convenience features, especially "smart" conveniences will often bite users or simply become a hassle to maintain.
- Conveniences around turning on "warnings as errors" behavior in compilers have become prevalent in the ecosystem.
Just one
example
on how messy these facilities can get. These practices give rise to built-in functinality around controlling
different aspects of build (
COMPILE_WARNING_AS_ERROR
orMSVC_RUNTIME_LIBRARY
), previously requiring manual meddling with compiler flags. If such a feature is missing from CMake, carefully consider the cost/gain benefit of introducing a project-specific machinery. - Conveniences around dependency management, see later.
Conveniences around obtaining dependencies on behalf of the user almost always gets in the way of some use case,
but primarily in the way of advanced users. (The author) having cooked many such conveniences for AMD, Khronos and
teaching material to novices and has done maintaining a lot of these: "it's not worth it". The only robust solution
in CMake (currently) is find_package(<name> REQUIRED)
and expecting said dependency to be found somehow and be a
pre-built install tree of said dependency. Everything else will break.
Accept C/C++/HIP/CUDA/etc projects having to use a package manager of sorts for the least painful experience for devs and users alike. Make sure your dependencies are available in popular package managers. (The author is an advocate of Vcpkg, but anything is better than nothing.)
Git submodules is not dependency management!
Self-hosting snapshots of dependency sources is not dependency management!
The OOP idea behind encapsulation is that there is a public interface to an object and users need not concern
themselves with how the sausage is made. This is true for build interfaces as well. Surface any option, setting which
has an effect on the build near the top of the root CMakeLists.txt
file. It may be tempting to introduce said options
near the module or the use-site, but it contradicts with easy discoverability of your build interface.
LLVM notoriously violates this, all modules introducing their options in their respective folders, thus forcing the user to traverse dozens of folders just to discover all the options they are allowed to set.
Variables starting with CMAKE_
and CPACK_
are generally owned by the user. They are global variables controlling
many of the default values for most properties on targets. Users will get frustrated if they set CMAKE_CXX_FLAGS
during configuration and realize that some parts (or the entire) build don't respect the values put in there.
Do not think as a project owner that you are smarter than your users!
Carefully consider when and how you make changes to these variables. Preference, even as a project owner, is weightless as opposed to user preference. A recurring mistake is hardcoding "warnings as errors" in some form, because the project aspires to build cleanly. Do not enforce that on your users! They may be using newer or totally different compilers than those available to your project. Enforce this in CI, but not on all of your users. ABI controlling flags are no different in this regard. Even if some ABI configuration is known to misbehave, at most issue a silencable diagnostic.
Changing the defaults will surprise people. Don't think that your users are all novices or that they need to be
saved from themselves. Changing defaults, such as
CMAKE_BUILD_TYPE
is an
(anti-pattern).
Changing defaults out of personal preference is even worse. Seasoned users of a tool will know the defaults and expect
them to be what they are. Overriding that will frustrate them to no end, even if you think some default is dangerous to
novices.
Container technologies serve developers well in defining an environment that is known to work when building some software. The series of commands however that build, install, package as software are often captured as CI scripts, often wrapped into YAML. To help replay the exact same commands for local testing/development, they may ship as shell scripts with CI becoming a oneliner. However, this doesn't help developers much diagnose failures with debuggers, tracers, profilers in their IDE of choice.
Every IDE has come up with some configuration file for devs to communicate to the IDE how they want CMake to be invoked on their behalf. These configuration files however don't transfer between dev team members using a different IDE or even to CI scripts.
CMake presets is Kitware's take (having consulted with IDE vendors) on a common, extensible configuration file which captures the usual:
Step | Command-line fragment |
---|---|
Configure | cmake -G <generator> -S <path> -B <path> ... |
Build | cmake --build <path> --target <name> --config <name> ... |
Test | ctest --test-dir <path> --build-config <name> ... |
Install | cmake --install <path> --prefix <path> --config <name> |
Package | cpack -G <generator> --config <path> |
workflow of dealing with CMake projects. When codifying the invocations as presets, these steps become:
Step | Command-line fragment |
---|---|
Configure | cmake --preset <name> |
Build | cmake --build <path> --preset <name> |
Test | ctest --preset <name> |
Install | cmake --build <path> --preset <name> |
Package | cpack --preset <name> |
To allow invoking the entire process as a oneliner, CMake presets allows assembling this workflow.
Step | Command-line fragment |
---|---|
Workflow | cmake --workflow <name> |
This way CI and developers using any IDE can invoke the entire process as a one-liner or invoke individual steps from any command-line or using integrations provided by IDEs. For more on presets, refer to the relevant docs.
Should users have an environment which differs from that of CI, or they simply want to develop using consciously chosen
differing CMake invocations (lacking resources to build all GPU device targets regularly in their dev loop, using
out-of-band compilers which don't build cleanly with
COMPILE_WARNING_AS_ERROR
, etc.), they
can define their own presets. CMake will look for a CMakePresets.json
file in the project root, which is owned by the
project, should be commited to version control and should reflect invocations used in CI/CD known to work in
environments used in CI/CD, and it will be merged with another file next to it CMakeUserPrests.json
. This latter
should regularly be ignored by version control. It also implicitly includes the project's presets, so if only a few
deltas are needed, the majority of the JSON need not be copied over.
In that spirit, projects should:
- not include files in
CMakePresets.json
which are absent from the repo itself. - try to define your presets using multiple groups of orthogonal settings (
hidden: true
) presets which the user can individually override without having to rehash all the commonality they agree with.
CMake presets currently have a few shortcomings:
- It lacks dedicated install presets. This is only a minor nuisance, as it can nearly fully be emulated using a build
preset invoking the
install
target. The only feature lost is the ability to install multiple configurations to possibly multiple prefixes from a single configuration. Kitware issue - Currently there's no way to mitigate the combinatorial explosion of entries depending on the number of supported configurations, builds, tests, etc. The size of the presets JSON grows rapidly when wanting to support multiple platforms, configurations that manifest as necessarily different CMake invocations. Kitware issue
- MSVC on Windows traditionally (with Clang following suit) require some environment variables be set in order to function properly (find their own STL, libs, etc.). These variables are set by shell scripts / PowerShell modules, with results being referred to as developer command-prompts / shells. Presets do capture the ability to set env vars which the build or tests may require, however Kitware seems unresponsive in wanting to tie the knot in capturing env requirements of toolchains using Makefile generators. Kitware issue In all honesty, not that they should. Toolchain vendors could expose their toolchains in preset-friendly ways to CMake too. Somewhat related CMake Tools issue