Skip to content

Commit 206c6ee

Browse files
Introducing new cmake.shell setting for custom shell executable for CMake subprocesses (#4762)
* Initial plan * feat: Add cmake.shell setting for custom shell execution (#1750) Add a `cmake.shell` setting (string|null, default null) that routes all cmake/ctest/cpack subprocess invocations through a user-specified shell (e.g., Git Bash, MSYS2). This enables embedded devs using GNU toolchains with Unix Makefiles on Windows where POSIX path translation is required. Root cause fix: `!!options.shell` in proc.ts coerced string shell paths to boolean `true`, discarding the actual path. Changed to `options.shell ?? false` to preserve string values that Node.js child_process.spawn() natively supports. Fixes #1750 Co-authored-by: hanniavalera <[email protected]> * fix: resolve bad merge in config.ts and config.test.ts Fix three merge conflicts between the shell and setBuildTargetSameAsLaunchTarget features: 1. config.ts: Add missing closing brace for shell getter 2. config.ts: Remove duplicate additionalBuildProblemMatchers emitter, add missing comma 3. config.test.ts: Remove duplicate additionalBuildProblemMatchers entry, add missing comma Co-authored-by: hanniavalera <[email protected]> * fix: determineShell precedence, wire shell onChange, audit call sites 1. cmakeDriver.ts executeCommand(): On Windows, determineShell's command-type-specific detection (.cmd→cmd, .ps1→powershell) now takes precedence over config.shell so .cmd/.bat commands aren't routed through Git Bash. 2. cmakeProject.ts: Wire shell onChange emitter to trigger driver reload when cmake.shell changes at runtime. 3. cmakeTaskProvider.ts: Apply same determineShell precedence logic for direct proc.execute calls. 4. debuggerScriptDriver.ts: Propagate config.shell with determineShell precedence for CMake script debugging. 5. shell-propagation.test.ts: Add 9 tests for determineShell detection and precedence logic. Co-authored-by: hanniavalera <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: hanniavalera <[email protected]>
1 parent b69f08d commit 206c6ee

12 files changed

Lines changed: 214 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 1.23
44

55
Features:
6+
- Add `cmake.shell` setting to route CMake/CTest/CPack subprocess invocations through a custom shell (e.g., Git Bash, MSYS2), enabling embedded toolchains that require POSIX path translation on Windows. [#1750](https://github.com/microsoft/vscode-cmake-tools/issues/1750)
67
- triple: Add riscv32be riscv64be support. [#4648](https://github.com/microsoft/vscode-cmake-tools/pull/4648) [@lygstate](https://github.com/lygstate)
78
- Add command to clear build diagnostics from the Problems pane. [#4691](https://github.com/microsoft/vscode-cmake-tools/pull/4691)
89
- 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)

docs/cmake-settings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Options that support substitution, in the table below, allow variable references
7878
| `cmake.saveBeforeBuild` | If `true` (the default), saves open text documents when build or configure is invoked before running CMake. | `true` | no |
7979
| `cmake.setBuildTargetSameAsLaunchTarget` | If `true`, setting the launch/debug target automatically sets the build target to match. | `false` | no |
8080
| `cmake.setBuildTypeOnMultiConfig` | If `true`, set build type on multi-config generators. | `false` | no |
81+
| `cmake.shell` | Path to a shell executable to route all CMake/CTest/CPack subprocess invocations through (e.g., Git Bash or MSYS2). Useful for embedded toolchains that require POSIX path translation on Windows. When `null`, the default system shell behavior is used. | `null` | no |
8182
| `cmake.showConfigureWithDebuggerNotification` | If `true`, show notification when configure with debugger. | `true` | no |
8283
| `cmake.showNotAllDocumentsSavedQuestion` | If `true`, show not all documents saved question. | `true` | no |
8384
| `cmake.showSystemKits` | If `true`, show system kits in kit selection. | `true` | no |

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2394,6 +2394,15 @@
23942394
"description": "%cmake-tools.configuration.cmake.platform.description%",
23952395
"scope": "resource"
23962396
},
2397+
"cmake.shell": {
2398+
"type": [
2399+
"string",
2400+
"null"
2401+
],
2402+
"default": null,
2403+
"description": "%cmake-tools.configuration.cmake.shell.description%",
2404+
"scope": "resource"
2405+
},
23972406
"cmake.configureArgs": {
23982407
"type": "array",
23992408
"description": "%cmake-tools.configuration.cmake.configureArgs.description%",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"cmake-tools.configuration.cmake.generator.description": "The CMake generator to use.",
122122
"cmake-tools.configuration.cmake.toolset.description": "The CMake toolset to use when configuring.",
123123
"cmake-tools.configuration.cmake.platform.description": "The CMake platform to use when configuring.",
124+
"cmake-tools.configuration.cmake.shell.description": "Path to a shell executable to use when running CMake, CTest, and CPack commands (e.g., Git Bash or MSYS2). When set, all subprocess invocations are routed through this shell. Useful for embedded toolchains that require POSIX path translation. When null, the default system shell behavior is used.",
124125
"cmake-tools.configuration.cmake.configureArgs.description": "Additional arguments to pass to CMake when configuring. When using CMake Presets, these arguments are temporarily appended to the arguments provided by the active configure preset.",
125126
"cmake-tools.configuration.cmake.buildArgs.description": "Additional arguments to pass to CMake when building. When using CMake Presets, these arguments are temporarily appended to the arguments provided by the active build preset.",
126127
"cmake-tools.configuration.cmake.buildToolArgs.description": "Additional arguments to pass to the underlying build tool when building. When using CMake Presets, these arguments are temporarily appended to the arguments provided by the active build preset to invoke the build tool.",

src/cmakeProject.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,11 @@ export class CMakeProject {
847847
await this.reloadCMakeDriver();
848848
});
849849

850+
private readonly shellSub = this.workspaceContext.config.onChange('shell', async () => {
851+
log.info(localize('shell.changed.restart.driver', "Restarting the CMake driver after a shell change."));
852+
await this.reloadCMakeDriver();
853+
});
854+
850855
/**
851856
* The variant manager keeps track of build variants. Has two-phase init.
852857
*/
@@ -890,7 +895,8 @@ export class CMakeProject {
890895
this.generatorSub,
891896
this.preferredGeneratorsSub,
892897
this.communicationModeSub,
893-
this.cmakePathSub
898+
this.cmakePathSub,
899+
this.shellSub
894900
]) {
895901
sub.dispose();
896902
}

src/cmakeTaskProvider.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,10 @@ export class CustomBuildTaskTerminal extends proc.CommandConsumer implements vsc
587587
this.writeEmitter.fire(localize("build.started", "{0} task started....", taskName) + endOfLine);
588588
this.writeEmitter.fire(proc.buildCmdStr(cmakePath, args) + endOfLine);
589589
try {
590-
this._process = proc.execute(cmakePath, args, this, this.options);
590+
// On Windows, command-type-specific detection takes precedence over config.shell
591+
const commandShell = process.platform === 'win32' ? proc.determineShell(cmakePath) : false;
592+
const shell = (commandShell || undefined) ?? cmakeDriver.config.shell ?? undefined;
593+
this._process = proc.execute(cmakePath, args, this, { ...this.options, shell });
591594
const result: proc.ExecutionResult = await this._process.result;
592595
this._process = undefined;
593596
if (result.retc) {

src/config.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export interface ExtensionConfigurationSettings {
237237
useFolderPropertyInBuildTargetDropdown: boolean;
238238
setBuildTargetSameAsLaunchTarget: boolean;
239239
additionalBuildProblemMatchers: BuildProblemMatcherConfig[];
240+
shell: string | null;
240241
}
241242

242243
type EmittersOf<T> = {
@@ -628,6 +629,10 @@ export class ConfigurationReader implements vscode.Disposable {
628629
return this.configData.useFolderPropertyInBuildTargetDropdown;
629630
}
630631

632+
get shell(): string | null {
633+
return this.configData.shell;
634+
}
635+
631636
get setBuildTargetSameAsLaunchTarget(): boolean {
632637
return this.configData.setBuildTargetSameAsLaunchTarget;
633638
}
@@ -703,8 +708,9 @@ export class ConfigurationReader implements vscode.Disposable {
703708
postRunCoverageTarget: new vscode.EventEmitter<string | null>(),
704709
coverageInfoFiles: new vscode.EventEmitter<string[]>(),
705710
useFolderPropertyInBuildTargetDropdown: new vscode.EventEmitter<boolean>(),
706-
setBuildTargetSameAsLaunchTarget: new vscode.EventEmitter<boolean>(),
707-
additionalBuildProblemMatchers: new vscode.EventEmitter<BuildProblemMatcherConfig[]>()
711+
additionalBuildProblemMatchers: new vscode.EventEmitter<BuildProblemMatcherConfig[]>(),
712+
shell: new vscode.EventEmitter<string | null>(),
713+
setBuildTargetSameAsLaunchTarget: new vscode.EventEmitter<boolean>()
708714
};
709715

710716
/**

src/debug/cmakeDebugger/debuggerScriptDriver.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ export async function executeScriptWithDebugger(scriptPath: string, scriptArgs:
4646
cmakeLogger.info(localize('run.script', "Executing CMake script: {0}", scriptPath));
4747

4848
const env = EnvironmentUtils.merge([process.env, EnvironmentUtils.create(scriptEnv)]);
49-
const child = proc.execute(cmakeExe.path, concreteArgs, outputConsumer, { environment: env});
49+
const commandShell = process.platform === 'win32' ? proc.determineShell(cmakeExe.path) : false;
50+
const configShell = cmakeProject.workspaceContext.config.shell;
51+
const shell = (commandShell || undefined) ?? configShell ?? undefined;
52+
const child = proc.execute(cmakeExe.path, concreteArgs, outputConsumer, { environment: env, shell });
5053

5154
while (
5255
!outputConsumer.stateMessages.includes(

src/drivers/cmakeDriver.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,12 @@ export abstract class CMakeDriver implements vscode.Disposable {
519519

520520
executeCommand(command: string, args?: string[], consumer?: proc.OutputConsumer, options?: proc.ExecutionOptions): proc.Subprocess {
521521
const environment = this.getEffectiveSubprocessEnvironment(options);
522-
const exec_options = { ...options, environment };
522+
// On Windows, command-type-specific detection (e.g. .cmd → cmd, .ps1 → powershell)
523+
// must take precedence over config.shell to avoid routing commands through
524+
// an incompatible shell (e.g. .cmd files through Git Bash).
525+
const commandShell = process.platform === 'win32' ? proc.determineShell(command) : false;
526+
const shell = options?.shell ?? (commandShell || undefined) ?? this.config.shell ?? undefined;
527+
const exec_options = { ...options, environment, shell };
523528
return proc.execute(command, args, consumer, exec_options);
524529
}
525530

@@ -554,7 +559,7 @@ export abstract class CMakeDriver implements vscode.Disposable {
554559
existing = undefined;
555560
}
556561
if (!existing) {
557-
const shellPath = process.platform === 'win32' ? 'cmd.exe' : undefined;
562+
const shellPath = this.config.shell ?? (process.platform === 'win32' ? 'cmd.exe' : undefined);
558563
const term = vscode.window.createTerminal({
559564
name: localize('file.compilation', 'File Compilation'),
560565
cwd: cmd.directory,

src/proc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export function execute(command: string, args?: string[], outputConsumer?: Outpu
189189

190190
const spawn_opts: proc.SpawnOptions = {
191191
env: final_env,
192-
shell: !!options.shell
192+
shell: options.shell ?? false
193193
};
194194
if (options?.cwd !== undefined) {
195195
util.createDirIfNotExistsSync(options.cwd);

0 commit comments

Comments
 (0)