Skip to content

Commit 13e235f

Browse files
support system toolchains from Swiftly (#1846)
1 parent 10649c8 commit 13e235f

File tree

3 files changed

+217
-9
lines changed

3 files changed

+217
-9
lines changed

src/commands/installSwiftlyToolchain.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ import { QuickPickItem } from "vscode";
1616

1717
import { WorkspaceContext } from "../WorkspaceContext";
1818
import {
19-
AvailableToolchain,
2019
Swiftly,
2120
SwiftlyProgressData,
21+
SwiftlyToolchain,
2222
isSnapshotVersion,
2323
isStableVersion,
2424
} from "../toolchain/swiftly";
2525
import { showReloadExtensionNotification } from "../ui/ReloadExtension";
2626

2727
interface SwiftlyToolchainItem extends QuickPickItem {
28-
toolchain: AvailableToolchain;
28+
toolchain: SwiftlyToolchain;
2929
}
3030

3131
async function downloadAndInstallToolchain(selected: SwiftlyToolchainItem, ctx: WorkspaceContext) {
@@ -225,7 +225,7 @@ export async function installSwiftlySnapshotToolchain(ctx: WorkspaceContext): Pr
225225
/**
226226
* Sorts toolchains by version with most recent first
227227
*/
228-
function sortToolchainsByVersion(toolchains: AvailableToolchain[]): AvailableToolchain[] {
228+
function sortToolchainsByVersion(toolchains: SwiftlyToolchain[]): SwiftlyToolchain[] {
229229
return toolchains.sort((a, b) => {
230230
// First sort by type (stable before snapshot)
231231
if (a.version.type !== b.version.type) {

src/toolchain/swiftly.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const ListResult = z.object({
3131
z.object({
3232
inUse: z.boolean(),
3333
isDefault: z.boolean(),
34-
version: z.discriminatedUnion("type", [
34+
version: z.union([
3535
z.object({
3636
major: z.union([z.number(), z.undefined()]),
3737
minor: z.union([z.number(), z.undefined()]),
@@ -47,6 +47,11 @@ const ListResult = z.object({
4747
name: z.string(),
4848
type: z.literal("snapshot"),
4949
}),
50+
z.object({
51+
name: z.string(),
52+
type: z.literal("system"),
53+
}),
54+
z.object(),
5055
]),
5156
})
5257
),
@@ -77,12 +82,20 @@ const SnapshotVersion = z.object({
7782

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

85+
export interface SwiftlyToolchain {
86+
inUse: boolean;
87+
installed: boolean;
88+
isDefault: boolean;
89+
version: StableVersion | SnapshotVersion;
90+
}
91+
8092
const AvailableToolchain = z.object({
8193
inUse: z.boolean(),
8294
installed: z.boolean(),
8395
isDefault: z.boolean(),
84-
version: z.discriminatedUnion("type", [StableVersion, SnapshotVersion]),
96+
version: z.union([StableVersion, SnapshotVersion, z.object()]),
8597
});
98+
type AvailableToolchain = z.infer<typeof AvailableToolchain>;
8699

87100
export function isStableVersion(
88101
version: StableVersion | SnapshotVersion
@@ -99,7 +112,6 @@ export function isSnapshotVersion(
99112
const ListAvailableResult = z.object({
100113
toolchains: z.array(AvailableToolchain),
101114
});
102-
export type AvailableToolchain = z.infer<typeof AvailableToolchain>;
103115

104116
export interface SwiftlyProgressData {
105117
step?: {
@@ -180,7 +192,9 @@ export class Swiftly {
180192
try {
181193
const { stdout } = await execFile("swiftly", ["list", "--format=json"]);
182194
const response = ListResult.parse(JSON.parse(stdout));
183-
return response.toolchains.map(t => t.version.name);
195+
return response.toolchains
196+
.filter(t => ["stable", "snapshot", "system"].includes(t.version?.type))
197+
.map(t => t.version.name);
184198
} catch (error) {
185199
logger?.error(`Failed to retrieve Swiftly installations: ${error}`);
186200
return [];
@@ -293,7 +307,7 @@ export class Swiftly {
293307
public static async listAvailable(
294308
logger?: SwiftLogger,
295309
branch?: string
296-
): Promise<AvailableToolchain[]> {
310+
): Promise<SwiftlyToolchain[]> {
297311
if (!this.isSupported()) {
298312
return [];
299313
}
@@ -315,7 +329,10 @@ export class Swiftly {
315329
args.push(branch);
316330
}
317331
const { stdout: availableStdout } = await execFile("swiftly", args);
318-
return ListAvailableResult.parse(JSON.parse(availableStdout)).toolchains;
332+
const result = ListAvailableResult.parse(JSON.parse(availableStdout));
333+
return result.toolchains.filter((t): t is SwiftlyToolchain =>
334+
["stable", "snapshot"].includes(t.version.type)
335+
);
319336
} catch (error) {
320337
logger?.error(`Failed to retrieve available Swiftly toolchains: ${error}`);
321338
return [];

test/unit-tests/toolchain/swiftly.test.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ suite("Swiftly Unit Tests", () => {
7373
// Mock list-available command with JSON output
7474
const jsonOutput = {
7575
toolchains: [
76+
{
77+
inUse: false,
78+
isDefault: false,
79+
version: {
80+
name: "xcode",
81+
type: "system",
82+
},
83+
},
7684
{
7785
inUse: true,
7886
isDefault: true,
@@ -118,6 +126,100 @@ suite("Swiftly Unit Tests", () => {
118126
const result = await Swiftly.listAvailableToolchains();
119127

120128
expect(result).to.deep.equal([
129+
"xcode",
130+
"swift-5.9.0-RELEASE",
131+
"swift-5.8.0-RELEASE",
132+
"swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a",
133+
]);
134+
135+
expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", ["--version"]);
136+
expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", [
137+
"list",
138+
"--format=json",
139+
]);
140+
});
141+
142+
test("should be able to parse future additions to the output and ignore unexpected types", async () => {
143+
// Mock version check to return 1.1.0
144+
mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({
145+
stdout: "1.1.0\n",
146+
stderr: "",
147+
});
148+
149+
// Mock list-available command with JSON output
150+
const jsonOutput = {
151+
toolchains: [
152+
{
153+
inUse: false,
154+
isDefault: false,
155+
version: {
156+
name: "xcode",
157+
type: "system",
158+
newProp: 1, // Try adding a new property.
159+
},
160+
newProp: 1, // Try adding a new property.
161+
},
162+
{
163+
inUse: false,
164+
isDefault: false,
165+
version: {
166+
// Try adding an unexpected version type.
167+
type: "something_else",
168+
},
169+
newProp: 1, // Try adding a new property.
170+
},
171+
{
172+
inUse: true,
173+
isDefault: true,
174+
version: {
175+
major: 5,
176+
minor: 9,
177+
patch: 0,
178+
name: "swift-5.9.0-RELEASE",
179+
type: "stable",
180+
newProp: 1, // Try adding a new property.
181+
},
182+
newProp: 1, // Try adding a new property.
183+
},
184+
{
185+
inUse: false,
186+
isDefault: false,
187+
version: {
188+
major: 5,
189+
minor: 8,
190+
patch: 0,
191+
name: "swift-5.8.0-RELEASE",
192+
type: "stable",
193+
newProp: 1, // Try adding a new property.
194+
},
195+
newProp: "", // Try adding a new property.
196+
},
197+
{
198+
inUse: false,
199+
isDefault: false,
200+
version: {
201+
major: 5,
202+
minor: 10,
203+
branch: "development",
204+
date: "2023-10-15",
205+
name: "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a",
206+
type: "snapshot",
207+
newProp: 1, // Try adding a new property.
208+
},
209+
newProp: 1, // Try adding a new property.
210+
},
211+
],
212+
};
213+
214+
mockUtilities.execFile.withArgs("swiftly", ["list", "--format=json"]).resolves({
215+
stdout: JSON.stringify(jsonOutput),
216+
stderr: "",
217+
});
218+
219+
const result = await Swiftly.listAvailableToolchains();
220+
221+
expect(result).to.deep.equal([
222+
"xcode",
121223
"swift-5.9.0-RELEASE",
122224
"swift-5.8.0-RELEASE",
123225
"swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a",
@@ -342,6 +444,95 @@ suite("Swiftly Unit Tests", () => {
342444
]);
343445
});
344446

447+
test("should be able to parse future additions to the output and ignore unexpected types", async () => {
448+
mockedPlatform.setValue("darwin");
449+
450+
mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({
451+
stdout: "1.1.0\n",
452+
stderr: "",
453+
});
454+
455+
const availableResponse = {
456+
toolchains: [
457+
{
458+
inUse: false,
459+
installed: false,
460+
isDefault: false,
461+
version: {
462+
// Try adding an unexpected version type.
463+
type: "something_else",
464+
},
465+
newProp: 1, // Try adding a new property.
466+
},
467+
{
468+
inUse: false,
469+
installed: false,
470+
isDefault: false,
471+
version: {
472+
type: "stable",
473+
major: 6,
474+
minor: 0,
475+
patch: 0,
476+
name: "6.0.0",
477+
newProp: 1, // Try adding a new property.
478+
},
479+
newProp: 1, // Try adding a new property.
480+
},
481+
{
482+
inUse: false,
483+
installed: false,
484+
isDefault: false,
485+
version: {
486+
type: "snapshot",
487+
major: 6,
488+
minor: 1,
489+
branch: "main",
490+
date: "2025-01-15",
491+
name: "main-snapshot-2025-01-15",
492+
newProp: 1, // Try adding a new property.
493+
},
494+
newProp: 1, // Try adding a new property.
495+
},
496+
],
497+
};
498+
499+
mockUtilities.execFile
500+
.withArgs("swiftly", ["list-available", "--format=json"])
501+
.resolves({
502+
stdout: JSON.stringify(availableResponse),
503+
stderr: "",
504+
});
505+
506+
const result = await Swiftly.listAvailable();
507+
expect(result).to.deep.equal([
508+
{
509+
inUse: false,
510+
installed: false,
511+
isDefault: false,
512+
version: {
513+
type: "stable",
514+
major: 6,
515+
minor: 0,
516+
patch: 0,
517+
name: "6.0.0",
518+
},
519+
},
520+
{
521+
inUse: false,
522+
installed: false,
523+
isDefault: false,
524+
version: {
525+
type: "snapshot",
526+
major: 6,
527+
minor: 1,
528+
branch: "main",
529+
date: "2025-01-15",
530+
name: "main-snapshot-2025-01-15",
531+
},
532+
},
533+
]);
534+
});
535+
345536
test("should handle errors when fetching available toolchains", async () => {
346537
mockedPlatform.setValue("darwin");
347538
mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({

0 commit comments

Comments
 (0)