Escape special options in CMake - cmake

CMake use named arguments, but how to specify an argument which value is exactly the same as the name of the argument?
Example:
set(msg "COMMAND")
add_custom_command(TARGET tgt COMMAND echo ${msg})
will not work because ${msg} is interpreted as the name of the option, not the argument of the command echo. So it will execute echo without any argument.
Output: command ECHO activated. (on Windows)
Wanted output: COMMAND

CMake use named arguments
CMake does not use named arguments! It has argument lists and the command-specific "keywords" are merely delimiters for sublists of arguments.
For this specific command, you can use an always-true generator expression to "escape" it:
set(msg COMMAND)
add_custom_command(
TARGET tgt
COMMAND echo "$<1:${msg}>"
)
This works because the arguments are matched immediately during configure time (and $<1:COMMAND> is not the same as COMMAND), while the generator expression will be evaluated later, at generation time.

Related

How to delete files stored in a cmake list in an add_custom_target?

I have files stored in a list variable MatrixSSL_configure_files.
I'd like to remove all files in a add_custom_target, like so
add_custom_target( maintainer-clean-evio
COMMAND ${CMAKE_COMMAND} -E remove -f "${MatrixSSL_configure_files}"
)
This doesn't work because the list is semi-colon separated and the
list isn't expanded (the COMMAND simply tries to execute the files).
Of course I also tried
COMMAND rm -f ${MatrixSSL_configure_files}
with the same result.
Assuming the file names contain spaces, what would be the correct
way to do this? If not possible, assume they don't contain spaces :/
This is precisely what the COMMAND_EXPAND_LISTS option is for. From the add_custom_target documentation:
COMMAND_EXPAND_LISTS
Lists in COMMAND arguments will be expanded, including those created with generator expressions, allowing COMMAND arguments such as ${CC} "-I$<JOIN:$<TARGET_PROPERTY:foo,INCLUDE_DIRECTORIES>,;-I>" foo.cc to be properly expanded.
So something like this should allow the MatrixSSL_configure_files list to be expanded:
add_custom_target( maintainer-clean-evio
COMMAND ${CMAKE_COMMAND} -E remove -f "${MatrixSSL_configure_files}"
COMMAND_EXPAND_LISTS
)

Do not expand CMake list variable

I have a CMake script that runs some tests via add_test(), running under Windows (Server 2008, don't ask) in CMake 3.15. When these tests are called, the PYTHONPATH environment variable in the environment they run in seems to get reset to the environment default, and doesn't contain some paths that it needs to.
I therefore need to set PYTHONPATH when the tests are run to the value of the $ENV{PYTHONPATH} variable when CMake runs. This has a number of semicolon-separated paths, so CMake thinks it's a list and tries to expand it into a number of space-separated strings, which obviously ends badly.
I cannot work out how to stop CMake doing this. From everything I can see, you should be able to do just surround with quotes:
add_test(
NAME mytest
COMMAND cmake -E env PYTHONPATH="$ENV{PYTHONPATH}"
run_test_here)
...but it always does the expansion. I also tried setting with set_tests_properties:
set_tests_properties(mytest PROPERTIES
ENVIRONMENT PYTHONPATH="$ENV{PYTHONPATH}")
...but that didn't appear to do anything at all - PYTHONPATH at test time wasn't altered. I thought it was because it's an environment variable, but using a regular CMake variable via set() makes no difference, so I'm doing something wrong. Help please!
The following should work:
COMMAND cmake -E env "PYTHONPATH=$ENV{PYTHONPATH}"
You need to quote the full part of the command line, to make properly expanded message.
Tested with:
set(tmp "C:\\Python27\\Scripts;E:\\JenkinsMIDEBLD\\workspace\\...;...")
add_test(NAME MYtest1 COMMAND cmake -S . -E env "tmp=${tmp}")
add_test(NAME MYtest2 COMMAND cmake -S . -E env tmp="${tmp}")
After running ctest I get:
1: Test command: /bin/cmake "-S" "." "-E" "env" "tmp=C:\Python27\Scripts;E:\JenkinsMIDEBLD\workspace\...;..."
2: Test command: /bin/cmake "-S" "." "-E" "env" "tmp="C:\Python27\Scripts" "E:\JenkinsMIDEBLD\workspace\..." "...""
The first test has proper ; passed to var, while the second one passes space separated list.
This is how cmake parses quoted arguments. An argument is either fully quoted or not quoted at all - partial quotes are interpreted as a literal ". So assumnig that:
set(var a;b;c)
The following:
var="$var"
Is not a quoted argument and " are taken literally! It expands the $var list into space separated list and the " stay, there is one " between = and a, and there is additional " on the end. The var="$var" is equal to:
var=\"a b c\"
^^ ^^ - the quotes stay!
^^^^^^^ ^ ^^^ - these are 3 arguments, the last one is `c"`
Without quotes is:
var=$var
is equal to (notice the missing quotes):
var=a c c
To quotes argument you have to quote it all, with first and last character of the element beeing ":
"var=$var"
will expand to:
"var=a;b;c"
You can make this work with the ENVIRONMENT test property, but there's a catch:
Semicolon separates different environment variables to set; you need to escape semicolons in your environment variable. For example instead of
set_tests_properties(mytest PROPERTIES
ENVIRONMENT "PYTHONPATH=foo;bar")
you need to use
set_tests_properties(mytest PROPERTIES
ENVIRONMENT "PYTHONPATH=foo\\;bar")
The fact that an environment variable may contain semicolons makes some transformation necessary: since ; is used to separate list elements you can simply use list(JOIN) to replace those with "\\;".
The following example works with PATH, not PYTHONPATH, since I don't have python installed:
CMakeLists.txt
cmake_minimum_required(VERSION 3.12.4) # required for list(JOIN)
project(TestProject)
# get a version of the PATH environment var that can be used in the ENVIRONMENT test property
set(_PATH $ENV{PATH})
list(JOIN _PATH "\\;" _PATH_CLEAN)
# just use a cmake script so we don't need to require any program able to retrieve environment vars
add_test(NAME Test1 COMMAND "${CMAKE_COMMAND}" -P "${CMAKE_CURRENT_SOURCE_DIR}/test_script.cmake")
# we add another simpler var to test in the cmake script
set_tests_properties(Test1 PROPERTIES ENVIRONMENT "PATH=${_PATH_CLEAN};FOO=foo")
enable_testing()
test_script.cmake
message(STATUS "PATH=$ENV{PATH}")
if (NOT "$ENV{FOO}" STREQUAL "foo")
# the following command results in a non-0 exit code, if executed
message(FATAL_ERROR "FOO environment var should contain \"foo\" but contains \"$ENV{FOO}\"")
endif()

How do I include a literal double quote in a custom CMake command?

I'm trying to create a custom command that runs with some environment variables, such as LDFLAGS, whose value needs to be quoted if it contains spaces:
LDFLAGS="-Lmydir -Lmyotherdir"
I cannot find a way to include this argument in a CMake custom command, due to CMake's escaping rules. Here's what I've tried so far:
COMMAND LDFLAGS="-Ldir -Ldir2" echo blah VERBATIM)
yields "LDFLAGS=\"-Ldir -Ldir2\"" echo blah
COMMAND LDFLAGS=\"-Ldir -Ldir2\" echo blah VERBATIM)
yields LDFLAGS=\"-Ldir -Ldir2\" echo blah
It seems I either get the whole string quoted, or the escaped quotes don't resolve when used as part of the command.
I would appreciate either a way to include the literal double-quote or as an alternative a better way to set environment variables for a command. Please note that I'm still on CMake 2.8, so I don't have the new "env" command available in 3.2.
Note that this is not a duplicate of When to quote variables? as none of those quoting methods work for this particular case.
The obvious choice - often recommended when hitting the boundaries of COMMAND especially with older versions of CMake - is to use an external script.
I just wanted to add some simple COMMAND only variations that do work and won't need a shell, but are - I have to admit - still partly platform dependent.
One example would be to put only the quoted part into a variable:
set(vars_as_string "-Ldir -Ldir2")
add_custom_target(
QuotedEnvVar
COMMAND env LD_FLAGS=${vars_as_string} | grep LD_FLAGS
)
Which actually does escape the space and not the quotes.
Another example would be to add it with escaped quotes as a "launcher" rule:
add_custom_target(
LauncherEnvVar
COMMAND env | grep LD_FLAGS
)
set_target_properties(
LauncherEnvVar
PROPERTIES RULE_LAUNCH_CUSTOM "env LD_FLAGS=\"-Ldir -Ldir2\""
)
Edit: Added examples for multiple quoted arguments without the need of escaping quotes
Another example would be to "hide some of the complexity" in a function and - if you want to add this to all your custom command calls - use the global/directory RULE_LAUNCH_CUSTOM property:
function(set_env)
get_property(_env GLOBAL PROPERTY RULE_LAUNCH_CUSTOM)
if (NOT _env)
set_property(GLOBAL PROPERTY RULE_LAUNCH_CUSTOM "env")
endif()
foreach(_arg IN LISTS ARGN)
set_property(GLOBAL APPEND_STRING PROPERTY RULE_LAUNCH_CUSTOM " ${_arg}")
endforeach()
endfunction(set_env)
set_env(LDFLAGS="-Ldir1 -Ldir2" CFLAGS="-Idira -Idirb")
add_custom_target(
MultipleEnvVar
COMMAND env | grep -E 'LDFLAGS|CFLAGS'
)
Alternative (for CMake >= 3.0)
I think what we actually are looking for here (besides the cmake -E env ...) is named Bracket Argument and does allow any character without the need of adding backslashes:
set_property(
GLOBAL PROPERTY
RULE_LAUNCH_CUSTOM [=[env LDFLAGS="-Ldir1 -Ldir2" CFLAGS="-Idira -Idirb"]=]
)
add_custom_target(
MultipleEnvVarNew
COMMAND env | grep -E 'LDFLAGS|CFLAGS'
)
References
0005145: Set environment variables for ADD_CUSTOM_COMMAND/ADD_CUSTOM_TARGET
How to modify environment variables passed to custom CMake target?
[CMake] How to set environment variable for custom command
cmake: when to quote variables?
You need three backslashes. I needed this recently to get a preprocessor define from PkgConfig and apply it to my C++ flags:
pkg_get_variable(SHADERDIR movit shaderdir)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSHADERDIR=\\\"${SHADERDIR}\\\"")
Florian's answer is wrong on several counts:
Putting the quoted part in a variable makes no difference.
You should definitely use VERBATIM. It fixes platform-specific quoting bugs.
You definitely shouldn't use RULE_LAUNCH_CUSTOM for this. It isn't intended for this and only works with some generators.
You shouldn't use env as the command. It isn't available on Windows.
It turns out the real reason OPs code doesn't work is that CMake always fully quotes the first word after COMMAND because it's supposed to be the name of an executable. You simply shouldn't put environment variables first.
For example:
add_custom_command(
OUTPUT q1.txt
COMMAND ENV_VAR="a b" echo "hello" > q1.txt
VERBATIM
)
add_custom_target(q1 ALL DEPENDS q1.txt)
$ VERBOSE=1 make
...
"ENV_VAR=\"a b\"" echo hello > q1.txt
/bin/sh: ENV_VAR="a b": command not found
So how do you pass an environment variable with spaces? Simple.
add_custom_command(
OUTPUT q1.txt
COMMAND ${CMAKE_COMMAND} -E env ENV_VAR="a b" echo "hello" > q1.txt
VERBATIM
)
Ok, I removed my original answer as the one proposed by #Florian is better. There is one additional tweak needed for multiple quoted args. Consider a list of environment variables as such:
set(my_env_vars LDFLAGS="-Ldir1 -Ldir2" CFLAGS="-Idira -Idirb")
In order to produce the desired expansion, convert to string and then replace ; with a space.
set(my_env_string "${my_env_vars}") #produces LDFLAGS="...";CFLAGS="..."
string(REPLACE ";" " " my_env_string "${my_env_string}")
Then you can proceed with #Florian's brilliant answer and add the custom launch rule. If you need semicolons in your string then you'll need to convert them to something else first.
Note that in this case I didn't need to launch with env:
set_target_properties(mytarget PROPERTIES RULE_LAUNCH_CUSTOM "${my_env_string}")
This of course depends on your shell.
On second thought, my original answer is below as I also have a case where I don't have access to the target name.
set(my_env LDFLAGS=\"-Ldir -Ldir2" CFLAGS=\"-Idira -Idirb\")
add_custom_command(COMMAND sh -c "${my_env} grep LDFLAGS" VERBATIM)
This technique still requires that the semicolons from the list->string conversion be replaced.
Some folks suggest to use ${CMAKE_COMMAND} and pass your executable as an argument, e.g:
COMMAND ${CMAKE_COMMAND} -E env "$(WindowsSdkDir)/bin/x64/makecert.exe" ...
That worked for me.

CMake generator expression is not being evaluated

Due to the following warning:
CMake Error at test/CMakeLists.txt:29 (get_target_property):
The LOCATION property may not be read from target "my_exe". Use the
target name directly with add_custom_command, or use the generator
expression $<TARGET_FILE>, as appropriate.
which is the result from lines like this:
get_target_property(my_exe_path my_exe LOCATION)
Like recommended in the docs, I tried to use a generator expression like this:
add_executable(my_exe_path main.cpp)
message("path to executable: $<TARGET_FILE:my_exe_path>")
But TARGET_FILE is not being evaluated
path to executable: $<TARGET_FILE:my_exe>
I'm using CMake 3.4 and added cmake_minimum_required(VERSION 3.4) to my CMakeLists.txt so what am I doing wrong?
Here is a quick and easy way to print the value of a generator expression:
add_custom_target(print
${CMAKE_COMMAND} -E echo $<1:hello> $<0:world>
)
In this example, if you run cmake . and then make print, you will see "hello" (without the quotation marks) in the output.
However, if you just use message($<1:hello> $<0:world>), you will see "$<1:hello> $<0:world>" as output (again, without the quotation marks).
While generator expression is stored at configuration stage (when corresponded CMake command is executed),
evaluation of generator expressions is performed at build stage.
This is why message() command prints generator expression in non-dereferenced form: value denoted by the generator expression is not known at this stage.
Moreover, CMake never dereferences generator expressions by itself. Instead, it generates appropriate string in the build file, which is then interpreted by build utility (make, Visual Studio, etc.).
Note, that not every CMake command accepts generator expressions. Each possible usage of generator expressions is explicitely described in documentation for specific command. Moreover, different CMake command flows or different options have different policy about using of generator expressions.
For example, command flow
add_test(NAME <name> COMMAND <executable>)
accepts generator expressions for COMMAND option,
but command flow
add_test(<name> <executable>)
doesn't!
Another example of policies difference:
install(DIRECTORY <dir> DESTINATION <dest>)
In this command flow generator expressions are allowed for DESTINATION, but not for DIRECTORY option.
Again, read documentation carefully.

How to pass a list variable to another CMake call?

In my CMake scripts, I run other CMake instances using exec_program(${CMAKE_COMMAND} ...). I want to make the ${CMAKE_MODULES_PATH} of the parent environment available to the child environments. Therefore, I tried to pass the variable as argument:
exec_program(${CMAKE_COMMAND} ... ARGS ...
-DCMAKE_MODULE_PATH=${CMAKE_MODULE_PATH} ...)
This messes up the other parameters and I get a CMake Error: The source directory <first-module-path> does not appear to contain CMakeLists.txt.. Therefore, I tried escaping the variable:
exec_program(${CMAKE_COMMAND} ... ARGS ...
-DCMAKE_MODULE_PATH="${CMAKE_MODULE_PATH}" ...)
When I print the ${CMAKE_MODULE_PATH} form within the child environment, all the paths get printed, separated by a space each. However, CMake doesn't find scripts inside those paths. I guess it has something to do with the list being passed as string rather than a semi-color separated list.
How can I pass a CMake variable holding a list of strings to another CMake command?
In my CMake scripts, I run other CMake instances using exec_program(${CMAKE_COMMAND} ...)
According to documentation this command is deprecated:
Deprecated. Use the execute_process() command instead.
Example with the list variable, CMakeLists.txt:
execute_process(
COMMAND "${CMAKE_COMMAND}" "-DVAR=A;B;C" -P script.cmake
OUTPUT_VARIABLE output
)
script.cmake:
message("VAR: ${VAR}")
foreach(x ${VAR})
message("x: ${x}")
endforeach()
output:
VAR: A;B;C
x: A
x: B
x: C