Variable reference in string does not get evaluated by "set" - cmake

I want the same as in
How to load variables in a "bar=foo" syntax in CMake?
with the difference that my "value" entries contain references to the "keys" (prospective variables).
Eg the BASEPATH variable:
BASEPATH:=/home/SomePath
LIB_BASE?=$(BASEPATH)/lib/$(LIBREV)
ACCELMOD_BASE=$(BASEPATH)/SomeOtherPath
I have modified my CMakeLists.txt script to extract the [Key/value] pair from each line to two variables(see code below) and end up with pairs like (note that the value still contains a reference to some variable's name which is usually defined at the beginning of the file):
[BASEPATH, /home/SomePath],
[LIB_BASE, ${BASEPATH}/lib/${LIBREV}],
[ACCELMOD_BASE, ${BASEPATH}/SomeOtherPath],
The code I have written is the following:
# Part2
file(STRINGS revisions.mk ConfigContents)
foreach(NameAndValue ${ConfigContents})
# Strip leading spaces
string(REGEX REPLACE "^[ ]+" "" NameAndValue ${NameAndValue})
#remove commented lines
string(FIND ${NameAndValue} "#" commentIndex)
if(${commentIndex} EQUAL 0)
continue()
endif()
# Find variable name
string(REGEX MATCH "^[^\\?:=]+" Name ${NameAndValue})
# Find the value
string(REGEX REPLACE "^(.+)=(.+)$" "\\2" Value ${NameAndValue})
# Replace () with {} to denote a cmake's variable reference
string(REGEX REPLACE "\\(([^)]+)\\)" "{\\1}" Value ${Value})
# Set the variable
set(${Name} ${Value})
message("> Value of " ${Name} " : " ${${Name}})
endforeach()
I expect that when I define a Key (Name) as a variable (using the set command) and set its value to the Value counterpart of the Key, the reference inside the string will be replaced with the current value of the referenced variable.
However this is not the case.
e.g. for the give input, the message command before the end of the loop returns:
>Value of BASEPATH: /home/SomePath
>Value of LIB_BASE : ${BASEPATH}/lib/${LIBREV}
>Value of ACCELMOD_BASE: $(BASEPATH)/SomeOtherPath
even though BASEPATH has already been defined.
To verify my expectation, I wrote the following simple code to simulate the behaviour in the loop:
set(BASE 123)
set(RIGHT '${BASE}/SomePath/AA')
set(LEFT_STR "LEFT")
set(${LEFT_STR} ${RIGHT})
message(">" ${LEFT} "<>" ${${LEFT_STR}})
and in this case the ${BASE} reference gets correctly replaced and
'123/SomePath/AA'<>'123/SomePath/AA'
is returned as expected.
What could I been getting wrong?

Since CMake 3.18 there is eval in CMake - cmake_language(EVAL CODE. See https://cmake.org/cmake/help/latest/command/cmake_language.html?highlight=eval .
cmake_langauge(EVAL CODE "set(${Name} ${Value})")
There is no eval in cmake. The popular and error-prone way around it is to create a script and then include it:
file(STRINGS revisions.mk ConfigContents)
set(varlist "")
foreach(NameAndValue ${ConfigContents})
# Strip leading spaces
string(REGEX REPLACE "^[ ]+" "" NameAndValue ${NameAndValue})
#remove commented lines
string(FIND ${NameAndValue} "#" commentIndex)
if(${commentIndex} EQUAL 0)
continue()
endif()
# Find variable name
string(REGEX MATCH "^[^\\?:=]+" Name ${NameAndValue})
# Find the value
string(REGEX REPLACE "^(.+)=(.+)$" "\\2" Value ${NameAndValue})
# Replace () with {} to denote a cmake's variable reference
string(REGEX REPLACE "\\(([^)]+)\\)" "{\\1}" Value ${Value})
# Set the variable
set(${Name} ${Value})
list(APPEND varlist ${Name})
endforeach()
set(script "")
foreach(i IN LISTS varlist)
string(APPEND script "set(${i} \"${${i}}\")\n")
endforeach()
file(WRITE script2.cmake ${script})
include(script2.cmake)
foreach(i IN LISTS varlist)
message(STATUS ${i}=${${i}})
endforeach()
This creates the script script2.cmake with the content:
set(BASEPATH "/home/SomePath")
set(LIB_BASE "${BASEPATH}/lib/${LIBREV}")
set(ACCELMOD_BASE "${BASEPATH}/SomeOtherPath")
and then includes it. Including such script would re-evaulate expressions thus resolve references.

Related

Start reading file from a specific line using CMAKE

I want to start reading a file from a specific line. Cmake official docs suggest using file() with offset but I am not sure about its usage.
The file that I want to read is test.mak:
# -----------------------------------------------------------------------------
## TEST
# -----------------------------------------------------------------------------
TEST_COMPONENTS ?= ABC DEF GHI
# SYMBOLS
SYMBOLS_PROJ ?= A002
SYMBOLS_LABEL ?= TEST_A002_FINAL
I have a cmake file (the function is from internet and it works with my use case) where i want to read the test.mak file starting from "#SYMBOLS" so that the macros defined before this line are ignored/skipped, and then i want to set the macros in my current cmake:
function(Fileread MKFile)
file(READ "${MKFile}" FileContents [OFFSET "# SYMBOLS"])
string(REPLACE "?" "" FileContents ${FileContents})
string(REPLACE "\\\n" "" FileContents ${FileContents})
string(REPLACE "\n" ";" FileLines ${FileContents})
list(REMOVE_ITEM FileLines "")
foreach(line ${FileLines})
string(REPLACE "=" ";" line_split ${line})
list(LENGTH line_split count)
if (count LESS 2)
message(STATUS "Skipping ${line}")
continue()
endif()
list(GET line_split -1 value)
string(STRIP "${value}" value)
separate_arguments(value)
list(REMOVE_AT line_split -1)
foreach(var_name ${line_split})
string(STRIP ${var_name} var_name)
set(${var_name} ${value} PARENT_SCOPE)
endforeach()
endforeach()
endfunction()
Fileread("test.mak")
The offset setting is not working as a result of which i am also getting the macro TEST_COMPONENTS which i don't need. NOTE: TEST_COMPONENTS is just an example, there are multiple lines of macro definitions before "# SYMBOLS" that i would like to skip. Thanks for any suggestions to solve this in advance.
Use file(STRINGS) to read the lines of the text file into a list variable. You could then use list(POP_FRONT) until you encounter a matching line.
# Line 1
# Line 2
# Line 3
# Line 4
file(STRINGS ${CMAKE_CURRENT_LIST_FILE} THIS_FILE)
set(REDUCTION_SUCCESS False)
while(THIS_FILE)
list(POP_FRONT THIS_FILE LINE)
if (LINE MATCHES "^# SYMBOLS.*")
set(REDUCED_FILE ${LINE} ${THIS_FILE})
set(REDUCTION_SUCCESS True)
break()
endif()
endwhile()
if (REDUCTION_SUCCESS)
# use the contents of the REDUCED_FILE variable
# (print all remaining lines for the purpose of demonstation)
foreach(_LINE IN LISTS REDUCED_FILE)
message(STATUS "${_LINE}")
endforeach()
else()
message(FATAL_ERROR "No line containing '# SYMBOLS' found")
endif()
Replace one of the # Line 1 comments with # SYMBOLS to get a successful outcome. For simplicity this is just a cmake script that can be run with cmake -P script.cmake. The script file parses itself.
If the number of lines to skip is known, you could simplify the logic after the file(STRINGS) command to a single list(SUBLIST) call.

CMAKE: Updating a list in a function does not work

I followed this and this link to write a function that:
takes a list of files as input,
appends path to each file
Creates a new list (passed as an output
parameter to the function itself) by appending all the files to it
function(concat_path iLiItems oLiItems cVal)
foreach(pfile ${${iLiItems}})
string(CONCAT l ${${cVal}} ${pfile})
message(STATUS ${pfile} " - " ${l})
set(${oLiItems} ${${oLiItems}} ${l} PARENT_SCOPE)
endforeach()
endfunction()
function(list_print liItems)
message(STATUS "The list contains: ")
foreach(f ${${liItems}})
message(STATUS ${f})
endforeach()
endfunction()
set(PROTO_SRCS base.proto dht.proto)
foreach(pfile ${PROTO_SRCS})
string(REPLACE ".proto" ".pb" fname ${pfile})
string(CONCAT cc ${fname} ".cc")
string(CONCAT h ${fname} ".h")
set(PROTO_CPP_SRCS ${PROTO_CPP_SRCS} ${cc} ${h})
endforeach()
string(CONCAT path_prefix ${CMAKE_CURRENT_SOURCE_DIR} "/")
concat_path(PROTO_SRCS PROTO_SRCS_PATH path_prefix)
list_print(PROTO_SRCS_PATH)
The problem I see is that when I finally print using the function, "list_print" i see that only one element is present in the output list (PROTO_SRCS_PATH) where as I was expecting two corresponding to both the input files:
-- base.proto - C:/Users/vaddina/workspace/protobuf-tests/app-wo-findprotobuf/base.proto
-- dht.proto - C:/Users/vaddina/workspace/protobuf-tests/app-wo-findprotobuf/dht.proto
-- The list contains:
-- C:/Users/vaddina/workspace/protobuf-tests/app-wo-findprotobuf/dht.proto
What am I doing wrong ? Thank you.
With PARENT_SCOPE option you change value of variable in the parent scope, but a variable in the current scope is unchanged. This is explicitely descrbed in the documentation about PARENT_SCOPE:
This command will set the value of a variable into the parent directory or calling function (whichever is applicable to the case at hand). The previous state of the variable’s value stays the same in the current scope (e.g., if it was undefined before, it is still undefined and if it had a value, it is still that value).
Because of that, the call to
set(${oLiItems} ${${oLiItems}} ${l} PARENT_SCOPE)
always see empty value as the second argument (you never assign the variable in the current scope). So the variable in the parent scope is always assigned (not appended!) by ${l}.
Normally, setting a variable with PARENT_SCOPE is performed only once. Intermediate calculation should use and update variable in the current scope:
function(concat_path iLiItems oLiItems cVal)
foreach(pfile ${${iLiItems}})
string(CONCAT l ${${cVal}} ${pfile})
message(STATUS ${pfile} " - " ${l})
set(${oLiItems} ${${oLiItems}} ${l}) # Update list in the current scope only
endforeach()
# Before return, propagate variable's value to the parent scope.
set(${oLiItems} ${${oLiItems}} PARENT_SCOPE)
endfunction()

String REG REPLACE in CMAKE

I am searching header files in a directory
Replacing LONG string in all header files to SVN_LONG.
I don't wan't to replace LONG,LONG,LONG to SVN_LONG.
When I run below code it's replacing SLONG to SSVN_LONG, LONG to SVN_LONG etc..
So how to avoid SLONG, LONGINT not to replace, only LONG string have to replace SVN_LONG?
file ( GLOB headerfiles "../common/include/tar/*.h")
if ("${grep_word}" STREQUAL "")
SET (searchreg "LONG")
foreach( eachheaderfile ${headerfiles} )
MESSAGE(STATUS " INFILES= ${eachheaderfile}\n")
FILE(READ ${eachheaderfile} file_content)
#MESSAGE(STATUS " FILES_content= ${file_content}")
string(REGEX REPLACE "${searchreg}" "SVN_LONG" modified_file_content "${file_content}" )
FILE(WRITE ${eachheaderfile} ${modified_file_content})
FILE(READ ${eachheaderfile} file_content1)
MESSAGE(STATUS " FILES_content= ${file_content1}")
endforeach(eachheaderfile)
#MESSAGE(STATUS " outFILES= ${headerfiles}\n")
endif()
As usual with regular expressions, you need to match symbols before replaced string and restore them in replacement string. Command string(REGEX REPLACE) supports backslashed references for that purpose:
string(REGEX REPLACE "([^a-zA-Z])LONG|^LONG" "\\1SVN_LONG"
modified_file_content ${file_content})
Given command matches single symbol, which should be different from the letter, before word LONG and restore this symbol via backslash reference \1 (symbol "\" need to be doubled in the cmake command, because it is parsed by CMake itself before going to the command).
Alternation started with ^ is needed for match LONG at the beginning of the string.

Excluding directory somewhere in file structure from cmake sourcefile list

In any subdirectory of my project it shall be possible to create anywhere a directory called 'hide' and put some code files (.c, .cpp, .h, ...) inside. CMake shall ignore those files. How can I achieve this?
Any proposals for a simple approach? Searched the net but could not find a solution.
What I tried:
file & glob
file(GLOB_RECURSE SOURCE_FILES "*.cpp" "*.c")
file(GLOB_RECURSE REMOVE_SOURCES
"*/hide/*"
"${PROJECT_SOURCE_DIR}/CMakeFiles/*"
"*main.c")
file(GLOB_RECURSE REMOVE_SOURCES "*/hide/*")
list(REMOVE_ITEM SOURCE_FILES ${REMOVE_SOURCES})
The 2rd line works fine if the directory path is known (as for the 'CMakeFiles' directory) or if the file is known (as for the 'main.c' file). But it does not work if there are two '*' for a string. I can not find a simple solution for a directory which is located somewhere.
Regex
Then I tried with REGEX.
file(GLOB_RECURSE SOURCE_FILES "*.cpp" "*.c")
string(REGEX REPLACE ";.*/hide/.*;" ";" FOO ${SOURCE_FILES})
Because the source file list is separated by semicolon, the line above shall remove the string from one semicolon to the next in case it contained the string 'hide'.
The expression seems to have a problem with the semicolon. Having any semicolon makes the command to find nothing.
foreach loop
I tried with some foreach loops, but could not achieve to get a list of all my 'hide'-directories.
One of the ways, I suppose, would be just like this:
set (EXCLUDE_DIR "/hide/")
file (GLOB_RECURSE SOURCE_FILES "*.cpp" "*.c")
foreach (TMP_PATH ${SOURCE_FILES})
string (FIND ${TMP_PATH} ${EXCLUDE_DIR} EXCLUDE_DIR_FOUND)
if (NOT ${EXCLUDE_DIR_FOUND} EQUAL -1)
list (REMOVE_ITEM SOURCE_FILES ${TMP_PATH})
endif ()
endforeach(TMP_PATH)
Simply searching for /hide/ substring in strings from the list. If the substring found --- remove whole string from the list.
Beware, this might be only solution for Linux. And might not work on older versions of CMake (<2.8.5, according to this).
In mentioned case You can do something like:
# try to replace substring with empty string, compare to original:
string (REPLACE ${EXCLUDE_DIR} "" REPLACED_PATH ${TMP_PATH})
string (COMPARE EQUAL ${TMP_PATH} ${REPLACED_PATH} EXCLUDE_DIR_FOUND)
NOTE: GLOB_RECURSE is considered to be root of all evil.
NOTE2: another thing to be aware of - CMake bug in matching empty variable to string.
NOTE3: took into account gg99's comment
Found a solution that works for me. Added a function to make it easy to exclude another directory.
# Function: EXCLUDE_FILES_FROM_DIR_IN_LIST
# Description: Exclude all files from a list under a specific directory.
# Param _InFileList: Input and returned List
# Param _excludeDirName: Name of the directory, which shall be ignored.
# Param _verbose: Print the names of the files handled
FUNCTION (EXCLUDE_FILES_FROM_DIR_IN_LIST _InFileList _excludeDirName _verbose)
foreach (ITR ${_InFileList})
if ("${_verbose}")
message(STATUS "ITR=${ITR}")
endif ("${_verbose}")
if ("${ITR}" MATCHES "(.*)${_excludeDirName}(.*)") # Check if the item matches the directory name in _excludeDirName
message(STATUS "Remove Item from List:${ITR}")
list (REMOVE_ITEM _InFileList ${ITR}) # Remove the item from the list
endif ("${ITR}" MATCHES "(.*)${_excludeDirName}(.*)")
endforeach(ITR)
set(SOURCE_FILES ${_InFileList} PARENT_SCOPE) # Return the SOURCE_FILES variable to the calling parent
ENDFUNCTION (EXCLUDE_FILES_FROM_DIR_IN_LIST)
EXCLUDE_FILES_FROM_DIR_IN_LIST("${SOURCE_FILES}" "/hide/" FALSE)
Generalizing the function solution mentioned above you get something like:
# Remove strings matching given regular expression from a list.
# #param(in,out) aItems Reference of a list variable to filter.
# #param aRegEx Value of regular expression to match.
function (filter_items aItems aRegEx)
# For each item in our list
foreach (item ${${aItems}})
# Check if our items matches our regular expression
if ("${item}" MATCHES ${aRegEx})
# Remove current item from our list
list (REMOVE_ITEM ${aItems} ${item})
endif ("${item}" MATCHES ${aRegEx})
endforeach(item)
# Provide output parameter
set(${aItems} ${${aItems}} PARENT_SCOPE)
endfunction (filter_items)
You can then use it thus:
file(GLOB_RECURSE MyFunFiles "*.fun")
filter_items(MyFunFiles ".*NotCool.*")

Cmake and regex dont work as expected

I have two variables in an text file that I would like to exhange. [V1] and [V2]. When I run my program it only changes one of them. Why?
#Read up into variable
file (STRINGS "myfile.txt" v1)
#Store text to replace in variable (we need to escape the '[' and ']' since we will use a regexp later on)
set(v1_placeholder "\\[v1\\]")
set(v2_placeholder "\\[v2\\]")
#New reading of documents to process
set(doc_files [v1]_Hello_Documentation.txt myfile.txt)
#Lets iterate over each documentation file
foreach(doc_file ${doc_files})
message(STATUS "Proccessing document file: " ${doc_file})
#Read up content of documentation file
file(READ ${doc_file} FILE_CONTENT)
#Remove occurences of [v2] with nothing
string(REGEX REPLACE "${v2_placeholder}" "" MODIFIED_FILE_CONTENT "${FILE_CONTENT}")
#Replace occurences of [v1] with the real variable in the content
string(REGEX REPLACE "${v1_placeholder}" "${V1}" MODIFIED_FILE_CONTENT "${FILE_CONTENT}")
#Replace occurences of [v1] with the real variable in the file name
string(REGEX REPLACE "${v1_placeholder}" "${V1}" MODIFIED_FILE_NAME "${doc_file}")
#Write modified content back into modifed file name
file(WRITE ${MODIFIED_FILE_NAME} "${MODIFIED_FILE_CONTENT}")
#Add the files to the package in dir /doc
install (FILES ${MODIFIED_FILE_NAME} DESTINATION doc)
endforeach(doc_file)
after I have run this my file will look liket this: (it never deletes the v2.)
The application is currently r.
Main features
When running Hello r the user is presented with the text "Hello World" on stdout.
History
[v2]
r
a
b
c
d
You probably need to pass MODIFIED_FILE_CONTENT instead of FILE_CONTENT to the second call to string(REGEX REPLACE, i.e.:
#Remove occurences of [v2] with nothing
string(REGEX REPLACE "${v2_placeholder}" "" MODIFIED_FILE_CONTENT "${FILE_CONTENT}")
#Replace occurences of [v1] with the real variable in the content
string(REGEX REPLACE "${v1_placeholder}" "${V1}" MODIFIED_FILE_CONTENT "${MODIFIED_FILE_CONTENT}")