Execute git describe in custom_target - cmake

I would like to write output of git describe as a string to a file, so I can embed the information in my binary (C++). This has to work across platforms.
The best I can yet come up with was:
add_custom_target( SubmarineGitVersion
COMMAND cmd /c "${CMAKE_EXECUTABLE}" echo czstring GIT_VERSION = STRINGIFY\( > "${CMAKE_CURRENT_BINARY_DIR}/GitVersion.hpp"
COMMAND cmd /c "${GIT_EXECUTABLE}" describe --tags --always >> "${CMAKE_CURRENT_BINARY_DIR}/GitVersion.hpp"
COMMAND cmd /c "${CMAKE_EXECUTABLE}" echo \) >> "${CMAKE_CURRENT_BINARY_DIR}/GitVersion.hpp"
)
This roughly works on Windows (is missing a ; at the end):
czstring GIT_VERSION = STRINGIFY(
tag-343434
)
Is there any better/more cross-platform way of doing this?

Common way for create "version" files is using configure_file command. Such way file will be created at configure stage:
GitVersion.hpp.in:
czstring GIT_VERSION = STRINGIFY(
${GIT_REPO_VERSION}
)
CMakeLists.txt:
# Store version into variable
execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --always
OUTPUT_VARIABLE GIT_REPO_VERSION)
# The variable will be used when file is configured
configure_file("GitVersion.hpp.in" "GitVersion.hpp")
If you want to create version file on build stage, move above cmake commands into some file, and execute this file in CMake script mode:
generate_version.cmake:
# Git executable is extracted from parameters.
execute_process(COMMAND ${GIT_EXECUTABLE} describe --tags --always
OUTPUT_VARIABLE GIT_REPO_VERSION)
# Input and output files are extracted from parameters.
configure_file(${INPUT_FILE} ${OUTPUT_FILE})
CMakeLists.txt:
add_custom_target( SubmarineGitVersion
COMMAND ${CMAKE_COMMAND}
-D GIT_EXECUTABLE=${GIT_EXECUTABLE}
-D INPUT_FILE=${CMAKE_CURRENT_SOURCE_DIR}/GitVersion.hpp.in
-D OUTPUT_FILE=${CMAKE_CURRENT_BINARY_DIR}/GitVersion.hpp
-P ${CMAKE_CURRENT_SOURCE_DIR}/generate_version.cmake
)

Related

Force custom command to always run without first deleting the output

I have a setup where I use a custom command to check the current hash of a git repository so that other commands can clone it if it has updated
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt
COMMAND ${GIT_EXECUTABLE} ls-remote ${MODULE_URL} master > ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt.tmp
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt.tmp ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt
COMMAND ${CMAKE_COMMAND} -E rm ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt.tmp
)
Of course this will only run once as CMake sees no reason to rerun it. I can force it to run by adding a second (dummy) output - CMake then recognises that this output doesn't exist and then reruns the rule. However the Makefile that this generates actually deletes module_VERSION.txt before running the command rendering the whole pursuit pointless (Ninja does not have this problem).
I am able to get this to work but in an extremely hacky way: creating another target that always runs and then generating a dependency on this.
# Use echo_append as a no-op
add_custom_command(
OUTPUT module_FORCERUN
COMMAND ${CMAKE_COMMAND} -E echo_append
)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt
COMMAND ${GIT_EXECUTABLE} ls-remote ${MODULE_URL} master > ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt.tmp
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt.tmp ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt
COMMAND ${CMAKE_COMMAND} -E rm ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt.tmp
DEPENDS module_FORCERUN
)
This seems just really hacky and like it could be relying on some corner cases in cmake which aren't guaranteed to be stable. Is there a better way to get this working?
I am using cmake 3.21.3
Use add_custom_target for implement "always run" functionality and via BYPRODUCTS keyword specify the file which it could produce/update:
add_custom_target(update_module_version
BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt
COMMAND ${GIT_EXECUTABLE} ls-remote ${MODULE_URL} master > ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt.tmp
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt.tmp ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt
COMMAND ${CMAKE_COMMAND} -E rm ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt.tmp
)
That way, if any other target will depend on update_module_version one, the module_VERSION.txt file will be created/updated before evaluation of the target.
Such target-level dependency will be created automatically by CMake, if given file will be listed as dependency for target/command in the same directory, where target update_module_version is created:
add_custom_command(OUTPUT <...>
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt
COMMAND <...>
)
From other directories the target-level dependency should be specified explicitly:
# If used in other directories
add_custom_command(OUTPUT <...>
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/module_VERSION.txt
update_module_version
COMMAND <...>
)

Using generator expression in `cmake -E copy` command

