diff --git a/Extension/package.json b/Extension/package.json index 5e02712e1..5ca4ab1ac 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\".", 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%",