cmake

How in a CMake project automatically run a custom script during a build whenever files in a folder have changed?


I have a CMake project that generates build artifacts based on some image files using GLOB like so:

file(GLOB IN_FILES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/images/*.png)
add_custom_command(
    OUTPUT ${OUT_FILE}
    COMMAND command-to-generate-build-artifact.sh
    DEPENDS ${IN_FILES}
    COMMENT "Generating stuff"
)
add_custom_target(generate_image_data ALL DEPENDS ${OUT_FILE})
add_dependencies(project generate_image_data)

The annoying thing about this is that if I add, remove, or rename any of these images, I need to remember to manually reconfigure CMake, and I pretty much always forget to do this, meaning my next build will break and it'll take me a moment to remember why.

I want to automatically re-run the command any time the list of files changes, as well as when the modification time of the files is newer than generated.h.

Note that I'm using CONFIGURE_DEPENDS, and it still doesn't work properly. If I change the contents of images in some way, the configure step does print [build] -- GLOB mismatch! but it still doesn't re-run the custom command I have set up.

Is there some way I can either change my CMakeLists.txt file so the command itself will be re-run on such changes? I'm using Visual Studio Code for this project, with Unix Makefiles generator.

Here is a complete example that demonstrates the issue:

This will generate a C header file named generated.h in the build directory. This header simply defines a macro whose value is a C string comma separated list of the contents of the images directory. The executable then prints the value of that string. The idea is that any time the contents of images is changed, building will regenerate generated.h and cause the executable to be rebuilt.

The project has the following structure:

TestProj
│  CMakeLists.txt
│  generate-build-artifact.sh
│  main.c
│  
└──images
   │  foo.png
   │  bar.png

Note that it's not important what contents are in either foo.png or bar.png; they can be empty files.

Here are the file contents:

CMakeLists.txt:

cmake_minimum_required(VERSION 3.12)
project(TestProj VERSION 1.0 LANGUAGES C)

add_executable(TestProj main.c)
target_include_directories(TestProj PRIVATE ${CMAKE_CURRENT_BINARY_DIR})

set(OUT_FILE ${CMAKE_CURRENT_BINARY_DIR}/generated.h)
file(GLOB IN_FILES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/images/*.png)
add_custom_command(
    OUTPUT ${OUT_FILE}
    COMMAND ${CMAKE_SOURCE_DIR}/generate-build-artifact.sh ${OUT_FILE}
    DEPENDS ${IN_FILES}
    COMMENT "Generating stuff"
)
add_custom_target(generate_image_data ALL DEPENDS ${OUT_FILE})
add_dependencies(TestProj generate_image_data)

generate-build-artifact.sh:

#!/bin/bash
OUTPUT_FILE="$1"
IMAGES_DIR="$(dirname "$0")/images"
PNG_FILES=$(find "$IMAGES_DIR" -maxdepth 1 -type f -name "*.png" -exec basename {} \; | paste -sd, -)
echo "#define GENERATED_VAL \"$PNG_FILES\"" > "$OUTPUT_FILE"
echo "Generated: $OUTPUT_FILE"

main.c:

#include <stdio.h>
#include "generated.h"

int main(int argc, char *argv[])
{
    printf("generated: %s", GENERATED_VAL);
    return 0;
}

Here is the output from running cmake for the first time from within Visual Studio Code, using the "CMake: Configure" command:

[main] Configuring project: testproj 
[proc] Executing command: /opt/local/bin/cmake -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE --no-warn-unused-cli -S/path/to/testproj -B/path/to/testproj/build
[cmake] Not searching for unused variables given on the command line.
[cmake] -- The C compiler identification is AppleClang 14.0.3.14030022
[cmake] -- Detecting C compiler ABI info
[cmake] -- Detecting C compiler ABI info - done
[cmake] -- Check for working C compiler: /Applications/Xcode-14.3.1.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
[cmake] -- Detecting C compile features
[cmake] -- Detecting C compile features - done
[cmake] -- Configuring done (0.4s)
[cmake] -- Generating done (0.0s)
[cmake] -- Build files have been written to: /path/to/testproj/build

And the first time building using Visual Studio Code's "CMake: Build" command:

[main] Building folder: /path/to/testproj/build 
[build] Starting build
[proc] Executing command: /opt/local/bin/cmake --build /path/to/testproj/build --config Debug --target all -j 18 --
[build] [ 33%] Generating stuff
[build] Generated: /path/to/testproj/build/generated.h
[build] [ 33%] Built target generate_image_data
[build] [ 66%] Building C object CMakeFiles/TestProj.dir/main.c.o
[build] [100%] Linking C executable TestProj
[build] [100%] Built target TestProj
[driver] Build completed: 00:00:00.503
[build] Build finished with exit code 0

Running the generated executable correctly prints: generated: foo.png,bar.png

If I change something about the images directory, such as renaming bar.png to aaaa.png, or deleting bar.png, and then I rebuild, here is what gets printed:

[main] Building folder: /path/to/testproj/build 
[build] Starting build
[proc] Executing command: /opt/local/bin/cmake --build /path/to/testproj/build --config Debug --target all -j 18 --
[build] -- GLOB mismatch!
[build] -- Configuring done (0.0s)
[build] -- Generating done (0.0s)
[build] -- Build files have been written to: /path/to/testproj/build
[build] [ 33%] Built target generate_image_data
[build] [100%] Built target TestProj
[driver] Build completed: 00:00:00.324
[build] Build finished with exit code 0

Note that it prints GLOB mismatch! indicating that it has detected the images directory has changed. However it does not print Generating stuff or [build] Generated: /path/to/testproj/build/generated.h. The executable is not rebuilt, and running it will print the old contents of the images directory.

Update

Thanks to the help I've received so far, I've discovered that the issue revolves around the fact that the logic CMake is generating in my Makefile is only using the timestamps of the files collected with file(GLOB IN_FILES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/images/*.png) in order to determine when to re-run the custom command. So if I add a new file to the folder whose timestamp is newer than the generated file, it will regenerate generated.h.

However, for my use case I need it to re-run the command any time the list of files changes, as well as when the modification time of the files is newer than generated. For example, adding a file with an older timestamp, renaming a file, or deleting a file needs to re-trigger the command.


Solution

  • I need it to re-run the command any time the list of files changes, as well as when the modification time of the files is newer than generated.h.

    By using option DEPENDS ${IN_FILES} you have solved the second conditional command re-run. For solve the first conditional - "on the list of files changes" - you need to express that condition in a way that Make understands. Because Make understands only files and their timestamps as condition, then you may just create a file which contains the list of files located in the directory.

    That creation can be performed at configuration stage (in CMakeLists.txt): thanks to file(GLOB ... CONFIGURE_DEPENDS) expression CMake automatically re-runs the configuration when content of the directory has been changed.

    ...
    file(GLOB IN_FILES CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/images/*.png)
    
    # File where the list of files will be stored.
    #
    # Having such file allows us to tell Make to re-run the command upon the list is changed.
    set(list_file ${CMAKE_CURRENT_BINARY_DIR}/images_list.txt)
    
    # While CMake automatically re-runs configuration whenever the list is changed,
    # the configuration may be re-run in other cases too.
    # We do not want to change the file's timestamp when the list is not changed.
    if (EXISTS "${list_file}")
      # The file has already existed
      # Need to compare it current content with the one we want to write into it
      file(READ "${list_file}" current_file_content)
      if (NOT current_file_content STREQUAL "${IN_FILES}")
        # List is actually changed.
        # Update the file with the new list.
        file(WRITE "${list_file}" "${IN_FILES}")
      endif()
    else()
      # The file has not been created yet
    
      # Create the file and write the list to it.
      file(WRITE "${list_file}" "${IN_FILES}")
    endif()
    
    add_custom_command(
        OUTPUT ${OUT_FILE}
        COMMAND ${CMAKE_SOURCE_DIR}/generate-build-artifact.sh ${OUT_FILE}
        # Make the command to depends both from the files list
        # and from content of these files.
        DEPENDS ${list_file} ${IN_FILES}
        COMMENT "Generating stuff"
    )
    add_custom_target(generate_image_data ALL DEPENDS ${OUT_FILE})
    add_dependencies(TestProj generate_image_data)