Skip to content

Commit 1c46812

Browse files
Fix CMakePresets.json discovery after CMakeLists.txt selection in subdirectory (#4728)
* Initial plan * Fix CMakePresets.json discovery in workspace when sourceDir changes via CMakeLists.txt dialog (#4727) Co-authored-by: hanniavalera <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: hanniavalera <[email protected]>
1 parent 889cf8a commit 1c46812

4 files changed

Lines changed: 111 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Improvements:
1414
- Allow preset modification commands to target CMakeUserPresets.json. The target file is determined by the focused editor, or by prompting the user when both files exist. [#4564](https://github.com/microsoft/vscode-cmake-tools/issues/4564)
1515

1616
Bug Fixes:
17+
- Fix CMakePresets.json discovery failing in multi-folder workspaces when the presets file is in a subdirectory specified by `cmake.sourceDirectory`. [#4727](https://github.com/microsoft/vscode-cmake-tools/issues/4727)
1718
- Fix initial kit scan ignoring `cmake.enableAutomaticKitScan: false` on first workspace open, and prevent redundant concurrent scans in multi-project workspaces. [#4726](https://github.com/microsoft/vscode-cmake-tools/issues/4726)
1819
- Fix `cmake.installPrefix` not being passed to CMake as `CMAKE_INSTALL_PREFIX` when using presets. [#4358](https://github.com/microsoft/vscode-cmake-tools/issues/4358)
1920
- Fix CPack commands not appearing in the command palette when not using CMake Presets. Also fix CPack environment variables not being set up in non-preset mode. [#4453](https://github.com/microsoft/vscode-cmake-tools/issues/4453)

src/cmakeProject.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,9 @@ export class CMakeProject {
10141014
if (selectedFile) {
10151015
const newSourceDirectory = path.dirname(selectedFile);
10161016
await this.setSourceDir(await util.normalizeAndVerifySourceDir(newSourceDirectory, CMakeDriver.sourceDirExpansionOptions(this.workspaceContext.folder.uri.fsPath)));
1017+
// Update the PresetsController so that CMakePresets.json is
1018+
// looked up relative to the new source directory (fixes #4727).
1019+
await this.presetsController.updateSourceDir(this._sourceDir);
10171020
void vscode.workspace.getConfiguration('cmake', this.workspaceFolder.uri).update("sourceDirectory", this._sourceDir);
10181021
if (config) {
10191022
// Updating sourceDirectory here, at the beginning of the configure process,

src/presets/presetsController.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ export class PresetsController implements vscode.Disposable {
148148
return this._presetsParser.presetsFileExists;
149149
}
150150

151+
/**
152+
* Updates the source directory used for locating CMakePresets.json and
153+
* CMakeUserPresets.json, then reloads the presets from the new location.
154+
* This is needed when the source directory changes after the PresetsController
155+
* has already been initialized (e.g., when the user selects a CMakeLists.txt
156+
* in a subdirectory via the missing-CMakeLists dialog).
157+
*/
158+
async updateSourceDir(sourceDir: string) {
159+
this._presetsParser.sourceDir = sourceDir;
160+
await this.reapplyPresets();
161+
}
162+
151163
/**
152164
* Call configurePresets, buildPresets, testPresets, packagePresets or workflowPresets to get the latest presets when thie event is fired.
153165
*/

test/unit-tests/presets/presetsController.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,98 @@ suite('PresetsController file watcher protection', () => {
341341
expect(path.basename(resolvedPath)).to.equal('CMakePresets.json');
342342
});
343343
});
344+
345+
/**
346+
* Tests for preset file discovery when sourceDir differs from folderPath.
347+
* Validates the scenario from issue #4727 where CMakePresets.json lives in a
348+
* subdirectory (e.g., engine/cmake/) rather than the workspace root.
349+
*/
350+
suite('Preset file path resolution with subdirectory sourceDir (#4727)', () => {
351+
let tempDir: string;
352+
let workspaceRoot: string;
353+
let subDir: string;
354+
355+
setup(() => {
356+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cmake-presets-subdir-test-'));
357+
workspaceRoot = tempDir;
358+
subDir = path.join(tempDir, 'engine', 'cmake');
359+
fs.mkdirSync(subDir, { recursive: true });
360+
});
361+
362+
teardown(() => {
363+
if (tempDir && fs.existsSync(tempDir)) {
364+
fs.rmSync(tempDir, { recursive: true, force: true });
365+
}
366+
});
367+
368+
/**
369+
* Test that CMakePresets.json is found when sourceDir is a subdirectory.
370+
* This simulates the scenario where folderPath (workspace root) differs
371+
* from sourceDir (subdirectory with CMakeLists.txt and CMakePresets.json).
372+
*/
373+
test('presetsPath points to sourceDir, not folderPath', () => {
374+
// The presetsPath should be constructed from sourceDir, not folderPath
375+
const folderPath = workspaceRoot;
376+
const sourceDir = subDir;
377+
378+
// This is how PresetsParser computes presetsPath
379+
const presetsPath = path.join(sourceDir, 'CMakePresets.json');
380+
const userPresetsPath = path.join(sourceDir, 'CMakeUserPresets.json');
381+
382+
// Verify paths point to subdirectory, not workspace root
383+
expect(presetsPath).to.equal(path.join(subDir, 'CMakePresets.json'));
384+
expect(userPresetsPath).to.equal(path.join(subDir, 'CMakeUserPresets.json'));
385+
expect(presetsPath).to.not.equal(path.join(folderPath, 'CMakePresets.json'));
386+
});
387+
388+
/**
389+
* Test that preset files in subdirectory are found on disk.
390+
* This validates the core issue: the extension must look for
391+
* CMakePresets.json relative to sourceDir (where CMakeLists.txt is),
392+
* not relative to the workspace root.
393+
*/
394+
test('preset files are found in sourceDir subdirectory', () => {
395+
// Create CMakePresets.json in the subdirectory
396+
const presetsContent = JSON.stringify({
397+
version: 3,
398+
configurePresets: [{
399+
name: "subdir-preset",
400+
generator: "Ninja",
401+
binaryDir: "${sourceDir}/build"
402+
}]
403+
}, null, 2);
404+
const presetsFilePath = path.join(subDir, 'CMakePresets.json');
405+
fs.writeFileSync(presetsFilePath, presetsContent);
406+
407+
// Verify the file exists at sourceDir, not at workspace root
408+
expect(fs.existsSync(path.join(subDir, 'CMakePresets.json'))).to.be.true;
409+
expect(fs.existsSync(path.join(workspaceRoot, 'CMakePresets.json'))).to.be.false;
410+
411+
// Verify content is correct
412+
const content = JSON.parse(fs.readFileSync(presetsFilePath, 'utf8'));
413+
expect(content.configurePresets[0].name).to.equal('subdir-preset');
414+
});
415+
416+
/**
417+
* Test that updating sourceDir changes where presets are looked up.
418+
* This validates the fix for #4727: after the user selects a CMakeLists.txt
419+
* in a subdirectory, the preset lookup path must be updated accordingly.
420+
*/
421+
test('sourceDir update changes preset lookup path', () => {
422+
// Initially, sourceDir is the workspace root
423+
let sourceDir = workspaceRoot;
424+
let presetsPath = path.join(sourceDir, 'CMakePresets.json');
425+
expect(presetsPath).to.equal(path.join(workspaceRoot, 'CMakePresets.json'));
426+
427+
// User selects CMakeLists.txt in subdirectory, sourceDir is updated
428+
sourceDir = subDir;
429+
presetsPath = path.join(sourceDir, 'CMakePresets.json');
430+
expect(presetsPath).to.equal(path.join(subDir, 'CMakePresets.json'));
431+
432+
// Create the file in the subdirectory
433+
fs.writeFileSync(path.join(subDir, 'CMakePresets.json'), '{"version": 3}');
434+
435+
// After sourceDir update, the file should be found
436+
expect(fs.existsSync(presetsPath)).to.be.true;
437+
});
438+
});

0 commit comments

Comments
 (0)