Introduction
Understanding scope in CMake is crucial for managing variable visibility and behavior in your build system.
- A function creates a scope where local variables do not affect parent scope variables.
- A macro operates in the caller’s scope, allowing variables change there but treats arguments as immutable strings.
- A subdirectory creates its own scope.
- Function and subdirectory have access to a copy of their parent scope variables.
- Function, macro and subdirectory define globally accessible targets.
This guide explores the accessibility of a variable with regards to the scope it is defined with practical examples to clarify their usage.
Prerequisits
I am assuming
- you had a look at my previous posts on CMake programming
- you have CMake v3.23+ on your machine,
- you have a compiler like GCC, Clang, Intel, or MS Visual C++ installed on your operating system.
I call the variables
- localName: a variable local to function or macro
- parentName: a variable defined in the parent scope of where function or macro is called.
Run CMake snippets
Create a desired directory and CMakeLists.txt
file in it. In a terminal navigate to that directory:
mkdir build
cd build
Then run
cmake ..
You will see the results on screen.
Function Scope
In CMake, function
creates a new scope where it is called. Variables defined in a function are private to the function and NOT defined/accessible outside of the function where it is called. However, anything defined in a macro
is defined where the macro is called. Let’s show it with examples.
In the example below, variable localName
is not defined outside the function where it is called:
function(initFuncLocalDef)
set(localName "particle.h")
endfunction()
initFuncLocalDef() # call function
message("localName=" ${localName}) # prints: localName=
The below example shows that in a function you have access to a copy of parent scope variables, your change to the copies of parent variables is local and has no effect on the parent variables out of the function. Also any variable definitions in a function is private to the function.
function(initFuncParentAccess)
message("In function, access parent, parentName=" ${parentName})
set(parentName "particle.h")
message("In function, after set parent, parentName=" ${parentName})
set(root_dir "/path/to/myRoot00")
endfunction()
set(parentName "sphere.h")
initFuncParentAccess()
message("outside function, parentName=" ${parentName})
message("outside function, root_dir=" ${root_dir})
It produces the below output:
In function, access parent, parentName=sphere.h
In function, after set parent, parentName=particle.h
outside function, parentName=sphere.h
outside function, root_dir=
In the example below, it is shown that function argument similiar to pass by value in C/C++. A copy of a variable is passed to the function, any change to that copy has no effect on the original variable.
function(initFuncLocalAccess localName)
message("In function, before change, localName=" ${localName})
set(localName "particle.h")
message("In function, after change, localName=" ${localName})
endfunction()
set(parentName sphere.h)
initFuncLocalAccess(${parentName})
message("outside function, localName=" ${localName})
message("outside function, parentName=" ${parentName})
It produces this output:
In function, before change, localName=sphere.h
In function, after change, localName=particle.h
outside function, localName=
outside function, parentName=sphere.h
A CMake function also allows you to define a variable in the caller’s (or parent) scope. In the example below, parentName
is changed in the parent scope with PARENT_SCOPE
setting:
function(initFuncLocalAccess)
message("In function, before change, parentName=" ${parentName})
set(parentName "particle.h" PARENT_SCOPE) # sets in parent scope not inside function
message("In function, before change, parentName=" ${parentName})
endfunction()
set(parentName sphere.h)
initFuncLocalAccess()
message("outside function, parentName=" ${parentName})
The outcome is
In function, before change, parentName=sphere.h
In function, before change, parentName=sphere.h
outside function, parentName=particle.h
To also change parentName
locally, you need to write another set without PARENT_SCOPE
.
Now we can define a function that sets a variable in its parent scope, similiar to pass by reference in C/C++:
function(getName outFullName first last)
set(${outFullName} "${first} ${last}" PARENT_SCOPE)
endfunction()
getName(fullName "Chandler" "Bing")
message(${fullName}) # prints Chandler Bing
Macro scope
In contrast to function, a macro doesn’t create a private scope. We can assume that the code in macro is pasted where it is called.
In the example below, parentName is changed inside and outside of macro where it is called. The macro also defined root_dir
which is accessbile outside after it is called.
macro(initMacroParentAccess)
message("In Macro, before set parent, parentName=" ${parentName})
set(parentName "particle.h")
message("In Macro, after set parent, parentName=" ${parentName})
set(root_dir "/path/to/myRoot00")
endmacro()
set(parentName sphere.h)
initMacroParentAccess()
message("outside function, parentName=" ${parentName})
message("outside function, root_dir=" ${root_dir})
It produces below output
In Macro, before set parent, parentName=sphere.h
In Macro, after set parent, parentName=particle.h
outside function, parentName=particle.h
outside function, root_dir=/path/to/myRoot00
Macro argument is different to function argument. We can think of it as a read-only string that cannot be modified. So if you want to change a variable, don’t pass it to macro as argument, just change it in the macro by refering to its parent name. The example below is just to clear this up, but avoid setting macro argument inside macro.
macro(initMacroLocalAccess localName)
message("In Macro, before set arg, localName=" ${localName}) # macro arg string replacement
set(localName "particle.h") # defines localName in the outerscope
message("In Macro, after set arg, localName=" ${localName}) # still macro arg string replacement, doesn't use defined var
endmacro()
set(name sphere.h)
initMacroLocalAccess(${name})
message("outside Macro, localName=" ${localName})
The output is
In Macro, before set arg, localName=sphere.h
In Macro, after set arg, localName=sphere.h
outside Macro, localName=particle.h
Subdirectory scope
Each subdirectory has its own scope. It receives a copy of variables of parent directory, grandparent directory, and so on. However, they don’t have access to sibiling directories.
To show this, I created an example with directory tree of
root/folderA/folderB
root/folderC
each directory, root, folderA, folderB, and folderC, contains file CMakeLists.txt
.
root/CMakeLists.txt is:
cmake_minimum_required(VERSION 3.20)
project(example LANGUAGES CXX)
set(varRoot "varRootIsSet")
add_executable(rootTarget main.cpp)
function(printVars dir)
# some code ....
endfunction()
add_subdirectory(folderA)
add_subdirectory(folderC)
printVars("root")
For the sake of readability, I put the code for function printVars
at the end of this section.
folderA/CMakeLists.txt is:
add_executable(targetA ../main.cpp)
set(varA "VarAIsSet") # order is important, to have folderAVar to go into B, should be defined before
add_subdirectory("folderB")
printVars("folderA")
folderB/CMakeLists.txt is:
set(varB "VarBIsSet")
add_executable(targetB ../../main.cpp)
printVars("folderB")
folderC/CMakeLists.txt is:
add_executable(targetC ../main.cpp)
set(varC "VarCIsSet")
printVars("folderC")
And finally the definition of printVars is:
function(printVars dir)
message("-------")
if(TARGET rootTarget)
message(${dir}": root target is defined.")
endif()
if(TARGET targetA)
message(${dir}": targetA is defined.")
endif()
if(TARGET targetB)
message(${dir}": targetB is defined.")
endif()
if(TARGET targetC)
message(${dir}": targetC is defined.")
endif()
message(${dir}":varRoot:"${varRoot})
message(${dir}":varA:"${varA})
message(${dir}":varB:"${varB})
message(${dir}":varC:"${varC})
endfunction()
By running CMake, we get below terminal output:
-------
folderB": root target is defined."
folderB": targetA is defined."
folderB": targetB is defined."
folderB":varRoot:"varRootIsSet
folderB":varA:"VarAIsSet
folderB":varB:"VarBIsSet
folderB":varC:"
-------
folderA": root target is defined."
folderA": targetA is defined."
folderA": targetB is defined."
folderA":varRoot:"varRootIsSet
folderA":varA:"VarAIsSet
folderA":varB:"
folderA":varC:"
-------
folderC": root target is defined."
folderC": targetA is defined."
folderC": targetB is defined."
folderC": targetC is defined."
folderC":varRoot:"varRootIsSet
folderC":varA:"
folderC":varB:"
folderC":varC:"VarCIsSet
-------
root": root target is defined."
root": targetA is defined."
root": targetB is defined."
root": targetC is defined."
root":varRoot:"varRootIsSet
root":varA:"
root":varB:"
root":varC:"
The summary is:
- Variables defined in root are readable in subdirectories
- Variables in folderA are readable in folderB, but NOT vice versa.
- Variable in folderA and folderB are not readable in folderC.
- Variables in FolderC are not readable in folderA and folderB.
- The targets are accessible everywhere, however, the code flow must define them first.
If you want to set a variable in a subdirectory and the change is seen in the parent directory you can use set with PARENT_SCOPE
like we did with function.
Another option is to make it internal cache variable and it will be globally accessible/editable.
Accessibility of Targets
- A target defined in a function or in macro is globally accessible after they are called.
- A target defined in a subdirectory is globally accessible after the CMake configuration run passes through the subdirectory.
In the example below, defSpaceTargetFunc()
and defSpaceTargetMacro()
can interchangeably be called to define space
target.
function(defSpaceTargetFunc)
add_executable(space)
target_sources(space PUBLIC main.cpp)
endfunction()
macro(defSpaceTargetMacro)
add_executable(space)
target_sources(space PUBLIC main.cpp)
endmacro()
defSpaceTargetFunc() # comment this, and uncomment next line
#defSpaceTargetMacro()
get_target_property(spaceSources space SOURCES)
message("sources are:" ${spaceSources})
Function VS Macro
Function is generally safer and cleaner choice as it doesn’t pollute the caller scope with its private variables.
Latest Posts
- CMake scope: variables and targets in function, macro and subdirectory
- A C++ MPI code for 2D unstructured halo exchange
- Essential bash customizations: prompt, ls, aliases, and history date
- Script to copy a directory path in memory in Bash terminal
- What is the difference between .bashrc, .bash_profile, and .profile?