Define a pre-build command without creating a new target

After reading Professional CMake (almost) back to back, I’m once again asking for your support :upside_down_face:
I’m struggling to define a fixed pre-build command for a target. I need to generate a header with the current time at every build, and I really don’t want to define a custom target just to do that. Targets are a big deal, they’re global, and need work and attention to be reliable.
So, out of the three main options to define a custom command, as the book suggests, only add_custom_command meets the need.
From the documentation:

If DEPENDS is not specified, the command will run whenever the OUTPUT is missing; if the command does not actually create the OUTPUT , the rule will always run.

The idea was to create a custom command with two OUTPUTs: the generated header file, which establishes a dependency (and precedence) with the related target (the “light”, automatic dependency is enough in this case), and a dummy one, which causes the command to always trigger.

add_custom_command(OUTPUT "foo" "${VisualT_BINARY_DIR}/src/buildDate.h"
                   COMMAND ${CMAKE_COMMAND} -P "${VisualT_BINARY_DIR}/cmake/ConfigureBuildDate.cmake"
                   COMMENT "generating build date header"
                   )
add_library(VisualT_library SHARED "${private_headers}" "${public_headers}" "${sources}") #buildDate.h is contained in "private_headers"

It doesn’t work. Most of the times. Sometime it does. There’s definitely something wrong with the outputs because they seem to be order-sensitive. I’ve never managed to get it working with the outputs switched. I’ve never read anything about the output order ?
What I have now is:

====================[ Build | VisualT_library | Debug WSL ]=====================
/usr/bin/cmake --build "/mnt/d/Users/Nemo/OneDrive - Universita degli Studi di Udine/Shared/c/VisualT/cmake-build-debug-wsl" --target VisualT_library -- -j 12
[ 33%] generating build date header
Scanning dependencies of target VisualT_library
[ 33%] generating build date header
[ 66%] Building C object src/CMakeFiles/VisualT_library.dir/visualt.c.o
[100%] Linking C shared library libvisualt.so
[100%] Built target VisualT_library

Build finished

It’s currently working, but it looks like it’s generating the header twice, which is wrong.
What usually happens is that it says “generating build date header” but no file is generated, and the compilation fails due to a missing header. In the case the library was already up to date, it does the same, but no errors are displayed, as no compilation occurs (and that’s still wrong, since the header should have changed).

With the outputs switched:

CMake output
/usr/bin/cmake --build "/mnt/d/Users/Nemo/OneDrive - Universita degli Studi di Udine/Shared/c/VisualT/cmake-build-debug-wsl" --target VisualT_library -- -j 12
[ 33%] generating build date header
Scanning dependencies of target VisualT_library
[ 66%] Building C object src/CMakeFiles/VisualT_library.dir/visualt.c.o
/mnt/d/Users/Nemo/OneDrive - Universita degli Studi di Udine/Shared/c/VisualT/src/visualt.c:3:10: fatal error: buildDate.h: No such file or directory
 #include "buildDate.h"
          ^~~~~~~~~~~~~
compilation terminated.
src/CMakeFiles/VisualT_library.dir/build.make:89: recipe for target 'src/CMakeFiles/VisualT_library.dir/visualt.c.o' failed
make[3]: *** [src/CMakeFiles/VisualT_library.dir/visualt.c.o] Error 1
CMakeFiles/Makefile2:199: recipe for target 'src/CMakeFiles/VisualT_library.dir/all' failed
make[2]: *** [src/CMakeFiles/VisualT_library.dir/all] Error 2
CMakeFiles/Makefile2:206: recipe for target 'src/CMakeFiles/VisualT_library.dir/rule' failed
make[1]: *** [src/CMakeFiles/VisualT_library.dir/rule] Error 2
Makefile:149: recipe for target 'VisualT_library' failed
make: *** [VisualT_library] Error 2

Your idea is not wrong. The dummy output makes sure the custom command is always executed and the generated header file is then part of the target sources.

I cannot immediately spot the problem without a working example. The source is probably build in parallel with its generation.

Before moving on with your question - have you had a look at chapter 19.3 Source Control Commits? It implements the same concept, but differently.

I’ve made a minimal reproducible example: dummy.zip (1.0 KB)
The three behaviours I get are:

