Skip to content

Commit

Permalink
Always prepend to PATH instead of replacing it (microsoft#21906)
Browse files Browse the repository at this point in the history
For microsoft#20822 microsoft#11039

Replacing as-is has its problems, for eg. pyenv asks their users to
manipulate `PATH` in their init script:
https://github.com/pyenv/pyenv#set-up-your-shell-environment-for-pyenv,
which we could end up replacing.


![image](https://github.com/microsoft/vscode-python/assets/13199757/cc904f76-8d42-47e1-a6c8-6cfff6543db8)

Particularly for pyenv, it mean users not being able to find pyenv:


![image](https://github.com/microsoft/vscode-python/assets/13199757/26100328-c227-435b-a4f2-ec168099f4c1)

Prepending solves it for cases where initial PATH value is suffix of the
final value:


![image](https://github.com/microsoft/vscode-python/assets/13199757/a95e4ffd-68dc-4e73-905e-504b3051324f)

But, in other cases, this means that we end up with the whole `PATH`
thrice. This is because it prepends it twice:
- Once in shell integration script
- Once when creating a process

So the final value could be:
```
PATH=<activated_full_path><activated_full_path><original_path>
```
where `<activated_full_path>` refers to value of `PATH` environment
variable post activation. eg.


![image](https://github.com/microsoft/vscode-python/assets/13199757/7e771f62-eb53-49be-b261-d259096008f3)
  • Loading branch information
Kartik Raj authored Aug 31, 2023
1 parent 7a9294c commit d9b9c88
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
if (shouldSkip(key)) {
return;
}
const value = env[key];
let value = env[key];
const prevValue = processEnv[key];
if (prevValue !== value) {
if (value !== undefined) {
Expand All @@ -180,6 +180,26 @@ export class TerminalEnvVarCollectionService implements IExtensionActivationServ
});
return;
}
if (key === 'PATH') {
if (processEnv.PATH && env.PATH?.endsWith(processEnv.PATH)) {
// Prefer prepending to PATH instead of replacing it, as we do not want to replace any
// changes to PATH users might have made it in their init scripts (~/.bashrc etc.)
const prependedPart = env.PATH.slice(0, -processEnv.PATH.length);
value = prependedPart;
traceVerbose(`Prepending environment variable ${key} in collection with ${value}`);
envVarCollection.prepend(key, value, {
applyAtShellIntegration: true,
applyAtProcessCreation: true,
});
} else {
traceVerbose(`Prepending environment variable ${key} in collection to ${value}`);
envVarCollection.prepend(key, value, {
applyAtShellIntegration: true,
applyAtProcessCreation: true,
});
}
return;
}
traceVerbose(`Setting environment variable ${key} in collection to ${value}`);
envVarCollection.replace(key, value, {
applyAtShellIntegration: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,70 @@ suite('Terminal Environment Variable Collection Service', () => {
assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true });
});

test('Prepend only "prepend portion of PATH" where applicable', async () => {
const processEnv = { PATH: 'hello/1/2/3' };
reset(environmentActivationService);
when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve(
processEnv,
);
const prependedPart = 'path/to/activate/dir:';
const envVars: NodeJS.ProcessEnv = { PATH: `${prependedPart}${processEnv.PATH}` };
when(
environmentActivationService.getActivatedEnvironmentVariables(
anything(),
undefined,
undefined,
customShell,
),
).thenResolve(envVars);

when(collection.replace(anything(), anything(), anything())).thenResolve();
when(collection.delete(anything())).thenResolve();
let opts: EnvironmentVariableMutatorOptions | undefined;
when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => {
opts = o;
});

await terminalEnvVarCollectionService._applyCollection(undefined, customShell);

verify(collection.clear()).once();
verify(collection.prepend('PATH', prependedPart, anything())).once();
verify(collection.replace('PATH', anything(), anything())).never();
assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true });
});

test('Prepend full PATH otherwise', async () => {
const processEnv = { PATH: 'hello/1/2/3' };
reset(environmentActivationService);
when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve(
processEnv,
);
const finalPath = 'hello/3/2/1';
const envVars: NodeJS.ProcessEnv = { PATH: finalPath };
when(
environmentActivationService.getActivatedEnvironmentVariables(
anything(),
undefined,
undefined,
customShell,
),
).thenResolve(envVars);

when(collection.replace(anything(), anything(), anything())).thenResolve();
when(collection.delete(anything())).thenResolve();
let opts: EnvironmentVariableMutatorOptions | undefined;
when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => {
opts = o;
});

await terminalEnvVarCollectionService._applyCollection(undefined, customShell);

verify(collection.clear()).once();
verify(collection.prepend('PATH', finalPath, anything())).once();
verify(collection.replace('PATH', anything(), anything())).never();
assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true });
});

test('Verify envs are not applied if env activation is disabled', async () => {
const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env };
when(
Expand Down

0 comments on commit d9b9c88

Please sign in to comment.