I am trying to copy dll files from my bin folder to a different folder. I want to copy files from bin/Debug when building in Debug and from bin/Release when building in Release. This is what I currently use (and which does not work).
file(GLOB library_files_debug ${outputdirectory_root}/Debug/*.dll)
file(GLOB library_files_release ${outputdirectory_root}/Release/*.dll)
add_custom_target(copy_dlls_to_wheel ALL
DEPENDS setup.py
COMMAND ${CMAKE_COMMAND} -E echo "Debug files: $<$<CONFIG:Debug>:${library_files_debug}>"
COMMAND ${CMAKE_COMMAND} -E echo "Release files: $<$<CONFIG:Release>:${library_files_release}>"
COMMAND ${CMAKE_COMMAND} -E echo "Destination dir: ${CMAKE_BINARY_DIR}/python/${PROJECT_NAME}"
COMMAND ${CMAKE_COMMAND} -E copy $<$<CONFIG:Debug>:${library_files_debug}> $<$<CONFIG:Release>:${library_files_release}> ${CMAKE_BINARY_DIR}/python/${PROJECT_NAME}
)
I am running on Windows 10, and use Visual Studio to build. When the above target copy_dlls_to_wheel is built in Debug, the first echo statement prints out the correct dll files, and the second echo is empty. However, no files are copied. Instead I get the error message The system cannot find the path specified.
I have also tried to replace the last line with
COMMAND ${CMAKE_COMMAND} -E copy $<$<CONFIG:Debug>:${library_files_debug}> ${CMAKE_BINARY_DIR}/python/${PROJECT_NAME}
, but I get the same result.
However, when I remove the generator expression, and use
COMMAND ${CMAKE_COMMAND} -E copy ${library_files_debug} ${CMAKE_BINARY_DIR}/python/${PROJECT_NAME}
the files are copied correctly to my output folder. I am pretty confident my generator expression is correct, since I get the expected output from the echo commands. Are generator expressions not supported when using cmake -E copy, or is there something else I am doing wrong?
CMake's command line copy is able to process multiple files when you simply provide a list, which is why this works:
COMMAND ${CMAKE_COMMAND} -E copy ${library_files_debug} ${CMAKE_BINARY_DIR}/python/${PROJECT_NAME}
This is expanded to a space-separated list of files when the copy command is ultimately executed, which is the expected syntax.
cmake.exe -E copy mylib1.dll mylib2.dll /your/binary/dir/python/proj
However, when wrapped in a generator expression, the list will not be interpreted correctly by CMake. While the generator expression will be evaluated correctly, the list will be kept as a semicolon-separated list of files, which is the incorrect syntax:
cmake.exe -E copy "mylib1.dll;mylib2.dll" /your/binary/dir/python/proj
This causes the copy command to fail.
To work-around this issue, you could loop over each DLL file you want to copy, if there aren't too many. Something like this could work:
# Loop through the Debug files.
foreach(cur_file ${library_files_debug})
get_filename_component(file_name ${cur_file} NAME)
add_custom_target(copy_dlls_to_wheel_debug_${file_name} ALL
DEPENDS setup.py
COMMAND ${CMAKE_COMMAND} -E echo "DLL file: ${cur_file}"
COMMAND ${CMAKE_COMMAND} -E echo "Destination dir: ${CMAKE_BINARY_DIR}/python/${PROJECT_NAME}"
COMMAND ${CMAKE_COMMAND} -E copy $<$<CONFIG:Debug>:${cur_file}> ${CMAKE_BINARY_DIR}/python/${PROJECT_NAME}
)
endforeach()
# Loop through the Release files.
foreach(cur_file ${library_files_release})
get_filename_component(file_name ${cur_file} NAME)
add_custom_target(copy_dlls_to_wheel_release_${file_name} ALL
DEPENDS setup.py
COMMAND ${CMAKE_COMMAND} -E echo "DLL file: ${cur_file}"
COMMAND ${CMAKE_COMMAND} -E echo "Destination dir: ${CMAKE_BINARY_DIR}/python/${PROJECT_NAME}"
COMMAND ${CMAKE_COMMAND} -E copy $<$<CONFIG:Release>:${cur_file}> ${CMAKE_BINARY_DIR}/python/${PROJECT_NAME}
)
endforeach()
A quicker solution might be to bundle up your DLLs, using CMake's tar command line utility, copy them, then extract them, as suggested in this answer. CMake's tar command does not seem to accept lists wrapped in generator expressions either, so the list of files to bundle together is written to a file.
file(GLOB library_files_debug ${outputdirectory_root}/Debug/*.dll)
file(GLOB library_files_release ${outputdirectory_root}/Release/*.dll)
# Write the filenames (not full path) of the files to pack to file
set(debug_content "")
set(release_content "")
foreach(lib_file ${library_files_debug})
get_filename_component(file_name ${lib_file} NAME)
set(debug_content "${debug_content}${file_name}\n")
endforeach(lib_file ${library_files_debug})
foreach(lib_file ${library_files_release})
get_filename_component(file_name ${lib_file} NAME)
set(release_content "${release_content}${file_name}\n")
endforeach(lib_file ${library_files_release})
set(filenames_debug ${outputdirectory_root}/debug_files.txt)
set(filenames_release ${outputdirectory_root}/release_files.txt)
file(WRITE ${filenames_debug} ${debug_content})
file(WRITE ${filenames_release} ${release_content})
# Read either the list of debug or release files, and pack files
add_custom_command(
TARGET bdist PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E tar "cfj" ${outputdirectory_root}/temp.tar --files-from="$<IF:$<CONFIG:Debug>,${filenames_debug},${filenames_release}>"
WORKING_DIRECTORY ${outputdirectory_root}/$<CONFIG>)
# Unpack the files in the folder building the python wheel
add_custom_command(
TARGET bdist PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E rename ${outputdirectory_root}/temp.tar temp.tar
COMMAND ${CMAKE_COMMAND} -E tar "xfj" temp.tar
COMMAND ${CMAKE_COMMAND} -E remove temp.tar
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/python/${PROJECT_NAME})

How to append command to add_custom_target in CMake

Suppose I have a custom target in CMake for unit tests like the below
add_custom_target(
test
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/ATest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/BTest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/CTest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/DTest)
but I want to add an additional test to the target based on whether an external dependency is found. Currently, I did it with
if(EXTERNAL_FOUND)
add_custom_target(
test
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/ATest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/BTest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/CTest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/DTest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/ETest)
else()
add_custom_target(
test
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/ATest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/BTest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/CTest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/DTest)
endif()
This is not very elegant and it quickly becomes unmanageable when there are multiple conditions. Is there something like append to custom target so we can write the below instead?
add_custom_target(
test
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/ATest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/BTest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/CTest
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/DTest)
if(EXTERNAL_FOUND)
# I can't seem to find something like this
append_custom_target(test COMMAND ${CMAKE_CURRENT_BINARY_DIR}/ETest)
else()
Or is there a better way to do this?
You can use add_custom_command and use it as dependency to your target. With the custom command you can APPEND commands with same OUTPUT:
add_custom_target(
test
DEPENDS test-cmd
)
add_custom_command(
OUTPUT test-cmd
COMMAND ${CMAKE_COMMAND} -E echo "ATest"
COMMAND ${CMAKE_COMMAND} -E echo "BTest"
COMMAND ${CMAKE_COMMAND} -E echo "CTest"
COMMAND ${CMAKE_COMMAND} -E echo "DTest"
)
if(EXTERNAL_FOUND)
add_custom_command(
OUTPUT test-cmd APPEND
COMMAND ${CMAKE_COMMAND} -E echo "ETest"
)
endif()
# test-cmd is not actually generated so set it to symbolic
set_source_files_properties(test-cmd PROPERTIES SYMBOLIC "true")
See SYMBOLIC for the artifical source file property.

CMake not redirecting stderr with execute_process

I'm trying to redirect stdout and stderr to the same file using CMake. I'm using the execute_process option in CMake with the ERROR_FILE and OUTPUT_FILE option specified.
I'm successfully capturing the output, but the error is not there. What am I doing wrong?
File CMakeLists.txt
add_test(NAME test${ID}
COMMAND ${CMAKE_COMMAND}
-DEXE=../examples/test${exampleID}
-DID=${ID}
-DARGS=${args}
-P ${CMAKE_CURRENT_SOURCE_DIR}/Tester.cmake
)
File Tester.cmake
separate_arguments( ARGS )
# Run the test
execute_process(
COMMAND "${EXE}" ${ARGS}
ERROR_FILE test${ID}.out
OUTPUT_FILE test${ID}.out
)
Specifying the same file for both OUTPUT_FILE and ERROR_FILE has only recently been added in CMake 3.3. See release notes.
As a work-around for earlier versions, use the options OUTPUT_VARIABLE and ERROR_VARIABLE with the same variable and then write the contents of the variable to the file, e.g.:
execute_process(
COMMAND "${EXE}" ${ARGS}
ERROR_VARIABLE _testOut
OUTPUT_VARIABLE _testOut
)
file (WRITE "test${ID}.out" "${_testOut}")

How do I initialize a CMake variable with the result of a shell command

Is there a way to set a variable in a CMake script to the output of a shell command?
Something like SET(FOO COMMAND "echo bar") would come to mind
You want the execute_process command.
In your case, on Windows:
execute_process(COMMAND CMD /c echo bar OUTPUT_VARIABLE FOO)
or on Linux, simply:
execute_process(COMMAND echo bar OUTPUT_VARIABLE FOO)
In this particular case, CMake offers a cross-platform solution. CMake can itself be used to run commands that can be used on all systems, one of which is echo. To do this, CMake should be passed the command line arg -E. For the full list of such commands, run cmake -E help
Inside a CMake script, the CMake executable is referred to by ${CMAKE_COMMAND}, so the script needs to do:
execute_process(COMMAND ${CMAKE_COMMAND} -E echo bar OUTPUT_VARIABLE FOO)