with add_custom_command(OUTPUT "${dummy_BINARY_DIR}/date.h" "foo"

output
====================[ Build | dummy | Debug ]===================================
/usr/bin/cmake --build /mnt/d/Users/Nemo/Desktop/dummy/cmake-build-debug --target dummy -- -j 12
Scanning dependencies of target dummy
[ 33%] Building C object CMakeFiles/dummy.dir/main.c.o
/mnt/d/Users/Nemo/Desktop/dummy/main.c:2:10: fatal error: date.h: No such file or directory
 #include "date.h"
          ^~~~~~~~
compilation terminated.
CMakeFiles/dummy.dir/build.make:88: recipe for target 'CMakeFiles/dummy.dir/main.c.o' failed
make[3]: *** [CMakeFiles/dummy.dir/main.c.o] Error 1
CMakeFiles/Makefile2:94: recipe for target 'CMakeFiles/dummy.dir/all' failed
make[2]: *** [CMakeFiles/dummy.dir/all] Error 2
CMakeFiles/Makefile2:101: recipe for target 'CMakeFiles/dummy.dir/rule' failed
make[1]: *** [CMakeFiles/dummy.dir/rule] Error 2
Makefile:137: recipe for target 'dummy' failed
make: *** [dummy] Error 2

with add_custom_command(OUTPUT "foo" "${dummy_BINARY_DIR}/date.h"

  • case 1: the header change doesn’t trigger a recompilation fixed by clearing the CMake cache
output
====================[ Build | dummy | Debug ]===================================
/usr/bin/cmake --build /mnt/d/Users/Nemo/Desktop/dummy/cmake-build-debug --target dummy -- -j 12
[ 33%] generating build date header
[100%] Built target dummy

Build finished
  • case 2: it works, but two “generating” messages are print, which is suspicious
output
====================[ Build | dummy | Debug ]===================================
/usr/bin/cmake --build /mnt/d/Users/Nemo/Desktop/dummy/cmake-build-debug --target dummy -- -j 12
[ 33%] generating build date header
Scanning dependencies of target dummy
[ 33%] generating build date header
[ 66%] Building C object CMakeFiles/dummy.dir/main.c.o
[100%] Linking C executable dummy
[100%] Built target dummy

Build finished

It implements the same concept, but he runs it at configure time, with execute_process(), so he doesn’t really have to handle dependencies.

nope, it runs at build time. Try it! :wink:

personally I wouldn’t use configure_file twice, that’s very confusing. Try it in one go. You don’t need ConfigureDate.cmake.in. You can set time and paths directly without the need of execute_process or custom_command.

You are right it runs at configuration time. There is no clean way to generate sources at build time, it is not a good idea anyway since it always forces your build out of date.

Anyway, here is a suggestion:

add_custom_command( TARGET dummy
	PRE_BUILD
	COMMAND ${CMAKE_COMMAND} -P ${dummy_SOURCE_DIR}/ConfigureDate.cmake
)

This adds an extra build step every time dummy target is build. The command runs CMake in script mode to generate the header file, which is then used to build dummy.

That’s exactly what I’d need, but apparently PRE_BUILD is supported by the Visual Studio generator only. Even The Book states:

POST_BUILD tasks are relatively common, but PRE_LINK and PRE_BUILD are rarely needed since they can usually be avoided by using the OUTPUT form of add_custom_command() instead (see the next section).

Well they could be avoided if add_custom_command() worked as intended.

hmm, yes I can see that PRE_BUILD acts like PRE_LINK when using GNU Makefiles generator. The binary contains the time from the previous build :grimacing:

Note that building the example project using the Ninja generator doesn’t show this particular problem with the header file being created twice.

You can see the issue running “make VERBOSE=1” where all the dependency data is invalidated and the custom commands are rerun. Looking at the Makefiles it seems that the dependency file and the target both depend on the output of the custom commands.

This might be an additional reason why custom targets are suggested in this kind of use case. It’ll also should avoid the parallel make error you mentioned (but I wasn’t able to reproduce that problem).

1 Like
[ 33%] Generating date.h, foo
cmake -P /tmp/test/ConfigureDate.cmake
cd /tmp/test/build && cmake -E cmake_depends "Unix Makefiles" /tmp/test /tmp/test /tmp/test/build /tmp/test/build /tmp/test/build/CMakeFiles/dummy.dir/DependInfo.cmake --color=
Deleting primary custom command output "/tmp/test/build/date.h" because another output "/tmp/test/build/foo" does not exist.
Dependee "/tmp/test/build/CMakeFiles/dummy.dir/DependInfo.cmake" is newer than depender "/tmp/test/build/CMakeFiles/dummy.dir/depend.internal".
Dependee "/tmp/test/build/CMakeFiles/CMakeDirectoryInformation.cmake" is newer than depender "/tmp/test/build/CMakeFiles/dummy.dir/depend.internal".
Scanning dependencies of target dummy

Deleting primary custom command output -> I cannot find any explanation on the internet as of why the file has been deleted, and why it hasn’t been created again before building the c object.

The documentation states that SYMBOLIC property must be set for foo because the file does not exist but this didn’t make any difference.

Dependee messages are normal and generally harmless.

1 Like

There’s definitely something weird going on with add_custom_command()

So is add_custom_command()'s behaviour the expected one for the Makefile generator?
Is it a bug? Is it something else?

1 Like

I was hoping to find time to investigate this one, but that is looking unlikely. If you can provide a minimal example with steps to reproduce the problem (I think the elements are mostly here in this discussion already), please report this as a bug in the issue tracker. Hopefully someone else can then pick it up from there, or explain why the current behavior is like it is (seems a bug from a quick scan through the discussion so far though).

https://gitlab.kitware.com/cmake/cmake/-/issues/

1 Like

The problem was explained in the issue:
https://gitlab.kitware.com/cmake/cmake/-/issues/21061

It currently can’t be done, a custom target must be used