Environment Variables and Conditional Presets

Learn to make your CMake presets portable by using environment variables and conditional logic to adapt to different machine configurations.

Greg Filak
Published

In the last two lessons, we've transformed our build process with CMake Presets. We've replaced long, complex command lines with simple names, and we've used inheritance and user presets to create a scalable, layered configuration that separates team-wide standards from personal preferences.

We've solved one major problem, but another remains. Our presets still contain hardcoded paths to tools like vcpkg. Previously, we've overridden these in a CMakeUserPresets.json file:

CMakeUserPresets.json

{
  "version": 3,
  "configurePresets": [{
    "name": "user-windows-debug",
    "inherits": ["windows-debug"],
    "cacheVariables": {
      "VCPKG_TOOLCHAIN_FILE":
        "D:/vcpkg/scripts/buildsystems/vcpkg.cmake"
    }
  }]
}

This works, but still requires every developer to create a file with their specific machine's paths. This isn't ideal, especially for automated build servers which don't have a "user" to create such a file.

This lesson introduces the final pieces of the puzzle. We'll learn how to read environment variables to remove hardcoded paths and how to use conditional presets to make our build configurations intelligently adapt to the machine they're running on.

What are Environment Variables?

An environment variable is a key-value pair that is part of your operating system's "environment". It's a global setting that can be accessed by any program or script, including CMake.

They are the standard, universal mechanism for telling tools where to find things without hardcoding paths. For example, your system's PATH environment variable is a list of directories that your terminal searches to find executables like cmake, vcpkg, and conan.

The convention for a tool like vcpkg is to set an environment variable, often named VCPKG_ROOT, that points to its installation directory.

Setting Environment Variables

Let's set up the VCPKG_ROOT environment variable on our system. The process differs slightly between platforms.

On Windows

  1. In the Start Menu, search for and open "Edit the system environment variables".
  2. In the "Advanced" tab, click the "Environment Variables..." button.
  3. In the "System variables" section at the bottom, click "New...".
  4. For "Variable name", enter VCPKG_ROOT. For Variable value, enter the path you installed vcpkg (the directory where vcpkg.exe is)
  5. Click OK on all windows to save.
  6. Important: You must close and reopen any terminal or IDE for the new variable to take effect.

On macOS and Linux

Open your shell's configuration file in a text editor. This is usually ~/.zshrc for Zsh (the default on modern macOS) or ~/.bashrc for Bash.

Add the following line to the end of the file, replacing the path with your actual vcpkg installation directory.

export VCPKG_ROOT="/path/to/your/vcpkg"

Save the file and restart your terminal, or run source ~/.zshrc (or source ~/.bashrc) to apply the changes immediately.

On MSYS2 UCRT64

The process is the same as for Linux. From within the MSYS2 terminal, edit your ~/.bashrc file and add the export VCPKG_ROOT="..." line.

Testing Environment Variables

You can verify that the variable is set correctly by opening a new terminal and running echo $VCPKG_ROOT on Linux/macOS, echo %VCPKG_ROOT% on Windows Command Prompt, or $env:VCPKG_ROOT on Windows PowerShell.

The output should be the path you just set. For example:

echo $VCPKG_ROOT
~/vcpkg

Using Environment Variables in Presets

Now that we have a standard way to find vcpkg on any machine, we can update our preset to use it.

CMake Presets provide a special syntax, $env{VARIABLE_NAME}, to read the value of an environment variable.

Let's refactor our shared _base preset in CMakePresets.json to use this:

CMakePresets.json

{
  "version": 3,
  "configurePresets": [{
    "name": "_base",
    "hidden": true,
    "binaryDir": "${sourceDir}/build/${presetName}",
    "cacheVariables": {
      "VCPKG_TOOLCHAIN_FILE":
        "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
    }
  } /* Other presets unchanged */]
}

Any developer, on any machine, can now use a preset than inherits from this and have VCPKG_TOOLCHAIN_FILE automatically set. CMake will read their local VCPKG_ROOT environment variable and construct the correct path to the toolchain file at configure time.

They no longer need to create user presets for this - they just need to set their VCPKG_ROOT environment variable.

Conditional Presets

Our preset is portable, but what happens if a developer tries to use it without setting the VCPKG_ROOT environment variable? CMake will try to expand $env{VCPKG_ROOT}, find it's empty, and fail because the path is invalid.

