|
| 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +import * as path from 'path'; |
| 5 | +import { traceError } from '../../../../common/logger'; |
| 6 | +import { getEnvironmentVariable } from '../../../../common/utils/platform'; |
| 7 | +import { arePathsSame, pathExists, readFile } from '../../../common/externalDependencies'; |
| 8 | + |
| 9 | +function getSearchHeight() { |
| 10 | + // PIPENV_MAX_DEPTH tells pipenv the maximum number of directories to recursively search for |
| 11 | + // a Pipfile, defaults to 3: https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_MAX_DEPTH |
| 12 | + const maxDepthStr = getEnvironmentVariable('PIPENV_MAX_DEPTH'); |
| 13 | + if (maxDepthStr === undefined) { |
| 14 | + return 3; |
| 15 | + } |
| 16 | + const maxDepth = parseInt(maxDepthStr, 10); |
| 17 | + // eslint-disable-next-line no-restricted-globals |
| 18 | + if (isNaN(maxDepth)) { |
| 19 | + traceError(`PIPENV_MAX_DEPTH is incorrectly set. Converting value '${maxDepthStr}' to number results in NaN`); |
| 20 | + return 1; |
| 21 | + } |
| 22 | + return maxDepth; |
| 23 | +} |
| 24 | + |
| 25 | +/** |
| 26 | + * Returns the path to Pipfile associated with the provided directory. |
| 27 | + * @param searchDir the directory to look into |
| 28 | + * @param lookIntoParentDirectories set to true if we should also search for Pipfile in parent directory |
| 29 | + */ |
| 30 | +export async function _getAssociatedPipfile( |
| 31 | + searchDir: string, |
| 32 | + options: {lookIntoParentDirectories: boolean}, |
| 33 | +): Promise<string | undefined> { |
| 34 | + const pipFileName = getEnvironmentVariable('PIPENV_PIPFILE') || 'Pipfile'; |
| 35 | + let heightToSearch = options.lookIntoParentDirectories ? getSearchHeight() : 1; |
| 36 | + while (heightToSearch > 0 && !arePathsSame(searchDir, path.dirname(searchDir))) { |
| 37 | + const pipFile = path.join(searchDir, pipFileName); |
| 38 | + // eslint-disable-next-line no-await-in-loop |
| 39 | + if (await pathExists(pipFile)) { |
| 40 | + return pipFile; |
| 41 | + } |
| 42 | + searchDir = path.dirname(searchDir); |
| 43 | + heightToSearch -= 1; |
| 44 | + } |
| 45 | + return undefined; |
| 46 | +} |
| 47 | + |
| 48 | +/** |
| 49 | + * If interpreter path belongs to a pipenv environment which is located inside a project, return associated Pipfile, |
| 50 | + * otherwise return `undefined`. |
| 51 | + * @param interpreterPath Absolute path to any python interpreter. |
| 52 | + */ |
| 53 | +async function getPipfileIfLocal(interpreterPath: string): Promise<string | undefined> { |
| 54 | + // Local pipenv environments are created by setting PIPENV_VENV_IN_PROJECT to 1, which always names the environment |
| 55 | + // folder '.venv': https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_VENV_IN_PROJECT |
| 56 | + // This is the layout we wish to verify. |
| 57 | + // project |
| 58 | + // |__ Pipfile <--- check if Pipfile exists here |
| 59 | + // |__ .venv <--- check if name of the folder is '.venv' |
| 60 | + // |__ Scripts/bin |
| 61 | + // |__ python <--- interpreterPath |
| 62 | + const venvFolder = path.dirname(path.dirname(interpreterPath)); |
| 63 | + if (path.basename(venvFolder) !== '.venv') { |
| 64 | + return undefined; |
| 65 | + } |
| 66 | + const directoryWhereVenvResides = path.dirname(venvFolder); |
| 67 | + return _getAssociatedPipfile(directoryWhereVenvResides, { lookIntoParentDirectories: false }); |
| 68 | +} |
| 69 | + |
| 70 | +/** |
| 71 | + * Returns the project directory for pipenv environments given the environment folder |
| 72 | + * @param envFolder Path to the environment folder |
| 73 | + */ |
| 74 | +async function getProjectDir(envFolder: string): Promise<string | undefined> { |
| 75 | + // Global pipenv environments have a .project file with the absolute path to the project |
| 76 | + // See https://github.com/pypa/pipenv/blob/v2018.6.25/CHANGELOG.rst#features--improvements |
| 77 | + // This is the layout we expect |
| 78 | + // <Environment folder> |
| 79 | + // |__ .project <--- check if .project exists here |
| 80 | + // |__ Scripts/bin |
| 81 | + // |__ python <--- interpreterPath |
| 82 | + // We get the project by reading the .project file |
| 83 | + const dotProjectFile = path.join(envFolder, '.project'); |
| 84 | + if (!(await pathExists(dotProjectFile))) { |
| 85 | + return undefined; |
| 86 | + } |
| 87 | + const projectDir = await readFile(dotProjectFile); |
| 88 | + if (!(await pathExists(projectDir))) { |
| 89 | + traceError(`The .project file inside environment folder: ${envFolder} doesn't contain a valid path to the project`); |
| 90 | + return undefined; |
| 91 | + } |
| 92 | + return projectDir; |
| 93 | +} |
| 94 | + |
| 95 | +/** |
| 96 | + * If interpreter path belongs to a global pipenv environment, return associated Pipfile, otherwise return `undefined`. |
| 97 | + * @param interpreterPath Absolute path to any python interpreter. |
| 98 | + */ |
| 99 | +async function getPipfileIfGlobal(interpreterPath: string): Promise<string | undefined> { |
| 100 | + const envFolder = path.dirname(path.dirname(interpreterPath)); |
| 101 | + const projectDir = await getProjectDir(envFolder); |
| 102 | + if (projectDir === undefined) { |
| 103 | + return undefined; |
| 104 | + } |
| 105 | + |
| 106 | + // This is the layout we expect to see. |
| 107 | + // project |
| 108 | + // |__ Pipfile <--- check if Pipfile exists here and return it |
| 109 | + // The name of the project (directory where Pipfile resides) is used as a prefix in the environment folder |
| 110 | + const envFolderName = path.basename(envFolder); |
| 111 | + if (!envFolderName.startsWith(`${path.basename(projectDir)}-`)) { |
| 112 | + return undefined; |
| 113 | + } |
| 114 | + |
| 115 | + return _getAssociatedPipfile(projectDir, { lookIntoParentDirectories: false }); |
| 116 | +} |
| 117 | + |
| 118 | +/** |
| 119 | + * Checks if the given interpreter path belongs to a pipenv environment, by locating the Pipfile which was used to |
| 120 | + * create the environment. |
| 121 | + * @param interpreterPath: Absolute path to any python interpreter. |
| 122 | + */ |
| 123 | +export async function isPipenvEnvironment(interpreterPath: string): Promise<boolean> { |
| 124 | + if (await getPipfileIfLocal(interpreterPath)) { |
| 125 | + return true; |
| 126 | + } |
| 127 | + if (await getPipfileIfGlobal(interpreterPath)) { |
| 128 | + return true; |
| 129 | + } |
| 130 | + return false; |
| 131 | +} |
| 132 | + |
| 133 | +/** |
| 134 | + * Returns true if interpreter path belongs to a global pipenv environment which is associated with a particular folder, |
| 135 | + * false otherwise. |
| 136 | + * @param interpreterPath Absolute path to any python interpreter. |
| 137 | + */ |
| 138 | +export async function isPipenvEnvironmentRelatedToFolder(interpreterPath: string, folder: string): Promise<boolean> { |
| 139 | + const pipFileAssociatedWithEnvironment = await getPipfileIfGlobal(interpreterPath); |
| 140 | + if (!pipFileAssociatedWithEnvironment) { |
| 141 | + return false; |
| 142 | + } |
| 143 | + |
| 144 | + // PIPENV_NO_INHERIT is used to tell pipenv not to look for Pipfile in parent directories |
| 145 | + // https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_NO_INHERIT |
| 146 | + const lookIntoParentDirectories = (getEnvironmentVariable('PIPENV_NO_INHERIT') === undefined); |
| 147 | + const pipFileAssociatedWithFolder = await _getAssociatedPipfile(folder, { lookIntoParentDirectories }); |
| 148 | + if (!pipFileAssociatedWithFolder) { |
| 149 | + return false; |
| 150 | + } |
| 151 | + return arePathsSame(pipFileAssociatedWithEnvironment, pipFileAssociatedWithFolder); |
| 152 | +} |
0 commit comments