Chapter 16: Writing CMake Presets_《Modern CMake for C++》_Notes

Chapter 16: Writing CMake Presets


1. Preset Types and Structure

CMake supports 6 preset types with dedicated JSON schemas:

  • Configure Presets (configurePresets)
    Define CMake configuration options (generator, toolchain, variables).

  • Build Presets (buildPresets)
    Specify build parameters (targets, parallelism, configuration type).

  • Test Presets (testPresets)
    Configure CTest execution (test selection, output formatting).

  • Package Presets (packagePresets)
    Customize CPack packaging (formats, components).

  • Workflow Presets (workflowPresets)
    Chain multiple stages (configure → build → test → package).

  • Install Presets (installPresets) (New in CMake 3.28)
    Define installation paths and components.

Example File Structure (CMakePresets.json):

{
  "version": 6,
  "cmakeMinimumRequired": { "major": 3, "minor": 28 },
  "configurePresets": [
    {
      "name": "linux-debug",
      "displayName": "Linux Debug",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
    }
  ],
  "buildPresets": [
    {
      "name": "build-linux-debug",
      "configurePreset": "linux-debug",
      "configuration": "Debug"
    }
  ]
}

2. Variable Substitution and Macros

  • Use ${variable} syntax for dynamic values.
  • Predefined Variables:
    • ${sourceDir}: Project root directory
    • ${presetName}: Current preset’s name
    • ${dollar}: Literal $ character

Macro Example:

{
  "configurePresets": [
    {
      "name": "base",
      "cacheVariables": {
        "ENABLE_TESTS": "${{TESTING_ENABLED}}"
      }
    }
  ]
}

Use -DTESTING_ENABLED=ON at command line to override.


3. Inheritance and Conditions

  • Inheritance: Reduce duplication via "inherits": ["parent-preset"].
  • Conditions: Enable platform-specific logic with "condition".

Conditional Example:

{
  "configurePresets": [
    {
      "name": "windows-x64",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      },
      "architecture": "x64"
    }
  ]
}

4. Workflow Presets

Orchestrate multi-stage workflows:

{
  "workflowPresets": [
    {
      "name": "full-release",
      "steps": [
        { "type": "configure", "name": "win-release" },
        { "type": "build", "name": "build-win-release" },
        { "type": "test", "name": "test-release" },
        { "type": "package", "name": "package-msi" }
      ]
    }
  ]
}

Execute with:

cmake --workflow --preset=full-release

Code Walkthrough: Cross-Platform Project Setup

Scenario:
  • Windows: Use Visual Studio generator, build Release configuration.
  • Linux: Use Ninja generator, build Debug with Clang.
CMakePresets.json:
{
  "version": 6,
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": 20,
        "CMAKE_EXPORT_COMPILE_COMMANDS": true
      }
    },
    {
      "name": "windows-msvc",
      "inherits": "base",
      "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" },
      "generator": "Visual Studio 17 2022",
      "architecture": "x64",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
    },
    {
      "name": "linux-clang",
      "inherits": "base",
      "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" },
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/clang-debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_COMPILER": "clang++"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "build-windows",
      "configurePreset": "windows-msvc",
      "targets": ["ALL_BUILD"]
    },
    {
      "name": "build-linux",
      "configurePreset": "linux-clang",
      "parallel": 8
    }
  ]
}
Usage:
  • Windows:

    cmake --preset=windows-msvc
    cmake --build --preset=build-windows
    
  • Linux:

    cmake --preset=linux-clang
    cmake --build --preset=build-linux -j 8
    

Common Pitfalls & Solutions

  1. JSON Syntax Errors

    • Always validate with cmake --list-presets.
    • Use JSON linters (VS Code JSON validation).
  2. Variable Expansion Issues

    • Escape $ as "${dollar}{variable}" for non-CMake variables.
    • Prefer cacheVariables over environmentVariables for cross-platform compatibility.
  3. Condition Logic Failures

    • Test conditions locally:
      cmake --debug-output -S . --preset=my-preset
      
  4. Inheritance Conflicts

    • Later presets override earlier ones. Order matters in inherits arrays.

Key Takeaways

  • Presets standardize build environments across teams/CI.
  • Use inheritance to eliminate redundancy.
  • Conditions enable platform/context-aware configurations.
  • Workflows automate multi-stage processes.

Multiple Choice Questions

1. Which of the following are required fields in a configuration-stage preset? (Select two)
A. name
B. binaryDir
C. toolchainFile
D. generator

