Fixing Ninja Multi-Config Ignoring Build Type in CMake Presets

Summary

A developer encountered a significant issue where Ninja Multi-Config generators ignored intended build configurations, defaulting to Debug despite explicit attempts to set CMAKE_DEFAULT_BUILD_TYPE and CMAKE_BUILD_TYPE within a buildPreset. This resulted in a mismatch between the developer’s intent and the actual binary produced during the build phase.

Root Cause

The fundamental issue lies in a misunderstanding of the lifecycle stages in the CMake workflow.

  • Configuration vs. Build Phase: The developer attempted to pass configuration variables (like CMAKE_BUILD_TYPE) inside a buildPreset.
  • Scope Mismatch: In CMakePresets.json, configurePresets are used during the generation phase, while buildPresets are used during the build phase.
  • Generator Behavior: The Ninja Multi-Config generator is designed to support multiple configurations in a single build directory. Therefore, it relies on the CMAKE_CONFIGURATION_TYPES variable set during the configure step to know which configurations are valid.
  • Variable Irrelevance: Setting CMAKE_BUILD_TYPE inside a buildPreset‘s environment block is ineffective because the build tool (Ninja) expects the configuration to be specified via the --config flag at build-time, or it defaults to the first entry in CMAKE_CONFIGURATION_TYPES if no flag is provided.

Why This Happens in Real Systems

This happens because modern build systems like CMake have moved toward a two-stage abstraction model:

  • Stage 1 (Configure/Generate): Defines the project structure, available options, and the set of possible build types.
  • Stage 2 (Build): Selects one or more of those pre-defined types to actually compile.

When engineers try to apply “Single-Config” logic (where one directory equals one build type) to “Multi-Config” generators, they create a leaky abstraction. They assume that setting a variable in the build stage will influence the generation stage, which is architecturally impossible.

Real-World Impact

  • CI/CD Failures: Automated pipelines might build the wrong binary (e.g., a Debug build instead of a Release build), leading to massive performance regressions in production or incorrect testing results.
  • Developer Friction: Engineers waste hours debugging “why my optimizations aren’t working” only to realize the compiler flags were never applied.
  • Binary Bloat: Accidentally shipping Debug symbols to production because the build command defaulted to the first available type in the list.

Example or Code

To fix this, the configuration must be handled in the configurePreset, and the selection must be handled in the buildPreset via the configuration field, not the environment field.

{
  "version": 3,
  "cmakeMinimumRequired": {
    "major": 3,
    "minor": 22
  },
  "configurePresets": [
    {
      "name": "base",
      "generator": "Ninja Multi-Config",
      "binaryDir": "${sourceDir}/build",
      "cacheVariables": {
        "CMAKE_CONFIGURATION_TYPES": "Debug;RelWithDebug;Release"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "relwithdebug",
      "configurePreset": "base",
      "configuration": "RelWithDebug"
    },
    {
      "name": "debug",
      "configurePreset": "base",
      "configuration": "Debug"
    }
  ]
}

How Senior Engineers Fix It

Senior engineers focus on the separation of concerns:

  1. Define Capability in Configure: Use configurePresets to define the universe of possibilities using CMAKE_CONFIGURATION_TYPES.
  2. Define Selection in Build: Use the configuration key within buildPresets. This key is specifically designed to pass the --config <type> argument to the underlying build tool.
  3. Avoid Environment Variable Overload: Instead of polluting the environment block with CMAKE_BUILD_TYPE, they use the explicit configuration property provided by the CMake Presets schema.

Why Juniors Miss It

  • Linear Thinking: Juniors often view a build as a single continuous process rather than two distinct, decoupled phases (Generate $\rightarrow$ Build).
  • Over-reliance on Environment Variables: There is a tendency to believe that if a variable exists in CMake, setting it in the OS environment will “force” it to work everywhere.
  • Tooling Obfuscation: IDEs like CLion or VS Code often hide the actual command-line arguments being passed. If the IDE is automatically appending --config Debug, a junior might assume their configuration file is broken, whereas the issue is actually how the IDE’s preset parser maps to the build command.

Leave a Comment