Skip to content

Commit c18e8c9

Browse files
authored
Detect ActiveState Python runtimes (#20534)
Closes #20532
1 parent 2152cd9 commit c18e8c9

File tree

28 files changed

+369
-5
lines changed

28 files changed

+369
-5
lines changed

package.json

+6
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,12 @@
376376
],
377377
"configuration": {
378378
"properties": {
379+
"python.activeStateToolPath": {
380+
"default": "state",
381+
"description": "%python.activeStateToolPath.description%",
382+
"scope": "machine-overridable",
383+
"type": "string"
384+
},
379385
"python.autoComplete.extraPaths": {
380386
"default": [],
381387
"description": "%python.autoComplete.extraPaths.description%",

package.nls.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"python.command.python.launchTensorBoard.title": "Launch TensorBoard",
2626
"python.command.python.refreshTensorBoard.title": "Refresh TensorBoard",
2727
"python.menu.createNewFile.title": "Python File",
28+
"python.activeStateToolPath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).",
2829
"python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.",
2930
"python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).",
3031
"python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See [here](https://aka.ms/AAfekmf) to understand when this is used",

resources/report_issue_user_settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"envFile": "placeholder",
88
"venvPath": "placeholder",
99
"venvFolders": "placeholder",
10+
"activeStateToolPath": "placeholder",
1011
"condaPath": "placeholder",
1112
"pipenvPath": "placeholder",
1213
"poetryPath": "placeholder",

src/client/common/configSettings.ts

+7
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ export class PythonSettings implements IPythonSettings {
9898

9999
public venvFolders: string[] = [];
100100

101+
public activeStateToolPath = '';
102+
101103
public condaPath = '';
102104

103105
public pipenvPath = '';
@@ -254,6 +256,11 @@ export class PythonSettings implements IPythonSettings {
254256

255257
this.venvPath = systemVariables.resolveAny(pythonSettings.get<string>('venvPath'))!;
256258
this.venvFolders = systemVariables.resolveAny(pythonSettings.get<string[]>('venvFolders'))!;
259+
const activeStateToolPath = systemVariables.resolveAny(pythonSettings.get<string>('activeStateToolPath'))!;
260+
this.activeStateToolPath =
261+
activeStateToolPath && activeStateToolPath.length > 0
262+
? getAbsolutePath(activeStateToolPath, workspaceRoot)
263+
: activeStateToolPath;
257264
const condaPath = systemVariables.resolveAny(pythonSettings.get<string>('condaPath'))!;
258265
this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath;
259266
const pipenvPath = systemVariables.resolveAny(pythonSettings.get<string>('pipenvPath'))!;

src/client/common/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export interface IPythonSettings {
184184
readonly pythonPath: string;
185185
readonly venvPath: string;
186186
readonly venvFolders: string[];
187+
readonly activeStateToolPath: string;
187188
readonly condaPath: string;
188189
readonly pipenvPath: string;
189190
readonly poetryPath: string;

src/client/interpreter/configuration/environmentTypeComparer.ts

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { injectable, inject } from 'inversify';
55
import { Resource } from '../../common/types';
66
import { Architecture } from '../../common/utils/platform';
7+
import { isActiveStateEnvironmentForWorkspace } from '../../pythonEnvironments/common/environmentManagers/activestate';
78
import { isParentPath } from '../../pythonEnvironments/common/externalDependencies';
89
import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pythonEnvironments/info';
910
import { PythonVersion } from '../../pythonEnvironments/info/pythonVersion';
@@ -93,6 +94,14 @@ export class EnvironmentTypeComparer implements IInterpreterComparer {
9394
if (isProblematicCondaEnvironment(i)) {
9495
return false;
9596
}
97+
if (
98+
i.envType === EnvironmentType.ActiveState &&
99+
(!i.path ||
100+
!workspaceUri ||
101+
!isActiveStateEnvironmentForWorkspace(i.path, workspaceUri.folderUri.fsPath))
102+
) {
103+
return false;
104+
}
96105
if (getEnvLocationHeuristic(i, workspaceUri?.folderUri.fsPath || '') === EnvLocationHeuristic.Local) {
97106
return true;
98107
}
@@ -237,6 +246,7 @@ function getPrioritizedEnvironmentType(): EnvironmentType[] {
237246
EnvironmentType.VirtualEnvWrapper,
238247
EnvironmentType.Venv,
239248
EnvironmentType.VirtualEnv,
249+
EnvironmentType.ActiveState,
240250
EnvironmentType.Conda,
241251
EnvironmentType.Pyenv,
242252
EnvironmentType.MicrosoftStore,

src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export namespace EnvGroups {
7575
export const Venv = 'Venv';
7676
export const Poetry = 'Poetry';
7777
export const VirtualEnvWrapper = 'VirtualEnvWrapper';
78+
export const ActiveState = 'ActiveState';
7879
export const Recommended = Common.recommended;
7980
}
8081

src/client/pythonEnvironments/base/info/envKind.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function getKindDisplayName(kind: PythonEnvKind): string {
2222
[PythonEnvKind.VirtualEnvWrapper, 'virtualenv'],
2323
[PythonEnvKind.Pipenv, 'pipenv'],
2424
[PythonEnvKind.Conda, 'conda'],
25+
[PythonEnvKind.ActiveState, 'ActiveState'],
2526
// For now we treat OtherVirtual like Unknown.
2627
] as [PythonEnvKind, string][]) {
2728
if (kind === candidate) {
@@ -63,6 +64,7 @@ export function getPrioritizedEnvKinds(): PythonEnvKind[] {
6364
PythonEnvKind.Venv,
6465
PythonEnvKind.VirtualEnvWrapper,
6566
PythonEnvKind.VirtualEnv,
67+
PythonEnvKind.ActiveState,
6668
PythonEnvKind.OtherVirtual,
6769
PythonEnvKind.OtherGlobal,
6870
PythonEnvKind.System,

src/client/pythonEnvironments/base/info/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export enum PythonEnvKind {
1515
MicrosoftStore = 'global-microsoft-store',
1616
Pyenv = 'global-pyenv',
1717
Poetry = 'poetry',
18+
ActiveState = 'activestate',
1819
Custom = 'global-custom',
1920
OtherGlobal = 'global-other',
2021
// "virtual"

src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts

+21
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { parseVersionFromExecutable } from '../../info/executable';
3030
import { traceError, traceWarn } from '../../../../logging';
3131
import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs';
3232
import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis';
33+
import { ActiveState } from '../../../common/environmentManagers/activestate';
3334

3435
function getResolvers(): Map<PythonEnvKind, (env: BasicEnvInfo) => Promise<PythonEnvInfo>> {
3536
const resolvers = new Map<PythonEnvKind, (_: BasicEnvInfo) => Promise<PythonEnvInfo>>();
@@ -42,6 +43,7 @@ function getResolvers(): Map<PythonEnvKind, (env: BasicEnvInfo) => Promise<Pytho
4243
resolvers.set(PythonEnvKind.Conda, resolveCondaEnv);
4344
resolvers.set(PythonEnvKind.MicrosoftStore, resolveMicrosoftStoreEnv);
4445
resolvers.set(PythonEnvKind.Pyenv, resolvePyenvEnv);
46+
resolvers.set(PythonEnvKind.ActiveState, resolveActiveStateEnv);
4547
return resolvers;
4648
}
4749

@@ -247,6 +249,25 @@ async function resolvePyenvEnv(env: BasicEnvInfo): Promise<PythonEnvInfo> {
247249
return envInfo;
248250
}
249251

252+
async function resolveActiveStateEnv(env: BasicEnvInfo): Promise<PythonEnvInfo> {
253+
const info = buildEnvInfo({
254+
kind: env.kind,
255+
executable: env.executablePath,
256+
});
257+
const projects = await ActiveState.getState().then((v) => v?.getProjects());
258+
if (projects) {
259+
for (const project of projects) {
260+
for (const dir of project.executables) {
261+
if (arePathsSame(dir, path.dirname(env.executablePath))) {
262+
info.name = `${project.organization}/${project.name}`;
263+
return info;
264+
}
265+
}
266+
}
267+
}
268+
return info;
269+
}
270+
250271
async function isBaseCondaPyenvEnvironment(executablePath: string) {
251272
if (!(await isCondaEnvironment(executablePath))) {
252273
return false;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { ActiveState } from '../../../common/environmentManagers/activestate';
7+
import { PythonEnvKind } from '../../info';
8+
import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator';
9+
import { traceError, traceVerbose } from '../../../../logging';
10+
import { LazyResourceBasedLocator } from '../common/resourceBasedLocator';
11+
import { findInterpretersInDir } from '../../../common/commonUtils';
12+
13+
export class ActiveStateLocator extends LazyResourceBasedLocator {
14+
public readonly providerId: string = 'activestate';
15+
16+
// eslint-disable-next-line class-methods-use-this
17+
public async *doIterEnvs(): IPythonEnvsIterator<BasicEnvInfo> {
18+
const state = await ActiveState.getState();
19+
if (state === undefined) {
20+
traceVerbose(`Couldn't locate the state binary.`);
21+
return;
22+
}
23+
const projects = await state.getProjects();
24+
if (projects === undefined) {
25+
traceVerbose(`Couldn't fetch State Tool projects.`);
26+
return;
27+
}
28+
for (const project of projects) {
29+
if (project.executables) {
30+
for (const dir of project.executables) {
31+
try {
32+
traceVerbose(`Looking for Python in: ${project.name}`);
33+
for await (const exe of findInterpretersInDir(dir)) {
34+
traceVerbose(`Found Python executable: ${exe.filename}`);
35+
yield { kind: PythonEnvKind.ActiveState, executablePath: exe.filename };
36+
}
37+
} catch (ex) {
38+
traceError(`Failed to process State Tool project: ${JSON.stringify(project)}`, ex);
39+
}
40+
}
41+
}
42+
}
43+
}
44+
}

src/client/pythonEnvironments/common/environmentIdentifier.ts

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
isVirtualenvwrapperEnvironment as isVirtualEnvWrapperEnvironment,
1616
} from './environmentManagers/simplevirtualenvs';
1717
import { isMicrosoftStoreEnvironment } from './environmentManagers/microsoftStoreEnv';
18+
import { isActiveStateEnvironment } from './environmentManagers/activestate';
1819

1920
function getIdentifiers(): Map<PythonEnvKind, (path: string) => Promise<boolean>> {
2021
const notImplemented = () => Promise.resolve(false);
@@ -32,6 +33,7 @@ function getIdentifiers(): Map<PythonEnvKind, (path: string) => Promise<boolean>
3233
identifier.set(PythonEnvKind.Venv, isVenvEnvironment);
3334
identifier.set(PythonEnvKind.VirtualEnvWrapper, isVirtualEnvWrapperEnvironment);
3435
identifier.set(PythonEnvKind.VirtualEnv, isVirtualEnvEnvironment);
36+
identifier.set(PythonEnvKind.ActiveState, isActiveStateEnvironment);
3537
identifier.set(PythonEnvKind.Unknown, defaultTrue);
3638
identifier.set(PythonEnvKind.OtherGlobal, isGloballyInstalledEnv);
3739
return identifier;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import * as path from 'path';
7+
import { dirname } from 'path';
8+
import {
9+
arePathsSame,
10+
getPythonSetting,
11+
onDidChangePythonSetting,
12+
pathExists,
13+
shellExecute,
14+
} from '../externalDependencies';
15+
import { cache } from '../../../common/utils/decorators';
16+
import { traceError, traceVerbose } from '../../../logging';
17+
import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform';
18+
19+
export const ACTIVESTATETOOLPATH_SETTING_KEY = 'activeStateToolPath';
20+
21+
const STATE_GENERAL_TIMEOUT = 5000;
22+
23+
export type ProjectInfo = {
24+
name: string;
25+
organization: string;
26+
local_checkouts: string[]; // eslint-disable-line camelcase
27+
executables: string[];
28+
};
29+
30+
export async function isActiveStateEnvironment(interpreterPath: string): Promise<boolean> {
31+
const execDir = path.dirname(interpreterPath);
32+
const runtimeDir = path.dirname(execDir);
33+
return pathExists(path.join(runtimeDir, '_runtime_store'));
34+
}
35+
36+
export class ActiveState {
37+
private static statePromise: Promise<ActiveState | undefined> | undefined;
38+
39+
public static async getState(): Promise<ActiveState | undefined> {
40+
if (ActiveState.statePromise === undefined) {
41+
ActiveState.statePromise = ActiveState.locate();
42+
}
43+
return ActiveState.statePromise;
44+
}
45+
46+
constructor() {
47+
onDidChangePythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY, () => {
48+
ActiveState.statePromise = undefined;
49+
});
50+
}
51+
52+
public static getStateToolDir(): string | undefined {
53+
const home = getUserHomeDir();
54+
if (!home) {
55+
return undefined;
56+
}
57+
return getOSType() === OSType.Windows
58+
? path.join(home, 'AppData', 'Local', 'ActiveState', 'StateTool')
59+
: path.join(home, '.local', 'ActiveState', 'StateTool');
60+
}
61+
62+
private static async locate(): Promise<ActiveState | undefined> {
63+
const stateToolDir = this.getStateToolDir();
64+
const stateCommand =
65+
getPythonSetting<string>(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand;
66+
if (stateToolDir && ((await pathExists(stateToolDir)) || stateCommand !== this.defaultStateCommand)) {
67+
return new ActiveState();
68+
}
69+
return undefined;
70+
}
71+
72+
public async getProjects(): Promise<ProjectInfo[] | undefined> {
73+
return this.getProjectsCached();
74+
}
75+
76+
private static readonly defaultStateCommand: string = 'state';
77+
78+
@cache(30_000, true, 10_000)
79+
// eslint-disable-next-line class-methods-use-this
80+
private async getProjectsCached(): Promise<ProjectInfo[] | undefined> {
81+
try {
82+
const stateCommand =
83+
getPythonSetting<string>(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand;
84+
const result = await shellExecute(`${stateCommand} projects -o editor`, {
85+
timeout: STATE_GENERAL_TIMEOUT,
86+
});
87+
if (!result) {
88+
return undefined;
89+
}
90+
let output = result.stdout.trimEnd();
91+
if (output[output.length - 1] === '\0') {
92+
// '\0' is a record separator.
93+
output = output.substring(0, output.length - 1);
94+
}
95+
traceVerbose(`${stateCommand} projects -o editor: ${output}`);
96+
const projects = JSON.parse(output);
97+
ActiveState.setCachedProjectInfo(projects);
98+
return projects;
99+
} catch (ex) {
100+
traceError(ex);
101+
return undefined;
102+
}
103+
}
104+
105+
// Stored copy of known projects. isActiveStateEnvironmentForWorkspace() is
106+
// not async, so getProjects() cannot be used. ActiveStateLocator sets this
107+
// when it resolves project info.
108+
private static cachedProjectInfo: ProjectInfo[] = [];
109+
110+
public static getCachedProjectInfo(): ProjectInfo[] {
111+
return this.cachedProjectInfo;
112+
}
113+
114+
private static setCachedProjectInfo(projects: ProjectInfo[]): void {
115+
this.cachedProjectInfo = projects;
116+
}
117+
}
118+
119+
export function isActiveStateEnvironmentForWorkspace(interpreterPath: string, workspacePath: string): boolean {
120+
const interpreterDir = dirname(interpreterPath);
121+
for (const project of ActiveState.getCachedProjectInfo()) {
122+
if (project.executables) {
123+
for (const [i, dir] of project.executables.entries()) {
124+
// Note multiple checkouts for the same interpreter may exist.
125+
// Check them all.
126+
if (arePathsSame(dir, interpreterDir) && arePathsSame(workspacePath, project.local_checkouts[i])) {
127+
return true;
128+
}
129+
}
130+
}
131+
}
132+
return false;
133+
}

src/client/pythonEnvironments/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
import { EnvsCollectionService } from './base/locators/composite/envsCollectionService';
3737
import { IDisposable } from '../common/types';
3838
import { traceError } from '../logging';
39+
import { ActiveStateLocator } from './base/locators/lowLevel/activestateLocator';
3940

4041
/**
4142
* Set up the Python environments component (during extension activation).'
@@ -137,6 +138,7 @@ function createNonWorkspaceLocators(ext: ExtensionState): ILocator<BasicEnvInfo>
137138
// OS-independent locators go here.
138139
new PyenvLocator(),
139140
new CondaEnvironmentLocator(),
141+
new ActiveStateLocator(),
140142
new GlobalVirtualEnvironmentLocator(),
141143
new CustomVirtualEnvironmentLocator(),
142144
);

src/client/pythonEnvironments/info/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export enum EnvironmentType {
1919
MicrosoftStore = 'MicrosoftStore',
2020
Poetry = 'Poetry',
2121
VirtualEnvWrapper = 'VirtualEnvWrapper',
22+
ActiveState = 'ActiveState',
2223
Global = 'Global',
2324
System = 'System',
2425
}
@@ -114,6 +115,9 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string
114115
case EnvironmentType.VirtualEnvWrapper: {
115116
return 'virtualenvwrapper';
116117
}
118+
case EnvironmentType.ActiveState: {
119+
return 'activestate';
120+
}
117121
default: {
118122
return '';
119123
}

src/client/pythonEnvironments/legacyIOC.ts

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const convertedKinds = new Map(
3535
[PythonEnvKind.Poetry]: EnvironmentType.Poetry,
3636
[PythonEnvKind.Venv]: EnvironmentType.Venv,
3737
[PythonEnvKind.VirtualEnvWrapper]: EnvironmentType.VirtualEnvWrapper,
38+
[PythonEnvKind.ActiveState]: EnvironmentType.ActiveState,
3839
}),
3940
);
4041

0 commit comments

Comments
 (0)