Skip to content

Commit 3d12c9f

Browse files
committed
Add Swiftly toolchain management
1 parent abf1b4c commit 3d12c9f

File tree

6 files changed

+300
-91
lines changed

6 files changed

+300
-91
lines changed

package-lock.json

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1829,6 +1829,7 @@
18291829
"lcov-parse": "^1.0.0",
18301830
"plist": "^3.1.0",
18311831
"vscode-languageclient": "^9.0.1",
1832-
"xml2js": "^0.6.2"
1832+
"xml2js": "^0.6.2",
1833+
"zod": "^4.0.5"
18331834
}
18341835
}

src/toolchain/swiftly.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import * as path from "node:path";
2+
import { SwiftlyConfig } from "./ToolchainVersion";
3+
import * as fs from "node:fs/promises";
4+
import { execFile, ExecFileError } from "../utilities/utilities";
5+
import * as vscode from "vscode";
6+
import { Version } from "../utilities/version";
7+
import { z } from "zod";
8+
9+
const ListAvailableResult = z.object({
10+
toolchains: z.array(
11+
z.object({
12+
inUse: z.boolean(),
13+
installed: z.boolean(),
14+
isDefault: z.boolean(),
15+
name: z.string(),
16+
version: z.discriminatedUnion("type", [
17+
z.object({
18+
major: z.number(),
19+
minor: z.number(),
20+
patch: z.number().optional(),
21+
type: z.literal("stable"),
22+
}),
23+
z.object({
24+
major: z.number(),
25+
minor: z.number(),
26+
branch: z.string(),
27+
date: z.string(),
28+
29+
type: z.literal("snapshot"),
30+
}),
31+
]),
32+
})
33+
),
34+
});
35+
36+
export class Swiftly {
37+
/**
38+
* Finds the version of Swiftly installed on the system.
39+
*
40+
* @returns the version of Swiftly as a `Version` object, or `undefined`
41+
* if Swiftly is not installed or not supported.
42+
*/
43+
public async getSwiftlyVersion(): Promise<Version | undefined> {
44+
if (!this.isSupported()) {
45+
return undefined;
46+
}
47+
const { stdout } = await execFile("swiftly", ["--version"]);
48+
return Version.fromString(stdout.trim());
49+
}
50+
51+
/**
52+
* Finds the list of toolchains managed by Swiftly.
53+
*
54+
* @returns an array of toolchain paths
55+
*/
56+
public async getSwiftlyToolchainInstalls(): Promise<string[]> {
57+
if (!this.isSupported()) {
58+
return [];
59+
}
60+
const version = await swiftly.getSwiftlyVersion();
61+
if (version?.isLessThan(new Version(1, 1, 0))) {
62+
return await this.getToolchainInstallLegacy();
63+
}
64+
65+
return await this.getListAvailableToolchains();
66+
}
67+
68+
private async getListAvailableToolchains(): Promise<string[]> {
69+
try {
70+
const { stdout } = await execFile("swiftly", ["list-available", "--format=json"]);
71+
const response = ListAvailableResult.parse(JSON.parse(stdout));
72+
return response.toolchains.map(t => t.name);
73+
} catch (error) {
74+
throw new Error("Failed to retrieve Swiftly installations from disk.");
75+
}
76+
}
77+
78+
private async getToolchainInstallLegacy() {
79+
try {
80+
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
81+
if (!swiftlyHomeDir) {
82+
return [];
83+
}
84+
const swiftlyConfig = await swiftly.getSwiftlyConfig();
85+
if (!swiftlyConfig || !("installedToolchains" in swiftlyConfig)) {
86+
return [];
87+
}
88+
const installedToolchains = swiftlyConfig.installedToolchains;
89+
if (!Array.isArray(installedToolchains)) {
90+
return [];
91+
}
92+
return installedToolchains
93+
.filter((toolchain): toolchain is string => typeof toolchain === "string")
94+
.map(toolchain => path.join(swiftlyHomeDir, "toolchains", toolchain));
95+
} catch (error) {
96+
throw new Error("Failed to retrieve Swiftly installations from disk.");
97+
}
98+
}
99+
100+
private isSupported() {
101+
return process.platform === "linux" || process.platform === "darwin";
102+
}
103+
104+
public async swiftlyInUseLocation(swiftlyPath: string, cwd?: vscode.Uri) {
105+
const { stdout: inUse } = await execFile(swiftlyPath, ["use", "--print-location"], {
106+
cwd: cwd?.fsPath,
107+
});
108+
return inUse.trimEnd();
109+
}
110+
111+
/**
112+
* Determine if Swiftly is being used to manage the active toolchain and if so, return
113+
* the path to the active toolchain.
114+
* @returns The location of the active toolchain if swiftly is being used to manage it.
115+
*/
116+
public async swiftlyToolchain(cwd?: vscode.Uri): Promise<string | undefined> {
117+
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
118+
if (swiftlyHomeDir) {
119+
const { stdout: swiftLocation } = await execFile("which", ["swift"]);
120+
if (swiftLocation.startsWith(swiftlyHomeDir)) {
121+
// Print the location of the toolchain that swiftly is using. If there
122+
// is no cwd specified then it returns the global "inUse" toolchain otherwise
123+
// it respects the .swift-version file in the cwd and resolves using that.
124+
try {
125+
const inUse = await swiftly.swiftlyInUseLocation("swiftly", cwd);
126+
if (inUse.length > 0) {
127+
return path.join(inUse, "usr");
128+
}
129+
} catch (err: unknown) {
130+
const error = err as ExecFileError;
131+
// Its possible the toolchain in .swift-version is misconfigured or doesn't exist.
132+
void vscode.window.showErrorMessage(`${error.stderr}`);
133+
}
134+
}
135+
}
136+
return undefined;
137+
}
138+
139+
/**
140+
* Reads the Swiftly configuration file, if it exists.
141+
*
142+
* @returns A parsed Swiftly configuration.
143+
*/
144+
private async getSwiftlyConfig(): Promise<SwiftlyConfig | undefined> {
145+
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
146+
if (!swiftlyHomeDir) {
147+
return;
148+
}
149+
const swiftlyConfigRaw = await fs.readFile(
150+
path.join(swiftlyHomeDir, "config.json"),
151+
"utf-8"
152+
);
153+
return JSON.parse(swiftlyConfigRaw);
154+
}
155+
}
156+
157+
export const swiftly = new Swiftly();

