Skip to content

Commit 2a6761d

Browse files
author
Eric Snow
authored
Add more env-related helpers. (#14326)
The PR for "windows known paths locator" depends on this.
1 parent 5f293dc commit 2a6761d

20 files changed

Lines changed: 713 additions & 77 deletions

File tree

src/client/common/platform/constants.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,4 @@
55
// TODO : Drop all these in favor of IPlatformService.
66
// See https://github.com/microsoft/vscode-python/issues/8542.
77

8-
export const WINDOWS_PATH_VARIABLE_NAME = 'Path';
9-
export const NON_WINDOWS_PATH_VARIABLE_NAME = 'PATH';
108
export const IS_WINDOWS = /^win/.test(process.platform);

src/client/common/platform/fs-paths.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
import * as nodepath from 'path';
5+
import { getSearchPathEnvVarNames } from '../utils/exec';
56
import { getOSType, OSType } from '../utils/platform';
67
import { IExecutables, IFileSystemPaths, IFileSystemPathUtils } from './types';
78
// tslint:disable-next-line:no-var-requires no-require-imports
@@ -86,7 +87,7 @@ export class Executables {
8687
}
8788

8889
public get envVar(): string {
89-
return this.osType === OSType.Windows ? 'Path' : 'PATH';
90+
return getSearchPathEnvVarNames(this.osType)[0];
9091
}
9192
}
9293

src/client/common/platform/platformService.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import * as os from 'os';
77
import { coerce, SemVer } from 'semver';
88
import { sendTelemetryEvent } from '../../telemetry';
99
import { EventName, PlatformErrors } from '../../telemetry/constants';
10-
import { getOSType, OSType } from '../utils/platform';
10+
import { getSearchPathEnvVarNames } from '../utils/exec';
11+
import { Architecture, getArchitecture, getOSType, OSType } from '../utils/platform';
1112
import { parseVersion } from '../utils/version';
12-
import { NON_WINDOWS_PATH_VARIABLE_NAME, WINDOWS_PATH_VARIABLE_NAME } from './constants';
1313
import { IPlatformService } from './types';
1414

1515
@injectable()
@@ -24,7 +24,7 @@ export class PlatformService implements IPlatformService {
2424
}
2525
}
2626
public get pathVariableName() {
27-
return this.isWindows ? WINDOWS_PATH_VARIABLE_NAME : NON_WINDOWS_PATH_VARIABLE_NAME;
27+
return getSearchPathEnvVarNames(this.osType)[0];
2828
}
2929
public get virtualEnvBinName() {
3030
return this.isWindows ? 'Scripts' : 'bin';
@@ -72,8 +72,6 @@ export class PlatformService implements IPlatformService {
7272
return os.release();
7373
}
7474
public get is64bit(): boolean {
75-
// tslint:disable-next-line:no-require-imports
76-
const arch = require('arch');
77-
return arch() === 'x64';
75+
return getArchitecture() === Architecture.x64;
7876
}
7977
}

