diff --git a/src/kernels/raw/launcher/kernelEnvVarsService.node.ts b/src/kernels/raw/launcher/kernelEnvVarsService.node.ts index 887cbec7a2d..5f664f1281a 100644 --- a/src/kernels/raw/launcher/kernelEnvVarsService.node.ts +++ b/src/kernels/raw/launcher/kernelEnvVarsService.node.ts @@ -8,7 +8,8 @@ import { IConfigurationService, Resource } from '../../../platform/common/types' import { noop } from '../../../platform/common/utils/misc'; import { IEnvironmentVariablesService, - ICustomEnvironmentVariablesProvider + ICustomEnvironmentVariablesProvider, + EnvironmentVariables } from '../../../platform/common/variables/types'; import { IEnvironmentActivationService } from '../../../platform/interpreter/activation/types'; import { IInterpreterService } from '../../../platform/interpreter/contracts'; @@ -101,6 +102,15 @@ export class KernelEnvironmentVariablesService { kernelEnv = kernelEnv || {}; customEnvVars = customEnvVars || {}; + // Keep a list of the kernelSpec variables that need to be substituted. + const kernelSpecVariablesRequiringSubstitution: Record = {}; + for (const [key, value] of Object.entries(kernelEnv || {})) { + if (typeof value === 'string' && substituteEnvVars(key, value, process.env) !== value) { + kernelSpecVariablesRequiringSubstitution[key] = value; + delete kernelEnv[key]; + } + } + if (isPythonKernel || interpreter) { // Merge the env variables with that of the kernel env. interpreterEnv = interpreterEnv || customEnvVars; @@ -128,6 +138,40 @@ export class KernelEnvironmentVariablesService { `Kernel Env Variables for ${kernelSpec.specFile || kernelSpec.name}, PATH value is ${mergedVars.PATH}` ); + // env variables in kernelSpecs can contain variables that need to be substituted + for (const [key, value] of Object.entries(kernelSpecVariablesRequiringSubstitution)) { + mergedVars[key] = substituteEnvVars(key, value, mergedVars); + } + return mergedVars; } } + +const SUBST_REGEX = /\${([a-zA-Z]\w*)?([^}\w].*)?}/g; + +function substituteEnvVars(key: string, value: string, globalVars: EnvironmentVariables): string { + if (!value.includes('$')) { + return value; + } + // Substitution here is inspired a little by dotenv-expand: + // https://github.com/motdotla/dotenv-expand/blob/master/lib/main.js + + let invalid = false; + let replacement = value; + replacement = replacement.replace(SUBST_REGEX, (match, substName, bogus, offset, orig) => { + if (offset > 0 && orig[offset - 1] === '\\') { + return match; + } + if ((bogus && bogus !== '') || !substName || substName === '') { + invalid = true; + return match; + } + return globalVars[substName] || ''; + }); + if (!invalid && replacement !== value) { + traceVerbose(`${key} value in kernelSpec updated from ${value} to ${replacement}`); + value = replacement; + } + + return value.replace(/\\\$/g, '$'); +} diff --git a/src/kernels/raw/launcher/kernelEnvVarsService.unit.test.ts b/src/kernels/raw/launcher/kernelEnvVarsService.unit.test.ts index 166f71df0e1..171e97cb429 100644 --- a/src/kernels/raw/launcher/kernelEnvVarsService.unit.test.ts +++ b/src/kernels/raw/launcher/kernelEnvVarsService.unit.test.ts @@ -189,6 +189,55 @@ suite('Kernel Environment Variables Service', () => { assert.strictEqual(vars![processPath!], `pathInInterpreterEnv`); }); + test('No substitution of env variables in kernelSpec', async () => { + when(interpreterService.getInterpreterDetails(anything(), anything())).thenResolve({ + envType: EnvironmentType.Conda, + uri: Uri.joinPath(Uri.file('env'), 'foopath'), + id: Uri.joinPath(Uri.file('env'), 'foopath').fsPath, + sysPrefix: 'foosysprefix' + }); + when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ + PATH: 'pathInInterpreterEnv' + }); + when(customVariablesService.getCustomEnvironmentVariables(anything(), anything(), anything())).thenResolve({ + PATH: 'foobaz' + }); + kernelSpec.env = { + ONE: '1', + TWO: '2' + }; + // undefined for interpreter here, interpreterPath from the spec should be used + const vars = await kernelVariablesService.getEnvironmentVariables(undefined, undefined, kernelSpec); + assert.strictEqual(vars!['ONE'], `1`); + assert.strictEqual(vars!['TWO'], `2`); + }); + test('substitute env variables in kernelSpec', async () => { + when(interpreterService.getInterpreterDetails(anything(), anything())).thenResolve({ + envType: EnvironmentType.Conda, + uri: Uri.joinPath(Uri.file('env'), 'foopath'), + id: Uri.joinPath(Uri.file('env'), 'foopath').fsPath, + sysPrefix: 'foosysprefix' + }); + when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ + PATH: 'pathInInterpreterEnv' + }); + when(customVariablesService.getCustomEnvironmentVariables(anything(), anything(), anything())).thenResolve({ + PATH: 'foobaz' + }); + kernelSpec.env = { + ONE: '1', + TWO: '2', + THREE: 'HELLO_${ONE}', + PATH: 'some_path;${PATH};${ONE}' + }; + // undefined for interpreter here, interpreterPath from the spec should be used + const vars = await kernelVariablesService.getEnvironmentVariables(undefined, undefined, kernelSpec); + assert.strictEqual(vars!['ONE'], `1`); + assert.strictEqual(vars!['TWO'], `2`); + assert.strictEqual(vars!['THREE'], `HELLO_1`); + assert.strictEqual(vars!['PATH'], `some_path;pathInInterpreterEnv;1`); + }); + async function testPYTHONNOUSERSITE(envType: EnvironmentType, shouldBeSet: boolean) { when(interpreterService.getInterpreterDetails(anything(), anything())).thenResolve({ envType,