The condition key allows you to specify a condition that must be true for the preset to be considered valid and be shown to the user.

The following shows two examples of a condition:

  • The windows-only preset uses an equals condition to compare two strings, making the preset only available on Windows
  • The mac-or-linux preset uses an inList condition to make the preset only available on Darwin (macOS) or Linux

CMakePresets.json

{
  "version": 3,
  "configurePresets": [{
    "name": "windows-only",
    "condition": {
      "type": "equals",
      "lhs": "${hostSystemName}",
      "rhs": "Windows"
    }
  }, {
    "name": "mac-or-linux",
    "condition": {
      "type": "inList",
      "string": "${hostSystemName}",
      "list": ["Darwin", "Linux"]
    }
  }]
}

If we list our available conditions now then, depending on our system, we should see at most one option is available to us:

cmake --list-presets
Available configure presets:
  "mac-or-linux-preset"

Combining Conditions

What if we also need to check for the presence of our VCPKG_ROOT environment variable? The anyOf or allOf condition type can include a conditions array comprising of multiple checks:

{
  "version": 3,
  "configurePresets": [{
    "name": "windows-vcpkg-preset",
    "condition": {
      "type": "allOf",
      "conditions": [{
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      }, {
        "type": "notEquals",
        "lhs": "$env{VCPKG_ROOT}",
        "rhs": ""
      }]
    }
  }]
}

The allOf type acts like a logical AND. This preset will now only be available if all of the nested conditions are true:

  • the host system is Windows AND
  • the VCPKG_ROOT environment variable is set - that is, it does notEquals an empty string

The official documentation lists a few other conditional types that are available, as well as further variables that can be used similar to how we're using ${hostSystemName} in this example.

Environment Variables in CMakeLists.txt

While presets are the modern way to handle environment-specific configuration, it's also possible to read environment variables directly from within a CMakeLists.txt file.

The syntax is slightly different: $ENV{VARIABLE_NAME} (note the uppercase ENV).

Practical Example

Our build currently requires a VCPKG_TOOLCHAIN_FILE variable to be defined. That definition is expected to come from the command line, either through a -DVCPKG_TOOLCHAIN_FILE argument or by using a --preset that includes it.

However, if someone doesn't provide that argument, we could still make our build work simply by setting the variable within our CMakeLists.txt file. For example:

CMakeLists.txt

cmake_minimum_required(VERSION 3.21)
project(Greeter)

# Only set VCPKG_TOOLCHAIN_FILE if the user hasn't already
if(NOT DEFINED VCPKG_TOOLCHAIN_FILE AND DEFINED ENV{VCPKG_ROOT})
  set(
    VCPKG_TOOLCHAIN_FILE
    "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
    CACHE FILEPATH "vcpkg toolchain file"
  )
  message(STATUS
    "Using VCPKG_ROOT from environment: $ENV{VCPKG_ROOT}"
  )
endif()

add_subdirectory(app)

This script checks if VCPKG_TOOLCHAIN_FILE has already been set. If not, it checks if the VCPKG_ROOT environment variable exists. If it does, it sets the toolchain file itself, caching the result.

Note that the NOT DEFINED VCPKG_TOOLCHAIN_FILE check here is not strictly necessary. As we covered in our earlier lesson on , values provided on the command line will take precedence over our set() commands, unless we add the FORCE argument.

However, including this check anyway makes our intent clearer, and it also ensures our message() only shows when the set() command is effective.

Summary

In this final lesson on presets, we've learned how to decouple our build configurations from the specific machines they run on, creating portable and user-friendly workflows.

  • Environment Variables: They are the standard way to provide machine-specific information, like paths to tools, to your build system.
  • Reading Environment Variables in Presets: Use the $env{VAR_NAME} syntax inside your CMakePresets.json.
  • Conditional Presets: Use the "condition" key to control when a preset is available. This is used for creating presets that only appear when the necessary tools or environment variables are present.
  • Reading Environment Variables in CMakeLists.txt: For fallbacks or older CMake versions, you can use $ENV{VAR_NAME} directly in your scripts, but the preset approach is generally preferred.
Next Lesson
Lesson 40 of 41

Generator Expressions and Conditional Logic

Learn how to use generator expressions $<...> for build-time conditional logic.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate