Skip to content

Commit 7124b9a

Browse files
authored
Add Swiftly toolchain management (#1717)
1 parent 1e4385a commit 7124b9a

File tree

8 files changed

+354
-106
lines changed

8 files changed

+354
-106
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Added
66

7+
- Added Swiftly toolchain management support `.swift-version` files, and integration with the toolchain selection UI ([#1717](https://github.com/swiftlang/vscode-swift/pull/1717)
78
- Added code lenses to run suites/tests, configurable with the `swift.showTestCodeLenses` setting ([#1698](https://github.com/swiftlang/vscode-swift/pull/1698))
89
- New `swift.excludePathsFromActivation` setting to ignore specified sub-folders from being activated as projects ([#1693](https://github.com/swiftlang/vscode-swift/pull/1693))
910

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
@@ -1838,6 +1838,7 @@
18381838
"lcov-parse": "^1.0.0",
18391839
"plist": "^3.1.0",
18401840
"vscode-languageclient": "^9.0.1",
1841-
"xml2js": "^0.6.2"
1841+
"xml2js": "^0.6.2",
1842+
"zod": "^4.0.5"
18421843
}
18431844
}

src/toolchain/swiftly.ts

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

0 commit comments

Comments
 (0)