BUILD: CMake Variables — Types, Scopes, and Manipulation

CMake variables are the backbone of dynamic build configuration. Understanding the difference between built-in and user-defined variables, how to set strings and lists, how variable scope works across functions and directories, and how to access environment variables gives you full control over your CMake build scripts.

Variables in CMake are central to configuring, customizing, and automating your build process. They store paths, version numbers, lists of source files, compiler flags, and any other data you need to reference throughout your build scripts. Understanding how CMake variables work — including their scope and lifetime — is essential for writing maintainable CMake configurations.


Variable Types in CMake

CMake has two broad categories of variables:

CategoryDescription
Built-in variablesPredefined by CMake, control behavior automatically
User-defined variablesCreated by you with set(), string(), or list()

Built-in Variables

CMake provides a large set of built-in variables that control the build process. You can also set them to customize behavior:

cmake
# Common built-in variables

# Paths
CMAKE_SOURCE_DIR         # Top-level source directory (where root CMakeLists.txt lives)
CMAKE_BINARY_DIR         # Top-level build directory
CMAKE_CURRENT_SOURCE_DIR # Source directory of the current CMakeLists.txt
CMAKE_CURRENT_BINARY_DIR # Build directory for the current CMakeLists.txt

# Build type
CMAKE_BUILD_TYPE         # Debug, Release, RelWithDebInfo, MinSizeRel

# Compiler and language
CMAKE_CXX_COMPILER       # C++ compiler path
CMAKE_C_COMPILER         # C compiler path
CMAKE_CXX_STANDARD       # C++ standard (11, 14, 17, 20, 23)

# Platform
CMAKE_SYSTEM_NAME        # Operating system (Linux, Windows, Darwin)
CMAKE_SYSTEM_PROCESSOR   # CPU architecture (x86_64, ARM, etc.)
WIN32                    # True on Windows
UNIX                     # True on UNIX-like systems (Linux, macOS)
APPLE                    # True on macOS/iOS

A comprehensive list is available at: https://gitlab.kitware.com/cmake/community/-/wikis/doc/cmake/Useful-Variables


Creating User-Defined Variables with `set()`

The set() command creates or overwrites a variable:

cmake
# Simple string variable
set(MyString "Some Text")
message("${MyString}")  # Output: Some Text

# Variable referencing another variable
set(MyStringWithVar "Some other Text: ${MyString}")
message("${MyStringWithVar}")  # Output: Some other Text: Some Text

# Variable with embedded quotes (escaped)
set(MyStringWithQuot "A quoted value: \"${MyString}\"")
message("${MyStringWithQuot}")  # Output: A quoted value: "Some Text"

Variable references are wrapped in ${}:

cmake
set(MY_VAR "hello")
message("The value is: ${MY_VAR}")

Boolean Variables

cmake
set(ENABLE_TESTS ON)   # boolean true
set(ENABLE_TESTS OFF)  # boolean false

if(ENABLE_TESTS)
    add_subdirectory(tests)
endif()

Path Variables

cmake
set(SRC_DIR "${CMAKE_SOURCE_DIR}/src")
set(BUILD_OUTPUT "${CMAKE_BINARY_DIR}/output")

add_executable(myapp "${SRC_DIR}/main.cpp")


List Variables

A list in CMake is a variable whose value is a semicolon-separated string. CMake provides the list() command for working with lists:

cmake
# Creating a list
set(SOURCE_FILES
    src/main.cpp
    src/utils.cpp
    src/config.cpp)

# Equivalent to:
set(SOURCE_FILES "src/main.cpp;src/utils.cpp;src/config.cpp")

# Use in a command
add_executable(myapp ${SOURCE_FILES})

List Operations

cmake
# Append to a list
list(APPEND SOURCE_FILES src/new_file.cpp)

# Get list length
list(LENGTH SOURCE_FILES file_count)
message("Number of source files: ${file_count}")

# Get element at index
list(GET SOURCE_FILES 0 first_file)
message("First file: ${first_file}")

# Remove duplicates
list(REMOVE_DUPLICATES SOURCE_FILES)

# Sort
list(SORT SOURCE_FILES)


Variable Scope

CMake has a hierarchical scope system. Understanding scope prevents hard-to-debug issues where variables set in one place are not visible in another.

Directory Scope

Variables set with set() are in directory scope by default. Each CMakeLists.txt has its own scope. Child directories (added with add_subdirectory()) inherit copies of the parent's variables, but changes in the child do not affect the parent:

text
Root CMakeLists.txt        (parent scope)
  set(MY_VAR "parent")
    |
    +-- subdirectory/CMakeLists.txt   (child scope)
          # MY_VAR is "parent" here (inherited copy)
          set(MY_VAR "child")
          # MY_VAR is "child" in this scope
    |
  # MY_VAR is still "parent" here (unchanged)

