I have a file version.txt
VERSION_MAJOR 1
VERSION_MINOR 1
VERSION_PATCH 3
I want to use cmake to add a definition for major, minor and patch.
I've tries using
file(STRING "version.txt" myvar)
but this just puts the whole file in myvar.
How do I get the numbers?
Your use of file is incorrect, you want to use READ in order to read the contents of the file into a variable.
file(READ "version.txt" ver)
Once you have the file's contents into a variable, you can then use REGEX MATCH with a capture group, and access the capture group using CMAKE_MATCH_N
REGEX MATCH:
Create a regular expression with a capture group which will capture the numbers following "VERSION_MAJOR":
string(REGEX MATCH "VERSION_MAJOR ([0-9]*)" _ ${ver})
Note that if the regex matches the input variable, the entire match will be stored in the output variable. However, we don't want the entire match (as that includes the string "VERSION_MAJOR"), so I've just used a variable name _ as the output variable, which, by convention, tells the user I am not interested in this variable
CMAKE_MATCH_N:
If the match is successful, then the capture groups are available in CMAKE_MATCH_N. In this instance there is only one capture group, so we want to use CMAKE_MATCH_1
set(ver_major ${CMAKE_MATCH_1})
At this point ver_major contains just the major version no.
You can then repeat this for the other version components.
Full example below:
cmake_minimum_required(VERSION 3.5)
file(READ "version.txt" ver)
string(REGEX MATCH "VERSION_MAJOR ([0-9]*)" _ ${ver})
set(ver_major ${CMAKE_MATCH_1})
string(REGEX MATCH "VERSION_MINOR ([0-9]*)" _ ${ver})
set(ver_minor ${CMAKE_MATCH_1})
string(REGEX MATCH "VERSION_PATCH ([0-9]*)" _ ${ver})
set(ver_patch ${CMAKE_MATCH_1})
message("version: ${ver_major}.${ver_minor}.${ver_patch}")
Output:
$ cmake .
version: 1.1.3
-- Configuring done
-- Generating done
-- Build files have been written to: /tmp
For production code you would obviously want to make the cmake script more robust by checking whether the match was successful, and emitting an error if not.
Related
I have a project where one repeatable task to do involves manipulating files' contents.
Until now I used a Python script for it, but recently I discovered I can use standalone CMake scripts ("standalone" here means they can be invoked outside of configure/build/test/etc. workflow). As my project already uses CMake for project management I concluded I can save others' problem of installing a Python interpreter (welcome Windows users!) and use CMake project-wide.
Part of my script needs to read a file and cut off everything that appears before "[START-HERE]" and after "[END-HERE]" lines. I am stuck with that part and don't know how to implement it. How can it be done?
You could combine file(READ) with if(MATCHES) to accompilish this. The former is used to read the file, the latter allows you to check for the occurance of a regular expression and to extract a capturing group:
foo.cmake
#[===[
Params:
INPUT_FILE : the path to the file to read
#]===]
file(READ "${INPUT_FILE}" FILE_CONTENTS)
if (FILE_CONTENTS MATCHES "(^|[\r\n])\\[START-HERE\\][\r\n]+(.*)[\r\n]+\\[END-HERE\\]")
# todo: use extracted match stored in CMAKE_MATCH_2 for your own logic
message("Content: '${CMAKE_MATCH_2}'")
else()
message(FATAL_ERROR "[START-HERE]...[END-HERE] doesn't occur in the input file '${INPUT_FILE}'")
endif()
foo.txt
Definetly not
[START-HERE]
working
[END-HERE]
Try again!
Output:
> cmake -D INPUT_FILE=foo.txt -P foo.cmake
Content: 'working'
For the part where you are stuck, here's one approach using the string, file, and math commands:
file(READ data.txt file_str)
string(FIND "${file_str}" "[START-HERE]" start_offset)
# message("${start_offset}")
math(EXPR start_offset "${start_offset}+12")
# message("${start_offset}")
string(FIND "${file_str}" "[END-HERE]" end_offset)
math(EXPR substr_len "${end_offset}-${start_offset}")
# message("${substr_len}")
string(SUBSTRING "${file_str}" "${start_offset}" "${substr_len}" trimmed_str)
# message("${trimmed_str}")
You could also probably do it by using the file(STRINGS) command, which reads lines of a file into an array, and then use the list(FIND) command. The approach shown above has the advantage of working if your delimiters are not on their own lines.
As #fabian shows in their answer post, you can also do this using a regular expression with if(MATCHES) like this:
file(READ "${INPUT_FILE}" FILE_CONTENTS)
if (FILE_CONTENTS MATCHES "(^|[\r\n])\\[START-HERE\\][\r\n]+(.*)[\r\n]+\\[END-HERE\\]")
# todo: use extracted match stored in CMAKE_MATCH_2 for your own logic
message("Content: '${CMAKE_MATCH_2}'")
else()
message(FATAL_ERROR "[START-HERE]...[END-HERE] doesn't occur in the input file '${INPUT_FILE}'")
endif()
A CMake variable ${data} is holding multiple paths in a single line as shown below
C:/Users/s.xxx/Documents/CMake/25_10/xxxxx/xxxxx/variants/xxxxx/application/source/xxxC:/Users/s.xxxxx/Documents/CMake/25_10/xxxxx/xxxxx/variants/xxxxx/application/source/common/xxyyC:/Users/s.xxxxx/Documents/CMake/25_10/xxxxx/xxxxx/variants/xxxxx/application/source/common/yyC:/Users/s.xxxxx/Documents/CMake/25_10/xxxxx/xxxxx/variants/xxxxx/application/source/common/yyzzC:/Users/s.xxxxx/Documents/CMake/25_10/xxxxx/xxxxx/variants/xxxxx/application/source/common/zzz
How to bring like shown below using Cmake
C:/Users/s.xxx/Documents/CMake/25_10/xxxxx/xxxxx/variants/xxxxx/application/source/xxx
C:/Users/s.xxxxx/Documents/CMake/25_10/xxxxx/xxxxx/variants/xxxxx/application/source/common/xxyy
C:/Users/s.xxxxx/Documents/CMake/25_10/xxxxx/xxxxx/variants/xxxxx/application/source/common/yy
C:/Users/s.xxxxx/Documents/CMake/25_10/xxxxx/xxxxx/variants/xxxxx/application/source/common/yyzz
C:/Users/s.xxxxx/Documents/CMake/25_10/xxxxx/xxxxx/variants/xxxxx/application/source/common/zzz
I tried to write the variable ${data} to a file test1.txt and tried as below
file(READ ${test1} testread)
string(REGEX REPLACE "C:/" "\ C:/" ${new_line_test} ${testread})
file(WRITE ${Fin_test} ${new_line_test})
Here it is hard coded specific to C:/, what if the project will run in D:/ or in Linux?
How to make it a generic conversion using CMake?
I'm using CMake macros to extract the value of certain variables that I want to export as part of my Find-script:
string(REGEX REPLACE "([0-9]+)\\.[0-9]+" "\\1"
MY_PACKAGE_MAJOR_VERSION "${MY_PACKAGE_VERSION}"
How do I cache this variable after determing its value with a macro?
I've tried this:
set(MY_PACKAGE_MAJOR_VERSION ${MY_PACKAGE_MAJOR_VERSION}
CACHE STRING "Major version of my package.")
But without FORCE, it detects that MY_PACKAGE_MAJOR_VERSION is already set, and will thus not set (and thereby cache) the value output from string.
The only workaround I've found is to use an intermediate variable:
string(REGEX REPLACE "([0-9]+)\\.[0-9]+" "\\1"
_MY_PACKAGE_MAJOR_VERSION "${MY_PACKAGE_VERSION}"
set(MY_PACKAGE_MAJOR_VERSION ${_MY_PACKAGE_MAJOR_VERSION}
CACHE STRING "Major version of my package.")
Is there a better way?
I'm currently working on migrating our current build environment from MSBuild to CMake. I have a situation where I need to update the PATH variable in order for the units tests executable to run. This is not a issue for gtest_add_tests, as it uses the source to identify tests. But gtest_discover_tests, which executes the unit tests with the --gtest_list_tests flag, fails to identify any tests because a STATUS_DLL_NOT_FOUND error is encountered during the build.
For example:
add_executable(gTestExe ...)
target_include_directories(gTestExe ...)
target_compile_definitions(gTestExe ...)
target_link_libraries(gTestExe ...)
set (NEWPATH "/path/to/bin;$ENV{PATH}")
STRING(REPLACE ";" "\\;" NEWPATH "${NEWPATH}")
This works:
gtest_add_tests(TARGET gTestExe TEST_LIST allTests)
set_tests_properties(${all_tests} PROPERTIES ENVIRONMENT "PATH=${NEWPATH}")
But this does not:
#set_target_properties(gTestExe PROPERTIES ENVIRONMENT "PATH=${NEWPATH}")
#set_property(DIRECTORY PROPERTY ENVIRONMENT "PATH=${NEWPATH}")
gtest_discover_tests(gTestExe PROPERTIES ENVIRONMENT "PATH=${NEWPATH}")
Edit:
The tests themselves work when added using gtest_add_tests. The issue is the call to discover the tests, during the post build step that gtest_discover_tests registers, fails because the required libraries are not in the PATH.
I came across the same issue this morning and I found a (dirty ?) workaround. The reason why it won't work is a bit complicated, but the workaround is quite simple.
Why it won't work
gtest_discover_tests(gTestExe PROPERTIES ENVIRONMENT "PATH=${NEWPATH}")
Will not work is because the PATH contents are separated by semicolons and therefore are treated by CMake as a list value.
If you look a the GoogleTestAddTests.cmake file (located in C:\Program Files\CMake\share\cmake-3.17\Modules), it treats the PROPERTIES argument with a foreach.
The PROPERTIES value look like this for CMake at this point in the script : ENVIRONMENT;PATH=mypath;mypath2 and will treat mypath2 as a third argument instead of a value for the PATH environment variable.
CMake will then generate the following line :
set_tests_properties( mytest PROPERTIES ENVIRONMENT PATH=mypath mypath2)
Escaping the ; won't work because the list is automatically expended in add_custom_command() in GoogleTest.cmake (l. 420 in cmake 3.17.1) ignoring any form of escaping.
To prevent the cmake foreach to treat each value in the path as a list you can use a bracket argument like :
gtest_discover_tests(gTestExe PROPERTIES ENVIRONMENT "[==[PATH=${NEWPATH}]==]")
The cmake foreach will then treat your argument as one entity. Unfortunately CMake will also put a bracket in the generated code as it contains [ = and maybe spaces :
# This line
if(_arg MATCHES "[^-./:a-zA-Z0-9_]")
set(_args "${_args} [==[${_arg}]==]")
else()
set(_args "${_args} ${_arg}")
endif()
resulting in the following generated script :
set_tests_properties( mytest PROPERTIES ENVIRONMENT [==[ [==[PATH=mypath;mypath2] ]==])
And when executing the test cmake will attempt to read the value only removing the first bracket argument as they don't nest.
Possible workaround
So to do this we need CMake to not use bracket argument on our own bracket argument.
First make a local copy of GoogleTestAddTests.cmake file in your own repository (located in C:\Program Files\CMake\share\cmake-3.17\Modules).
At the beginning of your local copy of GoogleTestAddTests.cmake (l. 12) replace the function add_command by this one :
function(add_command NAME)
set(_args "")
foreach(_arg ${ARGN})
# Patch : allow us to pass a bracket arguments and escape the containing list.
if (_arg MATCHES "^\\[==\\[.*\\]==\\]$")
string(REPLACE ";" "\;" _arg "${_arg}")
set(_args "${_args} ${_arg}")
# end of patch
elseif(_arg MATCHES "[^-./:a-zA-Z0-9_]")
set(_args "${_args} [==[${_arg}]==]")
else()
set(_args "${_args} ${_arg}")
endif()
endforeach()
set(script "${script}${NAME}(${_args})\n" PARENT_SCOPE)
endfunction()
This will make cmake don't use bracket list on our bracket list and automatically escape the ; as set_tests_properties also treat the ; as a list.
Finally we need CMake to use our custom GoogleTestAddTests.cmake instead of the one in CMake.
After your call to include(GoogleTest) set the variable _GOOGLETEST_DISCOVER_TESTS_SCRIPT to the path to your local GoogleTestAddTests.cmake :
# Need google test
include(GoogleTest)
# Use our own version of GoogleTestAddTests.cmake
set(_GOOGLETEST_DISCOVER_TESTS_SCRIPT
${CMAKE_CURRENT_LIST_DIR}/GoogleTestAddTests.cmake
)
Note : In my example the GoogleTestAddTests.cmake is right next to the processing cmake file.
Then a simple call to
gtest_discover_tests(my_target
PROPERTIES ENVIRONMENT "[==[PATH=${my_path};$ENV{PATH}]==]"
)
should work.
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.*")