Skip to content

Use Swiftly's toolchain path if active toolchain is managed by Swiftly #1470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions src/toolchain/ToolchainVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the VS Code Swift open source project
//
// Copyright (c) 2025 the VS Code Swift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

export interface SwiftlyConfig {
installedToolchains: string[];
inUse: string;
version: string;
}

/**
* This code is a port of the toolchain version parsing in Swiftly.
* Until Swiftly can report the location of the toolchains under its management
* use `ToolchainVersion.parse(versionString)` to reconstruct the directory name of the toolchain on disk.
* https://github.com/swiftlang/swiftly/blob/bd6884316817e400a0ec512599f046fa437e9760/Sources/SwiftlyCore/ToolchainVersion.swift#
*/
//
// Enum representing a fully resolved toolchain version (e.g. 5.6.7 or 5.7-snapshot-2022-07-05).
export class ToolchainVersion {
private type: "stable" | "snapshot";
private value: StableRelease | Snapshot;

constructor(
value:
| {
type: "stable";
major: number;
minor: number;
patch: number;
}
| {
type: "snapshot";
branch: Branch;
date: string;
}
) {
if (value.type === "stable") {
this.type = "stable";
this.value = new StableRelease(value.major, value.minor, value.patch);
} else {
this.type = "snapshot";
this.value = new Snapshot(value.branch, value.date);
}
}

private static stableRegex = /^(?:Swift )?(\d+)\.(\d+)\.(\d+)$/;
private static mainSnapshotRegex = /^main-snapshot-(\d{4}-\d{2}-\d{2})$/;
private static releaseSnapshotRegex = /^(\d+)\.(\d+)-snapshot-(\d{4}-\d{2}-\d{2})$/;

/**
* Parse a toolchain version from the provided string
**/
static parse(string: string): ToolchainVersion {
let match: RegExpMatchArray | null;

// Try to match as stable release
match = string.match(this.stableRegex);
if (match) {
const major = parseInt(match[1], 10);
const minor = parseInt(match[2], 10);
const patch = parseInt(match[3], 10);

if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
throw new Error(`invalid stable version: ${string}`);
}

return new ToolchainVersion({
type: "stable",
major,
minor,
patch,
});
}

// Try to match as main snapshot
match = string.match(this.mainSnapshotRegex);
if (match) {
return new ToolchainVersion({
type: "snapshot",
branch: Branch.main(),
date: match[1],
});
}

// Try to match as release snapshot
match = string.match(this.releaseSnapshotRegex);
if (match) {
const major = parseInt(match[1], 10);
const minor = parseInt(match[2], 10);

if (isNaN(major) || isNaN(minor)) {
throw new Error(`invalid release snapshot version: ${string}`);
}

return new ToolchainVersion({
type: "snapshot",
branch: Branch.release(major, minor),
date: match[3],
});
}

throw new Error(`invalid toolchain version: "${string}"`);
}

get name(): string {
if (this.type === "stable") {
const release = this.value as StableRelease;
return `${release.major}.${release.minor}.${release.patch}`;
} else {
const snapshot = this.value as Snapshot;
if (snapshot.branch.type === "main") {
return `main-snapshot-${snapshot.date}`;
} else {
return `${snapshot.branch.major}.${snapshot.branch.minor}-snapshot-${snapshot.date}`;
}
}
}

get identifier(): string {
if (this.type === "stable") {
const release = this.value as StableRelease;
if (release.patch === 0) {
if (release.minor === 0) {
return `swift-${release.major}-RELEASE`;
}
return `swift-${release.major}.${release.minor}-RELEASE`;
}
return `swift-${release.major}.${release.minor}.${release.patch}-RELEASE`;
} else {
const snapshot = this.value as Snapshot;
if (snapshot.branch.type === "main") {
return `swift-DEVELOPMENT-SNAPSHOT-${snapshot.date}-a`;
} else {
return `swift-${snapshot.branch.major}.${snapshot.branch.minor}-DEVELOPMENT-SNAPSHOT-${snapshot.date}-a`;
}
}
}

get description(): string {
return this.value.description;
}
}

class Branch {
static main(): Branch {
return new Branch("main", null, null);
}

static release(major: number, minor: number): Branch {
return new Branch("release", major, minor);
}

private constructor(
public type: "main" | "release",
public _major: number | null,
public _minor: number | null
) {}

get description(): string {
switch (this.type) {
case "main":
return "main";
case "release":
return `${this._major}.${this._minor} development`;
}
}

get name(): string {
switch (this.type) {
case "main":
return "main";
case "release":
return `${this._major}.${this._minor}`;
}
}

get major(): number | null {
return this._major;
}

get minor(): number | null {
return this._minor;
}
}

// Snapshot class
class Snapshot {
// Branch enum

branch: Branch;
date: string;

constructor(branch: Branch, date: string) {
this.branch = branch;
this.date = date;
}

get description(): string {
if (this.branch.type === "main") {
return `main-snapshot-${this.date}`;
} else {
return `${this.branch.major}.${this.branch.minor}-snapshot-${this.date}`;
}
}
}

class StableRelease {
major: number;
minor: number;
patch: number;

constructor(major: number, minor: number, patch: number) {
this.major = major;
this.minor = minor;
this.patch = patch;
}

get description(): string {
return `Swift ${this.major}.${this.minor}.${this.patch}`;
}
}
61 changes: 54 additions & 7 deletions src/toolchain/toolchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { expandFilePathTilde, pathExists } from "../utilities/filesystem";
import { Version } from "../utilities/version";
import { BuildFlags } from "./BuildFlags";
import { Sanitizer } from "./Sanitizer";
import { SwiftlyConfig, ToolchainVersion } from "./ToolchainVersion";

