Skip to content

Commit 3039bb3

Browse files
authored
Use Swiftly's toolchain path if active toolchain is managed by Swiftly (#1470)
On macOS the toolchain path is determined by `xcrun --find swift`. `xcrun` will return paths inside the active Xcode. If the active toolchain is being managed by the newly released Swiftly 1.0 on macOS then the swift path and the toolchain path will be mismatched. Check to see if the active toolchain is managed by Swiftly, and if so point the toolchain path to the Swiftly installed toolchain. The name of the `.xctoolchain` folder needs to be reverse engineered from Swiftly's in use toolchain name using the ToolchainVersion.parse method which was ported from Swiftly's codebase. A nice feature in the future would be to be able to ask Swiftly where its managed toolchains are stored on disk.
1 parent 7d239b6 commit 3039bb3

File tree

3 files changed

+318
-7
lines changed

3 files changed

+318
-7
lines changed

src/toolchain/ToolchainVersion.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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+
export interface SwiftlyConfig {
16+
installedToolchains: string[];
17+
inUse: string;
18+
version: string;
19+
}
20+
21+
/**
22+
* This code is a port of the toolchain version parsing in Swiftly.
23+
* Until Swiftly can report the location of the toolchains under its management
24+
* use `ToolchainVersion.parse(versionString)` to reconstruct the directory name of the toolchain on disk.
25+
* https://github.com/swiftlang/swiftly/blob/bd6884316817e400a0ec512599f046fa437e9760/Sources/SwiftlyCore/ToolchainVersion.swift#
26+
*/
27+
//
28+
// Enum representing a fully resolved toolchain version (e.g. 5.6.7 or 5.7-snapshot-2022-07-05).
29+
export class ToolchainVersion {
30+
private type: "stable" | "snapshot";
31+
private value: StableRelease | Snapshot;
32+
33+
constructor(
34+
value:
35+
| {
36+
type: "stable";
37+
major: number;
38+
minor: number;
39+
patch: number;
40+
}
41+
| {
42+
type: "snapshot";
43+
branch: Branch;
44+
date: string;
45+
}
46+
) {
47+
if (value.type === "stable") {
48+
this.type = "stable";
49+
this.value = new StableRelease(value.major, value.minor, value.patch);
50+
} else {
51+
this.type = "snapshot";
52+
this.value = new Snapshot(value.branch, value.date);
53+
}
54+
}
55+
56+
private static stableRegex = /^(?:Swift )?(\d+)\.(\d+)\.(\d+)$/;
57+
private static mainSnapshotRegex = /^main-snapshot-(\d{4}-\d{2}-\d{2})$/;
58+
private static releaseSnapshotRegex = /^(\d+)\.(\d+)-snapshot-(\d{4}-\d{2}-\d{2})$/;
59+
60+
/**
61+
* Parse a toolchain version from the provided string
62+
**/
63+
static parse(string: string): ToolchainVersion {
64+
let match: RegExpMatchArray | null;
65+
66+
// Try to match as stable release
67+
match = string.match(this.stableRegex);
68+
if (match) {
69+
const major = parseInt(match[1], 10);
70+
const minor = parseInt(match[2], 10);
71+
const patch = parseInt(match[3], 10);
72+
73+
if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
74+
throw new Error(`invalid stable version: ${string}`);
75+
}
76+
77+
return new ToolchainVersion({
78+
type: "stable",
79+
major,
80+
minor,
81+
patch,
82+
});
83+
}
84+
85+
// Try to match as main snapshot
86+
match = string.match(this.mainSnapshotRegex);
87+
if (match) {
88+
return new ToolchainVersion({
89+
type: "snapshot",
90+
branch: Branch.main(),
91+
date: match[1],
92+
});
93+
}
94+
95+
// Try to match as release snapshot
96+
match = string.match(this.releaseSnapshotRegex);
97+
if (match) {
98+
const major = parseInt(match[1], 10);
99+
const minor = parseInt(match[2], 10);
100+
101+
if (isNaN(major) || isNaN(minor)) {
102+
throw new Error(`invalid release snapshot version: ${string}`);
103+
}
104+
105+
return new ToolchainVersion({
106+
type: "snapshot",
107+
branch: Branch.release(major, minor),
108+
date: match[3],
109+
});
110+
}
111+
112+
throw new Error(`invalid toolchain version: "${string}"`);
113+
}
114+
115+
get name(): string {
116+
if (this.type === "stable") {
117+
const release = this.value as StableRelease;
118+
return `${release.major}.${release.minor}.${release.patch}`;
119+
} else {
120+
const snapshot = this.value as Snapshot;
121+
if (snapshot.branch.type === "main") {
122+
return `main-snapshot-${snapshot.date}`;
123+
} else {
124+
return `${snapshot.branch.major}.${snapshot.branch.minor}-snapshot-${snapshot.date}`;
125+
}
126+
}
127+
}
128+
129+
get identifier(): string {
130+
if (this.type === "stable") {
131+
const release = this.value as StableRelease;
132+
if (release.patch === 0) {
133+
if (release.minor === 0) {
134+
return `swift-${release.major}-RELEASE`;
135+
}
136+
return `swift-${release.major}.${release.minor}-RELEASE`;
137+
}
138+
return `swift-${release.major}.${release.minor}.${release.patch}-RELEASE`;
139+
} else {
140+
const snapshot = this.value as Snapshot;
141+
if (snapshot.branch.type === "main") {
142+
return `swift-DEVELOPMENT-SNAPSHOT-${snapshot.date}-a`;
143+
} else {
144+
return `swift-${snapshot.branch.major}.${snapshot.branch.minor}-DEVELOPMENT-SNAPSHOT-${snapshot.date}-a`;
145+
}
146+
}
147+
}
148+
149+
get description(): string {
150+
return this.value.description;
151+
}
152+
}
153+
154+
class Branch {
155+
static main(): Branch {
156+
return new Branch("main", null, null);
157+
}
158+
159+
static release(major: number, minor: number): Branch {
160+
return new Branch("release", major, minor);
161+
}
162+
163+
private constructor(
164+
public type: "main" | "release",
165+
public _major: number | null,
166+
public _minor: number | null
167+
) {}
168+
169+
get description(): string {
170+
switch (this.type) {
171+
case "main":
172+
return "main";
173+
case "release":
174+
return `${this._major}.${this._minor} development`;
175+
}
176+
}
177+
178+
get name(): string {
179+
switch (this.type) {
180+
case "main":
181+
return "main";
182+
case "release":
183+
return `${this._major}.${this._minor}`;
184+
}
185+
}
186+
187+
get major(): number | null {
188+
return this._major;
189+
}
190+
191+
get minor(): number | null {
192+
return this._minor;
193+
}
194+
}
195+
196+
// Snapshot class
197+
class Snapshot {
198+
// Branch enum
199+
200+
branch: Branch;
201+
date: string;
202+
203+
constructor(branch: Branch, date: string) {
204+
this.branch = branch;
205+
this.date = date;
206+
}
207+
208+
get description(): string {
209+
if (this.branch.type === "main") {
210+
return `main-snapshot-${this.date}`;
211+
} else {
212+
return `${this.branch.major}.${this.branch.minor}-snapshot-${this.date}`;
213+
}
214+
}
215+
}
216+
217+
class StableRelease {
218+
major: number;
219+
minor: number;
220+
patch: number;
221+
222+
constructor(major: number, minor: number, patch: number) {
223+
this.major = major;
224+
this.minor = minor;
225+
this.patch = patch;
226+
}
227+
228+
get description(): string {
229+
return `Swift ${this.major}.${this.minor}.${this.patch}`;
230+
}
231+
}

