Skip to content

Commit 7f93968

Browse files
committed
Enhance generator mismatch detection and clean-up logic (#4892)
* Enhance generator mismatch detection and clean-up logic - Added a new method to clean prior configurations if the generator changes.. - Removed deprecated functions related to Visual Studio generator mapping. - Introduced unit tests to verify generator mismatch detection and kit change logic. * updating documentation * improve Visual Studio generator handling and add tests for version mapping - Modify kit scanning logic to derive preferred generators for Visual Studio kits lacking a predefined generator. - Introduce utility functions for mapping Visual Studio versions to CMake generators. - Add unit tests to verify correct generator mapping for various Visual Studio versions. * took in more feedback * adjust changelog --------- Co-authored-by: Hannia Valera <[email protected]>
1 parent 536a24a commit 7f93968

6 files changed

Lines changed: 301 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# What's New?
22

3+
## 1.23.52
4+
5+
Bug Fixes:
6+
- Fix regression where Visual Studio kits with an existing Ninja-based build cache would fail due to a generator mismatch. Ninja is now preferred again when available, stale VS kits derive the correct generator at runtime as a fallback, and the build directory is auto-cleaned on generator mismatches. [#4890](https://github.com/microsoft/vscode-cmake-tools/issues/4890)
7+
38
## 1.23
49

510
Features:

docs/kits.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ A _kit_ defines project-agnostic and configuration-agnostic info about how to bu
66
- A Visual Studio installation: building for Visual Studio involves more than just finding the necessary compiler executable. Visual C++ requires certain environment variables to be set to tell it how to find and link to the Visual C++ toolchain headers and libraries.
77
- A toolchain file: The low-level way to instruct CMake how to compile and link for a target. CMake Tools handles toolchain files using kits.
88

9-
Kits are mostly CMake-generator-agnostic (a CMake generator writes the input files for the native build system). Visual Studio kits have a preferred generator that will be used as a fallback to ensure a matching MSBuild and .sln generator are used for the Visual C++ compiler.
9+
Kits are mostly CMake-generator-agnostic (a CMake generator writes the input files for the native build system). The exception is Visual Studio kits: when you [scan for kits](#scan-for-kits), CMake Tools looks up the VS version and sets `preferredGenerator` on the kit to the matching CMake generator (e.g., `"Visual Studio 18 2026"` for VS 2026). If a kit was scanned before CMake Tools added support for that VS version, the extension derives the correct VS generator at runtime as a last-resort fallback — it is tried only after other generators like Ninja. If the version can't be determined, the kit falls through to default generators.
1010

1111
> **Note:**
12-
> * If you use the [Ninja](https://ninja-build.org/) build system, don't worry about Visual Studio CMake Generators. CMake Tools will prefer Ninja if it is present, unless configured otherwise.
12+
> * If you use the [Ninja](https://ninja-build.org/) build system, don't worry about Visual Studio CMake Generators. CMake Tools will prefer Ninja if it is present, unless configured otherwise or the kit has a `preferredGenerator` set at scan time. To explicitly use a specific generator, set `cmake.generator` in your settings.
1313
> * If you change the active kit while a project is configured, the project configuration will be re-generated with the chosen kit.
14+
> * When the selected generator doesn't match what's already in an existing `CMakeCache.txt`, CMake Tools cleans the prior configuration instead of letting CMake error out.
1415
> * Using a kit is recommended but optional. If you don't use a kit, CMake will perform its own automatic detection.
1516
1617
## How kits are found and defined
@@ -52,7 +53,7 @@ Update [user-local kits](#user-local-kits) by running **Scan for Kits** from the
5253

5354
- CMake tools includes `vswhere.exe`, which it uses to find Visual Studio instances installed on the system.
5455

55-
- For each of `x86`, `amd64`, `x86_amd64`, `x86_arm`, `x86_arm64`, `amd64_x86`, `amd64_arm`, and `amd64_arm64`, CMake Tools checks for installed Visual C++ environments. A kit is generated for each existing MSVC toolchain that is found.
56+
- For each of `x86`, `amd64`, `x86_amd64`, `x86_arm`, `x86_arm64`, `amd64_x86`, `amd64_arm`, and `amd64_arm64`, CMake Tools checks for installed Visual C++ environments. A kit is generated for each existing MSVC toolchain that is found. For known VS versions (2019, 2022, 2026, etc.), the kit gets a `preferredGenerator` pointing at the right CMake generator, like `"Visual Studio 18 2026"`. If the VS version wasn't recognized at scan time, CMake Tools derives the correct generator at runtime as a last-resort fallback. Re-running **Scan for Kits** will set `preferredGenerator` permanently.
5657

5758
**3. Save results to the user-local kits file**
5859

@@ -147,7 +148,15 @@ The following additional options may be specified:
147148

148149
`preferredGenerator`
149150

150-
> The CMake generator that should be used with this kit if not the default. CMake Tools will still search in `cmake.preferredGenerators` from `settings.json`, but will fall back to this option if no generator from the user settings is available
151+
> The CMake generator to use with this kit if not the default. For Visual Studio kits, this is set during [kit scanning](#scan-for-kits) based on the VS version. When picking a generator, CMake Tools checks these in order:
152+
>
153+
> 1. `cmake.generator` from your settings — if set, this wins outright; nothing below is consulted.
154+
> 2. The kit's `preferredGenerator` (set at scan time for VS kits).
155+
> 3. `cmake.preferredGenerators` from your settings, in order.
156+
> 4. `Ninja`, then `Unix Makefiles` — only consulted when neither #2 nor #3 produced any candidate (i.e. the kit has no `preferredGenerator` and `cmake.preferredGenerators` is empty).
157+
> 5. For VS kits that have no `preferredGenerator` of their own: the VS generator derived at runtime from the kit's VS version — pushed to the end of the candidate list, so it's tried after the Ninja/Unix Makefiles fallback when both apply.
158+
>
159+
> If a VS kit was scanned before the VS version mapping existed, CMake Tools derives the correct generator at runtime (#5) — but only after Ninja and Unix Makefiles. To make the VS generator the unconditional choice, either re-run **Scan for Kits** so the kit gets its own `preferredGenerator`, or set `cmake.generator` in your settings.
151160
152161
`cmakeSettings`
153162

src/drivers/cmakeDriver.ts

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import * as codeModel from '@cmt/drivers/codeModel';
3232
import { Environment, EnvironmentUtils } from '@cmt/environmentVariables';
3333
import { CMakeTask, CMakeTaskProvider, CustomBuildTaskTerminal } from '@cmt/cmakeTaskProvider';
3434
import { getValue } from '@cmt/presets/preset';
35-
import { CacheEntry } from '@cmt/cache';
35+
import { CacheEntry, CMakeCache } from '@cmt/cache';
3636
import { CMakeBuildRunner } from '@cmt/cmakeBuildRunner';
3737
import { DebuggerInformation } from '@cmt/debug/cmakeDebugger/debuggerConfigureDriver';
3838
import { onBuildSettingsChange, onTestSettingsChange, onPackageSettingsChange } from '@cmt/ui/util';
@@ -148,6 +148,18 @@ export interface InstallPath {
148148
*
149149
* This class defines the basis for what a driver must implement to work.
150150
*/
151+
/**
152+
* Compare a new generator name against a cached CMAKE_GENERATOR value.
153+
* Returns true when both are defined and differ (i.e. a mismatch that needs cleaning).
154+
* Pure function — no I/O — exported for direct unit testing.
155+
*/
156+
export function generatorMismatch(newGeneratorName: string | undefined, cachedGeneratorValue: string | undefined): boolean {
157+
if (!newGeneratorName || !cachedGeneratorValue) {
158+
return false;
159+
}
160+
return cachedGeneratorValue !== newGeneratorName;
161+
}
162+
151163
export abstract class CMakeDriver implements vscode.Disposable {
152164
/**
153165
* Do the configuration process for the current project.
@@ -622,6 +634,26 @@ export abstract class CMakeDriver implements vscode.Disposable {
622634
}
623635
}
624636

637+
/**
638+
* Check if the generator to be used differs from what is cached in CMakeCache.txt.
639+
*/
640+
protected async _hasGeneratorChanged(newGeneratorName: string | undefined): Promise<boolean> {
641+
if (!newGeneratorName) {
642+
return false;
643+
}
644+
const cachePath = this.cachePath;
645+
if (!await fs.exists(cachePath)) {
646+
return false;
647+
}
648+
const cache = await CMakeCache.fromPath(cachePath);
649+
const cachedGenerator = cache.get('CMAKE_GENERATOR');
650+
if (generatorMismatch(newGeneratorName, cachedGenerator?.value)) {
651+
log.info(localize('generator.changed', 'Generator changed from {0} to {1}; cleaning prior configuration', cachedGenerator!.value, newGeneratorName));
652+
return true;
653+
}
654+
return false;
655+
}
656+
625657
/**
626658
* Remove the entire build directory.
627659
*/
@@ -799,7 +831,8 @@ export abstract class CMakeDriver implements vscode.Disposable {
799831
await this._refreshExpansions();
800832
const scope = this.workspaceFolder ? vscode.Uri.file(this.workspaceFolder) : undefined;
801833
const newBinaryDir = util.lightNormalizePath(await expand.expandString(this.config.buildDirectory(this.isMultiProject, scope), this.expansionOptions));
802-
if (needsCleanIfKitChange && (newBinaryDir === oldBinaryDir)) {
834+
const generatorChanged = await this._hasGeneratorChanged(this._generator?.name);
835+
if ((needsCleanIfKitChange || generatorChanged) && (newBinaryDir === oldBinaryDir)) {
803836
await this._cleanPriorConfiguration();
804837
}
805838
});
@@ -811,15 +844,9 @@ export abstract class CMakeDriver implements vscode.Disposable {
811844
log.debug(localize('cmakedriver.kit.set.to', 'CMakeDriver Kit set to {0}', kit.name));
812845
this._kitEnvironmentVariables = await effectiveKitEnvironment(kit, this.expansionOptions);
813846

814-
// Place a kit preferred generator at the front of the list
815-
// For VS kits that don't have preferredGenerator (e.g., scanned before a VS version was added),
816-
// try to derive it from the VS installation.
817-
let kitPreferredGenerator = kit.preferredGenerator;
818-
if (!kitPreferredGenerator && kit.visualStudio) {
819-
kitPreferredGenerator = await getVsKitPreferredGenerator(kit);
820-
}
821-
if (kitPreferredGenerator) {
822-
preferredGenerators.unshift(kitPreferredGenerator);
847+
// Place a kit preferred generator at the front of the list.
848+
if (kit.preferredGenerator) {
849+
preferredGenerators.unshift(kit.preferredGenerator);
823850
}
824851

825852
// If no preferred generator is defined by the current kit or the user settings,
@@ -833,6 +860,16 @@ export abstract class CMakeDriver implements vscode.Disposable {
833860
preferredGenerators.push({ name: "Unix Makefiles" });
834861
}
835862

863+
// For VS kits that don't have preferredGenerator (e.g., scanned before
864+
// a VS version was added), derive the VS generator as a last-resort
865+
// fallback so it is tried only after Ninja / Unix Makefiles.
866+
if (!kit.preferredGenerator && kit.visualStudio) {
867+
const derived = await getVsKitPreferredGenerator(kit);
868+
if (derived) {
869+
preferredGenerators.push(derived);
870+
}
871+
}
872+
836873
// Use the "best generator" logic only if the user did not define a particular
837874
// generator to be used via the `cmake.generator` setting.
838875
if (this.config.generator) {
@@ -1601,6 +1638,20 @@ export abstract class CMakeDriver implements vscode.Disposable {
16011638
return { exitCode: -2, resultType: ConfigureResultType.Other };
16021639
}
16031640

1641+
// Safety net for the kits/variants path: if a setting change
1642+
// (cmake.generator / cmake.preferredGenerators) reloaded the driver
1643+
// without going through setKit(), the cached CMAKE_GENERATOR may
1644+
// differ from this._generator. Clean the prior configuration here
1645+
// so CMake doesn't fail with a "generator changed" error.
1646+
// Presets handle this in setConfigurePreset via configurePresetChangeNeedsClean.
1647+
if (!this.useCMakePresets
1648+
&& !showCommandOnly
1649+
&& !shouldUseCachedConfiguration
1650+
&& trigger !== ConfigureTrigger.configureWithCache
1651+
&& await this._hasGeneratorChanged(this._generator?.name)) {
1652+
await this._cleanPriorConfiguration();
1653+
}
1654+
16041655
let expanded_flags: string[];
16051656
let defaultPresetName: string | undefined;
16061657
if (this.useCMakePresets) {

src/kits/kit.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -801,8 +801,8 @@ const VsGenerators: { [key: string]: string } = {
801801
};
802802

803803
/**
804-
* Get the CMake generator name for a given Visual Studio version
805-
* @param version The major version of Visual Studio (e.g., '17' for VS 2022, '18' for VS 2026)
804+
* Get the CMake generator name for a given Visual Studio major version.
805+
* @param version The major version string (e.g., '17', '18')
806806
* @returns The CMake generator name, or undefined if the version is not recognized
807807
*/
808808
export function vsGeneratorForVersion(version: string): string | undefined {
@@ -844,12 +844,10 @@ export async function getVsKitPreferredGenerator(kit: Kit): Promise<CMakeGenerat
844844
const majorVersion = parseInt(vsInstall.installationVersion);
845845
const hostArch = kit.visualStudioArchitecture;
846846
const host: string = hostArch.toLowerCase().replace(/ /g, "").startsWith("host=") ? hostArch : "host=" + hostArch;
847-
// For VS kits, use the hostArch as the target platform (x64 -> x64)
848-
const targetArch = hostArch;
849847

850848
return {
851849
name: generatorName,
852-
platform: generatorPlatformFromVSArch[targetArch] as string || targetArch,
850+
platform: generatorPlatformFromVSArch[hostArch] as string || hostArch,
853851
toolset: majorVersion < 15 ? undefined : host
854852
};
855853
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/* eslint-disable no-unused-expressions */
2+
import { expect } from 'chai';
3+
import { CMakeCache, CacheEntry } from '@cmt/cache';
4+
import { generatorMismatch } from '@cmt/drivers/cmakeDriver';
5+
6+
suite('Generator mismatch detection', () => {
7+
suite('generatorMismatch (pure helper)', () => {
8+
test('Returns true when generators differ', () => {
9+
expect(generatorMismatch('Visual Studio 18 2026', 'Ninja')).to.be.true;
10+
});
11+
12+
test('Returns false when generators match', () => {
13+
expect(generatorMismatch('Ninja', 'Ninja')).to.be.false;
14+
});
15+
16+
test('Returns false when new generator is undefined', () => {
17+
expect(generatorMismatch(undefined, 'Ninja')).to.be.false;
18+
});
19+
20+
test('Returns false when cached generator is undefined', () => {
21+
expect(generatorMismatch('Ninja', undefined)).to.be.false;
22+
});
23+
24+
test('Returns false when both are undefined', () => {
25+
expect(generatorMismatch(undefined, undefined)).to.be.false;
26+
});
27+
28+
test('Returns false when cached value is empty string', () => {
29+
expect(generatorMismatch('Ninja', '')).to.be.false;
30+
});
31+
32+
test('Returns true for different VS versions', () => {
33+
expect(generatorMismatch('Visual Studio 18 2026', 'Visual Studio 17 2022')).to.be.true;
34+
});
35+
36+
test('Returns true switching from VS generator to Ninja', () => {
37+
expect(generatorMismatch('Ninja', 'Visual Studio 18 2026')).to.be.true;
38+
});
39+
40+
test('Returns true switching from Unix Makefiles to Ninja', () => {
41+
expect(generatorMismatch('Ninja', 'Unix Makefiles')).to.be.true;
42+
});
43+
});
44+
45+
suite('CMakeCache.parseCache integration', () => {
46+
test('Detects mismatch via parsed cache (Ninja cached, VS selected)', () => {
47+
const cacheContent = [
48+
'# This is the CMakeCache file.',
49+
'CMAKE_GENERATOR:INTERNAL=Ninja',
50+
''
51+
].join('\n');
52+
const entries = CMakeCache.parseCache(cacheContent);
53+
const cachedGenerator = entries.get('CMAKE_GENERATOR') as CacheEntry;
54+
expect(generatorMismatch('Visual Studio 18 2026', cachedGenerator.value)).to.be.true;
55+
});
56+
57+
test('No mismatch when cached generator matches selected', () => {
58+
const cacheContent = 'CMAKE_GENERATOR:INTERNAL=Ninja\n';
59+
const entries = CMakeCache.parseCache(cacheContent);
60+
const cachedGenerator = entries.get('CMAKE_GENERATOR') as CacheEntry;
61+
expect(generatorMismatch('Ninja', cachedGenerator.value)).to.be.false;
62+
});
63+
64+
test('No mismatch when CMAKE_GENERATOR absent from cache', () => {
65+
const cacheContent = 'CMAKE_BUILD_TYPE:STRING=Debug\n';
66+
const entries = CMakeCache.parseCache(cacheContent);
67+
const cachedGenerator = entries.get('CMAKE_GENERATOR');
68+
expect(generatorMismatch('Ninja', cachedGenerator?.value)).to.be.false;
69+
});
70+
71+
test('No mismatch when cache is empty', () => {
72+
const entries = CMakeCache.parseCache('');
73+
const cachedGenerator = entries.get('CMAKE_GENERATOR');
74+
expect(generatorMismatch('Ninja', cachedGenerator?.value)).to.be.false;
75+
});
76+
77+
test('Detects mismatch between two VS generator versions via cache', () => {
78+
const cacheContent = 'CMAKE_GENERATOR:INTERNAL=Visual Studio 17 2022\n';
79+
const entries = CMakeCache.parseCache(cacheContent);
80+
const cachedGenerator = entries.get('CMAKE_GENERATOR') as CacheEntry;
81+
expect(generatorMismatch('Visual Studio 18 2026', cachedGenerator.value)).to.be.true;
82+
});
83+
84+
test('Detects mismatch when switching from Unix Makefiles cached to Ninja', () => {
85+
const cacheContent = 'CMAKE_GENERATOR:INTERNAL=Unix Makefiles\n';
86+
const entries = CMakeCache.parseCache(cacheContent);
87+
const cachedGenerator = entries.get('CMAKE_GENERATOR') as CacheEntry;
88+
expect(generatorMismatch('Ninja', cachedGenerator.value)).to.be.true;
89+
});
90+
});
91+
});

0 commit comments

Comments
 (0)