diff --git a/package.json b/package.json index ff182edd..29c6a48b 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,11 @@ "description": "Additional environment variables to pass to the debugging (and debugged) process", "default": {} }, + "envFile": { + "type": "string", + "description": "Path to a file containing environment variable definitions (e.g., .env file). Variables defined in 'env' will override those in envFile.", + "default": "" + }, "showProtocolLog": { "type": "boolean", "description": "Show a log of DAP requests, events, and responses", diff --git a/src/config.ts b/src/config.ts index 7893469e..cd91e65b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -22,6 +22,7 @@ export interface LaunchConfiguration extends DebugConfiguration { cwd?: string; args?: string[]; env?: { [key: string]: string }; + envFile?: string; debugPort?: string; waitLaunchTime?: number; diff --git a/src/extension.ts b/src/extension.ts index 15cf6748..9c0bd0f2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -22,7 +22,7 @@ import { import { DebugProtocol } from "@vscode/debugprotocol"; import { registerInspectorView } from "./inspector"; import { AttachConfiguration, LaunchConfiguration } from "./config"; -import { VersionChecker, fullPath } from "./utils"; +import { VersionChecker, fullPath, parseEnvFile } from "./utils"; const asyncExec = promisify(child_process.exec); @@ -64,6 +64,41 @@ function customPath(workingDirectory: string): string { } } +/** + * Merge environment variables from envFile and config.env + * Variables in config.env take precedence over those in envFile + * @param config Launch configuration + * @returns Merged environment variables, or undefined if neither envFile nor env is specified + */ +function mergeEnvVariables(config: LaunchConfiguration): { [key: string]: string } | undefined { + let mergedEnv: { [key: string]: string } | undefined; + + if (config.envFile) { + const envFilePath = path.isAbsolute(config.envFile) + ? config.envFile + : path.join(config.cwd ? customPath(config.cwd) : workspaceFolder() || "", config.envFile); + + const envFromFile = parseEnvFile(envFilePath); + if (envFromFile === undefined) { + + outputChannel.appendLine(`Warning: Could not read envFile: ${envFilePath}`); + vscode.window.showWarningMessage(`Could not read envFile: ${envFilePath}`); + } else { + mergedEnv = { ...envFromFile }; + } + } + + if (config.env) { + if (mergedEnv) { + mergedEnv = { ...mergedEnv, ...config.env }; + } else { + mergedEnv = { ...config.env }; + } + } + + return mergedEnv; +} + function pp(obj: any) { outputChannel.appendLine(JSON.stringify(obj)); } @@ -513,9 +548,10 @@ class RdbgAdapterDescriptorFactory implements DebugAdapterDescriptorFactory, Ver getSockPath(config: LaunchConfiguration): Promise { return new Promise((resolve) => { const command = this.makeShellCommand(this.rdbgBin(config) + " --util=gen-sockpath"); + const mergedEnv = mergeEnvVariables(config); const p = child_process.exec(command, { cwd: config.cwd ? customPath(config.cwd) : workspaceFolder(), - env: { ...process.env, ...config.env } + env: { ...process.env, ...mergedEnv } }); let path: string; @@ -544,9 +580,10 @@ class RdbgAdapterDescriptorFactory implements DebugAdapterDescriptorFactory, Ver getTcpPortFile(config: LaunchConfiguration): Promise { return new Promise((resolve) => { const command = this.makeShellCommand(this.rdbgBin(config) + " --util=gen-portpath"); + const mergedEnv = mergeEnvVariables(config); const p = child_process.exec(command, { cwd: config.cwd ? customPath(config.cwd) : workspaceFolder(), - env: { ...process.env, ...config.env } + env: { ...process.env, ...mergedEnv } }); let path: string; @@ -568,9 +605,10 @@ class RdbgAdapterDescriptorFactory implements DebugAdapterDescriptorFactory, Ver getVersion(config: LaunchConfiguration): Promise { return new Promise((resolve) => { const command = this.makeShellCommand(this.rdbgBin(config) + " --version"); + const mergedEnv = mergeEnvVariables(config); const p = child_process.exec(command, { cwd: config.cwd ? customPath(config.cwd) : workspaceFolder(), - env: { ...process.env, ...config.env } + env: { ...process.env, ...mergedEnv } }); let version: string; @@ -748,7 +786,8 @@ class RdbgAdapterDescriptorFactory implements DebugAdapterDescriptorFactory, Ver throw error; } - let cmdline = this.envPrefix(config.env); + const mergedEnv = mergeEnvVariables(config); + let cmdline = this.envPrefix(mergedEnv); if (config.noDebug) { cmdline += execCommand; @@ -879,8 +918,9 @@ class RdbgAdapterDescriptorFactory implements DebugAdapterDescriptorFactory, Ver } throw error; } + const mergedEnv = mergeEnvVariables(config); const options: child_process.SpawnOptionsWithoutStdio = { - env: { ...process.env, ...config.env }, + env: { ...process.env, ...mergedEnv }, cwd: customPath(config.cwd || ""), }; if (process.platform === "win32") options.shell = "powershell"; diff --git a/src/utils.ts b/src/utils.ts index 4327b0db..4d719b17 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,3 +31,54 @@ export function fullPath(p: string, session: vscode.DebugSession | undefined) { } return p; } + +/** + * Parse a .env file and return environment variables as key-value pairs + * Supports: + * - KEY=VALUE format + * - Comments (lines starting with #) + * - Quoted values (single and double quotes) + * - Empty lines + * @param envFilePath Path to the .env file + * @returns Object containing environment variables, or undefined if file doesn't exist + */ +export function parseEnvFile(envFilePath: string): { [key: string]: string } | undefined { + if (!fs.existsSync(envFilePath)) { + return undefined; + } + + try { + const content = fs.readFileSync(envFilePath, "utf8"); + const env: { [key: string]: string } = {}; + const lines = content.split(/\r?\n/); + + for (let line of lines) { + line = line.trim(); + + if (line === "" || line.startsWith("#")) { + continue; + } + + const equalIndex = line.indexOf("="); + if (equalIndex === -1) { + continue; + } + + const key = line.substring(0, equalIndex).trim(); + let value = line.substring(equalIndex + 1).trim(); + + if ((value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'"))) { + value = value.substring(1, value.length - 1); + } + + if (key) { + env[key] = value; + } + } + + return env; + } catch (error) { + return undefined; + } +}