Skip to content

Commit

Permalink
Support env variable substitution in kernelSpecs (#15091)
Browse files Browse the repository at this point in the history
* Support env variable substitution in kernelSpecs

* Fix formatting
  • Loading branch information
DonJayamanne authored Jan 30, 2024
1 parent d4ee744 commit 8ce2b44
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 1 deletion.
46 changes: 45 additions & 1 deletion src/kernels/raw/launcher/kernelEnvVarsService.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string, string> = {};
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;
Expand Down Expand Up @@ -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, '$');
}
49 changes: 49 additions & 0 deletions src/kernels/raw/launcher/kernelEnvVarsService.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 8ce2b44

Please sign in to comment.