diff --git a/CHANGELOG.md b/CHANGELOG.md index b0663d6ea8..c4f4ceb9b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.23 Features: +- Add `singleConfig`/`multiConfig` object form for `cmake.buildDirectory` to allow separate build directory templates for single- and multi-config generators. [#2426](https://github.com/microsoft/vscode-cmake-tools/issues/2426) - triple: Add riscv32be riscv64be support. [#4648](https://github.com/microsoft/vscode-cmake-tools/pull/4648) [@lygstate](https://github.com/lygstate) - Add command to clear build diagnostics from the Problems pane. [#4691](https://github.com/microsoft/vscode-cmake-tools/pull/4691) - Clear build diagnostics from the Problems pane when a new build starts and populate them incrementally during the build. [#4608](https://github.com/microsoft/vscode-cmake-tools/issues/4608) diff --git a/package.json b/package.json index 46be1f8fe6..651833577c 100644 --- a/package.json +++ b/package.json @@ -2261,7 +2261,25 @@ "scope": "resource" }, "cmake.buildDirectory": { - "type": "string", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "singleConfig": { + "type": "string", + "description": "%cmake-tools.configuration.cmake.buildDirectory.singleConfig.description%" + }, + "multiConfig": { + "type": "string", + "description": "%cmake-tools.configuration.cmake.buildDirectory.multiConfig.description%" + } + }, + "additionalProperties": false + } + ], "default": "${workspaceFolder}/build", "description": "%cmake-tools.configuration.cmake.buildDirectory.description%", "scope": "resource" diff --git a/package.nls.json b/package.nls.json index ddea591cb7..3349d74f2a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -106,7 +106,9 @@ ] }, "cmake-tools.configuration.cmake.cmakePath.description": "Name/path of the CMake executable to use.", - "cmake-tools.configuration.cmake.buildDirectory.description": "The directory where CMake build files will go.", + "cmake-tools.configuration.cmake.buildDirectory.description": "The directory where CMake build files will go. Can be a string (used for all generators) or an object with `singleConfig` and/or `multiConfig` properties to specify separate build directories for single-config (e.g. Make, Ninja) and multi-config (e.g. Ninja Multi-Config, Visual Studio) generators.", + "cmake-tools.configuration.cmake.buildDirectory.singleConfig.description": "Build directory template for single-config generators (e.g. Make, Ninja).", + "cmake-tools.configuration.cmake.buildDirectory.multiConfig.description": "Build directory template for multi-config generators (e.g. Ninja Multi-Config, Visual Studio, Xcode).", "cmake-tools.configuration.cmake.installPrefix.description": "The directory where CMake installed files will go.", "cmake-tools.configuration.cmake.sourceDirectory.description": "Path or array of paths to the CMakeLists.txt root directory/directories.", "cmake-tools.configuration.cmake.saveBeforeBuild.description": "Save open files before building.", diff --git a/src/config.ts b/src/config.ts index 467dd6483e..cc6da2902c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -22,6 +22,8 @@ export function defaultNumJobs (): number { const log = logging.createLogger('config'); +export const defaultBuildDirectoryValue = '${workspaceFolder}/build'; + export type LogLevelKey = 'trace' | 'debug' | 'info' | 'note' | 'warning' | 'error' | 'fatal'; export type CMakeCommunicationMode = 'legacy' | 'serverApi' | 'fileApi' | 'automatic'; export type StatusBarOptionVisibility = "visible" | "compact" | "icon" | "hidden" | "inherit"; @@ -169,7 +171,7 @@ export interface ExtensionConfigurationSettings { defaultActiveFolder: string | null; exclude: string[]; cmakePath: string; - buildDirectory: string; + buildDirectory: string | { singleConfig?: string; multiConfig?: string }; installPrefix: string | null; sourceDirectory: string | string[]; saveBeforeBuild: boolean; @@ -341,11 +343,22 @@ export class ConfigurationReader implements vscode.Disposable { return this.configData.exclude; } - buildDirectory(multiProject: boolean, workspaceFolder?: vscode.ConfigurationScope): string { + buildDirectory(multiProject: boolean, workspaceFolder?: vscode.ConfigurationScope, isMultiConfig?: boolean): string { if (multiProject && this.isDefaultValue('buildDirectory', workspaceFolder)) { return '${sourceDirectory}/build'; } - return this.configData.buildDirectory; + const raw = this.configData.buildDirectory; + if (typeof raw === 'string') { + return raw; + } + // Object form: pick the branch based on the generator type. + // The isMultiConfig flag is typically derived from isMultiConfGeneratorFast(), + // a fast/pre-configure heuristic that may not be authoritative until after the first configure run. + if (isMultiConfig) { + return raw.multiConfig ?? raw.singleConfig ?? defaultBuildDirectoryValue; + } else { + return raw.singleConfig ?? raw.multiConfig ?? defaultBuildDirectoryValue; + } } get installPrefix(): string | null { return this.configData.installPrefix; diff --git a/src/drivers/cmakeDriver.ts b/src/drivers/cmakeDriver.ts index b65e8dfab1..e6d855e13a 100644 --- a/src/drivers/cmakeDriver.ts +++ b/src/drivers/cmakeDriver.ts @@ -742,7 +742,7 @@ export abstract class CMakeDriver implements vscode.Disposable { await this._setKit(kit, preferredGenerators); await this._refreshExpansions(); const scope = this.workspaceFolder ? vscode.Uri.file(this.workspaceFolder) : undefined; - const newBinaryDir = util.lightNormalizePath(await expand.expandString(this.config.buildDirectory(this.isMultiProject, scope), this.expansionOptions)); + const newBinaryDir = util.lightNormalizePath(await expand.expandString(this.config.buildDirectory(this.isMultiProject, scope, this.isMultiConfFast), this.expansionOptions)); if (needsCleanIfKitChange && (newBinaryDir === oldBinaryDir)) { await this._cleanPriorConfiguration(); } @@ -855,7 +855,7 @@ export abstract class CMakeDriver implements vscode.Disposable { if (!this.useCMakePresets) { const scope = this.workspaceFolder ? vscode.Uri.file(this.workspaceFolder) : undefined; - this._binaryDir = util.lightNormalizePath(await expand.expandString(this.config.buildDirectory(this.isMultiProject, scope), opts)); + this._binaryDir = util.lightNormalizePath(await expand.expandString(this.config.buildDirectory(this.isMultiProject, scope, this.isMultiConfFast), opts)); } }); } diff --git a/src/projectController.ts b/src/projectController.ts index 4ee27859ea..281c688785 100644 --- a/src/projectController.ts +++ b/src/projectController.ts @@ -307,9 +307,15 @@ export class ProjectController implements vscode.Disposable { if (sourceDirectories.length <= 1) { return; } - const unresolvedBuildDirectory: string = config.buildDirectory(sourceDirectories.length > 1); - - if (unresolvedBuildDirectory.includes("${sourceDirectory}") || unresolvedBuildDirectory.includes("${sourceDir}")) { + // When the object form is used, check both branches since the generator is not yet known. + const singleConfDir: string = config.buildDirectory(sourceDirectories.length > 1, undefined, false); + const multiConfDir: string = config.buildDirectory(sourceDirectories.length > 1, undefined, true); + const dirsToCheck = new Set([singleConfDir, multiConfDir]); + + const allContainSourceDir = [...dirsToCheck].every( + d => d.includes("${sourceDirectory}") || d.includes("${sourceDir}") + ); + if (allContainSourceDir) { return; } else { const sameBinaryDir = localize('duplicate.build.directory.1', 'Multiple CMake projects in this folder are using the same CMAKE_BINARY_DIR.'); diff --git a/test/unit-tests/config.test.ts b/test/unit-tests/config.test.ts index 39fd8fb910..90572146ff 100644 --- a/test/unit-tests/config.test.ts +++ b/test/unit-tests/config.test.ts @@ -156,4 +156,71 @@ suite('Configuration', () => { conf.updatePartial({ buildDirectory: 'Foo' }); expect(conf.parallelJobs).to.eq(5); }); + + test('buildDirectory plain string form returns the string', () => { + const conf = createConfig({ buildDirectory: '/my/build/path' }); + expect(conf.buildDirectory(false)).to.eq('/my/build/path'); + }); + + test('buildDirectory object form returns singleConfig when isMultiConfig is false', () => { + const conf = createConfig({ + buildDirectory: { + singleConfig: '/build/single-${buildType}', + multiConfig: '/build/multi' + } + }); + expect(conf.buildDirectory(false, undefined, false)).to.eq('/build/single-${buildType}'); + }); + + test('buildDirectory object form returns multiConfig when isMultiConfig is true', () => { + const conf = createConfig({ + buildDirectory: { + singleConfig: '/build/single-${buildType}', + multiConfig: '/build/multi' + } + }); + expect(conf.buildDirectory(false, undefined, true)).to.eq('/build/multi'); + }); + + test('buildDirectory object form with only singleConfig falls back for multi-config generator', () => { + const conf = createConfig({ + buildDirectory: { singleConfig: '/build/single' } + }); + // No multiConfig set, should fall back to singleConfig + expect(conf.buildDirectory(false, undefined, true)).to.eq('/build/single'); + }); + + test('buildDirectory object form with only multiConfig falls back for single-config generator', () => { + const conf = createConfig({ + buildDirectory: { multiConfig: '/build/multi' } + }); + // No singleConfig set, should fall back to multiConfig + expect(conf.buildDirectory(false, undefined, false)).to.eq('/build/multi'); + }); + + test('buildDirectory object form with empty object falls back to default', () => { + const conf = createConfig({ + buildDirectory: {} + }); + expect(conf.buildDirectory(false, undefined, false)).to.eq('${workspaceFolder}/build'); + }); + + test('buildDirectory object form defaults to singleConfig when isMultiConfig is undefined', () => { + const conf = createConfig({ + buildDirectory: { + singleConfig: '/build/single', + multiConfig: '/build/multi' + } + }); + // When isMultiConfig is not provided, defaults to false (single-config) + expect(conf.buildDirectory(false)).to.eq('/build/single'); + }); + + test('buildDirectory plain string form works with multiProject=false', () => { + // Note: we cannot test multiProject=true with a non-default value here because + // isDefaultValue() checks the real vscode.workspace.getConfiguration (not configData), + // which always reports the default in the test host environment. + const conf = createConfig({ buildDirectory: '/custom/build' }); + expect(conf.buildDirectory(false)).to.eq('/custom/build'); + }); });