| name | presets-vs-kits |
| description | Use when writing or reviewing code that must work in both CMake Presets and Kits/Variants modes. Covers configure, build, test, environment, and generator paths. Triggers: "presets vs kits", "useCMakePresets", "works in both modes".
|
Presets Mode vs Kits/Variants Mode
Guide for writing code that works correctly in both operating modes of CMake Tools. Any shared code path (configure, build, test, environment, targets) must handle both modes.
The two modes
CMake Tools operates in one of two mutually exclusive modes per project folder:
| Presets mode | Kits/variants mode |
|---|
| Source of truth | CMakePresets.json / CMakeUserPresets.json | Kit selection + variant settings |
| When active | cmake.useCMakePresets is 'always', or 'auto' and preset files exist | cmake.useCMakePresets is 'never', or 'auto' and no preset files exist |
| Runtime check | CMakeProject.useCMakePresets === true | CMakeProject.useCMakePresets === false |
How the mode is determined
export type UseCMakePresets = 'always' | 'never' | 'auto';
const usingCMakePresets =
useCMakePresets === 'always' ? true :
useCMakePresets === 'never' ? false :
await this.hasPresetsFiles();
The resolved boolean is stored in CMakeProject._useCMakePresets and exposed via the useCMakePresets getter.
When you MUST handle both modes
Any shared code path that touches configure, build, test, environment setup, or target resolution must branch on the mode. Omitting the check for one mode is a latent bug.
The canonical branching pattern
if (!this.useCMakePresets) {
if (!this.activeKit) {
await vscode.window.showErrorMessage(
localize('cannot.configure.no.kit', 'Cannot configure: No kit is active for this CMake project')
);
return { exitCode: -1, resultType: ConfigureResultType.Other };
}
if (!this.variantManager.haveVariant) {
await this.variantManager.selectVariant();
}
} else if (!this.configurePreset) {
void vscode.window.showErrorMessage(
localize('cannot.configure.no.config.preset', 'Cannot configure: No configure preset is active')
);
return { exitCode: -1, resultType: ConfigureResultType.Other };
}
Another real example — default build targets
if (this.useCMakePresets && (!defaultTarget || defaultTarget === this.targetsInPresetName)) {
targets = this.buildPreset?.targets;
}
if (!this.useCMakePresets && !defaultTarget) {
targets = await this.allTargetName;
}
Canonical data access — Presets mode
Merged preset tree
Use PresetsController (in src/presets/presetsController.ts) to access the merged, expanded preset tree. Never re-parse CMakePresets.json or CMakeUserPresets.json directly — the controller handles include chaining, expansion, and file watching.
Active presets on CMakeProject
CMakeProject.configurePreset
CMakeProject.buildPreset
CMakeProject.testPreset
CMakeProject.packagePreset
CMakeProject.workflowPreset
Preset types
All preset interfaces are defined in src/presets/preset.ts. Key types include ConfigurePreset, BuildPreset, TestPreset, PackagePreset, WorkflowPreset.
CMake executable override
In presets mode, the configure preset can override the CMake path:
const overWriteCMakePathSetting = this.useCMakePresets
? this.configurePreset?.cmakeExecutable
: undefined;
Canonical data access — Kits/variants mode
Active kit
CMakeProject.activeKit
Kit environment
import { effectiveKitEnvironment } from '@cmt/kits/kit';
this._kitEnvironmentVariables = await effectiveKitEnvironment(kit, this.expansionOptions);
This merges compiler paths, VS Developer Environment (on Windows via vcvarsall.bat), and any custom environmentVariables defined on the kit.
SpecialKits — always check before treating a kit as a real compiler
export enum SpecialKits {
ScanForKits = '__scanforkits__',
Unspecified = '__unspec__',
ScanSpecificDir = '__scan_specific_dir__',
}
These sentinel values appear in the kit list as UI actions. Before using kit.compilers, kit.toolchainFile, or kit.preferredGenerator, check:
if (kit.name === SpecialKits.Unspecified || kit.name === SpecialKits.ScanForKits) {
}
Kit interface
export interface Kit extends KitDetect {
name: string;
description?: string;
preferredGenerator?: CMakeGenerator;
cmakeSettings?: Record<string, string | string[]>;
compilers?: Record<string, string>;
toolchainFile?: string;
environmentVariables?: Record<string, string>;
environmentSetupScript?: string;
visualStudio?: string;
visualStudioArchitecture?: string;
}
Variant handling
Variants (src/kits/variant.ts) provide build type and other CMake variable overrides in kits mode. The VariantManager is only initialized when not using presets:
if (!this.useCMakePresets) {
await this.variantManager.initialize(this.folderName);
await drv.setVariant(this.variantManager.activeVariantOptions, ...);
}
Generator handling in both modes
Multi-config vs single-config
Always check the generator before any build-type logic:
import { isMultiConfGeneratorFast } from '@cmt/util';
export function isMultiConfGeneratorFast(gen?: string): boolean {
return gen !== undefined
&& (gen.includes('Visual Studio') || gen.includes('Xcode') || gen.includes('Multi-Config'));
}
| Generator type | Examples | Build type mechanism |
|---|
| Single-config | Ninja, Unix Makefiles | CMAKE_BUILD_TYPE set at configure time |
| Multi-config | Visual Studio, Xcode, Ninja Multi-Config | --config <type> passed at build time |
Exception: cmake.setBuildTypeOnMultiConfig
When this setting is true, CMAKE_BUILD_TYPE is also set at configure time for multi-config generators (used by some CMake scripts that read it). See src/drivers/cmakeDriver.ts.
How it shows up in both modes
- Presets mode: The generator is embedded in
configurePreset.generator. The preset may also set CMAKE_BUILD_TYPE in cacheVariables or use configuration in the build preset.
- Kits mode: The generator comes from
Kit.preferredGenerator.name or is auto-detected. Build type comes from the active variant.
Common mistakes
1. Testing only one mode
If you add or change behavior in a shared code path, you must verify it works in both presets mode and kits/variants mode. Many bugs ship because the developer only tested with presets (or only with kits).
2. Assuming single-config generator
Never assume CMAKE_BUILD_TYPE is the way to set the build configuration. Always check isMultiConfGeneratorFast() first.
args.push(`-DCMAKE_BUILD_TYPE=${buildType}`);
if (!isMultiConfGeneratorFast(generator)) {
args.push(`-DCMAKE_BUILD_TYPE=${buildType}`);
}
3. Reading preset files directly
const presets = JSON.parse(fs.readFileSync('CMakePresets.json', 'utf8'));
const configPreset = this.configurePreset;
4. Not checking for SpecialKits sentinel values
const compiler = kit.compilers?.['C'];
if (kit.name !== SpecialKits.Unspecified && kit.name !== SpecialKits.ScanForKits) {
const compiler = kit.compilers?.['C'];
}
5. Forgetting the driver instance guard
In kits mode, no driver is created without a kit:
if (!this.useCMakePresets && !this.activeKit) {
log.debug(localize('not.starting.no.kits', 'Not starting CMake driver: no kit selected'));
return null;
}
Always handle null driver returns in calling code.
Testing checklist
See also: .github/copilot-instructions.md for project-wide conventions.