src/client/common/utils/async.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,16 @@ export function createDeferredFromPromise<T>(promise: Promise<T>): Deferred<T> {
114114
//================================
115115
// iterators
116116

117+
export interface IAsyncIterator<T> extends AsyncIterator<T, void>, Partial<AsyncIterable<T>> {}
118+
119+
export interface IAsyncIterableIterator<T> extends IAsyncIterator<T>, AsyncIterable<T> {}
120+
117121
/**
118122
* An iterator that yields nothing.
119123
*/
120-
export function iterEmpty<T>(): AsyncIterator<T, void> {
124+
export function iterEmpty<T>(): IAsyncIterableIterator<T> {
121125
// tslint:disable-next-line:no-empty
122-
return ((async function* () {})() as unknown) as AsyncIterator<T, void>;
126+
return ((async function* () {})() as unknown) as IAsyncIterableIterator<T>;
123127
}
124128

125129
type NextResult<T> = { index: number } & (
@@ -153,7 +157,7 @@ export async function* chain<T>(
153157
iterators: AsyncIterator<T, T | void>[],
154158
onError?: (err: Error, index: number) => Promise<void>
155159
// Ultimately we may also want to support cancellation.
156-
): AsyncIterator<T, void> {
160+
): IAsyncIterableIterator<T> {
157161
const promises = iterators.map(getNext);
158162
let numRunning = iterators.length;
159163
while (numRunning > 0) {
@@ -193,7 +197,7 @@ export async function* mapToIterator<T, R = T>(
193197
items: T[],
194198
func: (item: T) => Promise<R>,
195199
race = true
196-
): AsyncIterator<R, void> {
200+
): IAsyncIterableIterator<R> {
197201
if (race) {
198202
const iterators = items.map((item) => {
199203
async function* generator() {
@@ -210,8 +214,8 @@ export async function* mapToIterator<T, R = T>(
210214
/**
211215
* Convert an iterator into an iterable, if it isn't one already.
212216
*/
213-
export function iterable<T>(iterator: AsyncIterator<T, void>): AsyncIterableIterator<T> {
214-
const it = iterator as AsyncIterableIterator<T>;
217+
export function iterable<T>(iterator: IAsyncIterator<T>): IAsyncIterableIterator<T> {
218+
const it = iterator as IAsyncIterableIterator<T>;
215219
if (it[Symbol.asyncIterator] === undefined) {
216220
it[Symbol.asyncIterator] = () => it;
217221
}
@@ -221,14 +225,10 @@ export function iterable<T>(iterator: AsyncIterator<T, void>): AsyncIterableIter
221225
/**
222226
* Get everything yielded by the iterator.
223227
*/
224-
export async function flattenIterator<T>(iterator: AsyncIterator<T, void>): Promise<T[]> {
228+
export async function flattenIterator<T>(iterator: IAsyncIterator<T>): Promise<T[]> {
225229
const results: T[] = [];
226-
// We are dealing with an iterator, not an iterable, so we have
227-
// to iterate manually rather than with a for-await loop.
228-
let result = await iterator.next();
229-
while (!result.done) {
230-
results.push(result.value);
231-
result = await iterator.next();
230+
for await (const item of iterable(iterator)) {
231+
results.push(item);
232232
}
233233
return results;
234234
}

src/client/common/utils/exec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as fsapi from 'fs';
5+
import * as path from 'path';
6+
import { getEnvironmentVariable, getOSType, OSType } from './platform';
7+
8+
/**
9+
* Determine the env var to use for the executable search path.
10+
*/
11+
export function getSearchPathEnvVarNames(ostype = getOSType()): ('Path' | 'PATH')[] {
12+
if (ostype === OSType.Windows) {
13+
// On Windows both are supported now.
14+
return ['Path', 'PATH'];
15+
}
16+
return ['PATH'];
17+
}
18+
19+
/**
20+
* Get the OS executable lookup "path" from the appropriate env var.
21+
*/
22+
export function getSearchPathEntries(): string[] {
23+
const envVars = getSearchPathEnvVarNames();
24+
for (const envVar of envVars) {
25+
const value = getEnvironmentVariable(envVar);
26+
if (value !== undefined) {
27+
return parseSearchPathEntries(value);
28+
}
29+
}
30+
// No env var was set.
31+
return [];
32+
}
33+
34+
function parseSearchPathEntries(envVarValue: string): string[] {
35+
return envVarValue
36+
.split(path.delimiter)
37+
.map((entry: string) => entry.trim())
38+
.filter((entry) => entry.length > 0);
39+
}
40+
41+
/**
42+
* Determine if the given file is executable by the current user.
43+
*
44+
* If the file does not exist or has any other problem when accessed
45+
* then `false` is returned. The caller is responsible to determine
46+
* whether or not the file exists.
47+
*/
48+
export async function isValidAndExecutable(filename: string): Promise<boolean | undefined> {
49+
try {
50+
await fsapi.promises.access(filename, fsapi.constants.X_OK);
51+
} catch (err) {
52+
return false;
53+
}
54+
return true;
55+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as path from 'path';
5+
import { getOSType, OSType } from './platform';
6+
7+
/**
8+
* Produce a uniform representation of the given filename.
9+
*
10+
* The result is especially suitable for cases where a filename is used
11+
* as a key (e.g. in a mapping).
12+
*/
13+
export function normalizeFilename(filename: string): string {
14+
// `path.resolve()` returns the absolute path. Note that it also
15+
// has the same behavior as `path.normalize()`.
16+
const resolved = path.resolve(filename);
17+
return getOSType() === OSType.Windows ? resolved.toLowerCase() : resolved;
18+
}
19+
20+
/**
21+
* Decide if the two filenames are the same file.
22+
*
23+
* This only checks the filenames (after normalizing) and does not
24+
* resolve symlinks or other indirection.
25+
*/
26+
export function areSameFilename(filename1: string, filename2: string): boolean {
27+
const norm1 = normalizeFilename(filename1);
28+
const norm2 = normalizeFilename(filename2);
29+
return norm1 === norm2;
30+
}

src/client/common/utils/platform.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,32 @@ export function getOSType(platform: string = process.platform): OSType {
3030
}
3131
}
3232

33+
const architectures: Record<string, Architecture> = {
34+
x86: Architecture.x86, // 32-bit
35+
x64: Architecture.x64, // 64-bit
36+
'': Architecture.Unknown
37+
};
38+
39+
/**
40+
* Identify the host's native architecture/bitness.
41+
*/
42+
export function getArchitecture(): Architecture {
43+
return architectures[process.arch] || Architecture.Unknown;
44+
}
45+
46+
/**
47+
* Look up the requested env var value (or undefined` if not set).
48+
*/
3349
export function getEnvironmentVariable(key: string): string | undefined {
3450
// tslint:disable-next-line: no-any
3551
return ((process.env as any) as EnvironmentVariables)[key];
3652
}
3753

38-
export function getPathEnvironmentVariable(): string | undefined {
39-
return getEnvironmentVariable('Path') || getEnvironmentVariable('PATH');
40-
}
41-
54+
/**
55+
* Get the current user's home directory.
56+
*
57+
* The lookup is limited to environment variables.
58+
*/
4259
export function getUserHomeDir(): string | undefined {
4360
if (getOSType() === OSType.Windows) {
4461
return getEnvironmentVariable('USERPROFILE');

src/client/pythonEnvironments/base/envsCache.ts

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,81 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
// tslint:disable-next-line:no-single-line-block-comment
5+
/* eslint-disable max-classes-per-file */
6+
47
import { cloneDeep } from 'lodash';
58
import { PythonEnvInfo } from './info';
6-
import { areSameEnv } from './info/env';
9+
import {
10+
areSameEnv,
11+
getEnvExecutable,
12+
haveSameExecutables,
13+
} from './info/env';
14+
15+
/**
16+
* A simple in-memory store of Python envs.
17+
*/
18+
export class PythonEnvsCache {
19+
private envs: PythonEnvInfo[] = [];
20+
21+
private byExecutable: Record<string, PythonEnvInfo> | undefined;
22+
23+
constructor(envs?: PythonEnvInfo[]) {
24+
if (envs !== undefined) {
25+
this.setEnvs(envs);
26+
}
27+
}
28+
29+
/**
30+
* Provide a copy of the cached envs.
31+
*/
32+
public getEnvs(): PythonEnvInfo[] {
33+
return cloneDeep(this.envs);
34+
}
35+
36+
/**
37+
* Replace the set of cached envs with the given set.
38+
*
39+
* If the given envs are the same as the cached ones
40+
* then the existing are left in place.
41+
*
42+
* @returns - `true` if the cached envs were actually replaced
43+
*/
44+
public setEnvs(envs: PythonEnvInfo[]): boolean {
45+
// We *could* compare additional properties, but checking
46+
// the executables is good enough for now.
47+
if (haveSameExecutables(this.envs, envs)) {
48+
return false;
49+
}
50+
this.envs = cloneDeep(envs);
51+
this.byExecutable = undefined;
52+
return true;
53+
}
54+
55+
/**
56+
* Find the matching env in the cache, if any.
57+
*/
58+
public lookUp(
59+
query: string | Partial<PythonEnvInfo>,
60+
): PythonEnvInfo | undefined {
61+
const executable = getEnvExecutable(query);
62+
if (executable === '') {
63+
return undefined;
64+
}
65+
if (this.byExecutable === undefined) {
66+
this.byExecutable = {};
67+
for (const env of this.envs) {
68+
this.byExecutable[env.executable.filename] = env;
69+
}
70+
}
71+
return this.byExecutable[executable];
72+
}
73+
74+
public filter(match: (env: PythonEnvInfo) => boolean | undefined): PythonEnvInfo[] {
75+
const matched = this.envs.filter(match);
76+
return cloneDeep(matched);
77+
}
78+
}
779

880
/**
981
* Represents the environment info cache to be used by the cache locator.
@@ -60,7 +132,7 @@ type CompleteEnvInfoFunction = (envInfo: PythonEnvInfo) => boolean;
60132
export class PythonEnvInfoCache implements IEnvsCache {
61133
private initialized = false;
62134

63-
private envsList: PythonEnvInfo[] | undefined;
135+
private inMemory: PythonEnvsCache | undefined;
64136

65137
private persistentStorage: IPersistentStorage | undefined;
66138

@@ -77,28 +149,27 @@ export class PythonEnvInfoCache implements IEnvsCache {
77149
this.initialized = true;
78150
if (this.getPersistentStorage !== undefined) {
79151
this.persistentStorage = this.getPersistentStorage();
80-
this.envsList = await this.persistentStorage.load();
152+
const envs = await this.persistentStorage.load();
153+
if (envs !== undefined) {
154+
this.setAllEnvs(envs);
155+
}
81156
}
82157
}
83158

84159
public getAllEnvs(): PythonEnvInfo[] | undefined {
85-
return cloneDeep(this.envsList);
160+
return this.inMemory?.getEnvs();
86161
}
87162

88163
public setAllEnvs(envs: PythonEnvInfo[]): void {
89-
this.envsList = cloneDeep(envs);
164+
this.inMemory = new PythonEnvsCache(envs);
90165
}
91166

92167
public filterEnvs(query: Partial<PythonEnvInfo>): PythonEnvInfo[] | undefined {
93-
if (this.envsList === undefined) {
94-
return undefined;
95-
}
96-
const result = this.envsList.filter((info) => areSameEnv(info, query));
97-
return cloneDeep(result);
168+
return this.inMemory?.filter((info) => areSameEnv(info, query));
98169
}
99170

100171
public async flush(): Promise<void> {
101-
const completeEnvs = this.envsList?.filter(this.isComplete);
172+
const completeEnvs = this.inMemory?.filter(this.isComplete);
102173

103174
if (completeEnvs?.length) {
104175
await this.persistentStorage?.store(completeEnvs);

0 commit comments

Comments
 (0)