Skip to content
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
15 changes: 14 additions & 1 deletion src/toolchain/swiftly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as vscode from "vscode";
import { Version } from "../utilities/version";
import { z } from "zod/v4/mini";
import { SwiftLogger } from "../logging/SwiftLogger";
import { findBinaryPath } from "../utilities/shell";

const ListResult = z.object({
toolchains: z.array(
Expand Down Expand Up @@ -148,7 +149,7 @@ export class Swiftly {
}
}

private static isSupported() {
public static isSupported() {
return process.platform === "linux" || process.platform === "darwin";
}

Expand Down Expand Up @@ -235,4 +236,16 @@ export class Swiftly {
);
return JSON.parse(swiftlyConfigRaw);
}

public static async isInstalled(): Promise<boolean> {
if (!this.isSupported()) {
return false;
}
try {
await findBinaryPath("swiftly");
return true;
} catch (error) {
return false;
}
}
}
17 changes: 2 additions & 15 deletions src/toolchain/toolchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { Sanitizer } from "./Sanitizer";
import { lineBreakRegex } from "../utilities/tasks";
import { Swiftly } from "./swiftly";
import { SwiftLogger } from "../logging/SwiftLogger";
import { findBinaryPath } from "../utilities/shell";
/**
* Contents of **Info.plist** on Windows.
*/
Expand Down Expand Up @@ -547,21 +548,7 @@ export class SwiftToolchain {
break;
}
default: {
// use `type swift` to find `swift`. Run inside /bin/sh to ensure
// we get consistent output as different shells output a different
// format. Tried running with `-p` but that is not available in /bin/sh
const { stdout, stderr } = await execFile("/bin/sh", [
"-c",
"LC_MESSAGES=C type swift",
]);
const swiftMatch = /^swift is (.*)$/.exec(stdout.trimEnd());
if (swiftMatch) {
swift = swiftMatch[1];
} else {
throw Error(
`/bin/sh -c LC_MESSAGES=C type swift: stdout: ${stdout}, stderr: ${stderr}`
);
}
swift = await findBinaryPath("swift");
break;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/ui/ToolchainSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ async function getQuickPickItems(
}
// Various actions that the user can perform (e.g. to install new toolchains)
const actionItems: ActionItem[] = [];
if (process.platform === "linux" || process.platform === "darwin") {
if (Swiftly.isSupported() && !(await Swiftly.isInstalled())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

praise: great idea to reuse the existing logic

const platformName = process.platform === "linux" ? "Linux" : "macOS";
actionItems.push({
type: "action",
Expand Down
33 changes: 33 additions & 0 deletions src/utilities/shell.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 { execFile } from "./utilities";

// use `type swift` to find `swift`. Run inside /bin/sh to ensure
// we get consistent output as different shells output a different
// format. Tried running with `-p` but that is not available in /bin/sh
export async function findBinaryPath(binaryName: string): Promise<string> {
const { stdout, stderr } = await execFile("/bin/sh", [
"-c",
`LC_MESSAGES=C type ${binaryName}`,
]);
const binaryNameMatch = new RegExp(`^${binaryName} is (.*)$`).exec(stdout.trimEnd());
if (binaryNameMatch) {
return binaryNameMatch[1];
} else {
throw Error(
`/bin/sh -c LC_MESSAGES=C type ${binaryName}: stdout: ${stdout}, stderr: ${stderr}`
);
}
}
31 changes: 31 additions & 0 deletions test/unit-tests/toolchain/swiftly.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
import { expect } from "chai";
import { Swiftly } from "../../../src/toolchain/swiftly";
import * as utilities from "../../../src/utilities/utilities";
import * as shell from "../../../src/utilities/shell";
import { mockGlobalModule, mockGlobalValue } from "../../MockUtils";

suite("Swiftly Unit Tests", () => {
const mockUtilities = mockGlobalModule(utilities);
const mockShell = mockGlobalModule(shell);
const mockedPlatform = mockGlobalValue(process, "platform");

setup(() => {
Expand Down Expand Up @@ -102,4 +104,33 @@ suite("Swiftly Unit Tests", () => {
expect(mockUtilities.execFile).not.have.been.called;
});
});

suite("isInstalled", () => {
test("should return true when swiftly is found", async () => {
mockShell.findBinaryPath.withArgs("swiftly").resolves("/usr/local/bin/swiftly");

const result = await Swiftly.isInstalled();

expect(result).to.be.true;
expect(mockShell.findBinaryPath).to.have.been.calledWith("swiftly");
});

test("should return false when swiftly is not found", async () => {
mockShell.findBinaryPath.withArgs("swiftly").rejects(new Error("not found"));

const result = await Swiftly.isInstalled();

expect(result).to.be.false;
expect(mockShell.findBinaryPath).to.have.been.calledWith("swiftly");
});

test("should return false when platform is not supported", async () => {
mockedPlatform.setValue("win32");

const result = await Swiftly.isInstalled();

expect(result).to.be.false;
expect(mockShell.findBinaryPath).not.to.have.been.called;
});
});
});
62 changes: 62 additions & 0 deletions test/unit-tests/utilities/shell.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//===----------------------------------------------------------------------===//
//
// 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 { beforeEach, afterEach } from "mocha";
import { expect } from "chai";
import * as sinon from "sinon";
import { findBinaryPath } from "../../../src/utilities/shell";
import * as utilities from "../../../src/utilities/utilities";

suite("Shell Unit Test Suite", () => {
let execFileStub: sinon.SinonStub;

beforeEach(() => {
execFileStub = sinon.stub(utilities, "execFile");
});

afterEach(() => {
sinon.restore();
});

suite("findBinaryPath", () => {
test("returns the path to a binary in the PATH", async () => {
execFileStub.resolves({
stdout: "node is /usr/local/bin/node\n",
stderr: "",
});

const binaryPath = await findBinaryPath("node");
expect(binaryPath).to.equal("/usr/local/bin/node");
expect(execFileStub).to.have.been.calledWith("/bin/sh", [
"-c",
"LC_MESSAGES=C type node",
]);
});

test("throws for a non-existent binary", async () => {
execFileStub.resolves({
stdout: "",
stderr: "sh: type: nonexistentbinary: not found\n",
});

try {
await findBinaryPath("nonexistentbinary");
expect.fail("Expected an error to be thrown for a non-existent binary");
} catch (error) {
expect(error).to.be.an("error");
expect((error as Error).message).to.include("nonexistentbinary");
}
});
});
});