-
Notifications
You must be signed in to change notification settings - Fork 71
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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}`; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<string[]> { | ||
// 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<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. | ||
* | ||
|
@@ -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<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(); | ||
if (swiftlyConfig) { | ||
const version = ToolchainVersion.parse(swiftlyConfig.inUse); | ||
return path.join( | ||
os.homedir(), | ||
"Library/Developer/Toolchains/", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Should this be a path to the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
); | ||
} | ||
} | ||
} | ||
return undefined; | ||
} | ||
|
||
/** | ||
* @param targetInfo swift target info | ||
* @returns path to Swift runtime | ||
|
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"); | ||
}); | ||
}); |
There was a problem hiding this comment.
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.