Skip to content
Merged
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
65 changes: 5 additions & 60 deletions src/commands/installSwiftlyToolchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,11 @@ import { QuickPickItem } from "vscode";

import { WorkspaceContext } from "../WorkspaceContext";
import { SwiftLogger } from "../logging/SwiftLogger";
import {
Swiftly,
SwiftlyProgressData,
SwiftlyToolchain,
isSnapshotVersion,
isStableVersion,
} from "../toolchain/swiftly";
import { AvailableToolchain, Swiftly, SwiftlyProgressData } from "../toolchain/swiftly";
import { showReloadExtensionNotification } from "../ui/ReloadExtension";

interface SwiftlyToolchainItem extends QuickPickItem {
toolchain: SwiftlyToolchain;
toolchain: AvailableToolchain;
}

async function downloadAndInstallToolchain(selected: SwiftlyToolchainItem, ctx: WorkspaceContext) {
Expand Down Expand Up @@ -147,15 +141,10 @@ export async function installSwiftlyToolchain(ctx: WorkspaceContext): Promise<vo
return;
}

// Sort toolchains with most recent versions first and filter only stable releases
const sortedToolchains = sortToolchainsByVersion(
uninstalledToolchains.filter(toolchain => toolchain.version.type === "stable")
);

ctx.logger.debug(
`Available toolchains for installation: ${sortedToolchains.map(t => t.version.name).join(", ")}`
`Available toolchains for installation: ${uninstalledToolchains.map(t => t.version.name).join(", ")}`
);
const quickPickItems = sortedToolchains.map(toolchain => ({
const quickPickItems = uninstalledToolchains.map(toolchain => ({
label: `$(cloud-download) ${toolchain.version.name}`,
toolchain: toolchain,
}));
Expand Down Expand Up @@ -226,10 +215,7 @@ export async function installSwiftlySnapshotToolchain(ctx: WorkspaceContext): Pr
return;
}

// Sort toolchains with most recent versions first
const sortedToolchains = sortToolchainsByVersion(uninstalledSnapshotToolchains);

const quickPickItems = sortedToolchains.map(toolchain => ({
const quickPickItems = uninstalledSnapshotToolchains.map(toolchain => ({
label: `$(cloud-download) ${toolchain.version.name}`,
description: "snapshot",
detail: `Date: ${
Expand All @@ -250,44 +236,3 @@ export async function installSwiftlySnapshotToolchain(ctx: WorkspaceContext): Pr

await downloadAndInstallToolchain(selected, ctx);
}

/**
* Sorts toolchains by version with most recent first
*/
function sortToolchainsByVersion(toolchains: SwiftlyToolchain[]): SwiftlyToolchain[] {
return toolchains.sort((a, b) => {
// First sort by type (stable before snapshot)
if (a.version.type !== b.version.type) {
return isStableVersion(a.version) ? -1 : 1;
}

// For stable releases, sort by semantic version
if (isStableVersion(a.version) && isStableVersion(b.version)) {
const versionA = a.version;
const versionB = b.version;

if (versionA && versionB) {
if (versionA.major !== versionB.major) {
return versionB.major - versionA.major;
}
if (versionA.minor !== versionB.minor) {
return versionB.minor - versionA.minor;
}
return versionB.patch - versionA.patch;
}
}

// For snapshots, sort by date (newer first)
if (isSnapshotVersion(a.version) && isSnapshotVersion(b.version)) {
const dateA = a.version.date;
const dateB = b.version.date;

if (dateA && dateB) {
return dateB.localeCompare(dateA);
}
}

// Fallback to string comparison
return b.version.name.localeCompare(a.version.name);
});
}
179 changes: 106 additions & 73 deletions src/toolchain/swiftly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,91 +31,77 @@ import { ExecFileError, execFile, execFileStreamOutput } from "../utilities/util
import { Version } from "../utilities/version";
import { SwiftlyConfig } from "./ToolchainVersion";

const ListResult = z.object({
toolchains: z.array(
z.object({
inUse: z.boolean(),
isDefault: z.boolean(),
version: z.union([
z.object({
major: z.union([z.number(), z.undefined()]),
minor: z.union([z.number(), z.undefined()]),
patch: z.union([z.number(), z.undefined()]),
name: z.string(),
type: z.literal("stable"),
}),
z.object({
major: z.union([z.number(), z.undefined()]),
minor: z.union([z.number(), z.undefined()]),
branch: z.string(),
date: z.string(),
name: z.string(),
type: z.literal("snapshot"),
}),
z.object({
name: z.string(),
type: z.literal("system"),
}),
z.object(),
]),
})
),
});

const InUseVersionResult = z.object({
version: z.string(),
const SystemVersion = z.object({
type: z.literal("system"),
name: z.string(),
});
export type SystemVersion = z.infer<typeof SystemVersion>;

const StableVersion = z.object({
type: z.literal("stable"),
name: z.string(),

major: z.number(),
minor: z.number(),
patch: z.number(),
name: z.string(),
type: z.literal("stable"),
});

export type StableVersion = z.infer<typeof StableVersion>;

const SnapshotVersion = z.object({
major: z.union([z.number(), z.undefined()]),
minor: z.union([z.number(), z.undefined()]),
type: z.literal("snapshot"),
name: z.string(),

major: z.optional(z.number()),
minor: z.optional(z.number()),
branch: z.string(),
date: z.string(),
name: z.string(),
type: z.literal("snapshot"),
});

export type SnapshotVersion = z.infer<typeof SnapshotVersion>;

export interface SwiftlyToolchain {
export type ToolchainVersion = SystemVersion | StableVersion | SnapshotVersion;

export interface AvailableToolchain {
inUse: boolean;
installed: boolean;
isDefault: boolean;
version: StableVersion | SnapshotVersion;
version: ToolchainVersion;
}

const AvailableToolchain = z.object({
inUse: z.boolean(),
installed: z.boolean(),
isDefault: z.boolean(),
version: z.union([StableVersion, SnapshotVersion, z.object()]),
const SwiftlyListResult = z.object({
toolchains: z.array(
z.object({
inUse: z.boolean(),
isDefault: z.boolean(),
version: z.union([
SystemVersion,
StableVersion,
SnapshotVersion,
// Allow matching against unexpected future version types
z.object(),
]),
})
),
});
type AvailableToolchain = z.infer<typeof AvailableToolchain>;

export function isStableVersion(
version: StableVersion | SnapshotVersion
): version is StableVersion {
return version.type === "stable";
}

export function isSnapshotVersion(
version: StableVersion | SnapshotVersion
): version is SnapshotVersion {
return version.type === "snapshot";
}
const SwiftlyListAvailableResult = z.object({
toolchains: z.array(
z.object({
inUse: z.boolean(),
installed: z.boolean(),
isDefault: z.boolean(),
version: z.union([
SystemVersion,
StableVersion,
SnapshotVersion,
// Allow matching against unexpected future version types
z.object(),
]),
})
),
});

const ListAvailableResult = z.object({
toolchains: z.array(AvailableToolchain),
const InUseVersionResult = z.object({
version: z.string(),
});

export interface SwiftlyProgressData {
Expand Down Expand Up @@ -228,6 +214,8 @@ export class Swiftly {
/**
* Finds the list of toolchains installed via Swiftly.
*
* Toolchains will be sorted by version number in descending order.
*
* @returns an array of toolchain version names.
*/
public static async list(logger?: SwiftLogger): Promise<string[]> {
Expand All @@ -250,10 +238,13 @@ export class Swiftly {
private static async listUsingJSONFormat(logger?: SwiftLogger): Promise<string[]> {
try {
const { stdout } = await execFile("swiftly", ["list", "--format=json"]);
const response = ListResult.parse(JSON.parse(stdout));
return response.toolchains
.filter(t => ["stable", "snapshot", "system"].includes(t.version?.type))
.map(t => t.version.name);
return SwiftlyListResult.parse(JSON.parse(stdout))
.toolchains.map(toolchain => toolchain.version)
.filter((version): version is ToolchainVersion =>
["system", "stable", "snapshot"].includes(version.type)
)
.sort(compareSwiftlyToolchainVersion)
.map(version => version.name);
} catch (error) {
logger?.error(`Failed to retrieve Swiftly installations: ${error}`);
return [];
Expand All @@ -274,8 +265,14 @@ export class Swiftly {
if (!Array.isArray(installedToolchains)) {
return [];
}
return installedToolchains.filter(
(toolchain): toolchain is string => typeof toolchain === "string"
return (
installedToolchains
.filter((toolchain): toolchain is string => typeof toolchain === "string")
// Sort alphabetically in descending order.
//
// This isn't perfect (e.g. "5.10" will come before "5.9"), but this is
// good enough for legacy support.
.sort((lhs, rhs) => rhs.localeCompare(lhs))
);
} catch (error) {
logger?.error(`Failed to retrieve Swiftly installations: ${error}`);
Expand Down Expand Up @@ -421,14 +418,16 @@ export class Swiftly {
/**
* Lists all toolchains available for installation from swiftly.
*
* Toolchains will be sorted by version number in descending order.
*
* @param branch Optional branch to filter available toolchains (e.g., "main" for snapshots).
* @param logger Optional logger for error reporting.
* @returns Array of available toolchains.
*/
public static async listAvailable(
branch?: string,
logger?: SwiftLogger
): Promise<SwiftlyToolchain[]> {
): Promise<AvailableToolchain[]> {
if (!this.isSupported()) {
return [];
}
Expand All @@ -450,10 +449,11 @@ export class Swiftly {
args.push(branch);
}
const { stdout: availableStdout } = await execFile("swiftly", args);
const result = ListAvailableResult.parse(JSON.parse(availableStdout));
return result.toolchains.filter((t): t is SwiftlyToolchain =>
["stable", "snapshot"].includes(t.version.type)
);
return SwiftlyListAvailableResult.parse(JSON.parse(availableStdout))
.toolchains.filter((t): t is AvailableToolchain =>
["system", "stable", "snapshot"].includes(t.version.type)
)
.sort(compareSwiftlyToolchain);
} catch (error) {
logger?.error(`Failed to retrieve available Swiftly toolchains: ${error}`);
return [];
Expand Down Expand Up @@ -878,3 +878,36 @@ export function checkForSwiftlyInstallation(contextKeys: ContextKeys, logger: Sw
});
});
}

function compareSwiftlyToolchain(lhs: AvailableToolchain, rhs: AvailableToolchain): number {
return compareSwiftlyToolchainVersion(lhs.version, rhs.version);
}

function compareSwiftlyToolchainVersion(lhs: ToolchainVersion, rhs: ToolchainVersion): number {
switch (lhs.type) {
case "system": {
if (rhs.type === "system") {
return lhs.name.localeCompare(rhs.name);
}
return -1;
}
case "stable": {
if (rhs.type === "stable") {
const lhsVersion = new Version(lhs.major, lhs.minor, lhs.patch);
const rhsVersion = new Version(rhs.major, rhs.minor, rhs.patch);
return rhsVersion.compare(lhsVersion);
}
if (rhs.type === "system") {
return 1;
}
return -1;
}
case "snapshot":
if (rhs.type === "snapshot") {
const lhsDate = new Date(lhs.date);
const rhsDate = new Date(rhs.date);
return rhsDate.getTime() - lhsDate.getTime();
}
return 1;
}
}
9 changes: 4 additions & 5 deletions src/ui/ToolchainSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,8 @@ async function getQuickPickItems(
});

// Find any Swift toolchains installed via Swiftly
const swiftlyToolchains = (await Swiftly.list(logger))
// Sort in descending order alphabetically
.sort((a, b) => -a.localeCompare(b))
.map<SwiftlyToolchainItem>(toolchainPath => ({
const swiftlyToolchains = (await Swiftly.list(logger)).map<SwiftlyToolchainItem>(
toolchainPath => ({
type: "toolchain",
label: path.basename(toolchainPath),
category: "swiftly",
Expand All @@ -267,7 +265,8 @@ async function getQuickPickItems(
);
}
},
}));
})
);

if (activeToolchain) {
const currentSwiftlyVersion = activeToolchain.isSwiftlyManaged
Expand Down
4 changes: 4 additions & 0 deletions src/utilities/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,8 @@ export class Version implements VersionInterface {
isGreaterThanOrEqual(rhs: VersionInterface): boolean {
return !this.isLessThan(rhs);
}

compare(rhs: VersionInterface): number {
return this.isGreaterThan(rhs) ? 1 : this.isLessThan(rhs) ? -1 : 0;
}
}
Loading