Search code examples
templatescmakedependenciespreprocessor

Why is there a dependency cycle on templated cmake project?


I have a project when all source files are preprocessed with custom template engine. I would want all template files to be generated first, then compiled, as separate stages, so that I do not get missing #includes later. In my mind that's simple - I create a target that depends on all generated files and add_dependency between the library and generated target.

Take the following structure:

CMakeLists
src/CMakelists.txt
src/a.c

And we have:

mkdir -p src
touch src/a.c

cat >CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.11)
project(test)

add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generatedtimestamp
  COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/generatedtimestamp
)
add_custom_target(generatedtarget DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generatedtimestamp)

set(GENERATED "" CACHE INTERNAL "") # bin to collect all generated files
function(template_generate A_SOURCE A_OUTPUT)
  add_custom_command(
    OUTPUT ${A_OUTPUT}
    DEPENDS ${A_SOURCE}
    COMMAND ${CMAKE_COMMAND} -E copy ${A_SOURCE} ${A_OUTPUT}
  )
  get_filename_component(A_OUTPUT ${A_OUTPUT} ABSOLUTE)
  list(APPEND GENERATED "${A_OUTPUT}")
  set(GENERATED "${GENERATED}" CACHE INTERNAL "")
endfunction()

add_subdirectory(src)
message(STATUS ${GENERATED})
add_custom_command(
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generatedtimestamp
    DEPENDS ${GENERATED}
    APPEND
)
EOF

cat >src/CMakeLists.txt <<'EOF'
template_generate(
  ${CMAKE_CURRENT_SOURCE_DIR}/a.c
  ${CMAKE_CURRENT_BINARY_DIR}/gen/a.c
)
add_executable(testtarget
  ${CMAKE_CURRENT_BINARY_DIR}/gen/a.c
)
add_dependencies(testtarget generatedtarget)
EOF

However this results in:

+ cmake -H. -B_build
-- Configuring done
-- Generating done
-- Build files have been written to: /dev/shm/.1000.home.tmp.dir/_build
+ cmake --build ./_build --parallel --verbose
ninja: error: dependency cycle: src/gen/a.c -> generatedtarget -> CMakeFiles/generatedtarget -> generatedtimestamp -> src/gen/a.c

I do not see that dependency cycle. In my mind, the dependency is linear (left <- right represents that "right depends on left"):

testtarget -> generatedtarget -> generatedtimestamp -> gen/a.c -> src/a.c
            \-------------------------->------------/

Why is there a dependency cycle?

... generatedtimestamp -> src/gen/a.c - why does gen/a.c depend on generatedtimestamp?


Solution

  • Why is there a dependency cycle?

    I'm going to name the participants in the cycle like so:

    • (A) file: src/a.c
    • (B) file: <bin>/src/gen/a.c
    • (C) file: <bin>/generatedtimestamp
    • (D) target: testtarget
    • (E) target: generatedtarget

    The error message in your question corresponds to the cycle (note that CMakeFiles/generatedtarget is just an implementation detail):

    (B) -> (E) -> (C)
       \--<---<--*
    

    Now here's what happens:

    1. With the first two add_custom_command and add_custom_target, you establish (E) -> (C).
    2. Inside src/CMakeLists.txt, the first lines of template_generate establish (B) -> (A).
    3. Then add_executable establishes (D) -> (B).
    4. The add_dependencies function then explicitly establishes (D) -> (E).
      1. It also establishes that (B) -> (E). This is the problem. add_dependencies forces the dependent to be completely built before the given target.
    5. The add_custom_command(APPEND) call then establishes (C) -> (B)

    So the whole graph is:

              /--<---<--*
    (D) -> (E) -> (C) -> (B) -> (A)
       *-->--->--->-->--/
    

    and you can see the original cycle in this log.