|
| 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +import { pathExists } from 'fs-extra'; |
| 5 | +import * as path from 'path'; |
| 6 | +import { getEnvironmentVariable } from '../../../../common/utils/platform'; |
| 7 | +import { readFile } from '../../../common/externalDependencies'; |
| 8 | + |
| 9 | +/** |
| 10 | + * Returns the path to Pipfile associated with the provided directory. |
| 11 | + * @param searchDir the directory to look into |
| 12 | + * @param lookIntoParentDirectories set to true if we should also search for Pipfile in parent directory |
| 13 | + */ |
| 14 | +async function getAssociatedPipfile( |
| 15 | + searchDir: string, |
| 16 | + lookIntoParentDirectories: boolean, |
| 17 | +): Promise<string | undefined> { |
| 18 | + const pipFileName = getEnvironmentVariable('PIPENV_PIPFILE') || 'Pipfile'; |
| 19 | + let depthToSearch = 1; |
| 20 | + if (lookIntoParentDirectories) { |
| 21 | + // PIPENV_MAX_DEPTH tells pipenv the maximum number of directories to recursively search for |
| 22 | + // a Pipfile, defaults to 3: https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_MAX_DEPTH |
| 23 | + const maxDepth = getEnvironmentVariable('PIPENV_MAX_DEPTH'); |
| 24 | + if (maxDepth) { |
| 25 | + depthToSearch = +maxDepth; |
| 26 | + } else { |
| 27 | + depthToSearch = 3; |
| 28 | + } |
| 29 | + } |
| 30 | + while (depthToSearch > 0 && searchDir !== path.dirname(searchDir)) { |
| 31 | + const pipFile = path.join(searchDir, pipFileName); |
| 32 | + // eslint-disable-next-line no-await-in-loop |
| 33 | + if (await pathExists(pipFile)) { |
| 34 | + return pipFile; |
| 35 | + } |
| 36 | + searchDir = path.dirname(searchDir); |
| 37 | + depthToSearch -= 1; |
| 38 | + } |
| 39 | + return undefined; |
| 40 | +} |
| 41 | + |
| 42 | +/** |
| 43 | + * Returns true if interpreter path belongs to a global pipenv environment which is associated with a particular folder, |
| 44 | + * false otherwise. |
| 45 | + * @param interpreterPath Absolute path to any python interpreter. |
| 46 | + */ |
| 47 | +export async function isPipenvEnvironmentRelatedToFolder(interpreterPath: string, folder: string): Promise<boolean> { |
| 48 | + const pipFileCorrespondingToEnvironment = await getPipfileIfGlobalPipenvEnvironment(interpreterPath); |
| 49 | + if (!pipFileCorrespondingToEnvironment) { |
| 50 | + return false; |
| 51 | + } |
| 52 | + const projectCorrespondingToEnvironment = path.dirname(pipFileCorrespondingToEnvironment); |
| 53 | + |
| 54 | + // PIPENV_NO_INHERIT is used to tell pipenv not to look for Pipfile in parent directories |
| 55 | + // https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_NO_INHERIT |
| 56 | + const pipFile = await getAssociatedPipfile(folder, !getEnvironmentVariable('PIPENV_NO_INHERIT')); |
| 57 | + if (!pipFile) { |
| 58 | + return false; |
| 59 | + } |
| 60 | + const projectCorrespondingToFolder = path.dirname(pipFile); |
| 61 | + |
| 62 | + return projectCorrespondingToEnvironment === projectCorrespondingToFolder; |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + * If interpreter path belongs to a pipenv environment which is located inside a project, return associated Pipfile, |
| 67 | + * otherwise return `undefined`. |
| 68 | + * @param interpreterPath Absolute path to any python interpreter. |
| 69 | + */ |
| 70 | +async function getPipfileIfLocalPipenvEnvironment(interpreterPath: string): Promise<string | undefined> { |
| 71 | + // Local pipenv environments are created by setting PIPENV_VENV_IN_PROJECT to 1, which always names the environment |
| 72 | + // folder '.venv': https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_VENV_IN_PROJECT |
| 73 | + // This is the layout we wish to verify. |
| 74 | + // project |
| 75 | + // |__ Pipfile <--- check if Pipfile exists here |
| 76 | + // |__ .venv <--- check if name of the folder is '.venv' |
| 77 | + // |__ Scripts/bin |
| 78 | + // |__ python <--- interpreterPath |
| 79 | + const venvFolder = path.dirname(path.dirname(interpreterPath)); |
| 80 | + if (path.basename(venvFolder) !== '.venv') { |
| 81 | + return undefined; |
| 82 | + } |
| 83 | + const directoryWhereVenvResides = path.dirname(venvFolder); |
| 84 | + return getAssociatedPipfile(directoryWhereVenvResides, false); |
| 85 | +} |
| 86 | + |
| 87 | +/** |
| 88 | + * If interpreter path belongs to a global pipenv environment, return associated Pipfile, otherwise return `undefined`. |
| 89 | + * @param interpreterPath Absolute path to any python interpreter. |
| 90 | + */ |
| 91 | +async function getPipfileIfGlobalPipenvEnvironment(interpreterPath: string): Promise<string | undefined> { |
| 92 | + // Global pipenv environments have a .project file with the absolute path to the project |
| 93 | + // See https://github.com/pypa/pipenv/blob/9299ae1f7353bdd523a1829f3c7cad0ee67c2e3b/CHANGELOG.rst#L754 |
| 94 | + // Also, the name of the directory where Pipfile resides is used as a prefix in the environment folder. |
| 95 | + // This is the layout we wish to verify. |
| 96 | + // <Environment folder> |
| 97 | + // |__ .project <--- check if .project exists here |
| 98 | + // |__ Scripts/bin |
| 99 | + // |__ python <--- interpreterPath |
| 100 | + const dotProjectFile = path.join(path.dirname(path.dirname(interpreterPath)), '.project'); |
| 101 | + if (!(await pathExists(dotProjectFile))) { |
| 102 | + return undefined; |
| 103 | + } |
| 104 | + |
| 105 | + const project = await readFile(dotProjectFile); |
| 106 | + if (!(await pathExists(project))) { |
| 107 | + return undefined; |
| 108 | + } |
| 109 | + |
| 110 | + // The name of the directory where Pipfile resides is used as a prefix in the environment folder. |
| 111 | + if (interpreterPath.indexOf(`${path.sep}${path.basename(project)}-`) === -1) { |
| 112 | + return undefined; |
| 113 | + } |
| 114 | + |
| 115 | + return getAssociatedPipfile(project, 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 getPipfileIfLocalPipenvEnvironment(interpreterPath)) { |
| 125 | + return true; |
| 126 | + } |
| 127 | + if (await getPipfileIfGlobalPipenvEnvironment(interpreterPath)) { |
| 128 | + return true; |
| 129 | + } |
| 130 | + return false; |
| 131 | +} |
0 commit comments