I have encountered the following surprising (to me) patterns that enough projects follow that I thought I should ask about them:
- Some projects expect you to run the equivalent of
cmake . && cmake --build . for every incremental build (ignoring single/multi-config differences).
- Some projects require running the configure step twice, with the same arguments.
Both of these patterns smell like bugs in the CMake files to me. The closest thing I’ve been able to find that validates this intuition is the following text from the User Interaction Guide:
The CMake files as a primary artifact should completely specify the buildsystem and there should be no reason to populate properties manually in an IDE for example after generating the buildsystem.
If I have to manually reconfigure CMake, then it seems the CMake files have not “completely specif[ied] the buildsystem”, as this text recommends.
Are either of these scenarios endorsed? Is there any document that lays out what users ought to expect from a random CMake build? My impression is that the build tool should know when to call CMake. Is this a reasonable expectation in general?
I can think of at least one corner case where you may want to explicitly run
cmake . before
cmake --build .. With the Xcode generator, targets have a dependency on the
ZERO_CHECK target, which is what re-runs
cmake for you automatically if something changed. But the problem is that the rest of the build in that run still uses the old details from before
cmake . gets run. If you build again, that rebuilds anything whose details changed, but explicitly re-running
cmake . first if you know something about the project changed will be more robust and probably avoid more unnecessary rebuilds. I don’t recall if Visual Studio has a similar problem or not.
Outside of that, a well-formed project shouldn’t require the user to manually run
cmake . themselves before a build. Indeed, CMake 3.20 even added the
CONFIGURE_HANDLED_BY_BUILD keyword to
ExternalProject_Add() which has the effect of giving the build step the responsibility of re-running the configure step if needed.
It’s sadly all too easy to create a project that doesn’t meet the “well-formed” requirement though. If a project uses
file(WRITE) to create a source file or some other file that other things depend on, you have to look at what could change the contents of that file. Have you told CMake about those things such that if the contents could change, CMake knows it has to re-run
cmake? A relatively common pattern is to use
file(READ) to read a file, do some processing on that content and then write out the modified content using
file(WRITE). If you forget to add the file that was read to the
CMAKE_CONFIGURE_DEPENDS directory property, CMake won’t know about that dependency and won’t re-run
cmake if the read-in file changes. If you instead use
file(GENERATE ... INPUT ...) to achieve your goal, either of those two commands will record the dependency (and have the added advantage of not updating the timestamp of the destination file if the contents don’t actually change).
As for needing to run the configure step twice, I personally consider that a bug in the project. It implies logic that changes its outcome on subsequent runs. A couple of scenarios come to mind for how this might occur:
The project has incorrect ordering of its dependencies or options. For example, it may contain conditional logic that checks the value of a cache option that hasn’t been defined yet. It makes a certain decision on the first run in the absence of that cache option. On the second and subsequent run, the cache option now exists and it can result in a different condition for the logic that was relying on it. Defining dependent cache options in the wrong order is an example of this. I’m not sure if the
CMakeDependentOption module helps avoid or contributes to this situation.
Setting a cache variable’s value can remove a normal variable of the same name from the current scope in a few specific scenarios. Some cases where this occurs are if the cache variable doesn’t yet exist or it has been defined without any type. On the first run, the regular variable is removed and the cache variable is created/updated. In the next run, the cache variable won’t be touched because it has a value and a type, so any regular non-cache variable will be left alone too. Because a non-cache variable takes precedence over a cache variable of the same name, this can result in different behavior between the two runs if the non-cache and cache variables have different values.
I think VS now reloads the project if it detects changes due to a rerun of CMake before it continues with the rest of the build. IIRC, VS 2012 was the first to do this out-of-the-box. There’s a plugin around for VS 2010 and older.
I’ve definitely run into situations where it is unusable, though these are odd patterns which involve running the same code twice with different variables to change the APIs that they call. But yes, not ordering your option dependencies in order can definitely cause weird “well it works on CI, but not locally?” issues.
Alas, this is a long-standing design consequence. There’s a silent-by-default policy (CMP0102) for when this happens in some cases (via
mark_as_advanced when no such cache variable exists but it is in the local scope). Any other such causes should be considered for policy changes as well IMO.
I’ve been burned by this before, actually. It might have been the single hardest-to-diagnose bug in any CMake code I’ve written. Would never have found it without Craig’s book.
Yeah, I ended up spelunking deep into CMake code to find that behavior. If you ever run into it again, I highly suggest filing issue(s) with reproducer cases so that we can get that behavior out of the codebase (even if “just” via a policy).
Here you go! I actually put together a reproducer a while ago while explaining the behavior to a colleague. https://gitlab.kitware.com/cmake/cmake/-/issues/22038
@craig.scott thank you very much for the very detailed explanation. I actually bumped into this very issue in my project (using
file(WRITE...) with content depending on other files) and noticed that the build was not regenerating the file properly so I would manually have to invoke
cmake again. I had no clue how to fix it and your post clearly will help me do the right thing!