Skip to content

Commit c2bc8da

Browse files
committed
Add Swiftly toolchain management
1 parent 6e5c04a commit c2bc8da

File tree

6 files changed

+300
-92
lines changed

6 files changed

+300
-92
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
@@ -1746,6 +1746,7 @@
17461746
"lcov-parse": "^1.0.0",
17471747
"plist": "^3.1.0",
17481748
"vscode-languageclient": "^9.0.1",
1749-
"xml2js": "^0.6.2"
1749+
"xml2js": "^0.6.2",
1750+
"zod": "^4.0.5"
17501751
}
17511752
}

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 & 89 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.
@@ -248,55 +248,6 @@ export class SwiftToolchain {
248248
return result;
249249
}
250250

251-
/**
252-
* Finds the list of toolchains managed by Swiftly.
253-
*
254-
* @returns an array of toolchain paths
255-
*/
256-
public static async getSwiftlyToolchainInstalls(): Promise<string[]> {
257-
// Swiftly is only available on Linux right now
258-
// TODO: Add support for macOS
259-
if (process.platform !== "linux") {
260-
return [];
261-
}
262-
try {
263-
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
264-
if (!swiftlyHomeDir) {
265-
return [];
266-
}
267-
const swiftlyConfig = await SwiftToolchain.getSwiftlyConfig();
268-
if (!swiftlyConfig || !("installedToolchains" in swiftlyConfig)) {
269-
return [];
270-
}
271-
const installedToolchains = swiftlyConfig.installedToolchains;
272-
if (!Array.isArray(installedToolchains)) {
273-
return [];
274-
}
275-
return installedToolchains
276-
.filter((toolchain): toolchain is string => typeof toolchain === "string")
277-
.map(toolchain => path.join(swiftlyHomeDir, "toolchains", toolchain));
278-
} catch (error) {
279-
throw new Error("Failed to retrieve Swiftly installations from disk.");
280-
}
281-
}
282-
283-
/**
284-
* Reads the Swiftly configuration file, if it exists.
285-
*
286-
* @returns A parsed Swiftly configuration.
287-
*/
288-
private static async getSwiftlyConfig(): Promise<SwiftlyConfig | undefined> {
289-
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
290-
if (!swiftlyHomeDir) {
291-
return;
292-
}
293-
const swiftlyConfigRaw = await fs.readFile(
294-
path.join(swiftlyHomeDir, "config.json"),
295-
"utf-8"
296-
);
297-
return JSON.parse(swiftlyConfigRaw);
298-
}
299-
300251
/**
301252
* Checks common directories for available swift toolchain installations.
302253
*
@@ -608,7 +559,7 @@ export class SwiftToolchain {
608559
let realSwift = await fs.realpath(swift);
609560
if (path.basename(realSwift) === "swiftly") {
610561
try {
611-
const inUse = await this.swiftlyInUseLocation(realSwift, cwd);
562+
const inUse = await swiftly.swiftlyInUseLocation(realSwift, cwd);
612563
if (inUse) {
613564
realSwift = path.join(inUse, "usr", "bin", "swift");
614565
}
@@ -660,7 +611,7 @@ export class SwiftToolchain {
660611
const swiftlyPath = path.join(configPath, "swiftly");
661612
if (await fileExists(swiftlyPath)) {
662613
try {
663-
const inUse = await this.swiftlyInUseLocation(swiftlyPath, cwd);
614+
const inUse = await swiftly.swiftlyInUseLocation(swiftlyPath, cwd);
664615
if (inUse) {
665616
return path.join(inUse, "usr");
666617
}
@@ -671,7 +622,7 @@ export class SwiftToolchain {
671622
return path.dirname(configuration.path);
672623
}
673624

674-
const swiftlyToolchainLocation = await this.swiftlyToolchain(cwd);
625+
const swiftlyToolchainLocation = await swiftly.swiftlyToolchain(cwd);
675626
if (swiftlyToolchainLocation) {
676627
return swiftlyToolchainLocation;
677628
}
@@ -691,41 +642,6 @@ export class SwiftToolchain {
691642
}
692643
}
693644

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