2. When defining a workflow preset, which stages must be explicitly included? (Select two)
A. configure
B. build
C. test
D. package

3. Which syntax is correct for using environment variables in a preset condition? (Select two)
A. "$env{ENV_VAR}"
B. "${ENV_VAR}"
C. "$ENV{ENV_VAR}"
D. "${env.ENV_VAR}"

4. What is the correct way to inherit presets? (Select two)
A. Use inherits: [parent_preset] in the preset definition.
B. Use extends: parent_preset in the preset definition.
C. List multiple presets in the inherits field to combine their settings.
D. Manually copy all fields from the parent preset.

5. Which fields are unique to the build-stage preset? (Select two)
A. jobs
B. targets
C. configuration
D. verbose

6. How do you enforce a preset to only run on Linux? (Select two)
A. Use condition: "$system.name == 'Linux'"
B. Use condition: "$host.system.name == 'Linux'"
C. Add platforms: ["Linux"] to the preset.
D. Use a macro like #ifdef LINUX in the preset.

7. Which statements about cacheVariables in presets are true? (Select two)
A. They override variables set in CMakeLists.txt.
B. They are stored in CMakeCache.txt.
C. They can only be set in configuration-stage presets.
D. They support generator expressions.

8. How do you define a custom macro in a CMake preset? (Select two)
A. Use #define CUSTOM_MACRO value in the preset file.
B. Define it in the macros field of the preset.
C. Use set(MACRO value) in a CMakeLists.txt file.
D. Use the define keyword in the preset condition.

9. What is the purpose of the package stage preset? (Select two)
A. To generate installation scripts.
B. To create distributable packages using CPack.
C. To install built targets to the system.
D. To configure packaging components.

10. Which are valid ways to specify the build directory in a preset? (Select two)
A. binaryDir: "build/${presetName}"
B. buildDir: "build"
C. binaryDir: "$env{BUILD_DIR}"
D. binaryDir: "${sourceDir}/build"


Answers and Explanations


1. A, D

  • name and generator are required for configuration presets. binaryDir is optional (defaults to ./build). toolchainFile is optional.

2. A, B

  • A workflow preset must include at least configure and build. test and package are optional.

3. A, C

  • Correct syntaxes are "$env{ENV_VAR}" (CMake 3.25+) and "$ENV{ENV_VAR}". ${ENV_VAR} is invalid, and ${env.ENV_VAR} is incorrect syntax.

4. A, C

  • Presets use the inherits field to list parent presets. Multiple inheritance is allowed. extends is not a valid keyword.

5. A, B

  • jobs (parallel job count) and targets (build targets) are build-stage specific. configuration and verbose can appear in other stages.

6. B, C

  • condition: "$host.system.name == 'Linux'" checks the host OS. The platforms field restricts the preset to specific OSs. $system.name is invalid.

7. B, C

  • cacheVariables are stored in CMakeCache.txt and can only be set during the configuration stage. They do not override CMakeLists.txt variables set via set(... CACHE).

8. B, C

  • Macros are defined in the macros field of a preset or via set() in CMakeLists.txt. #define and define are invalid in presets.

9. B, D

  • The package stage invokes CPack to create packages (e.g., .deb, .zip) and configures components. Installation is handled by the install stage.

10. A, D

  • binaryDir supports variables (e.g., ${presetName}) and paths relative to sourceDir. buildDir is not a valid field. $env{BUILD_DIR} requires CMake 3.25+.

Medium Difficulty Questions:


Question 1: Cross-Platform Build Preset Configuration
Task:
Create a CMakePresets.json file that:

  1. Defines a configuration preset named “linux-macos-config” using the Ninja generator for Linux/macOS.
  2. Defines another configuration preset named “windows-config” using Visual Studio 17 2022 for Windows.
  3. Both presets should inherit from a common base preset specifying a buildDir of ${sourceDir}/build.
  4. Use conditionals to automatically select the appropriate preset based on the host OS.

Requirements:

  • Use ${host.os} in conditions.
  • Avoid hardcoding toolchain paths.

Question 2: Test Preset with Conditional Parameters
Task:
Define a test preset in CMakePresets.json that:

  1. Inherits from a build preset named “debug-build”.
  2. Sets CTEST_OUTPUT_ON_FAILURE=1 only for Debug configurations.
  3. Adds a --timeout 300 argument to CTest only for Release configurations.
  4. Uses environment variables and command-line arguments conditionally.

