Skip to content

Commit 41a2b1d

Browse files
committed
Add new additionalPowerShellLocations option
Signed-off-by: Chawye Hsu <su+git@chawyehsu.com>
1 parent 9be1de4 commit 41a2b1d

4 files changed

Lines changed: 599 additions & 89 deletions

File tree

package.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,46 @@
968968
"type": "string"
969969
}
970970
},
971+
"powershell.additionalPowerShellLocations": {
972+
"type": "array",
973+
"default": [],
974+
"markdownDescription": "Specifies a list of named PowerShell executables tied to a specific platform. When this setting is non-empty, `#powershell.powerShellAdditionalExePaths#` and `#powershell.powerShellDefaultVersion#` are ignored. Entries are ranked by `weight` (highest first), and entries with the same `weight` keep definition order.",
975+
"items": {
976+
"type": "object",
977+
"required": [
978+
"name",
979+
"path",
980+
"platform"
981+
],
982+
"additionalProperties": false,
983+
"properties": {
984+
"name": {
985+
"type": "string",
986+
"markdownDescription": "The display name shown in the Session Menu."
987+
},
988+
"path": {
989+
"type": "string",
990+
"markdownDescription": "The absolute path to the PowerShell executable or containing folder."
991+
},
992+
"platform": {
993+
"type": "string",
994+
"enum": [
995+
"windows-x64",
996+
"windows-arm64",
997+
"macos-x64",
998+
"macos-arm64",
999+
"linux-x64",
1000+
"linux-aarch64"
1001+
],
1002+
"markdownDescription": "The target host platform for this PowerShell location."
1003+
},
1004+
"weight": {
1005+
"type": "number",
1006+
"markdownDescription": "Ranking for this location on its platform. Higher values are preferred; equal values preserve definition order."
1007+
}
1008+
}
1009+
}
1010+
},
9711011
"powershell.cwd": {
9721012
"type": "string",
9731013
"default": "",

src/platform.ts

Lines changed: 154 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ export enum OperatingSystem {
3434
Linux,
3535
}
3636

37+
export type SupportedPlatform =
38+
| "windows-x64"
39+
| "windows-arm64"
40+
| "macos-x64"
41+
| "macos-arm64"
42+
| "linux-x64"
43+
| "linux-aarch64";
44+
45+
export interface IAdditionalPowerShellLocation {
46+
readonly name: string;
47+
readonly path: string;
48+
readonly platform: SupportedPlatform;
49+
readonly weight?: number;
50+
}
51+
3752
export interface IPlatformDetails {
3853
operatingSystem: OperatingSystem;
3954
isOS64Bit: boolean;
@@ -46,6 +61,46 @@ export interface IPowerShellExeDetails {
4661
readonly supportsProperArguments: boolean;
4762
}
4863

64+
export function getSupportedPlatform(
65+
platformDetails: IPlatformDetails,
66+
processArchitecture: NodeJS.Architecture = process.arch,
67+
): SupportedPlatform | undefined {
68+
switch (platformDetails.operatingSystem) {
69+
case OperatingSystem.Windows:
70+
switch (processArchitecture) {
71+
case "x64":
72+
return "windows-x64";
73+
case "arm64":
74+
return "windows-arm64";
75+
default:
76+
return undefined;
77+
}
78+
79+
case OperatingSystem.MacOS:
80+
switch (processArchitecture) {
81+
case "x64":
82+
return "macos-x64";
83+
case "arm64":
84+
return "macos-arm64";
85+
default:
86+
return undefined;
87+
}
88+
89+
case OperatingSystem.Linux:
90+
switch (processArchitecture) {
91+
case "x64":
92+
return "linux-x64";
93+
case "arm64":
94+
return "linux-aarch64";
95+
default:
96+
return undefined;
97+
}
98+
99+
case OperatingSystem.Unknown:
100+
return undefined;
101+
}
102+
}
103+
49104
export function getPlatformDetails(): IPlatformDetails {
50105
let operatingSystem = OperatingSystem.Unknown;
51106

@@ -91,6 +146,8 @@ export class PowerShellExeFinder {
91146
// Additional configured PowerShells
92147
private additionalPowerShellExes: Record<string, string>,
93148
private logger?: ILogger,
149+
private additionalPowerShellLocations: IAdditionalPowerShellLocation[] = [],
150+
private processArchitecture: NodeJS.Architecture = process.arch,
94151
) {}
95152

96153
/**
@@ -158,7 +215,11 @@ export class PowerShellExeFinder {
158215

159216
// Also show any additionally configured PowerShells
160217
// These may be duplicates of the default installations, but given a different name.
161-
for await (const additionalPwsh of this.enumerateAdditionalPowerShellInstallations()) {
218+
const configuredPowerShells =
219+
this.additionalPowerShellLocations.length > 0
220+
? this.enumerateAdditionalPowerShellLocations()
221+
: this.enumerateAdditionalPowerShellInstallations();
222+
for await (const additionalPwsh of configuredPowerShells) {
162223
if (await additionalPwsh.exists()) {
163224
yield additionalPwsh;
164225
} else if (!additionalPwsh.suppressWarning) {
@@ -269,97 +330,123 @@ export class PowerShellExeFinder {
269330
* without checking for their existence.
270331
*/
271332
public async *enumerateAdditionalPowerShellInstallations(): AsyncIterable<IPossiblePowerShellExe> {
272-
for (const versionName in this.additionalPowerShellExes) {
273-
if (
274-
Object.prototype.hasOwnProperty.call(
275-
this.additionalPowerShellExes,
276-
versionName,
277-
)
278-
) {
279-
let exePath: string | undefined = utils.stripQuotePair(
280-
this.additionalPowerShellExes[versionName],
281-
);
282-
if (!exePath) {
283-
continue;
284-
}
333+
yield* this.enumerateConfiguredPowerShellInstallations(
334+
Object.entries(this.additionalPowerShellExes),
335+
);
336+
}
285337

286-
exePath = untildify(exePath);
287-
const args: [string, undefined, boolean, boolean] =
288-
// Must be a tuple type and is suppressing the warning
289-
[versionName, undefined, true, true];
338+
/**
339+
* Iterates through the configured additional PowerShell locations for the current platform,
340+
* without checking for their existence.
341+
*/
342+
public async *enumerateAdditionalPowerShellLocations(): AsyncIterable<IPossiblePowerShellExe> {
343+
const supportedPlatform = getSupportedPlatform(
344+
this.platformDetails,
345+
this.processArchitecture,
346+
);
347+
if (!supportedPlatform) {
348+
return;
349+
}
290350

291-
// Always search for what the user gave us first, but with the warning
292-
// suppressed so we can display it after all possibilities are exhausted
293-
let pwsh = new PossiblePowerShellExe(exePath, ...args);
294-
if (await pwsh.exists()) {
295-
yield pwsh;
296-
continue;
297-
}
351+
yield* this.enumerateConfiguredPowerShellInstallations(
352+
this.additionalPowerShellLocations
353+
.filter((location) => location.platform === supportedPlatform)
354+
// Higher weight wins. Equal weights preserve definition order.
355+
.sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0))
356+
.map((location): [string, string] => [
357+
location.name,
358+
location.path,
359+
]),
360+
);
361+
}
298362

299-
// Also search for `pwsh[.exe]` and `powershell[.exe]` if missing
363+
private async *enumerateConfiguredPowerShellInstallations(
364+
configuredPowerShells: Iterable<readonly [string, string]>,
365+
): AsyncIterable<IPossiblePowerShellExe> {
366+
for (const [versionName, configuredPath] of configuredPowerShells) {
367+
let exePath: string | undefined =
368+
utils.stripQuotePair(configuredPath);
369+
if (!exePath) {
370+
continue;
371+
}
372+
373+
exePath = untildify(exePath);
374+
const args: [string, undefined, boolean, boolean] =
375+
// Must be a tuple type and is suppressing the warning
376+
[versionName, undefined, true, true];
377+
378+
// Always search for what the user gave us first, but with the warning
379+
// suppressed so we can display it after all possibilities are exhausted
380+
let pwsh = new PossiblePowerShellExe(exePath, ...args);
381+
if (await pwsh.exists()) {
382+
yield pwsh;
383+
continue;
384+
}
385+
386+
// Also search for `pwsh[.exe]` and `powershell[.exe]` if missing
387+
if (
388+
this.platformDetails.operatingSystem === OperatingSystem.Windows
389+
) {
390+
// Handle Windows where '.exe' and 'powershell' are things
300391
if (
301-
this.platformDetails.operatingSystem ===
302-
OperatingSystem.Windows
392+
!exePath.endsWith("pwsh.exe") &&
393+
!exePath.endsWith("powershell.exe")
303394
) {
304-
// Handle Windows where '.exe' and 'powershell' are things
305395
if (
306-
!exePath.endsWith("pwsh.exe") &&
307-
!exePath.endsWith("powershell.exe")
396+
exePath.endsWith("pwsh") ||
397+
exePath.endsWith("powershell")
308398
) {
309-
if (
310-
exePath.endsWith("pwsh") ||
311-
exePath.endsWith("powershell")
312-
) {
313-
// Add extension if that was missing
314-
pwsh = new PossiblePowerShellExe(
315-
exePath + ".exe",
316-
...args,
317-
);
318-
if (await pwsh.exists()) {
319-
yield pwsh;
320-
continue;
321-
}
322-
}
323-
// Also add full exe names (this isn't an else just in case
324-
// the folder was named "pwsh" or "powershell")
325-
pwsh = new PossiblePowerShellExe(
326-
path.join(exePath, "pwsh.exe"),
327-
...args,
328-
);
329-
if (await pwsh.exists()) {
330-
yield pwsh;
331-
continue;
332-
}
399+
// Add extension if that was missing
333400
pwsh = new PossiblePowerShellExe(
334-
path.join(exePath, "powershell.exe"),
401+
exePath + ".exe",
335402
...args,
336403
);
337404
if (await pwsh.exists()) {
338405
yield pwsh;
339406
continue;
340407
}
341408
}
342-
} else if (!exePath.endsWith("pwsh")) {
343-
// Always just 'pwsh' on non-Windows
409+
410+
// Also add full exe names (this isn't an else just in case
411+
// the folder was named "pwsh" or "powershell")
344412
pwsh = new PossiblePowerShellExe(
345-
path.join(exePath, "pwsh"),
413+
path.join(exePath, "pwsh.exe"),
346414
...args,
347415
);
348416
if (await pwsh.exists()) {
349417
yield pwsh;
350418
continue;
351419
}
352-
}
353420

354-
// If we're still being iterated over, no permutation of the given path existed so yield an object with the warning unsuppressed
355-
yield new PossiblePowerShellExe(
356-
exePath,
357-
versionName,
358-
false,
359-
undefined,
360-
false,
421+
pwsh = new PossiblePowerShellExe(
422+
path.join(exePath, "powershell.exe"),
423+
...args,
424+
);
425+
if (await pwsh.exists()) {
426+
yield pwsh;
427+
continue;
428+
}
429+
}
430+
} else if (!exePath.endsWith("pwsh")) {
431+
// Always just 'pwsh' on non-Windows
432+
pwsh = new PossiblePowerShellExe(
433+
path.join(exePath, "pwsh"),
434+
...args,
361435
);
436+
if (await pwsh.exists()) {
437+
yield pwsh;
438+
continue;
439+
}
362440
}
441+
442+
// If we're still being iterated over, no permutation of the given path existed so yield an object with the warning unsuppressed
443+
yield new PossiblePowerShellExe(
444+
exePath,
445+
versionName,
446+
false,
447+
undefined,
448+
false,
449+
);
363450
}
364451
}
365452

0 commit comments

Comments
 (0)