Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,46 @@
"type": "string"
}
},
"powershell.additionalPowerShellLocations": {
"type": "array",
"default": [],
"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.",
"items": {
"type": "object",
"required": [
"name",
"path",
"platform"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"markdownDescription": "The display name shown in the Session Menu."
},
"path": {
"type": "string",
"markdownDescription": "The absolute path to the PowerShell executable or containing folder."
},
"platform": {
"type": "string",
"enum": [
"windows-x64",
"windows-arm64",
"macos-x64",
"macos-arm64",
"linux-x64",
"linux-aarch64"
],
"markdownDescription": "The target host platform for this PowerShell location."
},
"weight": {
"type": "number",
"markdownDescription": "Ranking for this location on its platform. Higher values are preferred; equal values preserve definition order."
}
}
}
},
"powershell.cwd": {
"type": "string",
"default": "",
Expand Down
233 changes: 161 additions & 72 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ export enum OperatingSystem {
Linux,
}

export type SupportedPlatform =
| "windows-x64"
| "windows-arm64"
| "macos-x64"
| "macos-arm64"
| "linux-x64"
| "linux-aarch64";

export interface IAdditionalPowerShellLocation {
readonly name: string;
readonly path: string;
readonly platform: SupportedPlatform;
readonly weight?: number;
}

export interface IPlatformDetails {
operatingSystem: OperatingSystem;
isOS64Bit: boolean;
Expand All @@ -46,6 +61,46 @@ export interface IPowerShellExeDetails {
readonly supportsProperArguments: boolean;
}

export function getSupportedPlatform(
platformDetails: IPlatformDetails,
processArchitecture: NodeJS.Architecture = process.arch,
): SupportedPlatform | undefined {
switch (platformDetails.operatingSystem) {
case OperatingSystem.Windows:
switch (processArchitecture) {
case "x64":
return "windows-x64";
case "arm64":
return "windows-arm64";
default:
return undefined;
}

case OperatingSystem.MacOS:
switch (processArchitecture) {
case "x64":
return "macos-x64";
case "arm64":
return "macos-arm64";
default:
return undefined;
}

case OperatingSystem.Linux:
switch (processArchitecture) {
case "x64":
return "linux-x64";
case "arm64":
return "linux-aarch64";
default:
return undefined;
}

case OperatingSystem.Unknown:
return undefined;
}
}

export function getPlatformDetails(): IPlatformDetails {
let operatingSystem = OperatingSystem.Unknown;

Expand Down Expand Up @@ -83,14 +138,18 @@ export class PowerShellExeFinder {
/**
* Create a new PowerShellFinder object to discover PowerShell installations.
* @param platformDetails Information about the machine we are running on.
* @param additionalPowerShellExes Additional PowerShell installations as configured in the settings.
* @param additionalPowerShellExePaths `additionalPowerShellExePaths` in the settings.
* @param additionalPowerShellLocations `additionalPowerShellLocations` in the settings.
*/
constructor(
// The platform details descriptor for the platform we're on
private platformDetails: IPlatformDetails,
// Additional configured PowerShells
private additionalPowerShellExes: Record<string, string>,
// (Old) Additional configured PowerShells
private additionalPowerShellExePaths: Record<string, string>,
private logger?: ILogger,
// (New) Additional configured PowerShell locations
private additionalPowerShellLocations: IAdditionalPowerShellLocation[] = [],
private processArchitecture: NodeJS.Architecture = process.arch,
) {}

/**
Expand Down Expand Up @@ -158,7 +217,11 @@ export class PowerShellExeFinder {

// Also show any additionally configured PowerShells
// These may be duplicates of the default installations, but given a different name.
for await (const additionalPwsh of this.enumerateAdditionalPowerShellInstallations()) {
const configuredPowerShells =
this.additionalPowerShellLocations.length > 0
? this.enumerateAdditionalPowerShellLocations()
: this.enumerateAdditionalPowerShellExePaths();
for await (const additionalPwsh of configuredPowerShells) {
Comment on lines +220 to +224
if (await additionalPwsh.exists()) {
yield additionalPwsh;
} else if (!additionalPwsh.suppressWarning) {
Expand Down Expand Up @@ -265,101 +328,127 @@ export class PowerShellExeFinder {
}

/**
* Iterates through the configured additional PowerShell executable locations,
* Iterates through the configured `additionalPowerShellExePaths`,
* without checking for their existence.
*/
public async *enumerateAdditionalPowerShellInstallations(): AsyncIterable<IPossiblePowerShellExe> {
for (const versionName in this.additionalPowerShellExes) {
if (
Object.prototype.hasOwnProperty.call(
this.additionalPowerShellExes,
versionName,
)
) {
let exePath: string | undefined = utils.stripQuotePair(
this.additionalPowerShellExes[versionName],
);
if (!exePath) {
continue;
}
public async *enumerateAdditionalPowerShellExePaths(): AsyncIterable<IPossiblePowerShellExe> {
yield* this.enumerateConfiguredPowerShellInstallations(
Object.entries(this.additionalPowerShellExePaths),
);
}

exePath = untildify(exePath);
const args: [string, undefined, boolean, boolean] =
// Must be a tuple type and is suppressing the warning
[versionName, undefined, true, true];
/**
* Iterates through the configured `additionalPowerShellLocations` for the current platform,
* without checking for their existence.
*/
public async *enumerateAdditionalPowerShellLocations(): AsyncIterable<IPossiblePowerShellExe> {
const supportedPlatform = getSupportedPlatform(
this.platformDetails,
this.processArchitecture,
);
if (!supportedPlatform) {
return;
}

// Always search for what the user gave us first, but with the warning
// suppressed so we can display it after all possibilities are exhausted
let pwsh = new PossiblePowerShellExe(exePath, ...args);
if (await pwsh.exists()) {
yield pwsh;
continue;
}
yield* this.enumerateConfiguredPowerShellInstallations(
this.additionalPowerShellLocations
.filter((location) => location.platform === supportedPlatform)
// Higher weight wins. Equal weights preserve definition order.
.sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0))
.map((location): [string, string] => [
location.name,
location.path,
]),
);
}

// Also search for `pwsh[.exe]` and `powershell[.exe]` if missing
private async *enumerateConfiguredPowerShellInstallations(
configuredPowerShells: Iterable<readonly [string, string]>,
): AsyncIterable<IPossiblePowerShellExe> {
for (const [versionName, configuredPath] of configuredPowerShells) {
let exePath: string | undefined =
utils.stripQuotePair(configuredPath);
if (!exePath) {
continue;
}

exePath = untildify(exePath);
const args: [string, undefined, boolean, boolean] =
// Must be a tuple type and is suppressing the warning
[versionName, undefined, true, true];

// Always search for what the user gave us first, but with the warning
// suppressed so we can display it after all possibilities are exhausted
let pwsh = new PossiblePowerShellExe(exePath, ...args);
if (await pwsh.exists()) {
yield pwsh;
continue;
}

// Also search for `pwsh[.exe]` and `powershell[.exe]` if missing
if (
this.platformDetails.operatingSystem === OperatingSystem.Windows
) {
// Handle Windows where '.exe' and 'powershell' are things
if (
this.platformDetails.operatingSystem ===
OperatingSystem.Windows
!exePath.endsWith("pwsh.exe") &&
!exePath.endsWith("powershell.exe")
) {
// Handle Windows where '.exe' and 'powershell' are things
if (
!exePath.endsWith("pwsh.exe") &&
!exePath.endsWith("powershell.exe")
exePath.endsWith("pwsh") ||
exePath.endsWith("powershell")
) {
if (
exePath.endsWith("pwsh") ||
exePath.endsWith("powershell")
) {
// Add extension if that was missing
pwsh = new PossiblePowerShellExe(
exePath + ".exe",
...args,
);
if (await pwsh.exists()) {
yield pwsh;
continue;
}
}
// Also add full exe names (this isn't an else just in case
// the folder was named "pwsh" or "powershell")
pwsh = new PossiblePowerShellExe(
path.join(exePath, "pwsh.exe"),
...args,
);
if (await pwsh.exists()) {
yield pwsh;
continue;
}
// Add extension if that was missing
pwsh = new PossiblePowerShellExe(
path.join(exePath, "powershell.exe"),
exePath + ".exe",
...args,
);
if (await pwsh.exists()) {
yield pwsh;
continue;
}
}
} else if (!exePath.endsWith("pwsh")) {
// Always just 'pwsh' on non-Windows

// Also add full exe names (this isn't an else just in case
// the folder was named "pwsh" or "powershell")
pwsh = new PossiblePowerShellExe(
path.join(exePath, "pwsh"),
path.join(exePath, "pwsh.exe"),
...args,
);
if (await pwsh.exists()) {
yield pwsh;
continue;
}
}

// If we're still being iterated over, no permutation of the given path existed so yield an object with the warning unsuppressed
yield new PossiblePowerShellExe(
exePath,
versionName,
false,
undefined,
false,
pwsh = new PossiblePowerShellExe(
path.join(exePath, "powershell.exe"),
...args,
);
if (await pwsh.exists()) {
yield pwsh;
continue;
}
}
} else if (!exePath.endsWith("pwsh")) {
// Always just 'pwsh' on non-Windows
pwsh = new PossiblePowerShellExe(
path.join(exePath, "pwsh"),
...args,
);
if (await pwsh.exists()) {
yield pwsh;
continue;
}
}

// If we're still being iterated over, no permutation of the given path existed so yield an object with the warning unsuppressed
yield new PossiblePowerShellExe(
exePath,
versionName,
false,
undefined,
false,
);
}
}

Expand Down
Loading
Loading