diff --git a/src/toolchain/ToolchainVersion.ts b/src/toolchain/ToolchainVersion.ts new file mode 100644 index 000000000..752d9199b --- /dev/null +++ b/src/toolchain/ToolchainVersion.ts @@ -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}`; + } +} diff --git a/src/toolchain/toolchain.ts b/src/toolchain/toolchain.ts index 1e6646f1f..4976def5e 100644 --- a/src/toolchain/toolchain.ts +++ b/src/toolchain/toolchain.ts @@ -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. @@ -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 { // Swiftly is only available on Linux right now + // TODO: Add support for macOS if (process.platform !== "linux") { return []; } @@ -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; @@ -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 { + 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. * @@ -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/")), @@ -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, }); @@ -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 { + 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(); + if (swiftlyConfig) { + const version = ToolchainVersion.parse(swiftlyConfig.inUse); + return path.join( + os.homedir(), + "Library/Developer/Toolchains/", + `${version.identifier}.xctoolchain`, + "usr" + ); + } + } + } + return undefined; + } + /** * @param targetInfo swift target info * @returns path to Swift runtime diff --git a/test/unit-tests/toolchain/ToolchainVersion.test.ts b/test/unit-tests/toolchain/ToolchainVersion.test.ts new file mode 100644 index 000000000..288159788 --- /dev/null +++ b/test/unit-tests/toolchain/ToolchainVersion.test.ts @@ -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"); + }); +});