Function Scope

Variables set inside a CMake function() are local to that function. They do not affect the calling scope:

cmake
function(my_function)
    set(LOCAL_VAR "inside function")
    message("${LOCAL_VAR}")  # Works
endfunction()

my_function()
# LOCAL_VAR is not accessible here

Cache Variables (Global Scope)

Cache variables persist between CMake runs and are visible in all scopes. They are the mechanism behind CMake options:

cmake
# Set a cache variable (visible in cmake-gui, accessible everywhere)
set(MY_OPTION "default_value" CACHE STRING "Description of this option")

# Declare a boolean option (shown in cmake-gui as a checkbox)
option(ENABLE_TESTS "Build the test suite" ON)

Cache variables can be set on the command line:

bash
cmake -DENABLE_TESTS=OFF -DMY_OPTION="custom_value" ..

Parent Scope

To set a variable in the parent scope from inside a function or subdirectory:

cmake
function(set_in_parent)
    set(RESULT "computed_value" PARENT_SCOPE)
endfunction()

set_in_parent()
message("${RESULT}")  # computed_value (set by the function)


The `string()` Command

The string() command provides string manipulation functions:

cmake
# Replace
set(myString "Hello, world!")
string(REPLACE "world" "CMake" myString "${myString}")
message("${myString}")  # Output: Hello, CMake!

# Convert to uppercase
string(TOUPPER "${myString}" upper)
message("${upper}")  # HELLO, CMAKE!

# Convert to lowercase
string(TOLOWER "${upper}" lower)
message("${lower}")  # hello, cmake!

# String length
string(LENGTH "${myString}" len)
message("Length: ${len}")

# Substring
string(SUBSTRING "${myString}" 0 5 sub)
message("First 5 chars: ${sub}")  # Hello

Regex Match

cmake
set(myString "Hello, CMake!")
string(REGEX MATCH "CMake" match "${myString}")
message("${match}")  # Output: CMake

# Extract all matches
string(REGEX MATCHALL "[A-Za-z]+" words "Hello World CMake")
message("${words}")  # Hello;World;CMake (a list)


Environment Variables

CMake can access system environment variables using the $ENV{} syntax:

cmake
# Read an environment variable
message("PATH: $ENV{PATH}")
message("HOME: $ENV{HOME}")

# Use in a condition
if(DEFINED ENV{CI})
    message("Running in CI environment")
    set(TESTING_ENABLED ON)
endif()

# Use in a file path
set(TOOLCHAIN_DIR "$ENV{HOME}/toolchains/arm")

Note: Environment variables are read at configure time (when CMake runs), not at build time. If the environment changes after configuration, you need to re-run CMake.


Variable Manipulation: `get_cmake_property` and `unset`

cmake
# Get the value of a CMake property into a variable
get_cmake_property(all_targets TARGETS)
message("All targets: ${all_targets}")

# Remove a variable
set(TEMP_VAR "temporary")
message("Before unset: ${TEMP_VAR}")
unset(TEMP_VAR)
message("After unset: ${TEMP_VAR}")  # empty


Practical Example: Collecting Source Files

A common pattern is to collect source files dynamically:

cmake
# Using file(GLOB) — easy but not recommended for production
file(GLOB SOURCES "src/*.cpp")

# Better: explicitly list files (CMake will re-run if CMakeLists.txt changes)
set(SOURCES
    src/main.cpp
    src/utils.cpp
    src/parser.cpp)

# Conditional file inclusion
if(WIN32)
    list(APPEND SOURCES src/platform_windows.cpp)
elseif(UNIX)
    list(APPEND SOURCES src/platform_unix.cpp)
endif()

add_executable(myapp ${SOURCES})


Variable Summary

text
CMake Variable Reference:
+------------------------+----------------------------------------+
| Syntax                 | Meaning                                |
+------------------------+----------------------------------------+
| ${VAR}                 | Expand a variable                      |
| $ENV{VAR}              | Expand an environment variable         |
| set(V "val")           | Set a directory-scope variable         |
| set(V "val" CACHE ...) | Set a cache (global persistent) var    |
| set(V "val" PARENT_SCOPE) | Set in the calling scope            |
| unset(V)               | Remove a variable                      |
| if(DEFINED V)          | Check if variable is defined           |
+------------------------+----------------------------------------+


Conclusion

CMake variables are the mechanism through which build configuration flows across your entire project. Built-in variables expose platform, compiler, and path information. User-defined variables let you parameterize every aspect of your build. Understanding scope — directory, function, cache, parent — is essential for writing modular CMake code that behaves predictably.

The next posts in this series cover CMake flow control (if, foreach, while) and functions, which build on variables to enable full scripting capabilities in your build definitions.