There’s a Dutch phrase, voortschrijdend inzicht, which can be used to describe new insights and continuous improvement in something. In social media terms, perhaps TIL comes close. Let’s talk about an under-illuminated, yet useful CMake feature. Script mode!

Note that “voortschrijdend inzicht” is also used as political jargon for “it was wrong, we knew it was wrong, we did it anyway and we’re going to pretend we’re not responsible anymore”. Today, I will use the “huh, this can be better” meaning.

The most common way to use CMake – at least in the UNIX world, for KDE things – it to use it in generate mode. The typical invocation cmake .. (from a build directory) will generate files for another buildsysystem, typically make or ninja. Then you run that tool for the actual build.

CMake has more options though, for how it’s run. You can generate, or build, or install, or ..

When building KDE, we use install mode for its obvious purpose, and sometimes command mode, e.g. to create a symlink – so we can avoid calling out to shell scripts or incompatible tooling (e.g. md5 versus md5sum).

Script Mode

The manual says

Process the given cmake file as a script written in the CMake language. No configure or generate step is performed and the cache is not modified. If variables are defined using -D, this must be done before the -P argument.

It’s a bit terse, but the cmake-language page describes more. Commands that define things that only make sense for generate, build or install-time are not allowed: so no add_library(), no project(), but many other things are possible.

Calamares Versioning

Let’s make a little detour through Calamares – which is where my case of voortschrijdend inzicht happened. I define the version of Calamares in the project() call, like so:

project( CALAMARES VERSION 3.2.39 LANGUAGES C CXX )

This is convenient, because afterwards I have a version variable inside the CMake bits with the version number; there are major, minor and patch-level variables as well. Like (probably) many projects, Calamares includes some build-version-stamping for debug builds, so it adds in a date and a git hash to debug builds. This is particularly useful when downstream distro’s report a problem, since all the version information is right there.

I put together a custom target that calls CMake in command mode to print out the version. It looks like this:

add_custom_target( show-version
    ${CMAKE_COMMAND} -E echo ${CALAMARES_VERSION_SHORT}
    USES_TERMINAL
)

To print out the version number, there’s this huge roundabout route that I’ve taken: generate a build system (e.g. for make), then run make to run CMake to print out a string that is passed to it.

I was looking at a PKGBUILD script from a Manjaro derivative – that distro was reporting a build problem – and found that

  • the distro wasn’t using the show-version target anyway, and
  • having to go through the generate step first is a pain in the butt; ideally you could ask the extracted tarball what version it is.

From a git clone, git describe does a pretty good job (because I systematically tag releases), so v3.2.38.1-67-g64f9a2df2 would be acceptable. But it’s nicest to be able to check what the Calamares source itself thinks the version is – e.g., what is passed to the project() call.

Script Mode (2)

Someone must have mentioned script mode to me, but right now I can’t remember who. That whole roundabout route could be handled at CMake-level with a single command:

message( "${CALAMARES_VERSION}" )

so why not use that instead?

It turns out project() isn’t a CMake command that you can use in script mode. But you can set variables, so I introduced a CALAMARES_VERSION variable, set to the current release. Years ago I already had such a variable, but then moved the version-setting to project when CMake 3.0 became a requirement. So voortschrijdend inzicht can also go in a circle!

When CMake runs in script mode, the variable CMAKE_SCRIPT_MODE_FILE is set; outside of script mode, it isn’t (unless you’re messing with the cache or command-line arguments, in which case you should be ashamed of yourself).

In script mode, CMAKE_SOURCE_DIR is set to the current directory, not the directory with the top-level CMakeLists.txt file (naturally: there’s no generation or build going on, so there need not be such a file!). I wrestled Teo Mrnjavac’s original date-and-git-stamping CMake code (written for Calamares in 2015, for CMake 2.8) into a function and stuffed it into a separate file. It takes a version string and extends it, placing the output value into a variable.

The top of my CMakeLists.txt before the project() call now looks something like this:

cmake_minimum_required( VERSION 3.3 FATAL_ERROR )
set( CALAMARES_VERSION 3.2.40 )
if ( CMAKE_SCRIPT_MODE_FILE )
    include( ${CMAKE_CURRENT_LIST_DIR}/CMakeModules/ExtendedVersion.cmake )
    set( CMAKE_SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR} )
    extend_version( ${CALAMARES_VERSION} extended_version )
    message( "${extended_version}" )
    return()
endif()

The upshot is that:

  • I can ask CMake for version information from the command-line, without going through generate or build steps at all: cmake -P CMakeLists.txt does the right thing.
  • The version information is still in one place, at the top of CMakeLists.txt. No longer as a literal in the project() call, which is the first place I would look, but at the end of a variable-chase of length one (1). That, I can handle.

Any consumer that needs versioning information, can get it right from CMake with no external tools. That includes PKGBUILD, so my next trick is to provide some voortschrijdend inzicht to that distro, to improve their package builds.