src/toolchain/toolchain.ts

Lines changed: 5 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ import * as plist from "plist";
1919
import * as vscode from "vscode";
2020
import configuration from "../configuration";
2121
import { SwiftOutputChannel } from "../ui/SwiftOutputChannel";
22-
import { execFile, ExecFileError, execSwift } from "../utilities/utilities";
22+
import { execFile, execSwift } from "../utilities/utilities";
2323
import { expandFilePathTilde, fileExists, pathExists } from "../utilities/filesystem";
2424
import { Version } from "../utilities/version";
2525
import { BuildFlags } from "./BuildFlags";
2626
import { Sanitizer } from "./Sanitizer";
27-
import { SwiftlyConfig } from "./ToolchainVersion";
2827
import { lineBreakRegex } from "../utilities/tasks";
28+
import { swiftly } from "./swiftly";
2929

3030
/**
3131
* Contents of **Info.plist** on Windows.
@@ -251,54 +251,6 @@ export class SwiftToolchain {
251251
return result;
252252
}
253253

254-
/**
255-
* Finds the list of toolchains managed by Swiftly.
256-
*
257-
* @returns an array of toolchain paths
258-
*/
259-
public static async getSwiftlyToolchainInstalls(): Promise<string[]> {
260-
// Swiftly is available on Linux and macOS
261-
if (process.platform !== "linux" && process.platform !== "darwin") {
262-
return [];
263-
}
264-
try {
265-
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
266-
if (!swiftlyHomeDir) {
267-
return [];
268-
}
269-
const swiftlyConfig = await SwiftToolchain.getSwiftlyConfig();
270-
if (!swiftlyConfig || !("installedToolchains" in swiftlyConfig)) {
271-
return [];
272-
}
273-
const installedToolchains = swiftlyConfig.installedToolchains;
274-
if (!Array.isArray(installedToolchains)) {
275-
return [];
276-
}
277-
return installedToolchains
278-
.filter((toolchain): toolchain is string => typeof toolchain === "string")
279-
.map(toolchain => path.join(swiftlyHomeDir, "toolchains", toolchain));
280-
} catch (error) {
281-
throw new Error("Failed to retrieve Swiftly installations from disk.");
282-
}
283-
}
284-
285-
/**
286-
* Reads the Swiftly configuration file, if it exists.
287-
*
288-
* @returns A parsed Swiftly configuration.
289-
*/
290-
private static async getSwiftlyConfig(): Promise<SwiftlyConfig | undefined> {
291-
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
292-
if (!swiftlyHomeDir) {
293-
return;
294-
}
295-
const swiftlyConfigRaw = await fs.readFile(
296-
path.join(swiftlyHomeDir, "config.json"),
297-
"utf-8"
298-
);
299-
return JSON.parse(swiftlyConfigRaw);
300-
}
301-
302254
/**
303255
* Checks common directories for available swift toolchain installations.
304256
*
@@ -615,7 +567,7 @@ export class SwiftToolchain {
615567
let realSwift = await fs.realpath(swift);
616568
if (path.basename(realSwift) === "swiftly") {
617569
try {
618-
const inUse = await this.swiftlyInUseLocation(realSwift, cwd);
570+
const inUse = await swiftly.swiftlyInUseLocation(realSwift, cwd);
619571
if (inUse) {
620572
realSwift = path.join(inUse, "usr", "bin", "swift");
621573
}
@@ -668,7 +620,7 @@ export class SwiftToolchain {
668620
const swiftlyPath = path.join(configPath, "swiftly");
669621
if (await fileExists(swiftlyPath)) {
670622
try {
671-
const inUse = await this.swiftlyInUseLocation(swiftlyPath, cwd);
623+
const inUse = await swiftly.swiftlyInUseLocation(swiftlyPath, cwd);
672624
if (inUse) {
673625
return path.join(inUse, "usr");
674626
}
@@ -679,7 +631,7 @@ export class SwiftToolchain {
679631
return path.dirname(configuration.path);
680632
}
681633

682-
const swiftlyToolchainLocation = await this.swiftlyToolchain(cwd);
634+
const swiftlyToolchainLocation = await swiftly.swiftlyToolchain(cwd);
683635
if (swiftlyToolchainLocation) {
684636
return swiftlyToolchainLocation;
685637
}
@@ -699,41 +651,6 @@ export class SwiftToolchain {
699651
}
700652
}
701653

702-
private static async swiftlyInUseLocation(swiftlyPath: string, cwd?: vscode.Uri) {
703-
const { stdout: inUse } = await execFile(swiftlyPath, ["use", "--print-location"], {
704-
cwd: cwd?.fsPath,
705-
});
706-
return inUse.trimEnd();
707-
}
708-
709-
/**
710-
* Determine if Swiftly is being used to manage the active toolchain and if so, return
711-
* the path to the active toolchain.
712-
* @returns The location of the active toolchain if swiftly is being used to manage it.
713-
*/
714-
private static async swiftlyToolchain(cwd?: vscode.Uri): Promise<string | undefined> {
715-
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
716-
if (swiftlyHomeDir) {
717-
const { stdout: swiftLocation } = await execFile("which", ["swift"]);
718-
if (swiftLocation.indexOf(swiftlyHomeDir) === 0) {
719-
// Print the location of the toolchain that swiftly is using. If there
720-
// is no cwd specified then it returns the global "inUse" toolchain otherwise
721-
// it respects the .swift-version file in the cwd and resolves using that.
722-
try {
723-
const inUse = await this.swiftlyInUseLocation("swiftly", cwd);
724-
if (inUse.length > 0) {
725-
return path.join(inUse, "usr");
726-
}
727-
} catch (err: unknown) {
728-
const error = err as ExecFileError;
729-
// Its possible the toolchain in .swift-version is misconfigured or doesn't exist.
730-
void vscode.window.showErrorMessage(`${error.stderr}`);
731-
}
732-
}
733-
}
734-
return undefined;
735-
}
736-
737654
/**
738655
* @param targetInfo swift target info
739656
* @returns path to Swift runtime

src/ui/ToolchainSelection.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { showReloadExtensionNotification } from "./ReloadExtension";
1818
import { SwiftToolchain } from "../toolchain/toolchain";
1919
import configuration from "../configuration";
2020
import { Commands } from "../commands";
21+
import { swiftly } from "../toolchain/swiftly";
2122

2223
/**
2324
* Open the installation page on Swift.org
@@ -192,7 +193,7 @@ async function getQuickPickItems(
192193
return result;
193194
});
194195
// Find any Swift toolchains installed via Swiftly
195-
const swiftlyToolchains = (await SwiftToolchain.getSwiftlyToolchainInstalls())
196+
const swiftlyToolchains = (await swiftly.getSwiftlyToolchainInstalls())
196197
.reverse()
197198
.map<SwiftToolchainItem>(toolchainPath => ({
198199
type: "toolchain",

0 commit comments

Comments
 (0)