From d6ba0a7779342c5872564b4b524e8c934f7518b0 Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 19 Jan 2023 15:18:52 -0500 Subject: [PATCH] Detect ActiveState Python runtimes (#20532) --- .../configuration/environmentTypeComparer.ts | 1 + .../commands/setInterpreter.ts | 1 + .../pythonEnvironments/base/info/envKind.ts | 2 + .../pythonEnvironments/base/info/index.ts | 3 ++ .../base/locators/composite/resolverUtils.ts | 25 +++++++++ .../locators/lowLevel/activestateLocator.ts | 39 ++++++++++++++ .../common/environmentIdentifier.ts | 2 + .../common/environmentManagers/activestate.ts | 54 +++++++++++++++++++ src/client/pythonEnvironments/index.ts | 2 + src/client/pythonEnvironments/info/index.ts | 6 +++ src/client/pythonEnvironments/legacyIOC.ts | 1 + .../base/info/envKind.unit.test.ts | 1 + .../lowLevel/activestateLocator.unit.test.ts | 51 ++++++++++++++++++ .../activestate.unit.test.ts | 53 ++++++++++++++++++ .../2af6390a/_runtime_store/completed | 0 .../activestate/2af6390a/exec/not-python3 | 0 .../activestate/2af6390a/exec/not-python3.exe | 0 .../activestate/b6a0705d/exec/python3 | 1 + .../activestate/b6a0705d/exec/python3.exe | 1 + .../c09080d1/_runtime_store/completed | 0 .../activestate/c09080d1/exec/python3 | 0 .../activestate/c09080d1/exec/python3.exe | 0 22 files changed, 243 insertions(+) create mode 100644 src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts create mode 100644 src/client/pythonEnvironments/common/environmentManagers/activestate.ts create mode 100644 src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts create mode 100644 src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe diff --git a/src/client/interpreter/configuration/environmentTypeComparer.ts b/src/client/interpreter/configuration/environmentTypeComparer.ts index e3c77a6b2d6d..641be5c1ace7 100644 --- a/src/client/interpreter/configuration/environmentTypeComparer.ts +++ b/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -234,6 +234,7 @@ function getPrioritizedEnvironmentType(): EnvironmentType[] { EnvironmentType.VirtualEnvWrapper, EnvironmentType.Venv, EnvironmentType.VirtualEnv, + EnvironmentType.ActiveState, EnvironmentType.Conda, EnvironmentType.Pyenv, EnvironmentType.MicrosoftStore, diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 39965adec0a0..1aee36a57cb7 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -65,6 +65,7 @@ export namespace EnvGroups { export const Venv = 'Venv'; export const Poetry = 'Poetry'; export const VirtualEnvWrapper = 'VirtualEnvWrapper'; + export const ActiveState = 'ActiveState'; export const Recommended = Common.recommended; } diff --git a/src/client/pythonEnvironments/base/info/envKind.ts b/src/client/pythonEnvironments/base/info/envKind.ts index 09490725e960..8828003c5ce7 100644 --- a/src/client/pythonEnvironments/base/info/envKind.ts +++ b/src/client/pythonEnvironments/base/info/envKind.ts @@ -22,6 +22,7 @@ export function getKindDisplayName(kind: PythonEnvKind): string { [PythonEnvKind.VirtualEnvWrapper, 'virtualenv'], [PythonEnvKind.Pipenv, 'pipenv'], [PythonEnvKind.Conda, 'conda'], + [PythonEnvKind.ActiveState, 'ActiveState'], // For now we treat OtherVirtual like Unknown. ] as [PythonEnvKind, string][]) { if (kind === candidate) { @@ -63,6 +64,7 @@ export function getPrioritizedEnvKinds(): PythonEnvKind[] { PythonEnvKind.Venv, PythonEnvKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnv, + PythonEnvKind.ActiveState, PythonEnvKind.OtherVirtual, PythonEnvKind.OtherGlobal, PythonEnvKind.System, diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index 4ef512c56ed6..a1c61d8067a3 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -15,6 +15,7 @@ export enum PythonEnvKind { MicrosoftStore = 'global-microsoft-store', Pyenv = 'global-pyenv', Poetry = 'poetry', + ActiveState = 'activestate', Custom = 'global-custom', OtherGlobal = 'global-other', // "virtual" @@ -28,6 +29,7 @@ export enum PythonEnvKind { export enum PythonEnvType { Conda = 'Conda', + ActiveState = 'ActiveState', Virtual = 'Virtual', } @@ -48,6 +50,7 @@ export const virtualEnvKinds = [ PythonEnvKind.VirtualEnvWrapper, PythonEnvKind.Conda, PythonEnvKind.VirtualEnv, + PythonEnvKind.ActiveState, ]; export const globallyInstalledEnvKinds = [ diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index c0506d4a06ba..d3dc65be2522 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -25,6 +25,7 @@ import { parseVersionFromExecutable } from '../../info/executable'; import { traceError, traceWarn } from '../../../../logging'; import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs'; import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis'; +import { ActiveState, isActiveStateEnvironment } from '../../../common/environmentManagers/activestate'; function getResolvers(): Map Promise> { const resolvers = new Map Promise>(); @@ -37,6 +38,7 @@ function getResolvers(): Map Promise { return envInfo; } +async function resolveActiveStateEnv(env: BasicEnvInfo): Promise { + const info = buildEnvInfo({ + kind: env.kind, + executable: env.executablePath, + type: PythonEnvType.ActiveState, + }); + const projects = await ActiveState.getProjects(); + if (projects) { + for (const project of projects) { + for (const dir of project.executables) { + if (dir === path.dirname(env.executablePath)) { + info.name = `${project.organization}/${project.name}`; + return info; + } + } + } + } + return info; +} + async function isBaseCondaPyenvEnvironment(executablePath: string) { if (!(await isCondaEnvironment(executablePath))) { return false; diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts new file mode 100644 index 000000000000..1b15ed1a20c7 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ActiveState } from '../../../common/environmentManagers/activestate'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { traceError, traceVerbose } from '../../../../logging'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; +import { findInterpretersInDir } from '../../../common/commonUtils'; + +export class ActiveStateLocator extends LazyResourceBasedLocator { + public readonly providerId: string = 'activestate'; + + // eslint-disable-next-line class-methods-use-this + public async *doIterEnvs(): IPythonEnvsIterator { + const projects = await ActiveState.getProjects(); + if (projects === undefined) { + traceVerbose(`Couldn't fetch State Tool projects.`); + return; + } + for (const project of projects) { + if (project.executables) { + for (const dir of project.executables) { + try { + traceVerbose(`Looking for Python in: ${project.name}`); + for await (const exe of findInterpretersInDir(dir)) { + traceVerbose(`Found Python executable: ${exe.filename}`); + yield { kind: PythonEnvKind.ActiveState, executablePath: exe.filename }; + } + } catch (ex) { + traceError(`Failed to process State Tool project: ${JSON.stringify(project)}`, ex); + } + } + } + } + } +} diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index 957321ed8e61..2dbc8b2b93d9 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -15,6 +15,7 @@ import { isVirtualenvwrapperEnvironment as isVirtualEnvWrapperEnvironment, } from './environmentManagers/simplevirtualenvs'; import { isMicrosoftStoreEnvironment } from './environmentManagers/microsoftStoreEnv'; +import { isActiveStateEnvironment } from './environmentManagers/activestate'; function getIdentifiers(): Map Promise> { const notImplemented = () => Promise.resolve(false); @@ -32,6 +33,7 @@ function getIdentifiers(): Map Promise identifier.set(PythonEnvKind.Venv, isVenvEnvironment); identifier.set(PythonEnvKind.VirtualEnvWrapper, isVirtualEnvWrapperEnvironment); identifier.set(PythonEnvKind.VirtualEnv, isVirtualEnvEnvironment); + identifier.set(PythonEnvKind.ActiveState, isActiveStateEnvironment); identifier.set(PythonEnvKind.Unknown, defaultTrue); identifier.set(PythonEnvKind.OtherGlobal, isGloballyInstalledEnv); return identifier; diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts new file mode 100644 index 000000000000..3e28a11d6d14 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { pathExists, shellExecute } from '../externalDependencies'; +import { cache } from '../../../common/utils/decorators'; +import { traceError, traceVerbose } from '../../../logging'; + +const STATE_GENERAL_TIMEOUT = 50000; + +export type ProjectInfo = { + name: string; + organization: string; + local_checkouts: string[]; // eslint-disable-line camelcase + executables: string[]; +}; + +export async function isActiveStateEnvironment(interpreterPath: string): Promise { + const execDir = path.dirname(interpreterPath); + const runtimeDir = path.dirname(execDir); + return pathExists(path.join(runtimeDir, '_runtime_store')); +} + +export class ActiveState { + public static readonly stateCommand: string = 'state'; + + public static async getProjects(): Promise { + return this.getProjectsCached(); + } + + @cache(30_000, true, 10_000) + private static async getProjectsCached(): Promise { + try { + const result = await shellExecute(`${this.stateCommand} projects -o editor`, { + timeout: STATE_GENERAL_TIMEOUT, + }); + if (!result) { + return undefined; + } + let output = result.stdout.trimEnd(); + if (output[output.length - 1] === '\0') { + // '\0' is a record separator. + output = output.substring(0, output.length - 1); + } + traceVerbose(`${this.stateCommand} projects -o editor: ${output}`); + return JSON.parse(output); + } catch (ex) { + traceError(ex); + return undefined; + } + } +} diff --git a/src/client/pythonEnvironments/index.ts b/src/client/pythonEnvironments/index.ts index 3f7bac7d670e..8d6a8cb7d4ba 100644 --- a/src/client/pythonEnvironments/index.ts +++ b/src/client/pythonEnvironments/index.ts @@ -36,6 +36,7 @@ import { import { EnvsCollectionService } from './base/locators/composite/envsCollectionService'; import { IDisposable } from '../common/types'; import { traceError } from '../logging'; +import { ActiveStateLocator } from './base/locators/lowLevel/activestateLocator'; /** * Set up the Python environments component (during extension activation).' @@ -137,6 +138,7 @@ function createNonWorkspaceLocators(ext: ExtensionState): ILocator // OS-independent locators go here. new PyenvLocator(), new CondaEnvironmentLocator(), + new ActiveStateLocator(), new GlobalVirtualEnvironmentLocator(), new CustomVirtualEnvironmentLocator(), ); diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index d0f41c45a5b1..1ee8b46202f7 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -19,6 +19,7 @@ export enum EnvironmentType { MicrosoftStore = 'MicrosoftStore', Poetry = 'Poetry', VirtualEnvWrapper = 'VirtualEnvWrapper', + ActiveState = 'ActiveState', Global = 'Global', System = 'System', } @@ -30,6 +31,7 @@ export const virtualEnvTypes = [ EnvironmentType.VirtualEnvWrapper, EnvironmentType.Conda, EnvironmentType.VirtualEnv, + EnvironmentType.ActiveState, ]; /** @@ -41,6 +43,7 @@ export enum ModuleInstallerType { Pip = 'Pip', Poetry = 'Poetry', Pipenv = 'Pipenv', + ActiveState = 'ActiveState', } /** @@ -114,6 +117,9 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string case EnvironmentType.VirtualEnvWrapper: { return 'virtualenvwrapper'; } + case EnvironmentType.ActiveState: { + return 'activestate'; + } default: { return ''; } diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index a3b8c3f0aaf7..ce9bfb4caf11 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -35,6 +35,7 @@ const convertedKinds = new Map( [PythonEnvKind.Poetry]: EnvironmentType.Poetry, [PythonEnvKind.Venv]: EnvironmentType.Venv, [PythonEnvKind.VirtualEnvWrapper]: EnvironmentType.VirtualEnvWrapper, + [PythonEnvKind.ActiveState]: EnvironmentType.ActiveState, }), ); diff --git a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts index c12777d3e653..fdf174b4c551 100644 --- a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts +++ b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts @@ -20,6 +20,7 @@ const KIND_NAMES: [PythonEnvKind, string][] = [ [PythonEnvKind.VirtualEnvWrapper, 'virtualenvWrapper'], [PythonEnvKind.Pipenv, 'pipenv'], [PythonEnvKind.Conda, 'conda'], + [PythonEnvKind.ActiveState, 'activestate'], [PythonEnvKind.OtherVirtual, 'otherVirtual'], ]; diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts new file mode 100644 index 000000000000..c534050f01e9 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { ActiveStateLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/activestateLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { ExecutionResult } from '../../../../../client/common/process/types'; +import { createBasicEnv } from '../../common'; +import { getOSType, OSType } from '../../../../../client/common/utils/platform'; + +suite('ActiveState Locator', () => { + const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); + let shellExecute: sinon.SinonStub; + let locator: ActiveStateLocator; + + suiteSetup(() => { + locator = new ActiveStateLocator(); + shellExecute = sinon.stub(externalDependencies, 'shellExecute'); + shellExecute.callsFake((command: string) => { + if (command === 'state projects -o editor') { + return Promise.resolve>({ + stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${testActiveStateDir}/c09080d1/exec"]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${testActiveStateDir}/2af6390a/exec"]}]\n\0`, + }); + } + return Promise.reject(new Error('Command failed')); + }); + }); + + suiteTeardown(() => sinon.restore()); + + test('iterEnvs()', async () => { + const actualEnvs = await getEnvs(locator.iterEnvs()); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.ActiveState, + path.join( + testActiveStateDir, + 'c09080d1', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts new file mode 100644 index 000000000000..8e86e7806ce5 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { getOSType, OSType } from '../../../../client/common/utils/platform'; +import { isActiveStateEnvironment } from '../../../../client/pythonEnvironments/common/environmentManagers/activestate'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +suite('isActiveStateEnvironment Tests', () => { + const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); + let fileSystem: TypeMoq.IMock; + + setup(() => { + fileSystem = TypeMoq.Mock.ofType(); + }); + + test('Return true if runtime is set up', async () => { + const runtimeStorePath = path.join(testActiveStateDir, 'c09080d1', '_runtime_store'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(runtimeStorePath))) + .returns(() => Promise.resolve(true)); + + const result = await isActiveStateEnvironment( + path.join( + testActiveStateDir, + 'c09080d1', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ); + expect(result).to.equal(true); + }); + + test(`Return false if the runtime is not set up`, async () => { + const runtimeStorePath = path.join(testActiveStateDir, 'b6a0705d', '_runtime_store'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(runtimeStorePath))) + .returns(() => Promise.resolve(false)); + + const result = await isActiveStateEnvironment( + path.join( + testActiveStateDir, + 'b6a0705d', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ); + expect(result).to.equal(false); + }); +}); diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 new file mode 100644 index 000000000000..0800f9b4dfd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 @@ -0,0 +1 @@ +invalid python interpreter: missing _runtime_store diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe new file mode 100644 index 000000000000..0800f9b4dfd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe @@ -0,0 +1 @@ +invalid python interpreter: missing _runtime_store diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe new file mode 100644 index 000000000000..e69de29bb2d1