|
| 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +import * as path from 'path'; |
| 5 | +import { CondaEnvironmentInfo } from '../../interpreter/contracts'; |
| 6 | +import { EXTENSION_ROOT_DIR } from '../constants'; |
| 7 | +import { traceError, traceInfo } from '../logger'; |
| 8 | +import { IFileSystem } from '../platform/types'; |
| 9 | +import { Architecture } from '../utils/platform'; |
| 10 | +import { parsePythonVersion } from '../utils/version'; |
| 11 | +import { |
| 12 | + ExecutionResult, |
| 13 | + InterpreterInfomation, |
| 14 | + IProcessService, |
| 15 | + PythonExecutionInfo, |
| 16 | + PythonVersionInfo, |
| 17 | + ShellOptions, |
| 18 | + SpawnOptions |
| 19 | +} from './types'; |
| 20 | + |
| 21 | +function getExecutionInfo(python: string[], pythonArgs: string[]): PythonExecutionInfo { |
| 22 | + const args = python.slice(1); |
| 23 | + args.push(...pythonArgs); |
| 24 | + return { command: python[0], args, python }; |
| 25 | +} |
| 26 | + |
| 27 | +class PythonEnvironment { |
| 28 | + private cachedInterpreterInformation: InterpreterInfomation | undefined | null = null; |
| 29 | + |
| 30 | + constructor( |
| 31 | + protected readonly pythonPath: string, |
| 32 | + // "deps" is the externally defined functionality used by the class. |
| 33 | + protected readonly deps: { |
| 34 | + getPythonArgv(python: string): string[]; |
| 35 | + getObservablePythonArgv(python: string): string[]; |
| 36 | + isValidExecutable(python: string): Promise<boolean>; |
| 37 | + // from ProcessService: |
| 38 | + exec(file: string, args: string[]): Promise<ExecutionResult<string>>; |
| 39 | + shellExec(command: string, timeout: number): Promise<ExecutionResult<string>>; |
| 40 | + } |
| 41 | + ) {} |
| 42 | + |
| 43 | + public getExecutionInfo(pythonArgs: string[] = []): PythonExecutionInfo { |
| 44 | + const python = this.deps.getPythonArgv(this.pythonPath); |
| 45 | + return getExecutionInfo(python, pythonArgs); |
| 46 | + } |
| 47 | + public getExecutionObservableInfo(pythonArgs: string[] = []): PythonExecutionInfo { |
| 48 | + const python = this.deps.getObservablePythonArgv(this.pythonPath); |
| 49 | + return getExecutionInfo(python, pythonArgs); |
| 50 | + } |
| 51 | + |
| 52 | + public async getInterpreterInformation(): Promise<InterpreterInfomation | undefined> { |
| 53 | + if (this.cachedInterpreterInformation === null) { |
| 54 | + this.cachedInterpreterInformation = await this.getInterpreterInformationImpl(); |
| 55 | + } |
| 56 | + return this.cachedInterpreterInformation; |
| 57 | + } |
| 58 | + |
| 59 | + public async getExecutablePath(): Promise<string> { |
| 60 | + // If we've passed the python file, then return the file. |
| 61 | + // This is because on mac if using the interpreter /usr/bin/python2.7 we can get a different value for the path |
| 62 | + if (await this.deps.isValidExecutable(this.pythonPath)) { |
| 63 | + return this.pythonPath; |
| 64 | + } |
| 65 | + |
| 66 | + const { command, args } = this.getExecutionInfo(['-c', 'import sys;print(sys.executable)']); |
| 67 | + const proc = await this.deps.exec(command, args); |
| 68 | + return proc.stdout.trim(); |
| 69 | + } |
| 70 | + |
| 71 | + public async isModuleInstalled(moduleName: string): Promise<boolean> { |
| 72 | + const { command, args } = this.getExecutionInfo(['-c', `import ${moduleName}`]); |
| 73 | + try { |
| 74 | + await this.deps.exec(command, args); |
| 75 | + } catch { |
| 76 | + return false; |
| 77 | + } |
| 78 | + return true; |
| 79 | + } |
| 80 | + |
| 81 | + private async getInterpreterInformationImpl(): Promise<InterpreterInfomation | undefined> { |
| 82 | + try { |
| 83 | + const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'interpreterInfo.py'); |
| 84 | + const { command, args } = this.getExecutionInfo([file]); |
| 85 | + const argv = [command, ...args]; |
| 86 | + |
| 87 | + // Concat these together to make a set of quoted strings |
| 88 | + const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${c.replace('\\', '\\\\')}"`), ''); |
| 89 | + |
| 90 | + // Try shell execing the command, followed by the arguments. This will make node kill the process if it |
| 91 | + // takes too long. |
| 92 | + // Sometimes the python path isn't valid, timeout if that's the case. |
| 93 | + // See these two bugs: |
| 94 | + // https://github.com/microsoft/vscode-python/issues/7569 |
| 95 | + // https://github.com/microsoft/vscode-python/issues/7760 |
| 96 | + const result = await this.deps.shellExec(quoted, 15000); |
| 97 | + if (result.stderr) { |
| 98 | + traceError(`Failed to parse interpreter information for ${argv} stderr: ${result.stderr}`); |
| 99 | + return; |
| 100 | + } |
| 101 | + let json: { |
| 102 | + versionInfo: PythonVersionInfo; |
| 103 | + sysPrefix: string; |
| 104 | + sysVersion: string; |
| 105 | + is64Bit: boolean; |
| 106 | + }; |
| 107 | + try { |
| 108 | + json = JSON.parse(result.stdout); |
| 109 | + } catch (ex) { |
| 110 | + throw Error(`${argv} returned bad JSON (${result.stdout}) (${ex})`); |
| 111 | + } |
| 112 | + traceInfo(`Found interpreter for ${argv}`); |
| 113 | + const versionValue = |
| 114 | + json.versionInfo.length === 4 |
| 115 | + ? `${json.versionInfo.slice(0, 3).join('.')}-${json.versionInfo[3]}` |
| 116 | + : json.versionInfo.join('.'); |
| 117 | + return { |
| 118 | + architecture: json.is64Bit ? Architecture.x64 : Architecture.x86, |
| 119 | + path: this.pythonPath, |
| 120 | + version: parsePythonVersion(versionValue), |
| 121 | + sysVersion: json.sysVersion, |
| 122 | + sysPrefix: json.sysPrefix |
| 123 | + }; |
| 124 | + } catch (ex) { |
| 125 | + traceError(`Failed to get interpreter information for '${this.pythonPath}'`, ex); |
| 126 | + } |
| 127 | + } |
| 128 | +} |
| 129 | + |
| 130 | +function createDeps( |
| 131 | + isValidExecutable: (filename: string) => Promise<boolean>, |
| 132 | + pythonArgv: string[] | undefined, |
| 133 | + observablePythonArgv: string[] | undefined, |
| 134 | + // from ProcessService: |
| 135 | + exec: (file: string, args: string[], options?: SpawnOptions) => Promise<ExecutionResult<string>>, |
| 136 | + shellExec: (command: string, options?: ShellOptions) => Promise<ExecutionResult<string>> |
| 137 | +) { |
| 138 | + return { |
| 139 | + getPythonArgv: (python: string) => pythonArgv || [python], |
| 140 | + getObservablePythonArgv: (python: string) => observablePythonArgv || [python], |
| 141 | + isValidExecutable, |
| 142 | + exec: async (cmd: string, args: string[]) => exec(cmd, args, { throwOnStdErr: true }), |
| 143 | + shellExec: async (text: string, timeout: number) => shellExec(text, { timeout }) |
| 144 | + }; |
| 145 | +} |
| 146 | + |
| 147 | +export function createPythonEnv( |
| 148 | + pythonPath: string, |
| 149 | + // These are used to generate the deps. |
| 150 | + procs: IProcessService, |
| 151 | + fs: IFileSystem |
| 152 | +): PythonEnvironment { |
| 153 | + const deps = createDeps( |
| 154 | + async (filename) => fs.fileExists(filename), |
| 155 | + // We use the default: [pythonPath]. |
| 156 | + undefined, |
| 157 | + undefined, |
| 158 | + (file, args, opts) => procs.exec(file, args, opts), |
| 159 | + (command, opts) => procs.shellExec(command, opts) |
| 160 | + ); |
| 161 | + return new PythonEnvironment(pythonPath, deps); |
| 162 | +} |
| 163 | + |
| 164 | +export function createCondaEnv( |
| 165 | + condaFile: string, |
| 166 | + condaInfo: CondaEnvironmentInfo, |
| 167 | + pythonPath: string, |
| 168 | + // These are used to generate the deps. |
| 169 | + procs: IProcessService, |
| 170 | + fs: IFileSystem |
| 171 | +): PythonEnvironment { |
| 172 | + const runArgs = ['run']; |
| 173 | + if (condaInfo.name === '') { |
| 174 | + runArgs.push('-p', condaInfo.path); |
| 175 | + } else { |
| 176 | + runArgs.push('-n', condaInfo.name); |
| 177 | + } |
| 178 | + const pythonArgv = [condaFile, ...runArgs, 'python']; |
| 179 | + const deps = createDeps( |
| 180 | + async (filename) => fs.fileExists(filename), |
| 181 | + pythonArgv, |
| 182 | + // tslint:disable-next-line:no-suspicious-comment |
| 183 | + // TODO(gh-8473): Use pythonArgv here once 'conda run' can be |
| 184 | + // run without buffering output. |
| 185 | + undefined, |
| 186 | + (file, args, opts) => procs.exec(file, args, opts), |
| 187 | + (command, opts) => procs.shellExec(command, opts) |
| 188 | + ); |
| 189 | + return new PythonEnvironment(pythonPath, deps); |
| 190 | +} |
| 191 | + |
| 192 | +export function createWindowsStoreEnv( |
| 193 | + pythonPath: string, |
| 194 | + // These are used to generate the deps. |
| 195 | + procs: IProcessService |
| 196 | +): PythonEnvironment { |
| 197 | + const deps = createDeps( |
| 198 | + /** |
| 199 | + * With windows store python apps, we have generally use the |
| 200 | + * symlinked python executable. The actual file is not accessible |
| 201 | + * by the user due to permission issues (& rest of exension fails |
| 202 | + * when using that executable). Hence lets not resolve the |
| 203 | + * executable using sys.executable for windows store python |
| 204 | + * interpreters. |
| 205 | + */ |
| 206 | + async (_f: string) => true, |
| 207 | + // We use the default: [pythonPath]. |
| 208 | + undefined, |
| 209 | + undefined, |
| 210 | + (file, args, opts) => procs.exec(file, args, opts), |
| 211 | + (command, opts) => procs.shellExec(command, opts) |
| 212 | + ); |
| 213 | + return new PythonEnvironment(pythonPath, deps); |
| 214 | +} |
0 commit comments