/**
* Contents of **Info.plist** on Windows.
Expand Down Expand Up @@ -229,12 +230,13 @@ export class SwiftToolchain {
}

/**
* Reads the swiftly configuration file to find a list of installed toolchains.
* Finds the list of toolchains managed by Swiftly.
*
* @returns an array of toolchain paths
*/
public static async getSwiftlyToolchainInstalls(): Promise<string[]> {
// Swiftly is only available on Linux right now
// TODO: Add support for macOS
if (process.platform !== "linux") {
return [];
}
Expand All @@ -243,12 +245,8 @@ export class SwiftToolchain {
if (!swiftlyHomeDir) {
return [];
}
const swiftlyConfigRaw = await fs.readFile(
path.join(swiftlyHomeDir, "config.json"),
"utf-8"
);
const swiftlyConfig: unknown = JSON.parse(swiftlyConfigRaw);
if (!(swiftlyConfig instanceof Object) || !("installedToolchains" in swiftlyConfig)) {
const swiftlyConfig = await SwiftToolchain.getSwiftlyConfig();
if (!swiftlyConfig || !("installedToolchains" in swiftlyConfig)) {
return [];
}
const installedToolchains = swiftlyConfig.installedToolchains;
Expand All @@ -263,6 +261,23 @@ export class SwiftToolchain {
}
}

/**
* Reads the Swiftly configuration file, if it exists.
*
* @returns A parsed Swiftly configuration.
*/
private static async getSwiftlyConfig(): Promise<SwiftlyConfig | undefined> {
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
if (!swiftlyHomeDir) {
return;
}
const swiftlyConfigRaw = await fs.readFile(
path.join(swiftlyHomeDir, "config.json"),
"utf-8"
);
return JSON.parse(swiftlyConfigRaw);
}

/**
* Checks common directories for available swift toolchain installations.
*
Expand All @@ -272,6 +287,7 @@ export class SwiftToolchain {
if (process.platform !== "darwin") {
return [];
}
// TODO: If Swiftly is managing these toolchains then omit them
return Promise.all([
this.findToolchainsIn("/Library/Developer/Toolchains/"),
this.findToolchainsIn(path.join(os.homedir(), "Library/Developer/Toolchains/")),
Expand Down Expand Up @@ -602,6 +618,12 @@ export class SwiftToolchain {
if (configuration.path !== "") {
return path.dirname(configuration.path);
}

const swiftlyToolchainLocation = await this.swiftlyToolchainLocation();
if (swiftlyToolchainLocation) {
return swiftlyToolchainLocation;
}

const { stdout } = await execFile("xcrun", ["--find", "swift"], {
env: configuration.swiftEnvironmentVariables,
});
Expand All @@ -617,6 +639,31 @@ export class SwiftToolchain {
}
}

/**
* Determine if Swiftly is being used to manage the active toolchain and if so, return
* the path to the active toolchain.
* @returns The location of the active toolchain if swiftly is being used to manage it.
*/
private static async swiftlyToolchainLocation(): Promise<string | undefined> {
const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"];
if (swiftlyHomeDir) {
const { stdout: swiftLocation } = await execFile("which", ["swift"]);
if (swiftLocation.indexOf(swiftlyHomeDir) === 0) {
const swiftlyConfig = await SwiftToolchain.getSwiftlyConfig();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: For the in use toolchain, swiftly has a swiftly use --print-location command that will give the filesystem path of the one that's in-use. It can be more expensive than reading the config.json, but it also takes into consideration toolchains that are in-use because of the .swift-version file.

if (swiftlyConfig) {
const version = ToolchainVersion.parse(swiftlyConfig.inUse);
return path.join(
os.homedir(),
"Library/Developer/Toolchains/",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Should this toolchain function indicates that it is macOS-only?

`${version.identifier}.xctoolchain`,
"usr"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Should this be a path to the <toolchain>/usr/bin where the binaries like swift are located?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with other toolchain paths in VS Code they're specified as name.xctoolchain/usr

);
}
}
}
return undefined;
}

/**
* @param targetInfo swift target info
* @returns path to Swift runtime
Expand Down
33 changes: 33 additions & 0 deletions test/unit-tests/toolchain/ToolchainVersion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the VS Code Swift open source project
//
// Copyright (c) 2025 the VS Code Swift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import { expect } from "chai";
import { ToolchainVersion } from "../../../src/toolchain/ToolchainVersion";

suite("ToolchainVersion Unit Test Suite", () => {
test("Parses snapshot", () => {
const version = ToolchainVersion.parse("main-snapshot-2025-03-28");
expect(version.identifier).to.equal("swift-DEVELOPMENT-SNAPSHOT-2025-03-28-a");
});

test("Parses release snapshot", () => {
const version = ToolchainVersion.parse("6.0-snapshot-2025-03-28");
expect(version.identifier).to.equal("swift-6.0-DEVELOPMENT-SNAPSHOT-2025-03-28-a");
});

test("Parses stable", () => {
const version = ToolchainVersion.parse("6.0.3");
expect(version.identifier).to.equal("swift-6.0.3-RELEASE");
});
});
Loading