From aebf397e778e33050f5272ed40a1b0ab48e97a2a Mon Sep 17 00:00:00 2001 From: Prashant Kumar Rai Date: Tue, 18 Aug 2026 19:36:37 +0530 Subject: [PATCH 1/6] Fix #12537: add env object schema to cppdbg and cppvsdbg launch configs --- Extension/package.json | 16 ++++++++++++++++ Extension/package.nls.json | 1 + 2 files changed, 17 insertions(+) diff --git a/Extension/package.json b/Extension/package.json index e4f44941d..457731837 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -4378,6 +4378,14 @@ }, "default": [] }, + "env": { + "type": "object", + "description": "%c_cpp.debuggers.env.description%", + "additionalProperties": { + "type": "string" + }, + "default": {} + }, "envFile": { "type": "string", "description": "%c_cpp.debuggers.envFile.description%", @@ -6046,6 +6054,14 @@ }, "default": [] }, + "env": { + "type": "object", + "description": "%c_cpp.debuggers.env.description%", + "additionalProperties": { + "type": "string" + }, + "default": {} + }, "envFile": { "type": "string", "description": "%c_cpp.debuggers.envFile.description%", diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 686234a1f..36778d3b6 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -927,6 +927,7 @@ "{Locked=\"[ { \\\"name\\\": \\\"\"} {Locked=\"\\\", \\\"value\\\": \\\"\"} {Locked=\"\\\" } ]\"} {Locked=\"[ { \\\"\"} {Locked=\"\\\": \\\"\"} {Locked=\"\\\" } ]\"}" ] }, + "c_cpp.debuggers.env.description": "Object of environment variables to add to the environment for the program. Example: { \"MY_VAR\": \"value\" }. Use `environment` for the array-of-objects form.", "c_cpp.debuggers.envFile.description": "Absolute path to a file containing environment variable definitions. This file has key value pairs separated by an equals sign per line. E.g. KEY=VALUE.", "c_cpp.debuggers.additionalSOLibSearchPath.description": "Semicolon separated list of directories to use to search for .so files. Example: \"c:\\dir1;c:\\dir2\".", "c_cpp.debuggers.MIMode.description": "Indicates the console debugger that the MIDebugEngine will connect to. Allowed values are \"gdb\" \"lldb\".", From deb60acb9a9221101e0e1746db9717e71c12f899 Mon Sep 17 00:00:00 2001 From: 8prashant Date: Tue, 18 Aug 2026 19:36:37 +0530 Subject: [PATCH 2/6] Fix #12537: add env object schema to cppdbg and cppvsdbg launch configs --- Extension/package.json | 16 ++++++++++++++++ Extension/package.nls.json | 1 + 2 files changed, 17 insertions(+) diff --git a/Extension/package.json b/Extension/package.json index e4f44941d..457731837 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -4378,6 +4378,14 @@ }, "default": [] }, + "env": { + "type": "object", + "description": "%c_cpp.debuggers.env.description%", + "additionalProperties": { + "type": "string" + }, + "default": {} + }, "envFile": { "type": "string", "description": "%c_cpp.debuggers.envFile.description%", @@ -6046,6 +6054,14 @@ }, "default": [] }, + "env": { + "type": "object", + "description": "%c_cpp.debuggers.env.description%", + "additionalProperties": { + "type": "string" + }, + "default": {} + }, "envFile": { "type": "string", "description": "%c_cpp.debuggers.envFile.description%", diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 686234a1f..36778d3b6 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -927,6 +927,7 @@ "{Locked=\"[ { \\\"name\\\": \\\"\"} {Locked=\"\\\", \\\"value\\\": \\\"\"} {Locked=\"\\\" } ]\"} {Locked=\"[ { \\\"\"} {Locked=\"\\\": \\\"\"} {Locked=\"\\\" } ]\"}" ] }, + "c_cpp.debuggers.env.description": "Object of environment variables to add to the environment for the program. Example: { \"MY_VAR\": \"value\" }. Use `environment` for the array-of-objects form.", "c_cpp.debuggers.envFile.description": "Absolute path to a file containing environment variable definitions. This file has key value pairs separated by an equals sign per line. E.g. KEY=VALUE.", "c_cpp.debuggers.additionalSOLibSearchPath.description": "Semicolon separated list of directories to use to search for .so files. Example: \"c:\\dir1;c:\\dir2\".", "c_cpp.debuggers.MIMode.description": "Indicates the console debugger that the MIDebugEngine will connect to. Allowed values are \"gdb\" \"lldb\".", From 13f598c3620592c63466d1cdaefa84b542abe05a Mon Sep 17 00:00:00 2001 From: Prashant Kumar Rai Date: Wed, 19 Aug 2026 15:54:27 +0530 Subject: [PATCH 3/6] Add support for env object in cppdbg and runWithoutDebugging configurations --- .../src/Debugger/configurationProvider.ts | 33 +++++++++++ .../Debugger/runWithoutDebuggingAdapter.ts | 30 ++++++---- .../RunWithoutDebugging/assets/envTest.cpp | 21 +++++++ .../runWithoutDebugging.integration.test.ts | 58 +++++++++++++++++++ Extension/tools/OptionsSchema.json | 16 +++++ 5 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp diff --git a/Extension/src/Debugger/configurationProvider.ts b/Extension/src/Debugger/configurationProvider.ts index 5bc427759..b8daa5f6f 100644 --- a/Extension/src/Debugger/configurationProvider.ts +++ b/Extension/src/Debugger/configurationProvider.ts @@ -270,6 +270,10 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv // Add environment variables from .env file this.resolveEnvFile(config, folder); + // cppdbg (MIEngine) consumes the legacy `environment` array, not `env`. + // Convert here so both syntaxes work while preserving `env` precedence. + this.resolveEnvObjectForCppdbg(config); + await this.expand(config, folder); this.resolveSourceFileMapVariables(config); @@ -706,6 +710,35 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv } } + private resolveEnvObjectForCppdbg(config: CppDebugConfiguration): void { + if (config.type !== DebuggerType.cppdbg || config.request !== 'launch') { + return; + } + + const envObject = config.env; + if (!util.isObject(envObject)) { + return; + } + + const environment: Environment[] = util.isArray(config.environment) ? config.environment : []; + const mergedEnvironment = new Map(); + + for (const entry of environment) { + if (util.isString(entry?.name) && util.isString(entry?.value)) { + mergedEnvironment.set(entry.name, entry.value); + } + } + + for (const [name, value] of Object.entries(envObject)) { + if (util.isString(value)) { + mergedEnvironment.set(name, value); + } + } + + config.environment = Array.from(mergedEnvironment.entries()).map(([name, value]) => ({ name, value })); + delete config.env; + } + private resolveSourceFileMapVariables(config: CppDebugConfiguration): void { const messages: string[] = []; if (config.sourceFileMap) { diff --git a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts index fc7c98a35..9beaa948b 100644 --- a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts +++ b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts @@ -14,6 +14,18 @@ import { isWindows } from '../constants'; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); const localize = nls.loadMessageBundle(); +type LaunchEnvironmentEntry = { name: string; value: string; }; + +type LaunchConfiguration = { + program?: string; + args?: string[]; + cwd?: string; + environment?: LaunchEnvironmentEntry[]; + env?: Record; + console?: string; + externalConsole?: boolean; +}; + /** * A minimal inline Debug Adapter that runs the target program directly without a debug adapter * when the user invokes "Run Without Debugging". @@ -59,26 +71,24 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { } private async launch(request: { command: string; seq: number; arguments?: any; }): Promise { - const config = request.arguments as { - program?: string; - args?: string[]; - cwd?: string; - environment?: { name: string; value: string; }[]; - console?: string; - externalConsole?: boolean; - }; + const config = request.arguments as LaunchConfiguration; const program: string = config.program ?? ''; const args: string[] = config.args ?? []; const cwd: string | undefined = config.cwd; - const environment: { name: string; value: string; }[] = config.environment ?? []; + const environment: LaunchEnvironmentEntry[] = config.environment ?? []; + const envObject: Record = config.env ?? {}; const consoleMode: string = config.console ?? (config.externalConsole ? 'externalTerminal' : 'integratedTerminal'); - // Merge the launch config's environment variables on top of the inherited process environment. + // Merge environment values in this order: inherited process environment, legacy + // `environment` entries, then shorthand `env` values (higher precedence). const env: NodeJS.ProcessEnv = { ...process.env }; for (const e of environment) { env[e.name] = e.value; } + for (const [key, value] of Object.entries(envObject)) { + env[key] = value; + } this.sendResponse(request, {}); diff --git a/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp b/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp new file mode 100644 index 000000000..0b1c7532e --- /dev/null +++ b/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp @@ -0,0 +1,21 @@ +#include +#include + +int main(int argc, char *argv[]) { + if (argc < 3) { + return 1; + } + + const char *value = std::getenv(argv[1]); + + std::ofstream resultFile(argv[2]); + if (!resultFile) { + return 2; + } + + if (value) { + resultFile << value; + } + + return 0; +} diff --git a/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts index 53542e36e..0de1a437f 100644 --- a/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts +++ b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts @@ -146,15 +146,39 @@ async function waitForResultFileValue(filePath: string, timeoutMs: number): Prom assert.fail(`Timed out waiting for numeric result in ${filePath}. Last contents: ${lastContents}`); } +async function waitForResultFileText(filePath: string, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + let lastContents = ''; + + while (Date.now() < deadline) { + try { + lastContents = await util.readFileText(filePath, 'utf8'); + return lastContents.trim(); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } + } + + await new Promise(resolve => setTimeout(resolve, 100)); + } + + assert.fail(`Timed out waiting for output in ${filePath}. Last contents: ${lastContents}`); +} + suite('Run Without Debugging Test', function (): void { const expectedResultValue = 37; const workspaceFolder = vscode.workspace.workspaceFolders?.[0] ?? assert.fail('No workspace folder available'); const workspacePath = workspaceFolder.uri.fsPath; const sourceFile = path.join(workspacePath, 'debugTest.cpp'); + const envSourceFile = path.join(workspacePath, 'envTest.cpp'); const sourceUri = vscode.Uri.file(sourceFile); const resultFilePath = path.join(workspacePath, 'runWithoutDebuggingResult.txt'); + const envResultFilePath = path.join(workspacePath, 'runWithoutDebuggingEnvResult.txt'); const executableName = isWindows ? 'debugTestProgram.exe' : 'debugTestProgram'; const executablePath = path.join(workspacePath, executableName); + const envExecutableName = isWindows ? 'envTestProgram.exe' : 'envTestProgram'; + const envExecutablePath = path.join(workspacePath, envExecutableName); const sessionName = 'Run Without Debugging Result File'; const debugType = isWindows ? 'cppvsdbg' : 'cppdbg'; const miMode = isMacOS ? 'lldb' : 'gdb'; @@ -165,6 +189,7 @@ suite('Run Without Debugging Test', function (): void { await extension.activate(); } await compileProgram(workspacePath, sourceFile, executablePath); + await compileProgram(workspacePath, envSourceFile, envExecutablePath); }); suiteTeardown(async function (): Promise { @@ -175,6 +200,37 @@ suite('Run Without Debugging Test', function (): void { setup(async function (): Promise { await util.deleteFile(resultFilePath); + await util.deleteFile(envResultFilePath); + }); + + test('Run Without Debugging should apply env and prefer it over environment entries', async () => { + const testVarName = 'CPPTOOLS_NO_DEBUG_ENV_TEST'; + const expectedValue = 'value-from-env-object'; + const fallbackValue = 'value-from-environment-array'; + const envConfig: Record = { + [testVarName]: expectedValue + }; + + const started = await vscode.debug.startDebugging( + workspaceFolder, + { + name: `${sessionName} Env`, + type: debugType, + request: 'launch', + program: envExecutablePath, + args: [testVarName, envResultFilePath], + cwd: workspacePath, + environment: [{ name: testVarName, value: fallbackValue }], + env: envConfig, + externalConsole: debugType === 'cppdbg' ? false : undefined, + console: debugType === 'cppvsdbg' ? 'internalConsole' : undefined + }, + { noDebug: true }); + + assert.strictEqual(started, true, 'The noDebug launch with env did not start successfully.'); + const actualValue = await waitForResultFileText(envResultFilePath, 10000); + + assert.strictEqual(actualValue, expectedValue, 'Expected env object values to be applied and take precedence over environment entries.'); }); test('Run Without Debugging should not break on breakpoints and write the expected result file', async () => { @@ -211,6 +267,7 @@ suite('Run Without Debugging Test', function (): void { tracker.dispose(); vscode.debug.removeBreakpoints([breakpoint]); await util.deleteFile(resultFilePath); + await util.deleteFile(envResultFilePath); } }); @@ -261,6 +318,7 @@ suite('Run Without Debugging Test', function (): void { tracker.dispose(); vscode.debug.removeBreakpoints([breakpoint]); await util.deleteFile(resultFilePath); + await util.deleteFile(envResultFilePath); } }); diff --git a/Extension/tools/OptionsSchema.json b/Extension/tools/OptionsSchema.json index 010af9a76..531a4ca1a 100644 --- a/Extension/tools/OptionsSchema.json +++ b/Extension/tools/OptionsSchema.json @@ -706,6 +706,14 @@ }, "default": [] }, + "env": { + "type": "object", + "description": "%c_cpp.debuggers.env.description%", + "additionalProperties": { + "type": "string" + }, + "default": {} + }, "envFile": { "type": "string", "description": "%c_cpp.debuggers.envFile.description%", @@ -1013,6 +1021,14 @@ }, "default": [] }, + "env": { + "type": "object", + "description": "%c_cpp.debuggers.env.description%", + "additionalProperties": { + "type": "string" + }, + "default": {} + }, "envFile": { "type": "string", "description": "%c_cpp.debuggers.envFile.description%", From 66f6d951013438c92442a36028bf4aa8a7b12d2c Mon Sep 17 00:00:00 2001 From: 8prashant Date: Wed, 19 Aug 2026 15:54:27 +0530 Subject: [PATCH 4/6] Add support for env object in cppdbg and runWithoutDebugging configurations --- .../src/Debugger/configurationProvider.ts | 33 +++++++++++ .../Debugger/runWithoutDebuggingAdapter.ts | 30 ++++++---- .../RunWithoutDebugging/assets/envTest.cpp | 21 +++++++ .../runWithoutDebugging.integration.test.ts | 58 +++++++++++++++++++ Extension/tools/OptionsSchema.json | 16 +++++ 5 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp diff --git a/Extension/src/Debugger/configurationProvider.ts b/Extension/src/Debugger/configurationProvider.ts index 5bc427759..b8daa5f6f 100644 --- a/Extension/src/Debugger/configurationProvider.ts +++ b/Extension/src/Debugger/configurationProvider.ts @@ -270,6 +270,10 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv // Add environment variables from .env file this.resolveEnvFile(config, folder); + // cppdbg (MIEngine) consumes the legacy `environment` array, not `env`. + // Convert here so both syntaxes work while preserving `env` precedence. + this.resolveEnvObjectForCppdbg(config); + await this.expand(config, folder); this.resolveSourceFileMapVariables(config); @@ -706,6 +710,35 @@ export class DebugConfigurationProvider implements vscode.DebugConfigurationProv } } + private resolveEnvObjectForCppdbg(config: CppDebugConfiguration): void { + if (config.type !== DebuggerType.cppdbg || config.request !== 'launch') { + return; + } + + const envObject = config.env; + if (!util.isObject(envObject)) { + return; + } + + const environment: Environment[] = util.isArray(config.environment) ? config.environment : []; + const mergedEnvironment = new Map(); + + for (const entry of environment) { + if (util.isString(entry?.name) && util.isString(entry?.value)) { + mergedEnvironment.set(entry.name, entry.value); + } + } + + for (const [name, value] of Object.entries(envObject)) { + if (util.isString(value)) { + mergedEnvironment.set(name, value); + } + } + + config.environment = Array.from(mergedEnvironment.entries()).map(([name, value]) => ({ name, value })); + delete config.env; + } + private resolveSourceFileMapVariables(config: CppDebugConfiguration): void { const messages: string[] = []; if (config.sourceFileMap) { diff --git a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts index fc7c98a35..9beaa948b 100644 --- a/Extension/src/Debugger/runWithoutDebuggingAdapter.ts +++ b/Extension/src/Debugger/runWithoutDebuggingAdapter.ts @@ -14,6 +14,18 @@ import { isWindows } from '../constants'; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); const localize = nls.loadMessageBundle(); +type LaunchEnvironmentEntry = { name: string; value: string; }; + +type LaunchConfiguration = { + program?: string; + args?: string[]; + cwd?: string; + environment?: LaunchEnvironmentEntry[]; + env?: Record; + console?: string; + externalConsole?: boolean; +}; + /** * A minimal inline Debug Adapter that runs the target program directly without a debug adapter * when the user invokes "Run Without Debugging". @@ -59,26 +71,24 @@ export class RunWithoutDebuggingAdapter implements vscode.DebugAdapter { } private async launch(request: { command: string; seq: number; arguments?: any; }): Promise { - const config = request.arguments as { - program?: string; - args?: string[]; - cwd?: string; - environment?: { name: string; value: string; }[]; - console?: string; - externalConsole?: boolean; - }; + const config = request.arguments as LaunchConfiguration; const program: string = config.program ?? ''; const args: string[] = config.args ?? []; const cwd: string | undefined = config.cwd; - const environment: { name: string; value: string; }[] = config.environment ?? []; + const environment: LaunchEnvironmentEntry[] = config.environment ?? []; + const envObject: Record = config.env ?? {}; const consoleMode: string = config.console ?? (config.externalConsole ? 'externalTerminal' : 'integratedTerminal'); - // Merge the launch config's environment variables on top of the inherited process environment. + // Merge environment values in this order: inherited process environment, legacy + // `environment` entries, then shorthand `env` values (higher precedence). const env: NodeJS.ProcessEnv = { ...process.env }; for (const e of environment) { env[e.name] = e.value; } + for (const [key, value] of Object.entries(envObject)) { + env[key] = value; + } this.sendResponse(request, {}); diff --git a/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp b/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp new file mode 100644 index 000000000..0b1c7532e --- /dev/null +++ b/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp @@ -0,0 +1,21 @@ +#include +#include + +int main(int argc, char *argv[]) { + if (argc < 3) { + return 1; + } + + const char *value = std::getenv(argv[1]); + + std::ofstream resultFile(argv[2]); + if (!resultFile) { + return 2; + } + + if (value) { + resultFile << value; + } + + return 0; +} diff --git a/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts index 53542e36e..0de1a437f 100644 --- a/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts +++ b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts @@ -146,15 +146,39 @@ async function waitForResultFileValue(filePath: string, timeoutMs: number): Prom assert.fail(`Timed out waiting for numeric result in ${filePath}. Last contents: ${lastContents}`); } +async function waitForResultFileText(filePath: string, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + let lastContents = ''; + + while (Date.now() < deadline) { + try { + lastContents = await util.readFileText(filePath, 'utf8'); + return lastContents.trim(); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } + } + + await new Promise(resolve => setTimeout(resolve, 100)); + } + + assert.fail(`Timed out waiting for output in ${filePath}. Last contents: ${lastContents}`); +} + suite('Run Without Debugging Test', function (): void { const expectedResultValue = 37; const workspaceFolder = vscode.workspace.workspaceFolders?.[0] ?? assert.fail('No workspace folder available'); const workspacePath = workspaceFolder.uri.fsPath; const sourceFile = path.join(workspacePath, 'debugTest.cpp'); + const envSourceFile = path.join(workspacePath, 'envTest.cpp'); const sourceUri = vscode.Uri.file(sourceFile); const resultFilePath = path.join(workspacePath, 'runWithoutDebuggingResult.txt'); + const envResultFilePath = path.join(workspacePath, 'runWithoutDebuggingEnvResult.txt'); const executableName = isWindows ? 'debugTestProgram.exe' : 'debugTestProgram'; const executablePath = path.join(workspacePath, executableName); + const envExecutableName = isWindows ? 'envTestProgram.exe' : 'envTestProgram'; + const envExecutablePath = path.join(workspacePath, envExecutableName); const sessionName = 'Run Without Debugging Result File'; const debugType = isWindows ? 'cppvsdbg' : 'cppdbg'; const miMode = isMacOS ? 'lldb' : 'gdb'; @@ -165,6 +189,7 @@ suite('Run Without Debugging Test', function (): void { await extension.activate(); } await compileProgram(workspacePath, sourceFile, executablePath); + await compileProgram(workspacePath, envSourceFile, envExecutablePath); }); suiteTeardown(async function (): Promise { @@ -175,6 +200,37 @@ suite('Run Without Debugging Test', function (): void { setup(async function (): Promise { await util.deleteFile(resultFilePath); + await util.deleteFile(envResultFilePath); + }); + + test('Run Without Debugging should apply env and prefer it over environment entries', async () => { + const testVarName = 'CPPTOOLS_NO_DEBUG_ENV_TEST'; + const expectedValue = 'value-from-env-object'; + const fallbackValue = 'value-from-environment-array'; + const envConfig: Record = { + [testVarName]: expectedValue + }; + + const started = await vscode.debug.startDebugging( + workspaceFolder, + { + name: `${sessionName} Env`, + type: debugType, + request: 'launch', + program: envExecutablePath, + args: [testVarName, envResultFilePath], + cwd: workspacePath, + environment: [{ name: testVarName, value: fallbackValue }], + env: envConfig, + externalConsole: debugType === 'cppdbg' ? false : undefined, + console: debugType === 'cppvsdbg' ? 'internalConsole' : undefined + }, + { noDebug: true }); + + assert.strictEqual(started, true, 'The noDebug launch with env did not start successfully.'); + const actualValue = await waitForResultFileText(envResultFilePath, 10000); + + assert.strictEqual(actualValue, expectedValue, 'Expected env object values to be applied and take precedence over environment entries.'); }); test('Run Without Debugging should not break on breakpoints and write the expected result file', async () => { @@ -211,6 +267,7 @@ suite('Run Without Debugging Test', function (): void { tracker.dispose(); vscode.debug.removeBreakpoints([breakpoint]); await util.deleteFile(resultFilePath); + await util.deleteFile(envResultFilePath); } }); @@ -261,6 +318,7 @@ suite('Run Without Debugging Test', function (): void { tracker.dispose(); vscode.debug.removeBreakpoints([breakpoint]); await util.deleteFile(resultFilePath); + await util.deleteFile(envResultFilePath); } }); diff --git a/Extension/tools/OptionsSchema.json b/Extension/tools/OptionsSchema.json index 010af9a76..531a4ca1a 100644 --- a/Extension/tools/OptionsSchema.json +++ b/Extension/tools/OptionsSchema.json @@ -706,6 +706,14 @@ }, "default": [] }, + "env": { + "type": "object", + "description": "%c_cpp.debuggers.env.description%", + "additionalProperties": { + "type": "string" + }, + "default": {} + }, "envFile": { "type": "string", "description": "%c_cpp.debuggers.envFile.description%", @@ -1013,6 +1021,14 @@ }, "default": [] }, + "env": { + "type": "object", + "description": "%c_cpp.debuggers.env.description%", + "additionalProperties": { + "type": "string" + }, + "default": {} + }, "envFile": { "type": "string", "description": "%c_cpp.debuggers.envFile.description%", From 71303855292a14e3d67dd2830df9656c6c96a73f Mon Sep 17 00:00:00 2001 From: 8prashant Date: Tue, 1 Sep 2026 22:13:37 +0530 Subject: [PATCH 5/6] Update env description to include message and comment structure; enhance integration tests for env object handling in cppdbg --- Extension/package.nls.json | 7 +- .../RunWithoutDebugging/assets/envTest.cpp | 42 ++++----- .../runWithoutDebugging.integration.test.ts | 93 ++++++++++++++----- 3 files changed, 99 insertions(+), 43 deletions(-) diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 36778d3b6..4f853b43f 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -927,7 +927,12 @@ "{Locked=\"[ { \\\"name\\\": \\\"\"} {Locked=\"\\\", \\\"value\\\": \\\"\"} {Locked=\"\\\" } ]\"} {Locked=\"[ { \\\"\"} {Locked=\"\\\": \\\"\"} {Locked=\"\\\" } ]\"}" ] }, - "c_cpp.debuggers.env.description": "Object of environment variables to add to the environment for the program. Example: { \"MY_VAR\": \"value\" }. Use `environment` for the array-of-objects form.", + "c_cpp.debuggers.env.description": { + "message": "Object of environment variables to add to the environment for the program. Example: { \"MY_VAR\": \"value\" }. Use `environment` for the array-of-objects form.", + "comment": [ + "{Locked=\"{ \\\"MY_VAR\\\": \\\"value\\\" }\"} {Locked=\"`environment`\"}" + ] + }, "c_cpp.debuggers.envFile.description": "Absolute path to a file containing environment variable definitions. This file has key value pairs separated by an equals sign per line. E.g. KEY=VALUE.", "c_cpp.debuggers.additionalSOLibSearchPath.description": "Semicolon separated list of directories to use to search for .so files. Example: \"c:\\dir1;c:\\dir2\".", "c_cpp.debuggers.MIMode.description": "Indicates the console debugger that the MIDebugEngine will connect to. Allowed values are \"gdb\" \"lldb\".", diff --git a/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp b/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp index 0b1c7532e..45df63b0b 100644 --- a/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp +++ b/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp @@ -1,21 +1,21 @@ -#include -#include - -int main(int argc, char *argv[]) { - if (argc < 3) { - return 1; - } - - const char *value = std::getenv(argv[1]); - - std::ofstream resultFile(argv[2]); - if (!resultFile) { - return 2; - } - - if (value) { - resultFile << value; - } - - return 0; -} +#include +#include + +int main(int argc, char *argv[]) { + if (argc < 3) { + return 1; + } + + const char *value = std::getenv(argv[1]); + + std::ofstream resultFile(argv[2]); + if (!resultFile) { + return 2; + } + + if (value) { + resultFile << value; + } + + return 0; +} diff --git a/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts index 0de1a437f..84cbc7114 100644 --- a/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts +++ b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts @@ -10,6 +10,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as util from '../../../../src/common'; import { isMacOS, isWindows } from '../../../../src/constants'; +import { ConfigurationAssetProviderFactory, DebugConfigurationProvider } from '../../../../src/Debugger/configurationProvider'; +import { DebuggerType } from '../../../../src/Debugger/configurations'; import { compileProgram } from './compileProgram'; interface TrackerState { @@ -153,7 +155,10 @@ async function waitForResultFileText(filePath: string, timeoutMs: number): Promi while (Date.now() < deadline) { try { lastContents = await util.readFileText(filePath, 'utf8'); - return lastContents.trim(); + const trimmedContents = lastContents.trim(); + if (trimmedContents.length > 0) { + return trimmedContents; + } } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw error; @@ -203,6 +208,33 @@ suite('Run Without Debugging Test', function (): void { await util.deleteFile(envResultFilePath); }); + test('DebugConfigurationProvider should convert env object to environment array for cppdbg and preserve precedence', async () => { + const provider = new DebugConfigurationProvider(ConfigurationAssetProviderFactory.getConfigurationProvider(), DebuggerType.cppdbg); + const inputConfig: any = { + name: 'Test Cppdbg Env Resolution', + type: 'cppdbg', + request: 'launch', + program: envExecutablePath, + environment: [ + { name: 'TEST_VAR', value: 'from_environment' }, + { name: 'OTHER_VAR', value: 'from_environment_2' } + ], + env: { + TEST_VAR: 'from_env', + NEW_VAR: 'from_env_2' + } + }; + + const resolvedConfig = await provider.resolveDebugConfigurationWithSubstitutedVariables(workspaceFolder, inputConfig); + assert.ok(resolvedConfig, 'Resolved config should not be undefined or null.'); + assert.strictEqual(resolvedConfig.env, undefined, 'config.env should be removed after conversion.'); + assert.deepStrictEqual(resolvedConfig.environment, [ + { name: 'TEST_VAR', value: 'from_env' }, + { name: 'OTHER_VAR', value: 'from_environment_2' }, + { name: 'NEW_VAR', value: 'from_env_2' } + ], 'config.environment should merge environment entries with env precedence.'); + }); + test('Run Without Debugging should apply env and prefer it over environment entries', async () => { const testVarName = 'CPPTOOLS_NO_DEBUG_ENV_TEST'; const expectedValue = 'value-from-env-object'; @@ -210,27 +242,46 @@ suite('Run Without Debugging Test', function (): void { const envConfig: Record = { [testVarName]: expectedValue }; + const envSessionName = `${sessionName} Env`; + const debugSessionTerminated = createSessionTerminatedPromise(envSessionName); - const started = await vscode.debug.startDebugging( - workspaceFolder, - { - name: `${sessionName} Env`, - type: debugType, - request: 'launch', - program: envExecutablePath, - args: [testVarName, envResultFilePath], - cwd: workspacePath, - environment: [{ name: testVarName, value: fallbackValue }], - env: envConfig, - externalConsole: debugType === 'cppdbg' ? false : undefined, - console: debugType === 'cppvsdbg' ? 'internalConsole' : undefined - }, - { noDebug: true }); - - assert.strictEqual(started, true, 'The noDebug launch with env did not start successfully.'); - const actualValue = await waitForResultFileText(envResultFilePath, 10000); - - assert.strictEqual(actualValue, expectedValue, 'Expected env object values to be applied and take precedence over environment entries.'); + let launchedSession: vscode.DebugSession | undefined; + const startedSubscription = vscode.debug.onDidStartDebugSession((session) => { + if (session.name === envSessionName) { + launchedSession = session; + } + }); + + try { + const started = await vscode.debug.startDebugging( + workspaceFolder, + { + name: envSessionName, + type: debugType, + request: 'launch', + program: envExecutablePath, + args: [testVarName, envResultFilePath], + cwd: workspacePath, + environment: [{ name: testVarName, value: fallbackValue }], + env: envConfig, + externalConsole: debugType === 'cppdbg' ? false : undefined, + console: debugType === 'cppvsdbg' ? 'internalConsole' : undefined + }, + { noDebug: true }); + + assert.strictEqual(started, true, 'The noDebug launch with env did not start successfully.'); + const actualValue = await waitForResultFileText(envResultFilePath, 10000); + + assert.strictEqual(actualValue, expectedValue, 'Expected env object values to be applied and take precedence over environment entries.'); + await debugSessionTerminated; + } finally { + startedSubscription.dispose(); + const sessionToStop = launchedSession ?? (vscode.debug.activeDebugSession?.name === envSessionName ? vscode.debug.activeDebugSession : undefined); + if (sessionToStop) { + await vscode.debug.stopDebugging(sessionToStop); + } + await util.deleteFile(envResultFilePath); + } }); test('Run Without Debugging should not break on breakpoints and write the expected result file', async () => { From 84a0467da873c984b998c05508d0813f5946bf91 Mon Sep 17 00:00:00 2001 From: 8prashant Date: Tue, 1 Sep 2026 22:13:37 +0530 Subject: [PATCH 6/6] Update env description to include message and comment structure; enhance integration tests for env object handling in cppdbg --- Extension/package.nls.json | 7 +- .../RunWithoutDebugging/assets/envTest.cpp | 42 ++++----- .../runWithoutDebugging.integration.test.ts | 93 ++++++++++++++----- 3 files changed, 99 insertions(+), 43 deletions(-) diff --git a/Extension/package.nls.json b/Extension/package.nls.json index 36778d3b6..4f853b43f 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -927,7 +927,12 @@ "{Locked=\"[ { \\\"name\\\": \\\"\"} {Locked=\"\\\", \\\"value\\\": \\\"\"} {Locked=\"\\\" } ]\"} {Locked=\"[ { \\\"\"} {Locked=\"\\\": \\\"\"} {Locked=\"\\\" } ]\"}" ] }, - "c_cpp.debuggers.env.description": "Object of environment variables to add to the environment for the program. Example: { \"MY_VAR\": \"value\" }. Use `environment` for the array-of-objects form.", + "c_cpp.debuggers.env.description": { + "message": "Object of environment variables to add to the environment for the program. Example: { \"MY_VAR\": \"value\" }. Use `environment` for the array-of-objects form.", + "comment": [ + "{Locked=\"{ \\\"MY_VAR\\\": \\\"value\\\" }\"} {Locked=\"`environment`\"}" + ] + }, "c_cpp.debuggers.envFile.description": "Absolute path to a file containing environment variable definitions. This file has key value pairs separated by an equals sign per line. E.g. KEY=VALUE.", "c_cpp.debuggers.additionalSOLibSearchPath.description": "Semicolon separated list of directories to use to search for .so files. Example: \"c:\\dir1;c:\\dir2\".", "c_cpp.debuggers.MIMode.description": "Indicates the console debugger that the MIDebugEngine will connect to. Allowed values are \"gdb\" \"lldb\".", diff --git a/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp b/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp index 0b1c7532e..45df63b0b 100644 --- a/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp +++ b/Extension/test/scenarios/RunWithoutDebugging/assets/envTest.cpp @@ -1,21 +1,21 @@ -#include -#include - -int main(int argc, char *argv[]) { - if (argc < 3) { - return 1; - } - - const char *value = std::getenv(argv[1]); - - std::ofstream resultFile(argv[2]); - if (!resultFile) { - return 2; - } - - if (value) { - resultFile << value; - } - - return 0; -} +#include +#include + +int main(int argc, char *argv[]) { + if (argc < 3) { + return 1; + } + + const char *value = std::getenv(argv[1]); + + std::ofstream resultFile(argv[2]); + if (!resultFile) { + return 2; + } + + if (value) { + resultFile << value; + } + + return 0; +} diff --git a/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts index 0de1a437f..84cbc7114 100644 --- a/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts +++ b/Extension/test/scenarios/RunWithoutDebugging/tests/runWithoutDebugging.integration.test.ts @@ -10,6 +10,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; import * as util from '../../../../src/common'; import { isMacOS, isWindows } from '../../../../src/constants'; +import { ConfigurationAssetProviderFactory, DebugConfigurationProvider } from '../../../../src/Debugger/configurationProvider'; +import { DebuggerType } from '../../../../src/Debugger/configurations'; import { compileProgram } from './compileProgram'; interface TrackerState { @@ -153,7 +155,10 @@ async function waitForResultFileText(filePath: string, timeoutMs: number): Promi while (Date.now() < deadline) { try { lastContents = await util.readFileText(filePath, 'utf8'); - return lastContents.trim(); + const trimmedContents = lastContents.trim(); + if (trimmedContents.length > 0) { + return trimmedContents; + } } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { throw error; @@ -203,6 +208,33 @@ suite('Run Without Debugging Test', function (): void { await util.deleteFile(envResultFilePath); }); + test('DebugConfigurationProvider should convert env object to environment array for cppdbg and preserve precedence', async () => { + const provider = new DebugConfigurationProvider(ConfigurationAssetProviderFactory.getConfigurationProvider(), DebuggerType.cppdbg); + const inputConfig: any = { + name: 'Test Cppdbg Env Resolution', + type: 'cppdbg', + request: 'launch', + program: envExecutablePath, + environment: [ + { name: 'TEST_VAR', value: 'from_environment' }, + { name: 'OTHER_VAR', value: 'from_environment_2' } + ], + env: { + TEST_VAR: 'from_env', + NEW_VAR: 'from_env_2' + } + }; + + const resolvedConfig = await provider.resolveDebugConfigurationWithSubstitutedVariables(workspaceFolder, inputConfig); + assert.ok(resolvedConfig, 'Resolved config should not be undefined or null.'); + assert.strictEqual(resolvedConfig.env, undefined, 'config.env should be removed after conversion.'); + assert.deepStrictEqual(resolvedConfig.environment, [ + { name: 'TEST_VAR', value: 'from_env' }, + { name: 'OTHER_VAR', value: 'from_environment_2' }, + { name: 'NEW_VAR', value: 'from_env_2' } + ], 'config.environment should merge environment entries with env precedence.'); + }); + test('Run Without Debugging should apply env and prefer it over environment entries', async () => { const testVarName = 'CPPTOOLS_NO_DEBUG_ENV_TEST'; const expectedValue = 'value-from-env-object'; @@ -210,27 +242,46 @@ suite('Run Without Debugging Test', function (): void { const envConfig: Record = { [testVarName]: expectedValue }; + const envSessionName = `${sessionName} Env`; + const debugSessionTerminated = createSessionTerminatedPromise(envSessionName); - const started = await vscode.debug.startDebugging( - workspaceFolder, - { - name: `${sessionName} Env`, - type: debugType, - request: 'launch', - program: envExecutablePath, - args: [testVarName, envResultFilePath], - cwd: workspacePath, - environment: [{ name: testVarName, value: fallbackValue }], - env: envConfig, - externalConsole: debugType === 'cppdbg' ? false : undefined, - console: debugType === 'cppvsdbg' ? 'internalConsole' : undefined - }, - { noDebug: true }); - - assert.strictEqual(started, true, 'The noDebug launch with env did not start successfully.'); - const actualValue = await waitForResultFileText(envResultFilePath, 10000); - - assert.strictEqual(actualValue, expectedValue, 'Expected env object values to be applied and take precedence over environment entries.'); + let launchedSession: vscode.DebugSession | undefined; + const startedSubscription = vscode.debug.onDidStartDebugSession((session) => { + if (session.name === envSessionName) { + launchedSession = session; + } + }); + + try { + const started = await vscode.debug.startDebugging( + workspaceFolder, + { + name: envSessionName, + type: debugType, + request: 'launch', + program: envExecutablePath, + args: [testVarName, envResultFilePath], + cwd: workspacePath, + environment: [{ name: testVarName, value: fallbackValue }], + env: envConfig, + externalConsole: debugType === 'cppdbg' ? false : undefined, + console: debugType === 'cppvsdbg' ? 'internalConsole' : undefined + }, + { noDebug: true }); + + assert.strictEqual(started, true, 'The noDebug launch with env did not start successfully.'); + const actualValue = await waitForResultFileText(envResultFilePath, 10000); + + assert.strictEqual(actualValue, expectedValue, 'Expected env object values to be applied and take precedence over environment entries.'); + await debugSessionTerminated; + } finally { + startedSubscription.dispose(); + const sessionToStop = launchedSession ?? (vscode.debug.activeDebugSession?.name === envSessionName ? vscode.debug.activeDebugSession : undefined); + if (sessionToStop) { + await vscode.debug.stopDebugging(sessionToStop); + } + await util.deleteFile(envResultFilePath); + } }); test('Run Without Debugging should not break on breakpoints and write the expected result file', async () => {