Constraints:

  • Use $if conditions with $lower${presetName} to detect “debug” in the inherited build preset.

Hard Difficulty Question:

Question 3: Multi-Stage Workflow with Component Packaging
Task:
Create a workflow preset named “package-all” in CMakePresets.json that:

  1. Chains configuration → build → test → package stages.
  2. Packages both a .tar.gz and .zip on Linux/macOS, but only .zip on Windows.
  3. For shared libraries, ensure versioned symlinks (e.g., libfoo.so.1 → libfoo.so.1.2.3) are created during installation.
  4. Use a custom macro $env{PACKAGE_FORMATS} to define formats per OS.

Requirements:

  • Use $if conditions with $host.os and environment variables.
  • Ensure the package stage uses CPack with component-specific settings.

Answers & Explanations:


Answer 1: Cross-Platform Build Preset Configuration

{
  "version": 6,
  "cmakeMinimumRequired": { "major": 3, "minor": 23 },
  "configurePresets": [
    {
      "name": "base-config",
      "hidden": true,
      "binaryDir": "${sourceDir}/build"
    },
    {
      "name": "linux-macos-config",
      "inherits": "base-config",
      "generator": "Ninja",
      "condition": { "string": { "$host.os": "Linux|Darwin" } }
    },
    {
      "name": "windows-config",
      "inherits": "base-config",
      "generator": "Visual Studio 17 2022",
      "architecture": "x64",
      "condition": { "string": { "$host.os": "Windows" } }
    }
  ]
}

Explanation:

  • The base-config sets binaryDir to avoid redundancy.
  • linux-macos-config and windows-config use condition with $host.os to activate on specific OSes.
  • Ninja is used for Linux/macOS, while Visual Studio is selected for Windows.

Answer 2: Test Preset with Conditional Parameters

{
  "version": 6,
  "cmakeMinimumRequired": { "major": 3, "minor": 23 },
  "buildPresets": [
    { "name": "debug-build", "configuration": "Debug" }
  ],
  "testPresets": [
    {
      "name": "run-tests",
      "inherits": "debug-build",
      "environment": {
        "CTEST_OUTPUT_ON_FAILURE": "$if:$lower{$presetName}~debug:1"
      },
      "args": [
        "$if:$lower{$presetName}~release:--timeout,300"
      ]
    }
  ]
}

Explanation:

  • The test preset inherits from debug-build to get Debug configuration.
  • CTEST_OUTPUT_ON_FAILURE is set to 1 only if the inherited build preset name contains “debug”.
  • --timeout 300 is added as a CTest argument for Release configurations.

Answer 3: Multi-Stage Workflow with Component Packaging

{
  "version": 6,
  "cmakeMinimumRequired": { "major": 3, "minor": 23 },
  "configurePresets": [ ... ],
  "buildPresets": [ ... ],
  "testPresets": [ ... ],
  "packagePresets": [
    {
      "name": "package-unix",
      "generator": "TGZ",
      "configurations": ["Release"],
      "environment": { "PACKAGE_FORMATS": "TGZ;ZIP" },
      "condition": { "string": { "$host.os": "Linux|Darwin" } }
    },
    {
      "name": "package-win",
      "generator": "ZIP",
      "condition": { "string": { "$host.os": "Windows" } }
    }
  ],
  "workflowPresets": [
    {
      "name": "package-all",
      "steps": [
        { "type": "configure", "name": "linux-macos-config" },
        { "type": "build", "name": "release-build" },
        { "type": "test", "name": "run-tests" },
        { 
          "type": "package", 
          "name": "$if:$host.os~Windows:package-win,package-unix",
          "environment": { "PACKAGE_FORMATS": "$env{PACKAGE_FORMATS}" }
        }
      ]
    }
  ]
}

Explanation:

  • Component Symlinks: Ensure CMake installs shared libraries with LIBRARY_OUTPUT_NAME and uses install(CODE "...") to create symlinks.
  • Workflow: The package-all preset chains configure → build → test → package.
  • Conditional Formats: package-unix sets PACKAGE_FORMATS to TGZ;ZIP, while Windows uses only ZIP.
  • Macro Usage: $env{PACKAGE_FORMATS} passes formats to CPack via environment variables.

These exercises reinforce key Chapter 16 concepts: preset inheritance, OS-specific conditions, workflow chaining, and macro usage.

你可能感兴趣的:(C/C++,c++,软件构建,笔记)