set


Two ways to append a new argument to CMAKE_ARGS list for ExternalProject_Add

Recently, we wanted to pass a new cached value to an external project in CMake via the CMAKE_ARGS variable.

CMAKE_ARGS holds various types of arguments. From which, the arguments in the form -Dname:type=value are passed to the CMake command line and cannot be changed by the user.

We found two ways to add a new argument pair to the CMAKE_ARGS of the external project.

The first method uses the set function:

set(CMAKE_ARGS "${CMAKE_ARGS} -D${CACHE_VAR}${CACHE_VAR_TYPE}=\"${${CACHE_VAR}}\"")

where basically we create a new string that starts with the existing value of CMAKE_ARGS and then we append to its end the new pair.

The second method uses the list function:

list(APPEND CMAKE_ARGS "-D${CACHE_VAR}${CACHE_VAR_TYPE}=${${CACHE_VAR}}")

that treats CMAKE_ARGS as a list and it appends to its end the new pair.

To verify by hand that they were equivalent we did the following small test with success

#Copy the original value of ${CMAKE_ARGS}
set(CMAKE_ARGS_ORIGINAL ${CMAKE_ARGS})

#Define the Name, Type and Value for the new argument pairs
set(CACHE_VAR_P_NAME PRODUCTS_DIR)
set(CACHE_VAR_P_TYPE string)
set(CACHE_VAR_P_VALUE "someplace/with spaces")

set(CACHE_VAR_C_NAME CLIENTS_DIR)
set(CACHE_VAR_C_TYPE string)
set(CACHE_VAR_C_VALUE "another/place")

#Print the original value of ${CMAKE_ARGS}
MESSAGE(STATUS "CMAKE_ARGS: '" ${CMAKE_ARGS} "'")

#Append the two pairs to ${CMAKE_ARGS} using set
set(CMAKE_ARGS "${CMAKE_ARGS} -D${CACHE_VAR_P_NAME}:${CACHE_VAR_P_TYPE}=\"${CACHE_VAR_P_VALUE}\"")
set(CMAKE_ARGS "${CMAKE_ARGS} -D${CACHE_VAR_C_NAME}:${CACHE_VAR_C_TYPE}=\"${CACHE_VAR_C_VALUE}\"")

#Print the modified value of ${CMAKE_ARGS}
MESSAGE(STATUS "CMAKE_ARGS: '" ${CMAKE_ARGS} "'")

#Reset ${CMAKE_ARGS} to its original value
set(CMAKE_ARGS ${CMAKE_ARGS_ORIGINAL})

#Print the original value of ${CMAKE_ARGS} for verification
MESSAGE( STATUS "CMAKE_ARGS: '" ${CMAKE_ARGS} "'")

#Append the two pairs to ${CMAKE_ARGS} using list
list(APPEND CMAKE_ARGS "-D${CACHE_VAR_P_NAME}:${CACHE_VAR_P_TYPE}=\"${CACHE_VAR_P_VALUE}\"")
list(APPEND CMAKE_ARGS "-D${CACHE_VAR_C_NAME}:${CACHE_VAR_C_TYPE}=\"${CACHE_VAR_C_VALUE}\"")

#Print the modified value of ${CMAKE_ARGS}
#Notice that here we surrounded ${CMAKE_ARGS} with quotes so that it is printed as list of delimiter separated values
#Between each element the character ';' will be added because ${CMAKE_ARGS} is surrounded with quotes
MESSAGE(STATUS "CMAKE_ARGS: '" "${CMAKE_ARGS}" "'")

Which resulted in the following output

-- CMAKE_ARGS: ''
-- CMAKE_ARGS: ' -DPRODUCTS_DIR:string="someplace/with spaces" -DCLIENTS_DIR:string="another/place"'
-- CMAKE_ARGS: ''
-- CMAKE_ARGS: '-DPRODUCTS_DIR:string="someplace/with spaces";-DCLIENTS_DIR:string="another/place"'

From the output we can see that the two options treat the variable in a different way as using set just created a huge string containing both pairs while list created a list of two elements.
Both methods seem to work properly but we chose the list method for our external project.

The CMakeLists.txt file that we used to include the external project using custom CMAKE_ARGS resulted as follows:

cmake_minimum_required (VERSION 3.2.2)
include (ExternalProject)

set (TARGET c-banana-eat-banana)
project (${TARGET} C)

set (BANANA_ROOT ${CMAKE_CURRENT_BINARY_DIR} CACHE INTERNAL "")

#Add custom CMake arguments to be passed to the CMake command line of the external project
list(APPEND CMAKE_ARGS "-DARTIFACTS_DIR:string=\"${LIBRARIES_DIR}/${TARGET}\"")

ExternalProject_Add (${TARGET}
 URL ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET}.zip
 URL URL_HASH MD5=34734a678729967f426931d913326112
 CMAKE_ARGS "${CMAKE_ARGS}"
 BUILD_IN_SOURCE 1)

Bash: Determinate state of variable

A variable in bash (and any POSIX-compatible shell) will be in one of the three following states:

  • unset
  • set but empty
  • set and not empty

Variables in bash do not have data types. A variable can contain a number, a character, a string of characters or the empty string. Assigning a value to the reference of a variable will create it, you do not need to declare it beforehand.

Following, you will find some tests that allow you to find which state the variable is in. They will allow you to distinguish between ‘unset’ and ‘set but empty’ states.

# Test if VAR is 'unset' or 'set but empty'
if [ -z "${VAR}" ]; then
	echo "VAR is 'unset' or 'set but empty'";
fi

# Test if VAR is 'unset'
if [ -z "${VAR+BFNET}" ]; then
	echo "VAR is 'unset'";
fi
# Where ${VAR+BFNET} is a parameter expansion which evaluates to null if VAR is 'unset',
# if VAR is set to anything (including the empty string) the construct expands to BFNET.

# Test if VAR is 'set but empty'
if [ -z "${VAR-BFNET}" ]; then
	echo "VAR is 'set but empty'";
fi
# Where ${VAR-BFNET} is a parameter expansion which evaluates to the value of VAR if VAR is 'set' (either empty or not),
# if VAR 'unset' the construct expands to BFNET.
# This is useful for providing default values
# For example:
#function custom_echo () {
#    local DEFAULT_MSG="No message passed."; # Doesn't really need to be a local variable.
#    MESSAGE=${1:-$DEFAULT_MSG}; # Defaults to default message.
#    echo -e "$MESSAGE";
#    return;
#}
# In the above function if the user does not provide the message to be printed with the function call, then the default message will be used.

# Test if VAR is 'set and not empty'
if [ -n "${VAR}" ]; then
	echo "VAR is 'set and not empty'";
fi

Following is a truth table for the above:

                        +-------+-------+--------+
                VAR is: | unset | empty | filled |
+-----------------------+-------+-------+--------+
| [ -z "${VAR}" ]       | true  | true  | false  |
| [ -z "${VAR+BFNET}" ] | true  | false | false  |
| [ -z "${VAR-BFNET}" ] | false | true  | false  |
| [ -n "${VAR}" ]       | false | false | true   |
+-----------------------+-------+-------+--------+