cmakecmake-language

Optional dependencies for add_custom_command


If I include non-existent files in DEPENDS section of add_custom_command, it fails. Is there a way to specify the dependency may not exist, but if it's created the command should be rerun?

Specifically, I am using Coco/R, which requires Parser.frame and Scanner.frame file to exist either in the same directory as the grammar file or one specified by a command line argument, and Copyright.frame which can exist or not. So I do this:

foreach(FRAME_DIR IN ITEMS ${GRAMMAR_DIR} ${ARG_FRAME_DIR})
    foreach(FRAME_FILE_NAME IN ITEMS Copyright Scanner Parser)
        set(FULL_FRAME_FILE ${FRAME_DIR}/${FRAME_FILE_NAME}.frame)
        if(EXISTS ${FULL_FRAME_FILE})
            list(APPEND FRAME_FILES ${FULL_FRAME_FILE})
        endif()
    endforeach()
endforeach()

...

set(GEN_SOURCES ${ARG_OUTPUT_DIR}/Scanner.h ${ARG_OUTPUT_DIR}/Parser.h ${ARG_OUTPUT_DIR}/Scanner.cpp ${ARG_OUTPUT_DIR}/Parser.cpp)

add_custom_command(OUTPUT ${GEN_SOURCES}
    COMMAND ${COCOCPP} ${COCOCPP_ARGS}
    MAIN_DEPENDENCY ${ARG_GRAMMAR}
    DEPENDS ${FRAME_FILES}
    VERBATIM)

add_library(${ARG_TARGET} ${GEN_SOURCES})

If I comment out if inside foreach, I get

ninja: error: '../verification/debug/config/Copyright.frame', needed by 'verification/gen/config/Parser.h', missing and no known rule to make it

at build time. With this if, the files are generated but if I create Copyright.frame later, of course the build program will not know about it.


Solution

  • For common generators, you can use DEPFILE to generate Make-ish dependency just like gcc -MT -MD does with include files. You have to write a wrapper that will detect if the file exists or if not, and then generate the file.

    The depfile looks generally like this:

    output: source1 source2
    

    And output has to be relative to CMAKE_BINARY_DIR (this took me sooooooo long to find out) and source1 let be absolute. And you have to also escape spaces and tabs and newlines and # (!) in names. Generally:

    # CMakeLists.txt
    # Example for one file
    set(output ${ARG_OUTPUT_DIR}/Scanner.h)
    # Relative to BINARY_DIR!
    file(RELATIVE_PATH outputbinrela "${CMAKE_BINARY_DIR}" "${output}")
    set(depfile "${CMAKE_CURRENT_BINARY_DIR}/Scanner.h.d" ABSOLUTE)
    add_custom_command(
       OUTPUT ${someoutput}
       DEPFILE ${depfile}
       # TODO: write it in cmake, to be portable
       COMMAND sh ./thescript.sh
            "${outputbinrela}" "${depfile}" "${input}"
            ${FRAME_DIR}/Copyright.frame
       DEPENDS ${input}
       WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
       VERBATIM
    )
    

    # ./thescript.sh
    outputbinrela="$1"
    depfile="$2"
    input="$3"
    copyright="$4"
    
    # Check if the file exists - if it does, add it to depfile
    if [[ -e "$copyright" ]]; then
        copyright=$(readlink -f "$copyright") # absolute!
        echo "$outputbinrela: $copyright" > "$depfile"
    else
        echo "$outputbinrela:" > "$depfile"
    fi
    
    # Run the command to generate output.
    yourcommand "$copyright" > "$input"
    

    I once wrote such system for m4 preprocessor, you can check out this script that runs m4 preprocessor and grabs m4 included files and generates depfile, and this script that calls add_custom_command( ${CMAKE_COMMAND} -Pthe_other_script.cmake DEPFILE ...).

    Still, if you add Copyright.frame file, cmake will not pick it up on the first run, but if you modify something else, then depfile will be regenerated and the depdency will be picked. Or you could just re-generate depfile for example on each build.