From 3d12c9f25087fdfcac7fc381d5fdf09ae1527b1c Mon Sep 17 00:00:00 2001 From: Priyambada Roul Date: Mon, 14 Jul 2025 23:43:22 +0530 Subject: [PATCH 1/8] Add Swiftly toolchain management --- package-lock.json | 16 ++- package.json | 3 +- src/toolchain/swiftly.ts | 157 ++++++++++++++++++++++ src/toolchain/toolchain.ts | 93 +------------ src/ui/ToolchainSelection.ts | 3 +- test/unit-tests/toolchain/swiftly.test.ts | 119 ++++++++++++++++ 6 files changed, 300 insertions(+), 91 deletions(-) create mode 100644 src/toolchain/swiftly.ts create mode 100644 test/unit-tests/toolchain/swiftly.test.ts diff --git a/package-lock.json b/package-lock.json index 0ba253a54..0d749c07a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "lcov-parse": "^1.0.0", "plist": "^3.1.0", "vscode-languageclient": "^9.0.1", - "xml2js": "^0.6.2" + "xml2js": "^0.6.2", + "zod": "^4.0.5" }, "devDependencies": { "@types/archiver": "^6.0.3", @@ -10752,6 +10753,14 @@ "dependencies": { "safe-buffer": "~5.2.0" } + }, + "node_modules/zod": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz", + "integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -18371,6 +18380,11 @@ } } } + }, + "zod": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz", + "integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==" } } } diff --git a/package.json b/package.json index 112b4b594..0d9456c4b 100644 --- a/package.json +++ b/package.json @@ -1829,6 +1829,7 @@ "lcov-parse": "^1.0.0", "plist": "^3.1.0", "vscode-languageclient": "^9.0.1", - "xml2js": "^0.6.2" + "xml2js": "^0.6.2", + "zod": "^4.0.5" } } diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts new file mode 100644 index 000000000..eddb3790c --- /dev/null +++ b/src/toolchain/swiftly.ts @@ -0,0 +1,157 @@ +import * as path from "node:path"; +import { SwiftlyConfig } from "./ToolchainVersion"; +import * as fs from "node:fs/promises"; +import { execFile, ExecFileError } from "../utilities/utilities"; +import * as vscode from "vscode"; +import { Version } from "../utilities/version"; +import { z } from "zod"; + +const ListAvailableResult = z.object({ + toolchains: z.array( + z.object({ + inUse: z.boolean(), + installed: z.boolean(), + isDefault: z.boolean(), + name: z.string(), + version: z.discriminatedUnion("type", [ + z.object({ + major: z.number(), + minor: z.number(), + patch: z.number().optional(), + type: z.literal("stable"), + }), + z.object({ + major: z.number(), + minor: z.number(), + branch: z.string(), + date: z.string(), + + type: z.literal("snapshot"), + }), + ]), + }) + ), +}); + +export class Swiftly { + /** + * Finds the version of Swiftly installed on the system. + * + * @returns the version of Swiftly as a `Version` object, or `undefined` + * if Swiftly is not installed or not supported. + */ + public async getSwiftlyVersion(): Promise { + if (!this.isSupported()) { + return undefined; + } + const { stdout } = await execFile("swiftly", ["--version"]); + return Version.fromString(stdout.trim()); + } + + /** + * Finds the list of toolchains managed by Swiftly. + * + * @returns an array of toolchain paths + */ + public async getSwiftlyToolchainInstalls(): Promise { + if (!this.isSupported()) { + return []; + } + const version = await swiftly.getSwiftlyVersion(); + if (version?.isLessThan(new Version(1, 1, 0))) { + return await this.getToolchainInstallLegacy(); + } + + return await this.getListAvailableToolchains(); + } + + private async getListAvailableToolchains(): Promise { + try { + const { stdout } = await execFile("swiftly", ["list-available", "--format=json"]); + const response = ListAvailableResult.parse(JSON.parse(stdout)); + return response.toolchains.map(t => t.name); + } catch (error) { + throw new Error("Failed to retrieve Swiftly installations from disk."); + } + } + + private async getToolchainInstallLegacy() { + try { + const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; + if (!swiftlyHomeDir) { + return []; + } + const swiftlyConfig = await swiftly.getSwiftlyConfig(); + if (!swiftlyConfig || !("installedToolchains" in swiftlyConfig)) { + return []; + } + const installedToolchains = swiftlyConfig.installedToolchains; + if (!Array.isArray(installedToolchains)) { + return []; + } + return installedToolchains + .filter((toolchain): toolchain is string => typeof toolchain === "string") + .map(toolchain => path.join(swiftlyHomeDir, "toolchains", toolchain)); + } catch (error) { + throw new Error("Failed to retrieve Swiftly installations from disk."); + } + } + + private isSupported() { + return process.platform === "linux" || process.platform === "darwin"; + } + + public async swiftlyInUseLocation(swiftlyPath: string, cwd?: vscode.Uri) { + const { stdout: inUse } = await execFile(swiftlyPath, ["use", "--print-location"], { + cwd: cwd?.fsPath, + }); + return inUse.trimEnd(); + } + + /** + * 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. + */ + public async swiftlyToolchain(cwd?: vscode.Uri): Promise { + const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; + if (swiftlyHomeDir) { + const { stdout: swiftLocation } = await execFile("which", ["swift"]); + if (swiftLocation.startsWith(swiftlyHomeDir)) { + // Print the location of the toolchain that swiftly is using. If there + // is no cwd specified then it returns the global "inUse" toolchain otherwise + // it respects the .swift-version file in the cwd and resolves using that. + try { + const inUse = await swiftly.swiftlyInUseLocation("swiftly", cwd); + if (inUse.length > 0) { + return path.join(inUse, "usr"); + } + } catch (err: unknown) { + const error = err as ExecFileError; + // Its possible the toolchain in .swift-version is misconfigured or doesn't exist. + void vscode.window.showErrorMessage(`${error.stderr}`); + } + } + } + return undefined; + } + + /** + * Reads the Swiftly configuration file, if it exists. + * + * @returns A parsed Swiftly configuration. + */ + private 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); + } +} + +export const swiftly = new Swiftly(); diff --git a/src/toolchain/toolchain.ts b/src/toolchain/toolchain.ts index d3a1370ab..9dd056feb 100644 --- a/src/toolchain/toolchain.ts +++ b/src/toolchain/toolchain.ts @@ -19,13 +19,13 @@ import * as plist from "plist"; import * as vscode from "vscode"; import configuration from "../configuration"; import { SwiftOutputChannel } from "../ui/SwiftOutputChannel"; -import { execFile, ExecFileError, execSwift } from "../utilities/utilities"; +import { execFile, execSwift } from "../utilities/utilities"; import { expandFilePathTilde, fileExists, pathExists } from "../utilities/filesystem"; import { Version } from "../utilities/version"; import { BuildFlags } from "./BuildFlags"; import { Sanitizer } from "./Sanitizer"; -import { SwiftlyConfig } from "./ToolchainVersion"; import { lineBreakRegex } from "../utilities/tasks"; +import { swiftly } from "./swiftly"; /** * Contents of **Info.plist** on Windows. @@ -251,54 +251,6 @@ export class SwiftToolchain { return result; } - /** - * Finds the list of toolchains managed by Swiftly. - * - * @returns an array of toolchain paths - */ - public static async getSwiftlyToolchainInstalls(): Promise { - // Swiftly is available on Linux and macOS - if (process.platform !== "linux" && process.platform !== "darwin") { - return []; - } - try { - const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; - if (!swiftlyHomeDir) { - return []; - } - const swiftlyConfig = await SwiftToolchain.getSwiftlyConfig(); - if (!swiftlyConfig || !("installedToolchains" in swiftlyConfig)) { - return []; - } - const installedToolchains = swiftlyConfig.installedToolchains; - if (!Array.isArray(installedToolchains)) { - return []; - } - return installedToolchains - .filter((toolchain): toolchain is string => typeof toolchain === "string") - .map(toolchain => path.join(swiftlyHomeDir, "toolchains", toolchain)); - } catch (error) { - throw new Error("Failed to retrieve Swiftly installations from disk."); - } - } - - /** - * 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. * @@ -615,7 +567,7 @@ export class SwiftToolchain { let realSwift = await fs.realpath(swift); if (path.basename(realSwift) === "swiftly") { try { - const inUse = await this.swiftlyInUseLocation(realSwift, cwd); + const inUse = await swiftly.swiftlyInUseLocation(realSwift, cwd); if (inUse) { realSwift = path.join(inUse, "usr", "bin", "swift"); } @@ -668,7 +620,7 @@ export class SwiftToolchain { const swiftlyPath = path.join(configPath, "swiftly"); if (await fileExists(swiftlyPath)) { try { - const inUse = await this.swiftlyInUseLocation(swiftlyPath, cwd); + const inUse = await swiftly.swiftlyInUseLocation(swiftlyPath, cwd); if (inUse) { return path.join(inUse, "usr"); } @@ -679,7 +631,7 @@ export class SwiftToolchain { return path.dirname(configuration.path); } - const swiftlyToolchainLocation = await this.swiftlyToolchain(cwd); + const swiftlyToolchainLocation = await swiftly.swiftlyToolchain(cwd); if (swiftlyToolchainLocation) { return swiftlyToolchainLocation; } @@ -699,41 +651,6 @@ export class SwiftToolchain { } } - private static async swiftlyInUseLocation(swiftlyPath: string, cwd?: vscode.Uri) { - const { stdout: inUse } = await execFile(swiftlyPath, ["use", "--print-location"], { - cwd: cwd?.fsPath, - }); - return inUse.trimEnd(); - } - - /** - * 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 swiftlyToolchain(cwd?: vscode.Uri): Promise { - const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; - if (swiftlyHomeDir) { - const { stdout: swiftLocation } = await execFile("which", ["swift"]); - if (swiftLocation.indexOf(swiftlyHomeDir) === 0) { - // Print the location of the toolchain that swiftly is using. If there - // is no cwd specified then it returns the global "inUse" toolchain otherwise - // it respects the .swift-version file in the cwd and resolves using that. - try { - const inUse = await this.swiftlyInUseLocation("swiftly", cwd); - if (inUse.length > 0) { - return path.join(inUse, "usr"); - } - } catch (err: unknown) { - const error = err as ExecFileError; - // Its possible the toolchain in .swift-version is misconfigured or doesn't exist. - void vscode.window.showErrorMessage(`${error.stderr}`); - } - } - } - return undefined; - } - /** * @param targetInfo swift target info * @returns path to Swift runtime diff --git a/src/ui/ToolchainSelection.ts b/src/ui/ToolchainSelection.ts index 41e8bfcfb..84bb0a236 100644 --- a/src/ui/ToolchainSelection.ts +++ b/src/ui/ToolchainSelection.ts @@ -18,6 +18,7 @@ import { showReloadExtensionNotification } from "./ReloadExtension"; import { SwiftToolchain } from "../toolchain/toolchain"; import configuration from "../configuration"; import { Commands } from "../commands"; +import { swiftly } from "../toolchain/swiftly"; /** * Open the installation page on Swift.org @@ -192,7 +193,7 @@ async function getQuickPickItems( return result; }); // Find any Swift toolchains installed via Swiftly - const swiftlyToolchains = (await SwiftToolchain.getSwiftlyToolchainInstalls()) + const swiftlyToolchains = (await swiftly.getSwiftlyToolchainInstalls()) .reverse() .map(toolchainPath => ({ type: "toolchain", diff --git a/test/unit-tests/toolchain/swiftly.test.ts b/test/unit-tests/toolchain/swiftly.test.ts new file mode 100644 index 000000000..ee6b67a83 --- /dev/null +++ b/test/unit-tests/toolchain/swiftly.test.ts @@ -0,0 +1,119 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021 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 * as sinon from "sinon"; +import { Swiftly } from "../../../src/toolchain/swiftly"; +import * as utilities from "../../../src/utilities/utilities"; + +suite("Swiftly Unit Tests", () => { + let execFileStub: sinon.SinonStub; + let swiftly: Swiftly; + + setup(() => { + execFileStub = sinon.stub(utilities, "execFile"); + swiftly = new Swiftly(); + }); + + teardown(() => { + sinon.restore(); + }); + + suite("getSwiftlyToolchainInstalls", () => { + test("should return toolchain names from list-available command for version 1.1.0", async () => { + // Mock version check to return 1.1.0 + execFileStub.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.1.0\n", + stderr: "" + }); + + // Mock list-available command with JSON output + const jsonOutput = { + toolchains: [ + { + inUse: true, + installed: true, + isDefault: true, + name: "swift-5.9.0-RELEASE", + version: { + major: 5, + minor: 9, + patch: 0, + type: "stable" + } + }, + { + inUse: false, + installed: true, + isDefault: false, + name: "swift-5.8.0-RELEASE", + version: { + major: 5, + minor: 8, + patch: 0, + type: "stable" + } + }, + { + inUse: false, + installed: false, + isDefault: false, + name: "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a", + version: { + major: 5, + minor: 10, + branch: "development", + date: "2023-10-15", + type: "snapshot" + } + } + ] + }; + + execFileStub.withArgs("swiftly", ["list-available", "--format=json"]).resolves({ + stdout: JSON.stringify(jsonOutput), + stderr: "" + }); + + const result = await swiftly.getSwiftlyToolchainInstalls(); + + expect(result).to.deep.equal([ + "swift-5.9.0-RELEASE", + "swift-5.8.0-RELEASE", + "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a" + ]); + + expect(execFileStub.calledWith("swiftly", ["--version"])).to.be.true; + expect(execFileStub.calledWith("swiftly", ["list-available", "--format=json"])).to.be.true; + }); + + test("should return empty array when platform is not supported", async () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { + value: "win32", + writable: true + }); + + const result = await swiftly.getSwiftlyToolchainInstalls(); + + expect(result).to.deep.equal([]); + expect(execFileStub.called).to.be.false; + + Object.defineProperty(process, "platform", { + value: originalPlatform, + writable: true + }); + }); + }); +}); From 68790c812b62c5ba9ff30f82869d32afdce0abd5 Mon Sep 17 00:00:00 2001 From: Priyambada Roul Date: Wed, 16 Jul 2025 18:27:13 +0530 Subject: [PATCH 2/8] refactoring changes --- src/toolchain/swiftly.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts index eddb3790c..540b8af96 100644 --- a/src/toolchain/swiftly.ts +++ b/src/toolchain/swiftly.ts @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// 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 * as path from "node:path"; import { SwiftlyConfig } from "./ToolchainVersion"; import * as fs from "node:fs/promises"; @@ -71,7 +85,7 @@ export class Swiftly { const response = ListAvailableResult.parse(JSON.parse(stdout)); return response.toolchains.map(t => t.name); } catch (error) { - throw new Error("Failed to retrieve Swiftly installations from disk."); + throw new Error("Failed to retrieve Swiftly installations from disk: ${error.message}"); } } @@ -93,7 +107,7 @@ export class Swiftly { .filter((toolchain): toolchain is string => typeof toolchain === "string") .map(toolchain => path.join(swiftlyHomeDir, "toolchains", toolchain)); } catch (error) { - throw new Error("Failed to retrieve Swiftly installations from disk."); + throw new Error("Failed to retrieve Swiftly installations from disk: ${error.message}"); } } @@ -129,7 +143,7 @@ export class Swiftly { } catch (err: unknown) { const error = err as ExecFileError; // Its possible the toolchain in .swift-version is misconfigured or doesn't exist. - void vscode.window.showErrorMessage(`${error.stderr}`); + void vscode.window.showErrorMessage(`Failed to load toolchain from Swiftly: ${error.stderr}`); } } } From a4c094d562ef0654c6cfe7ad5e241fcbf46f8276 Mon Sep 17 00:00:00 2001 From: Priyambada Roul Date: Wed, 16 Jul 2025 22:57:03 +0530 Subject: [PATCH 3/8] refactoring changes --- src/toolchain/swiftly.ts | 4 ++-- test/unit-tests/toolchain/swiftly.test.ts | 18 ++++++---------- test/unit-tests/toolchain/toolchain.test.ts | 24 +++++++++++---------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts index 540b8af96..f6c5b1e41 100644 --- a/src/toolchain/swiftly.ts +++ b/src/toolchain/swiftly.ts @@ -85,7 +85,7 @@ export class Swiftly { const response = ListAvailableResult.parse(JSON.parse(stdout)); return response.toolchains.map(t => t.name); } catch (error) { - throw new Error("Failed to retrieve Swiftly installations from disk: ${error.message}"); + throw new Error(`Failed to retrieve Swiftly installations from disk: ${(error as Error).message}`); } } @@ -107,7 +107,7 @@ export class Swiftly { .filter((toolchain): toolchain is string => typeof toolchain === "string") .map(toolchain => path.join(swiftlyHomeDir, "toolchains", toolchain)); } catch (error) { - throw new Error("Failed to retrieve Swiftly installations from disk: ${error.message}"); + throw new Error(`Failed to retrieve Swiftly installations from disk: ${(error as Error).message}`); } } diff --git a/test/unit-tests/toolchain/swiftly.test.ts b/test/unit-tests/toolchain/swiftly.test.ts index ee6b67a83..a2eacc9ba 100644 --- a/test/unit-tests/toolchain/swiftly.test.ts +++ b/test/unit-tests/toolchain/swiftly.test.ts @@ -13,27 +13,23 @@ //===----------------------------------------------------------------------===// import { expect } from "chai"; -import * as sinon from "sinon"; import { Swiftly } from "../../../src/toolchain/swiftly"; import * as utilities from "../../../src/utilities/utilities"; +import { mockGlobalModule } from "../../MockUtils"; suite("Swiftly Unit Tests", () => { - let execFileStub: sinon.SinonStub; + const mockUtilities = mockGlobalModule(utilities); let swiftly: Swiftly; setup(() => { - execFileStub = sinon.stub(utilities, "execFile"); swiftly = new Swiftly(); }); - teardown(() => { - sinon.restore(); - }); suite("getSwiftlyToolchainInstalls", () => { test("should return toolchain names from list-available command for version 1.1.0", async () => { // Mock version check to return 1.1.0 - execFileStub.withArgs("swiftly", ["--version"]).resolves({ + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ stdout: "1.1.0\n", stderr: "" }); @@ -81,7 +77,7 @@ suite("Swiftly Unit Tests", () => { ] }; - execFileStub.withArgs("swiftly", ["list-available", "--format=json"]).resolves({ + mockUtilities.execFile.withArgs("swiftly", ["list-available", "--format=json"]).resolves({ stdout: JSON.stringify(jsonOutput), stderr: "" }); @@ -94,8 +90,8 @@ suite("Swiftly Unit Tests", () => { "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a" ]); - expect(execFileStub.calledWith("swiftly", ["--version"])).to.be.true; - expect(execFileStub.calledWith("swiftly", ["list-available", "--format=json"])).to.be.true; + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", ["--version"]); + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", ["list-available", "--format=json"]); }); test("should return empty array when platform is not supported", async () => { @@ -108,7 +104,7 @@ suite("Swiftly Unit Tests", () => { const result = await swiftly.getSwiftlyToolchainInstalls(); expect(result).to.deep.equal([]); - expect(execFileStub.called).to.be.false; + expect(mockUtilities.execFile).not.have.been.called; Object.defineProperty(process, "platform", { value: originalPlatform, diff --git a/test/unit-tests/toolchain/toolchain.test.ts b/test/unit-tests/toolchain/toolchain.test.ts index ac0345b53..63e719aeb 100644 --- a/test/unit-tests/toolchain/toolchain.test.ts +++ b/test/unit-tests/toolchain/toolchain.test.ts @@ -19,6 +19,7 @@ import * as utilities from "../../../src/utilities/utilities"; import { SwiftToolchain } from "../../../src/toolchain/toolchain"; import { Version } from "../../../src/utilities/version"; import { mockGlobalModule, mockGlobalValue } from "../../MockUtils"; +import { swiftly } from "../../../src/toolchain/swiftly"; suite("SwiftToolchain Unit Test Suite", () => { const mockedUtilities = mockGlobalModule(utilities); @@ -26,9 +27,10 @@ suite("SwiftToolchain Unit Test Suite", () => { setup(() => { mockFS({}); - mockedUtilities.execFile.rejects( - new Error("execFile was not properly mocked for the test") - ); + mockedUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.0.0\n", + stderr: "" + }); }); teardown(() => { @@ -311,7 +313,7 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await SwiftToolchain.getSwiftlyToolchainInstalls(); + const toolchains = await swiftly.getSwiftlyToolchainInstalls(); expect(toolchains).to.deep.equal([ path.join(mockHomeDir, "toolchains", "swift-5.9.0"), path.join(mockHomeDir, "toolchains", "swift-6.0.0"), @@ -329,7 +331,7 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await SwiftToolchain.getSwiftlyToolchainInstalls(); + const toolchains = await swiftly.getSwiftlyToolchainInstalls(); expect(toolchains).to.deep.equal([ path.join(mockHomeDir, "toolchains", "swift-5.9.0"), path.join(mockHomeDir, "toolchains", "swift-6.0.0"), @@ -340,7 +342,7 @@ suite("SwiftToolchain Unit Test Suite", () => { mockedPlatform.setValue("linux"); mockedEnv.setValue({}); - const toolchains = await SwiftToolchain.getSwiftlyToolchainInstalls(); + const toolchains = await swiftly.getSwiftlyToolchainInstalls(); expect(toolchains).to.be.empty; }); @@ -351,8 +353,8 @@ suite("SwiftToolchain Unit Test Suite", () => { mockFS({}); - await expect(SwiftToolchain.getSwiftlyToolchainInstalls()).to.be.rejectedWith( - "Failed to retrieve Swiftly installations from disk." + await expect(swiftly.getSwiftlyToolchainInstalls()).to.be.rejectedWith( + "Failed to retrieve Swiftly installations from disk: ENOENT, no such file or directory '/home/user/.swiftly/config.json'" ); }); @@ -367,13 +369,13 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await SwiftToolchain.getSwiftlyToolchainInstalls(); + const toolchains = await swiftly.getSwiftlyToolchainInstalls(); expect(toolchains).to.be.empty; }); test("returns empty array on Windows", async () => { mockedPlatform.setValue("win32"); - const toolchains = await SwiftToolchain.getSwiftlyToolchainInstalls(); + const toolchains = await swiftly.getSwiftlyToolchainInstalls(); expect(toolchains).to.be.empty; }); @@ -388,7 +390,7 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await SwiftToolchain.getSwiftlyToolchainInstalls(); + const toolchains = await swiftly.getSwiftlyToolchainInstalls(); expect(toolchains).to.deep.equal([ path.join(mockHomeDir, "toolchains", "swift-5.9.0"), path.join(mockHomeDir, "toolchains", "swift-6.0.0"), From fcd3803212888d9de9d6d33c0227d7bc10e201df Mon Sep 17 00:00:00 2001 From: Priyambada Roul Date: Wed, 16 Jul 2025 23:00:14 +0530 Subject: [PATCH 4/8] refactoring changes --- test/unit-tests/toolchain/swiftly.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit-tests/toolchain/swiftly.test.ts b/test/unit-tests/toolchain/swiftly.test.ts index a2eacc9ba..3dcd98730 100644 --- a/test/unit-tests/toolchain/swiftly.test.ts +++ b/test/unit-tests/toolchain/swiftly.test.ts @@ -2,7 +2,7 @@ // // This source file is part of the VS Code Swift open source project // -// Copyright (c) 2021 the VS Code Swift project authors +// Copyright (c) 2025 the VS Code Swift project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information From bb13c8175a9aed411b928d0515342dc5f6489192 Mon Sep 17 00:00:00 2001 From: Priyambada Roul Date: Wed, 16 Jul 2025 23:30:15 +0530 Subject: [PATCH 5/8] refactoring changes --- src/toolchain/swiftly.ts | 30 ++++++++++----------- src/toolchain/toolchain.ts | 9 +++---- src/ui/ToolchainSelection.ts | 4 +-- test/unit-tests/toolchain/swiftly.test.ts | 10 ++----- test/unit-tests/toolchain/toolchain.test.ts | 16 +++++------ 5 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts index f6c5b1e41..4cacc07e0 100644 --- a/src/toolchain/swiftly.ts +++ b/src/toolchain/swiftly.ts @@ -54,8 +54,8 @@ export class Swiftly { * @returns the version of Swiftly as a `Version` object, or `undefined` * if Swiftly is not installed or not supported. */ - public async getSwiftlyVersion(): Promise { - if (!this.isSupported()) { + public static async version(): Promise { + if (!Swiftly.isSupported()) { return undefined; } const { stdout } = await execFile("swiftly", ["--version"]); @@ -67,19 +67,19 @@ export class Swiftly { * * @returns an array of toolchain paths */ - public async getSwiftlyToolchainInstalls(): Promise { + public static async listAvailableToolchains(): Promise { if (!this.isSupported()) { return []; } - const version = await swiftly.getSwiftlyVersion(); + const version = await Swiftly.version(); if (version?.isLessThan(new Version(1, 1, 0))) { - return await this.getToolchainInstallLegacy(); + return await Swiftly.getToolchainInstallLegacy(); } - return await this.getListAvailableToolchains(); + return await Swiftly.getListAvailableToolchains(); } - private async getListAvailableToolchains(): Promise { + private static async getListAvailableToolchains(): Promise { try { const { stdout } = await execFile("swiftly", ["list-available", "--format=json"]); const response = ListAvailableResult.parse(JSON.parse(stdout)); @@ -89,13 +89,13 @@ export class Swiftly { } } - private async getToolchainInstallLegacy() { + private static async getToolchainInstallLegacy() { try { const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; if (!swiftlyHomeDir) { return []; } - const swiftlyConfig = await swiftly.getSwiftlyConfig(); + const swiftlyConfig = await Swiftly.getConfig(); if (!swiftlyConfig || !("installedToolchains" in swiftlyConfig)) { return []; } @@ -111,11 +111,11 @@ export class Swiftly { } } - private isSupported() { + private static isSupported() { return process.platform === "linux" || process.platform === "darwin"; } - public async swiftlyInUseLocation(swiftlyPath: string, cwd?: vscode.Uri) { + public static async inUseLocation(swiftlyPath: string, cwd?: vscode.Uri) { const { stdout: inUse } = await execFile(swiftlyPath, ["use", "--print-location"], { cwd: cwd?.fsPath, }); @@ -127,7 +127,7 @@ export class Swiftly { * the path to the active toolchain. * @returns The location of the active toolchain if swiftly is being used to manage it. */ - public async swiftlyToolchain(cwd?: vscode.Uri): Promise { + public static async toolchain(cwd?: vscode.Uri): Promise { const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; if (swiftlyHomeDir) { const { stdout: swiftLocation } = await execFile("which", ["swift"]); @@ -136,7 +136,7 @@ export class Swiftly { // is no cwd specified then it returns the global "inUse" toolchain otherwise // it respects the .swift-version file in the cwd and resolves using that. try { - const inUse = await swiftly.swiftlyInUseLocation("swiftly", cwd); + const inUse = await Swiftly.inUseLocation("swiftly", cwd); if (inUse.length > 0) { return path.join(inUse, "usr"); } @@ -155,7 +155,7 @@ export class Swiftly { * * @returns A parsed Swiftly configuration. */ - private async getSwiftlyConfig(): Promise { + private static async getConfig(): Promise { const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; if (!swiftlyHomeDir) { return; @@ -167,5 +167,3 @@ export class Swiftly { return JSON.parse(swiftlyConfigRaw); } } - -export const swiftly = new Swiftly(); diff --git a/src/toolchain/toolchain.ts b/src/toolchain/toolchain.ts index 9dd056feb..34caebd3d 100644 --- a/src/toolchain/toolchain.ts +++ b/src/toolchain/toolchain.ts @@ -25,8 +25,7 @@ import { Version } from "../utilities/version"; import { BuildFlags } from "./BuildFlags"; import { Sanitizer } from "./Sanitizer"; import { lineBreakRegex } from "../utilities/tasks"; -import { swiftly } from "./swiftly"; - +import { Swiftly } from "./swiftly"; /** * Contents of **Info.plist** on Windows. */ @@ -567,7 +566,7 @@ export class SwiftToolchain { let realSwift = await fs.realpath(swift); if (path.basename(realSwift) === "swiftly") { try { - const inUse = await swiftly.swiftlyInUseLocation(realSwift, cwd); + const inUse = await Swiftly.inUseLocation(realSwift, cwd); if (inUse) { realSwift = path.join(inUse, "usr", "bin", "swift"); } @@ -620,7 +619,7 @@ export class SwiftToolchain { const swiftlyPath = path.join(configPath, "swiftly"); if (await fileExists(swiftlyPath)) { try { - const inUse = await swiftly.swiftlyInUseLocation(swiftlyPath, cwd); + const inUse = await Swiftly.inUseLocation(swiftlyPath, cwd); if (inUse) { return path.join(inUse, "usr"); } @@ -631,7 +630,7 @@ export class SwiftToolchain { return path.dirname(configuration.path); } - const swiftlyToolchainLocation = await swiftly.swiftlyToolchain(cwd); + const swiftlyToolchainLocation = await Swiftly.toolchain(cwd); if (swiftlyToolchainLocation) { return swiftlyToolchainLocation; } diff --git a/src/ui/ToolchainSelection.ts b/src/ui/ToolchainSelection.ts index 84bb0a236..2cdb2a9bb 100644 --- a/src/ui/ToolchainSelection.ts +++ b/src/ui/ToolchainSelection.ts @@ -18,7 +18,7 @@ import { showReloadExtensionNotification } from "./ReloadExtension"; import { SwiftToolchain } from "../toolchain/toolchain"; import configuration from "../configuration"; import { Commands } from "../commands"; -import { swiftly } from "../toolchain/swiftly"; +import { Swiftly } from "../toolchain/swiftly"; /** * Open the installation page on Swift.org @@ -193,7 +193,7 @@ async function getQuickPickItems( return result; }); // Find any Swift toolchains installed via Swiftly - const swiftlyToolchains = (await swiftly.getSwiftlyToolchainInstalls()) + const swiftlyToolchains = (await Swiftly.listAvailableToolchains()) .reverse() .map(toolchainPath => ({ type: "toolchain", diff --git a/test/unit-tests/toolchain/swiftly.test.ts b/test/unit-tests/toolchain/swiftly.test.ts index 3dcd98730..daf6ae6bb 100644 --- a/test/unit-tests/toolchain/swiftly.test.ts +++ b/test/unit-tests/toolchain/swiftly.test.ts @@ -19,12 +19,6 @@ import { mockGlobalModule } from "../../MockUtils"; suite("Swiftly Unit Tests", () => { const mockUtilities = mockGlobalModule(utilities); - let swiftly: Swiftly; - - setup(() => { - swiftly = new Swiftly(); - }); - suite("getSwiftlyToolchainInstalls", () => { test("should return toolchain names from list-available command for version 1.1.0", async () => { @@ -82,7 +76,7 @@ suite("Swiftly Unit Tests", () => { stderr: "" }); - const result = await swiftly.getSwiftlyToolchainInstalls(); + const result = await Swiftly.listAvailableToolchains(); expect(result).to.deep.equal([ "swift-5.9.0-RELEASE", @@ -101,7 +95,7 @@ suite("Swiftly Unit Tests", () => { writable: true }); - const result = await swiftly.getSwiftlyToolchainInstalls(); + const result = await Swiftly.listAvailableToolchains(); expect(result).to.deep.equal([]); expect(mockUtilities.execFile).not.have.been.called; diff --git a/test/unit-tests/toolchain/toolchain.test.ts b/test/unit-tests/toolchain/toolchain.test.ts index 63e719aeb..8c24bed0a 100644 --- a/test/unit-tests/toolchain/toolchain.test.ts +++ b/test/unit-tests/toolchain/toolchain.test.ts @@ -19,7 +19,7 @@ import * as utilities from "../../../src/utilities/utilities"; import { SwiftToolchain } from "../../../src/toolchain/toolchain"; import { Version } from "../../../src/utilities/version"; import { mockGlobalModule, mockGlobalValue } from "../../MockUtils"; -import { swiftly } from "../../../src/toolchain/swiftly"; +import { Swiftly } from "../../../src/toolchain/swiftly"; suite("SwiftToolchain Unit Test Suite", () => { const mockedUtilities = mockGlobalModule(utilities); @@ -313,7 +313,7 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await swiftly.getSwiftlyToolchainInstalls(); + const toolchains = await Swiftly.listAvailableToolchains(); expect(toolchains).to.deep.equal([ path.join(mockHomeDir, "toolchains", "swift-5.9.0"), path.join(mockHomeDir, "toolchains", "swift-6.0.0"), @@ -331,7 +331,7 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await swiftly.getSwiftlyToolchainInstalls(); + const toolchains = await Swiftly.listAvailableToolchains(); expect(toolchains).to.deep.equal([ path.join(mockHomeDir, "toolchains", "swift-5.9.0"), path.join(mockHomeDir, "toolchains", "swift-6.0.0"), @@ -342,7 +342,7 @@ suite("SwiftToolchain Unit Test Suite", () => { mockedPlatform.setValue("linux"); mockedEnv.setValue({}); - const toolchains = await swiftly.getSwiftlyToolchainInstalls(); + const toolchains = await Swiftly.listAvailableToolchains(); expect(toolchains).to.be.empty; }); @@ -353,7 +353,7 @@ suite("SwiftToolchain Unit Test Suite", () => { mockFS({}); - await expect(swiftly.getSwiftlyToolchainInstalls()).to.be.rejectedWith( + await expect(Swiftly.listAvailableToolchains()).to.be.rejectedWith( "Failed to retrieve Swiftly installations from disk: ENOENT, no such file or directory '/home/user/.swiftly/config.json'" ); }); @@ -369,13 +369,13 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await swiftly.getSwiftlyToolchainInstalls(); + const toolchains = await Swiftly.listAvailableToolchains(); expect(toolchains).to.be.empty; }); test("returns empty array on Windows", async () => { mockedPlatform.setValue("win32"); - const toolchains = await swiftly.getSwiftlyToolchainInstalls(); + const toolchains = await Swiftly.listAvailableToolchains(); expect(toolchains).to.be.empty; }); @@ -390,7 +390,7 @@ suite("SwiftToolchain Unit Test Suite", () => { }), }); - const toolchains = await swiftly.getSwiftlyToolchainInstalls(); + const toolchains = await Swiftly.listAvailableToolchains(); expect(toolchains).to.deep.equal([ path.join(mockHomeDir, "toolchains", "swift-5.9.0"), path.join(mockHomeDir, "toolchains", "swift-6.0.0"), From 710da2fdd9e32a0adf60a40ace381aa84176f646 Mon Sep 17 00:00:00 2001 From: Priyambada Roul Date: Thu, 17 Jul 2025 10:41:45 +0530 Subject: [PATCH 6/8] refactoring changes --- src/toolchain/swiftly.ts | 12 +++++-- test/unit-tests/toolchain/swiftly.test.ts | 39 ++++++++++++--------- test/unit-tests/toolchain/toolchain.test.ts | 2 +- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts index 4cacc07e0..49b888220 100644 --- a/src/toolchain/swiftly.ts +++ b/src/toolchain/swiftly.ts @@ -85,7 +85,9 @@ export class Swiftly { const response = ListAvailableResult.parse(JSON.parse(stdout)); return response.toolchains.map(t => t.name); } catch (error) { - throw new Error(`Failed to retrieve Swiftly installations from disk: ${(error as Error).message}`); + throw new Error( + `Failed to retrieve Swiftly installations from disk: ${(error as Error).message}` + ); } } @@ -107,7 +109,9 @@ export class Swiftly { .filter((toolchain): toolchain is string => typeof toolchain === "string") .map(toolchain => path.join(swiftlyHomeDir, "toolchains", toolchain)); } catch (error) { - throw new Error(`Failed to retrieve Swiftly installations from disk: ${(error as Error).message}`); + throw new Error( + `Failed to retrieve Swiftly installations from disk: ${(error as Error).message}` + ); } } @@ -143,7 +147,9 @@ export class Swiftly { } catch (err: unknown) { const error = err as ExecFileError; // Its possible the toolchain in .swift-version is misconfigured or doesn't exist. - void vscode.window.showErrorMessage(`Failed to load toolchain from Swiftly: ${error.stderr}`); + void vscode.window.showErrorMessage( + `Failed to load toolchain from Swiftly: ${error.stderr}` + ); } } } diff --git a/test/unit-tests/toolchain/swiftly.test.ts b/test/unit-tests/toolchain/swiftly.test.ts index daf6ae6bb..66b897052 100644 --- a/test/unit-tests/toolchain/swiftly.test.ts +++ b/test/unit-tests/toolchain/swiftly.test.ts @@ -25,7 +25,7 @@ suite("Swiftly Unit Tests", () => { // Mock version check to return 1.1.0 mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ stdout: "1.1.0\n", - stderr: "" + stderr: "", }); // Mock list-available command with JSON output @@ -40,8 +40,8 @@ suite("Swiftly Unit Tests", () => { major: 5, minor: 9, patch: 0, - type: "stable" - } + type: "stable", + }, }, { inUse: false, @@ -52,8 +52,8 @@ suite("Swiftly Unit Tests", () => { major: 5, minor: 8, patch: 0, - type: "stable" - } + type: "stable", + }, }, { inUse: false, @@ -65,34 +65,39 @@ suite("Swiftly Unit Tests", () => { minor: 10, branch: "development", date: "2023-10-15", - type: "snapshot" - } - } - ] + type: "snapshot", + }, + }, + ], }; - mockUtilities.execFile.withArgs("swiftly", ["list-available", "--format=json"]).resolves({ - stdout: JSON.stringify(jsonOutput), - stderr: "" - }); + mockUtilities.execFile + .withArgs("swiftly", ["list-available", "--format=json"]) + .resolves({ + stdout: JSON.stringify(jsonOutput), + stderr: "", + }); const result = await Swiftly.listAvailableToolchains(); expect(result).to.deep.equal([ "swift-5.9.0-RELEASE", "swift-5.8.0-RELEASE", - "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a" + "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a", ]); expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", ["--version"]); - expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", ["list-available", "--format=json"]); + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", [ + "list-available", + "--format=json", + ]); }); test("should return empty array when platform is not supported", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "win32", - writable: true + writable: true, }); const result = await Swiftly.listAvailableToolchains(); @@ -102,7 +107,7 @@ suite("Swiftly Unit Tests", () => { Object.defineProperty(process, "platform", { value: originalPlatform, - writable: true + writable: true, }); }); }); diff --git a/test/unit-tests/toolchain/toolchain.test.ts b/test/unit-tests/toolchain/toolchain.test.ts index 8c24bed0a..3e12571db 100644 --- a/test/unit-tests/toolchain/toolchain.test.ts +++ b/test/unit-tests/toolchain/toolchain.test.ts @@ -29,7 +29,7 @@ suite("SwiftToolchain Unit Test Suite", () => { mockFS({}); mockedUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ stdout: "1.0.0\n", - stderr: "" + stderr: "", }); }); From 0048272e921b21856973c10d73a92159eb2ca8a6 Mon Sep 17 00:00:00 2001 From: Priyambada Roul Date: Thu, 17 Jul 2025 22:53:27 +0530 Subject: [PATCH 7/8] refactoring changes --- src/toolchain/swiftly.ts | 4 ++-- test/unit-tests/toolchain/swiftly.test.ts | 14 +++----------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts index 49b888220..4d33f28de 100644 --- a/src/toolchain/swiftly.ts +++ b/src/toolchain/swiftly.ts @@ -12,9 +12,9 @@ // //===----------------------------------------------------------------------===// -import * as path from "node:path"; +import * as path from "path"; import { SwiftlyConfig } from "./ToolchainVersion"; -import * as fs from "node:fs/promises"; +import * as fs from "fs/promises"; import { execFile, ExecFileError } from "../utilities/utilities"; import * as vscode from "vscode"; import { Version } from "../utilities/version"; diff --git a/test/unit-tests/toolchain/swiftly.test.ts b/test/unit-tests/toolchain/swiftly.test.ts index 66b897052..3f2838253 100644 --- a/test/unit-tests/toolchain/swiftly.test.ts +++ b/test/unit-tests/toolchain/swiftly.test.ts @@ -15,10 +15,11 @@ import { expect } from "chai"; import { Swiftly } from "../../../src/toolchain/swiftly"; import * as utilities from "../../../src/utilities/utilities"; -import { mockGlobalModule } from "../../MockUtils"; +import { mockGlobalModule, mockGlobalValue } from "../../MockUtils"; suite("Swiftly Unit Tests", () => { const mockUtilities = mockGlobalModule(utilities); + const mockedPlatform = mockGlobalValue(process, "platform"); suite("getSwiftlyToolchainInstalls", () => { test("should return toolchain names from list-available command for version 1.1.0", async () => { @@ -94,21 +95,12 @@ suite("Swiftly Unit Tests", () => { }); test("should return empty array when platform is not supported", async () => { - const originalPlatform = process.platform; - Object.defineProperty(process, "platform", { - value: "win32", - writable: true, - }); + mockedPlatform.setValue("win32"); const result = await Swiftly.listAvailableToolchains(); expect(result).to.deep.equal([]); expect(mockUtilities.execFile).not.have.been.called; - - Object.defineProperty(process, "platform", { - value: originalPlatform, - writable: true, - }); }); }); }); From 413fb8c93f48ef0291aceefc24c2f5803458af8f Mon Sep 17 00:00:00 2001 From: Priyambada Roul Date: Thu, 17 Jul 2025 23:34:07 +0530 Subject: [PATCH 8/8] Added changelog and outputchannel --- CHANGELOG.md | 1 + src/toolchain/swiftly.ts | 44 ++++++++++++++++++++++++++++---------- src/toolchain/toolchain.ts | 10 ++++++--- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d58a58e8d..4b231ebec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added Swiftly toolchain management support `.swift-version` files, and integration with the toolchain selection UI ([#1717](https://github.com/swiftlang/vscode-swift/pull/1717) - Added code lenses to run suites/tests, configurable with the `swift.showTestCodeLenses` setting ([#1698](https://github.com/swiftlang/vscode-swift/pull/1698)) ## 2.8.0 - 2025-07-14 diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts index 4d33f28de..139170691 100644 --- a/src/toolchain/swiftly.ts +++ b/src/toolchain/swiftly.ts @@ -54,12 +54,19 @@ export class Swiftly { * @returns the version of Swiftly as a `Version` object, or `undefined` * if Swiftly is not installed or not supported. */ - public static async version(): Promise { + public static async version( + outputChannel?: vscode.OutputChannel + ): Promise { if (!Swiftly.isSupported()) { return undefined; } - const { stdout } = await execFile("swiftly", ["--version"]); - return Version.fromString(stdout.trim()); + try { + const { stdout } = await execFile("swiftly", ["--version"]); + return Version.fromString(stdout.trim()); + } catch (error) { + outputChannel?.appendLine(`Failed to retrieve Swiftly version: ${error}`); + return undefined; + } } /** @@ -67,31 +74,41 @@ export class Swiftly { * * @returns an array of toolchain paths */ - public static async listAvailableToolchains(): Promise { + public static async listAvailableToolchains( + outputChannel?: vscode.OutputChannel + ): Promise { if (!this.isSupported()) { return []; } - const version = await Swiftly.version(); - if (version?.isLessThan(new Version(1, 1, 0))) { - return await Swiftly.getToolchainInstallLegacy(); + const version = await Swiftly.version(outputChannel); + if (!version) { + outputChannel?.appendLine("Swiftly is not installed"); + return []; + } + + if (version.isLessThan(new Version(1, 1, 0))) { + return await Swiftly.getToolchainInstallLegacy(outputChannel); } - return await Swiftly.getListAvailableToolchains(); + return await Swiftly.getListAvailableToolchains(outputChannel); } - private static async getListAvailableToolchains(): Promise { + private static async getListAvailableToolchains( + outputChannel?: vscode.OutputChannel + ): Promise { try { const { stdout } = await execFile("swiftly", ["list-available", "--format=json"]); const response = ListAvailableResult.parse(JSON.parse(stdout)); return response.toolchains.map(t => t.name); } catch (error) { + outputChannel?.appendLine(`Failed to retrieve Swiftly installations: ${error}`); throw new Error( `Failed to retrieve Swiftly installations from disk: ${(error as Error).message}` ); } } - private static async getToolchainInstallLegacy() { + private static async getToolchainInstallLegacy(outputChannel?: vscode.OutputChannel) { try { const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; if (!swiftlyHomeDir) { @@ -109,6 +126,7 @@ export class Swiftly { .filter((toolchain): toolchain is string => typeof toolchain === "string") .map(toolchain => path.join(swiftlyHomeDir, "toolchains", toolchain)); } catch (error) { + outputChannel?.appendLine(`Failed to retrieve Swiftly installations: ${error}`); throw new Error( `Failed to retrieve Swiftly installations from disk: ${(error as Error).message}` ); @@ -131,7 +149,10 @@ export class Swiftly { * the path to the active toolchain. * @returns The location of the active toolchain if swiftly is being used to manage it. */ - public static async toolchain(cwd?: vscode.Uri): Promise { + public static async toolchain( + outputChannel?: vscode.OutputChannel, + cwd?: vscode.Uri + ): Promise { const swiftlyHomeDir: string | undefined = process.env["SWIFTLY_HOME_DIR"]; if (swiftlyHomeDir) { const { stdout: swiftLocation } = await execFile("which", ["swift"]); @@ -145,6 +166,7 @@ export class Swiftly { return path.join(inUse, "usr"); } } catch (err: unknown) { + outputChannel?.appendLine(`Failed to retrieve Swiftly installations: ${err}`); const error = err as ExecFileError; // Its possible the toolchain in .swift-version is misconfigured or doesn't exist. void vscode.window.showErrorMessage( diff --git a/src/toolchain/toolchain.ts b/src/toolchain/toolchain.ts index 34caebd3d..96321590f 100644 --- a/src/toolchain/toolchain.ts +++ b/src/toolchain/toolchain.ts @@ -122,7 +122,7 @@ export class SwiftToolchain { outputChannel?: vscode.OutputChannel ): Promise { const swiftFolderPath = await this.getSwiftFolderPath(folder, outputChannel); - const toolchainPath = await this.getToolchainPath(swiftFolderPath, folder); + const toolchainPath = await this.getToolchainPath(swiftFolderPath, folder, outputChannel); const targetInfo = await this.getSwiftTargetInfo( this._getToolchainExecutable(toolchainPath, "swift") ); @@ -610,7 +610,11 @@ export class SwiftToolchain { /** * @returns path to Toolchain folder */ - private static async getToolchainPath(swiftPath: string, cwd?: vscode.Uri): Promise { + private static async getToolchainPath( + swiftPath: string, + cwd?: vscode.Uri, + channel?: vscode.OutputChannel + ): Promise { try { switch (process.platform) { case "darwin": { @@ -630,7 +634,7 @@ export class SwiftToolchain { return path.dirname(configuration.path); } - const swiftlyToolchainLocation = await Swiftly.toolchain(cwd); + const swiftlyToolchainLocation = await Swiftly.toolchain(channel, cwd); if (swiftlyToolchainLocation) { return swiftlyToolchainLocation; }