forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathcommonUtils.ts
395 lines (366 loc) · 14.7 KB
/
commonUtils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import * as fs from 'fs';
import * as path from 'path';
import { convertFileType, DirEntry, FileType, getFileFilter, getFileType } from '../../common/utils/filesystem';
import { getOSType, OSType } from '../../common/utils/platform';
import { traceError, traceVerbose } from '../../logging';
import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../base/info';
import { comparePythonVersionSpecificity } from '../base/info/env';
import { parseVersion } from '../base/info/pythonVersion';
import { getPythonVersionFromConda } from './environmentManagers/conda';
import { getPythonVersionFromPyvenvCfg } from './environmentManagers/simplevirtualenvs';
import { isFile, normCasePath } from './externalDependencies';
import * as posix from './posixUtils';
import * as windows from './windowsUtils';
const matchStandardPythonBinFilename =
getOSType() === OSType.Windows ? windows.matchPythonBinFilename : posix.matchPythonBinFilename;
type FileFilterFunc = (filename: string) => boolean;
/**
* Returns `true` if path provided is likely a python executable than a folder path.
*/
export async function isPythonExecutable(filePath: string): Promise<boolean> {
const isMatch = matchStandardPythonBinFilename(filePath);
if (isMatch && getOSType() === OSType.Windows) {
// On Windows it's fair to assume a path ending with `.exe` denotes a file.
return true;
}
if (await isFile(filePath)) {
return true;
}
return false;
}
/**
* Searches recursively under the given `root` directory for python interpreters.
* @param root : Directory where the search begins.
* @param recurseLevels : Number of levels to search for from the root directory.
* @param filter : Callback that identifies directories to ignore.
*/
export async function* findInterpretersInDir(
root: string,
recurseLevel?: number,
filterSubDir?: FileFilterFunc,
ignoreErrors = true,
): AsyncIterableIterator<DirEntry> {
// "checkBin" is a local variable rather than global
// so we can stub out getOSType() during unit testing.
const checkBin = getOSType() === OSType.Windows ? windows.matchPythonBinFilename : posix.matchPythonBinFilename;
const cfg = {
ignoreErrors,
filterSubDir,
filterFile: checkBin,
// Make no-recursion the default for backward compatibility.
maxDepth: recurseLevel || 0,
};
// We use an initial depth of 1.
for await (const entry of walkSubTree(root, 1, cfg)) {
const { filename, filetype } = entry;
if (filetype === FileType.File || filetype === FileType.SymbolicLink) {
if (matchFile(filename, checkBin, ignoreErrors)) {
yield entry;
}
}
// We ignore all other file types.
}
}
/**
* Find all Python executables in the given directory.
*/
export async function* iterPythonExecutablesInDir(
dirname: string,
opts: {
ignoreErrors: boolean;
} = { ignoreErrors: true },
): AsyncIterableIterator<DirEntry> {
const readDirOpts = {
...opts,
filterFile: matchStandardPythonBinFilename,
};
const entries = await readDirEntries(dirname, readDirOpts);
for (const entry of entries) {
const { filetype } = entry;
if (filetype === FileType.File || filetype === FileType.SymbolicLink) {
yield entry;
}
// We ignore all other file types.
}
}
// This function helps simplify the recursion case.
async function* walkSubTree(
subRoot: string,
// "currentDepth" is the depth of the current level of recursion.
currentDepth: number,
cfg: {
filterSubDir: FileFilterFunc | undefined;
maxDepth: number;
ignoreErrors: boolean;
},
): AsyncIterableIterator<DirEntry> {
const entries = await readDirEntries(subRoot, cfg);
for (const entry of entries) {
yield entry;
const { filename, filetype } = entry;
if (filetype === FileType.Directory) {
if (cfg.maxDepth < 0 || currentDepth <= cfg.maxDepth) {
if (matchFile(filename, cfg.filterSubDir, cfg.ignoreErrors)) {
yield* walkSubTree(filename, currentDepth + 1, cfg);
}
}
}
}
}
async function readDirEntries(
dirname: string,
opts: {
filterFilename?: FileFilterFunc;
ignoreErrors: boolean;
} = { ignoreErrors: true },
): Promise<DirEntry[]> {
const ignoreErrors = opts.ignoreErrors || false;
if (opts.filterFilename && getOSType() === OSType.Windows) {
// Since `readdir()` using "withFileTypes" is not efficient
// on Windows, we take advantage of the filter.
let basenames: string[];
try {
basenames = await fs.promises.readdir(dirname);
} catch (err) {
const exception = err as NodeJS.ErrnoException;
// Treat a missing directory as empty.
if (exception.code === 'ENOENT') {
return [];
}
if (ignoreErrors) {
traceError(`readdir() failed for "${dirname}" (${err})`);
return [];
}
throw err; // re-throw
}
const filenames = basenames
.map((b) => path.join(dirname, b))
.filter((f) => matchFile(f, opts.filterFilename, ignoreErrors));
return Promise.all(
filenames.map(async (filename) => {
const filetype = (await getFileType(filename, opts)) || FileType.Unknown;
return { filename, filetype };
}),
);
}
let raw: fs.Dirent[];
try {
raw = await fs.promises.readdir(dirname, { withFileTypes: true });
} catch (err) {
const exception = err as NodeJS.ErrnoException;
// Treat a missing directory as empty.
if (exception.code === 'ENOENT') {
return [];
}
if (ignoreErrors) {
traceError(`readdir() failed for "${dirname}" (${err})`);
return [];
}
throw err; // re-throw
}
// (FYI)
// Normally we would have to do an extra (expensive) `fs.lstat()`
// here for each file to determine its file type. However, we
// avoid this by using the "withFileTypes" option to `readdir()`
// above. On non-Windows the file type of each entry is preserved
// for free. Unfortunately, on Windows it actually does an
// `lstat()` under the hood, so it isn't a win. Regardless,
// if we needed more information than just the file type
// then we would be forced to incur the extra cost
// of `lstat()` anyway.
const entries = raw.map((entry) => {
const filename = path.join(dirname, entry.name);
const filetype = convertFileType(entry);
return { filename, filetype };
});
if (opts.filterFilename) {
return entries.filter((e) => matchFile(e.filename, opts.filterFilename, ignoreErrors));
}
return entries;
}
function matchFile(
filename: string,
filterFile: FileFilterFunc | undefined,
// If "ignoreErrors" is true then we treat a failed filter
// as though it returned `false`.
ignoreErrors = true,
): boolean {
if (filterFile === undefined) {
return true;
}
try {
return filterFile(filename);
} catch (err) {
if (ignoreErrors) {
traceError(`filter failed for "${filename}" (${err})`);
return false;
}
throw err; // re-throw
}
}
/**
* Looks for files in the same directory which might have version in their name.
* @param interpreterPath
*/
async function getPythonVersionFromNearByFiles(interpreterPath: string): Promise<PythonVersion> {
const root = path.dirname(interpreterPath);
let version = UNKNOWN_PYTHON_VERSION;
for await (const entry of findInterpretersInDir(root)) {
const { filename } = entry;
try {
const curVersion = parseVersion(path.basename(filename));
if (comparePythonVersionSpecificity(curVersion, version) > 0) {
version = curVersion;
}
} catch (ex) {
// Ignore any parse errors
}
}
return version;
}
/**
* This function does the best effort of finding version of python without running the
* python binary.
* @param interpreterPath Absolute path to the interpreter.
* @param hint Any string that might contain version info.
*/
export async function getPythonVersionFromPath(interpreterPath: string, hint?: string): Promise<PythonVersion> {
let versionA;
try {
versionA = hint ? parseVersion(hint) : UNKNOWN_PYTHON_VERSION;
} catch (ex) {
versionA = UNKNOWN_PYTHON_VERSION;
}
const versionB = interpreterPath ? await getPythonVersionFromNearByFiles(interpreterPath) : UNKNOWN_PYTHON_VERSION;
traceVerbose('Best effort version B for', interpreterPath, JSON.stringify(versionB));
const versionC = interpreterPath ? await getPythonVersionFromPyvenvCfg(interpreterPath) : UNKNOWN_PYTHON_VERSION;
traceVerbose('Best effort version C for', interpreterPath, JSON.stringify(versionC));
const versionD = interpreterPath ? await getPythonVersionFromConda(interpreterPath) : UNKNOWN_PYTHON_VERSION;
traceVerbose('Best effort version D for', interpreterPath, JSON.stringify(versionD));
let version = UNKNOWN_PYTHON_VERSION;
for (const v of [versionA, versionB, versionC, versionD]) {
version = comparePythonVersionSpecificity(version, v) > 0 ? version : v;
}
return version;
}
/**
* Decide if the file is meets the given criteria for a Python executable.
*/
async function checkPythonExecutable(
executable: string | DirEntry,
opts: {
matchFilename?: (f: string) => boolean;
filterFile?: (f: string | DirEntry) => Promise<boolean>;
},
): Promise<boolean> {
const matchFilename = opts.matchFilename || matchStandardPythonBinFilename;
const filename = typeof executable === 'string' ? executable : executable.filename;
if (!matchFilename(filename)) {
return false;
}
// This should occur after we match file names. This is to avoid doing potential
// `lstat` calls on too many files which can slow things down.
if (opts.filterFile && !(await opts.filterFile(executable))) {
return false;
}
// For some use cases it would also be a good idea to verify that
// the file is executable. That is a relatively expensive operation
// (a stat on linux and actually executing the file on Windows), so
// at best it should be an optional check. If we went down this
// route then it would be worth supporting `fs.Stats` as a type
// for the "executable" arg.
//
// Regardless, currently there is no code that would use such
// an option, so for now we don't bother supporting it.
return true;
}
const filterGlobalExecutable = getFileFilter({ ignoreFileType: FileType.SymbolicLink })!;
/**
* Decide if the file is a typical Python executable.
*
* This is a best effort operation with a focus on the common cases
* and on efficiency. The filename must be basic (python/python.exe).
* For global envs, symlinks are ignored.
*/
export async function looksLikeBasicGlobalPython(executable: string | DirEntry): Promise<boolean> {
// "matchBasic" is a local variable rather than global
// so we can stub out getOSType() during unit testing.
const matchBasic =
getOSType() === OSType.Windows ? windows.matchBasicPythonBinFilename : posix.matchBasicPythonBinFilename;
// We could be more permissive here by using matchPythonBinFilename().
// Originally one key motivation for the "basic" check was to avoid
// symlinks (which often look like python3.exe, etc., particularly
// on Windows). However, the symbolic link check here eliminates
// that rationale to an extent.
// (See: https://github.com/microsoft/vscode-python/issues/15447)
const matchFilename = matchBasic;
const filterFile = filterGlobalExecutable;
return checkPythonExecutable(executable, { matchFilename, filterFile });
}
/**
* Decide if the file is a typical Python executable.
*
* This is a best effort operation with a focus on the common cases
* and on efficiency. The filename must be basic (python/python.exe).
* For global envs, symlinks are ignored.
*/
export async function looksLikeBasicVirtualPython(executable: string | DirEntry): Promise<boolean> {
// "matchBasic" is a local variable rather than global
// so we can stub out getOSType() during unit testing.
const matchBasic =
getOSType() === OSType.Windows ? windows.matchBasicPythonBinFilename : posix.matchBasicPythonBinFilename;
// With virtual environments, we match only the simplest name
// (e.g. `python`) and we do not ignore symlinks.
const matchFilename = matchBasic;
const filterFile = undefined;
return checkPythonExecutable(executable, { matchFilename, filterFile });
}
/**
* This function looks specifically for 'python' or 'python.exe' binary in the sub folders of a given
* environment directory.
* @param envDir Absolute path to the environment directory
*/
export async function getInterpreterPathFromDir(
envDir: string,
opts: {
global?: boolean;
ignoreErrors?: boolean;
} = {},
): Promise<string | undefined> {
const recurseLevel = 2;
// Ignore any folders or files that not directly python binary related.
function filterDir(dirname: string): boolean {
const lower = path.basename(dirname).toLowerCase();
return ['bin', 'scripts'].includes(lower);
}
// Search in the sub-directories for python binary
const matchExecutable = opts.global ? looksLikeBasicGlobalPython : looksLikeBasicVirtualPython;
const executables = findInterpretersInDir(envDir, recurseLevel, filterDir, opts.ignoreErrors);
for await (const entry of executables) {
if (await matchExecutable(entry)) {
return entry.filename;
}
}
return undefined;
}
/**
* Gets the root environment directory based on the absolute path to the python
* interpreter binary.
* @param interpreterPath Absolute path to the python interpreter
*/
export function getEnvironmentDirFromPath(interpreterPath: string): string {
const skipDirs = ['bin', 'scripts'];
// env <--- Return this directory if it is not 'bin' or 'scripts'
// |__ python <--- interpreterPath
const dir = path.basename(path.dirname(interpreterPath));
if (!skipDirs.map((e) => normCasePath(e)).includes(normCasePath(dir))) {
return path.dirname(interpreterPath);
}
// This is the best next guess.
// env <--- Return this directory if it is not 'bin' or 'scripts'
// |__ bin or Scripts
// |__ python <--- interpreterPath
return path.dirname(path.dirname(interpreterPath));
}