src/toolchain/toolchain.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { expandFilePathTilde, pathExists } from "../utilities/filesystem";
2424
import { Version } from "../utilities/version";
2525
import { BuildFlags } from "./BuildFlags";
2626
import { Sanitizer } from "./Sanitizer";
27+
import { SwiftlyConfig, ToolchainVersion } from "./ToolchainVersion";
2728

2829
/**
2930
* Contents of **Info.plist** on Windows.
@@ -229,12 +230,13 @@ export class SwiftToolchain {
229230
}
230231

231232
/**
232-
* Reads the swiftly configuration file to find a list of installed toolchains.
233+
* Finds the list of toolchains managed by Swiftly.
233234
*
234235
* @returns an array of toolchain paths
235236
*/
236237
public static async getSwiftlyToolchainInstalls(): Promise<string[]> {
237238
// Swiftly is only available on Linux right now
239+
// TODO: Add support for macOS
238240
if (process.platform !== "linux") {
239241
return [];
240242
}
@@ -243,12 +245,8 @@ export class SwiftToolchain {
243245
if (!swiftlyHomeDir) {
244246
return [];
245247
}
246-
const swiftlyConfigRaw = await fs.readFile(
247-
path.join(swiftlyHomeDir, "config.json"),
248-
"utf-8"
249-
);
250-
const swiftlyConfig: unknown = JSON.parse(swiftlyConfigRaw);
251-
if (!(swiftlyConfig instanceof Object) || !("installedToolchains" in swiftlyConfig)) {
248+
const swiftlyConfig = await SwiftToolchain.getSwiftlyConfig();
249+
if (!swiftlyConfig || !("installedToolchains" in swiftlyConfig)) {
252250
return [];
253251
}
254252
const installedToolchains = swiftlyConfig.installedToolchains;
@@ -263,6 +261,23 @@ export class SwiftToolchain {
263261
}
264262
}
265263

264+
/**
265+
* Reads the Swiftly configuration file, if it exists.
266+
*
267+
* @returns A parsed Swiftly configuration.
268+
*/
269+
private static async getSwiftlyConfig(): Promise<SwiftlyConfig | undefined> {
270+
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
271+
if (!swiftlyHomeDir) {
272+
return;
273+
}
274+
const swiftlyConfigRaw = await fs.readFile(
275+
path.join(swiftlyHomeDir, "config.json"),
276+
"utf-8"
277+
);
278+
return JSON.parse(swiftlyConfigRaw);
279+
}
280+
266281
/**
267282
* Checks common directories for available swift toolchain installations.
268283
*
@@ -272,6 +287,7 @@ export class SwiftToolchain {
272287
if (process.platform !== "darwin") {
273288
return [];
274289
}
290+
// TODO: If Swiftly is managing these toolchains then omit them
275291
return Promise.all([
276292
this.findToolchainsIn("/Library/Developer/Toolchains/"),
277293
this.findToolchainsIn(path.join(os.homedir(), "Library/Developer/Toolchains/")),
@@ -602,6 +618,12 @@ export class SwiftToolchain {
602618
if (configuration.path !== "") {
603619
return path.dirname(configuration.path);
604620
}
621+
622+
const swiftlyToolchainLocation = await this.swiftlyToolchainLocation();
623+
if (swiftlyToolchainLocation) {
624+
return swiftlyToolchainLocation;
625+
}
626+
605627
const { stdout } = await execFile("xcrun", ["--find", "swift"], {
606628
env: configuration.swiftEnvironmentVariables,
607629
});
@@ -617,6 +639,31 @@ export class SwiftToolchain {
617639
}
618640
}
619641

642+
/**
643+
* Determine if Swiftly is being used to manage the active toolchain and if so, return
644+
* the path to the active toolchain.
645+
* @returns The location of the active toolchain if swiftly is being used to manage it.
646+
*/
647+
private static async swiftlyToolchainLocation(): Promise<string | undefined> {
648+
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
649+
if (swiftlyHomeDir) {
650+
const { stdout: swiftLocation } = await execFile("which", ["swift"]);
651+
if (swiftLocation.indexOf(swiftlyHomeDir) === 0) {
652+
const swiftlyConfig = await SwiftToolchain.getSwiftlyConfig();
653+
if (swiftlyConfig) {
654+
const version = ToolchainVersion.parse(swiftlyConfig.inUse);
655+
return path.join(
656+
os.homedir(),
657+
"Library/Developer/Toolchains/",
658+
`${version.identifier}.xctoolchain`,
659+
"usr"
660+
);
661+
}
662+
}
663+
}
664+
return undefined;
665+
}
666+
620667
/**
621668
* @param targetInfo swift target info
622669
* @returns path to Swift runtime
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 { expect } from "chai";
16+
import { ToolchainVersion } from "../../../src/toolchain/ToolchainVersion";
17+
18+
suite("ToolchainVersion Unit Test Suite", () => {
19+
test("Parses snapshot", () => {
20+
const version = ToolchainVersion.parse("main-snapshot-2025-03-28");
21+
expect(version.identifier).to.equal("swift-DEVELOPMENT-SNAPSHOT-2025-03-28-a");
22+
});
23+
24+
test("Parses release snapshot", () => {
25+
const version = ToolchainVersion.parse("6.0-snapshot-2025-03-28");
26+
expect(version.identifier).to.equal("swift-6.0-DEVELOPMENT-SNAPSHOT-2025-03-28-a");
27+
});
28+
29+
test("Parses stable", () => {
30+
const version = ToolchainVersion.parse("6.0.3");
31+
expect(version.identifier).to.equal("swift-6.0.3-RELEASE");
32+
});
33+
});

0 commit comments

Comments
 (0)