Skip to content

Commit d6ca9d1

Browse files
authored
Detect Swiftly Installation (#1772)
1 parent 2bdaabe commit d6ca9d1

File tree

6 files changed

+143
-17
lines changed

6 files changed

+143
-17
lines changed

src/toolchain/swiftly.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as vscode from "vscode";
2020
import { Version } from "../utilities/version";
2121
import { z } from "zod/v4/mini";
2222
import { SwiftLogger } from "../logging/SwiftLogger";
23+
import { findBinaryPath } from "../utilities/shell";
2324

2425
const ListResult = z.object({
2526
toolchains: z.array(
@@ -148,7 +149,7 @@ export class Swiftly {
148149
}
149150
}
150151

151-
private static isSupported() {
152+
public static isSupported() {
152153
return process.platform === "linux" || process.platform === "darwin";
153154
}
154155

@@ -235,4 +236,16 @@ export class Swiftly {
235236
);
236237
return JSON.parse(swiftlyConfigRaw);
237238
}
239+
240+
public static async isInstalled(): Promise<boolean> {
241+
if (!this.isSupported()) {
242+
return false;
243+
}
244+
try {
245+
await findBinaryPath("swiftly");
246+
return true;
247+
} catch (error) {
248+
return false;
249+
}
250+
}
238251
}

src/toolchain/toolchain.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { Sanitizer } from "./Sanitizer";
2626
import { lineBreakRegex } from "../utilities/tasks";
2727
import { Swiftly } from "./swiftly";
2828
import { SwiftLogger } from "../logging/SwiftLogger";
29+
import { findBinaryPath } from "../utilities/shell";
2930
/**
3031
* Contents of **Info.plist** on Windows.
3132
*/
@@ -548,21 +549,7 @@ export class SwiftToolchain {
548549
break;
549550
}
550551
default: {
551-
// use `type swift` to find `swift`. Run inside /bin/sh to ensure
552-
// we get consistent output as different shells output a different
553-
// format. Tried running with `-p` but that is not available in /bin/sh
554-
const { stdout, stderr } = await execFile("/bin/sh", [
555-
"-c",
556-
"LC_MESSAGES=C type swift",
557-
]);
558-
const swiftMatch = /^swift is (.*)$/.exec(stdout.trimEnd());
559-
if (swiftMatch) {
560-
swift = swiftMatch[1];
561-
} else {
562-
throw Error(
563-
`/bin/sh -c LC_MESSAGES=C type swift: stdout: ${stdout}, stderr: ${stderr}`
564-
);
565-
}
552+
swift = await findBinaryPath("swift");
566553
break;
567554
}
568555
}

src/ui/ToolchainSelection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ async function getQuickPickItems(
259259
}
260260
// Various actions that the user can perform (e.g. to install new toolchains)
261261
const actionItems: ActionItem[] = [];
262-
if (process.platform === "linux" || process.platform === "darwin") {
262+
if (Swiftly.isSupported() && !(await Swiftly.isInstalled())) {
263263
const platformName = process.platform === "linux" ? "Linux" : "macOS";
264264
actionItems.push({
265265
type: "action",

src/utilities/shell.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import { execFile } from "./utilities";
16+
17+
// use `type swift` to find `swift`. Run inside /bin/sh to ensure
18+
// we get consistent output as different shells output a different
19+
// format. Tried running with `-p` but that is not available in /bin/sh
20+
export async function findBinaryPath(binaryName: string): Promise<string> {
21+
const { stdout, stderr } = await execFile("/bin/sh", [
22+
"-c",
23+
`LC_MESSAGES=C type ${binaryName}`,
24+
]);
25+
const binaryNameMatch = new RegExp(`^${binaryName} is (.*)$`).exec(stdout.trimEnd());
26+
if (binaryNameMatch) {
27+
return binaryNameMatch[1];
28+
} else {
29+
throw Error(
30+
`/bin/sh -c LC_MESSAGES=C type ${binaryName}: stdout: ${stdout}, stderr: ${stderr}`
31+
);
32+
}
33+
}

test/unit-tests/toolchain/swiftly.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
import { expect } from "chai";
1616
import { Swiftly } from "../../../src/toolchain/swiftly";
1717
import * as utilities from "../../../src/utilities/utilities";
18+
import * as shell from "../../../src/utilities/shell";
1819
import { mockGlobalModule, mockGlobalValue } from "../../MockUtils";
1920

2021
suite("Swiftly Unit Tests", () => {
2122
const mockUtilities = mockGlobalModule(utilities);
23+
const mockShell = mockGlobalModule(shell);
2224
const mockedPlatform = mockGlobalValue(process, "platform");
2325

2426
setup(() => {
@@ -102,4 +104,33 @@ suite("Swiftly Unit Tests", () => {
102104
expect(mockUtilities.execFile).not.have.been.called;
103105
});
104106
});
107+
108+
suite("isInstalled", () => {
109+
test("should return true when swiftly is found", async () => {
110+
mockShell.findBinaryPath.withArgs("swiftly").resolves("/usr/local/bin/swiftly");
111+
112+
const result = await Swiftly.isInstalled();
113+
114+
expect(result).to.be.true;
115+
expect(mockShell.findBinaryPath).to.have.been.calledWith("swiftly");
116+
});
117+
118+
test("should return false when swiftly is not found", async () => {
119+
mockShell.findBinaryPath.withArgs("swiftly").rejects(new Error("not found"));
120+
121+
const result = await Swiftly.isInstalled();
122+
123+
expect(result).to.be.false;
124+
expect(mockShell.findBinaryPath).to.have.been.calledWith("swiftly");
125+
});
126+
127+
test("should return false when platform is not supported", async () => {
128+
mockedPlatform.setValue("win32");
129+
130+
const result = await Swiftly.isInstalled();
131+
132+
expect(result).to.be.false;
133+
expect(mockShell.findBinaryPath).not.to.have.been.called;
134+
});
135+
});
105136
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import { beforeEach, afterEach } from "mocha";
16+
import { expect } from "chai";
17+
import * as sinon from "sinon";
18+
import { findBinaryPath } from "../../../src/utilities/shell";
19+
import * as utilities from "../../../src/utilities/utilities";
20+
21+
suite("Shell Unit Test Suite", () => {
22+
let execFileStub: sinon.SinonStub;
23+
24+
beforeEach(() => {
25+
execFileStub = sinon.stub(utilities, "execFile");
26+
});
27+
28+
afterEach(() => {
29+
sinon.restore();
30+
});
31+
32+
suite("findBinaryPath", () => {
33+
test("returns the path to a binary in the PATH", async () => {
34+
execFileStub.resolves({
35+
stdout: "node is /usr/local/bin/node\n",
36+
stderr: "",
37+
});
38+
39+
const binaryPath = await findBinaryPath("node");
40+
expect(binaryPath).to.equal("/usr/local/bin/node");
41+
expect(execFileStub).to.have.been.calledWith("/bin/sh", [
42+
"-c",
43+
"LC_MESSAGES=C type node",
44+
]);
45+
});
46+
47+
test("throws for a non-existent binary", async () => {
48+
execFileStub.resolves({
49+
stdout: "",
50+
stderr: "sh: type: nonexistentbinary: not found\n",
51+
});
52+
53+
try {
54+
await findBinaryPath("nonexistentbinary");
55+
expect.fail("Expected an error to be thrown for a non-existent binary");
56+
} catch (error) {
57+
expect(error).to.be.an("error");
58+
expect((error as Error).message).to.include("nonexistentbinary");
59+
}
60+
});
61+
});
62+
});

0 commit comments

Comments
 (0)