Skip to content

Commit f82b363

Browse files
authored
Improve handling misconfigured Swift toolchain (#1801)
* Improve handling misconfigured Swift toolchain This patch makes a few improvements around handling a misconfigured toolchain. - Register the Select Toolchain command before prompting the user with an error dialog during extension activation. There was a button in the dialog to Select Toolchain, but the command wasn't registered yet so they'd get an error. - Prompt the user with this same error dialog if they have a misconfigured toolchain for a specific folder. Typically this happens when the user adds a folder to an existing workspace, and that folder has a misconfigured toolchain in the `.vscode/settings.json` file.
1 parent 62291c8 commit f82b363

File tree

5 files changed

+247
-13
lines changed

5 files changed

+247
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
### Fixed
1313

1414
- Don't start debugging XCTest cases if the swift-testing debug session was stopped ([#1797](https://github.com/swiftlang/vscode-swift/pull/1797))
15+
- Improve error handling when the swift path is misconfigured ([#1801](https://github.com/swiftlang/vscode-swift/pull/1801))
1516

1617
## 2.11.20250806 - 2025-08-06
1718

src/FolderContext.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { isPathInsidePath } from "./utilities/filesystem";
2626
import { SwiftToolchain } from "./toolchain/toolchain";
2727
import { SwiftLogger } from "./logging/SwiftLogger";
2828
import { TestRunProxy } from "./TestExplorer/TestRunner";
29+
import { showToolchainError } from "./ui/ToolchainSelection";
2930

3031
export class FolderContext implements vscode.Disposable {
3132
public backgroundCompilation: BackgroundCompilation;
@@ -85,7 +86,40 @@ export class FolderContext implements vscode.Disposable {
8586
const statusItemText = `Loading Package (${FolderContext.uriName(folder)})`;
8687
workspaceContext.statusItem.start(statusItemText);
8788

88-
const toolchain = await SwiftToolchain.create(folder);
89+
let toolchain: SwiftToolchain;
90+
try {
91+
toolchain = await SwiftToolchain.create(folder);
92+
} catch (error) {
93+
// This error case is quite hard for the user to get in to, but possible.
94+
// Typically on startup the toolchain creation failure is going to happen in
95+
// the extension activation in extension.ts. However if they incorrectly configure
96+
// their path post activation, and add a new folder to the workspace, this failure can occur.
97+
workspaceContext.logger.error(
98+
`Failed to discover Swift toolchain for ${FolderContext.uriName(folder)}: ${error}`,
99+
FolderContext.uriName(folder)
100+
);
101+
const userMadeSelection = await showToolchainError(folder);
102+
if (userMadeSelection) {
103+
// User updated toolchain settings, retry once
104+
try {
105+
toolchain = await SwiftToolchain.create(folder);
106+
workspaceContext.logger.info(
107+
`Successfully created toolchain for ${FolderContext.uriName(folder)} after user selection`,
108+
FolderContext.uriName(folder)
109+
);
110+
} catch (retryError) {
111+
workspaceContext.logger.error(
112+
`Failed to create toolchain for ${FolderContext.uriName(folder)} even after user selection: ${retryError}`,
113+
FolderContext.uriName(folder)
114+
);
115+
// Fall back to global toolchain
116+
toolchain = workspaceContext.globalToolchain;
117+
}
118+
} else {
119+
toolchain = workspaceContext.globalToolchain;
120+
}
121+
}
122+
89123
const { linuxMain, swiftPackage } =
90124
await workspaceContext.statusItem.showStatusWhileRunning(statusItemText, async () => {
91125
const linuxMain = await LinuxMain.create(folder);

src/extension.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,24 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
7474
// This can happen if the user has not installed Swift or if the toolchain is not
7575
// properly configured.
7676
if (!toolchain) {
77-
void showToolchainError();
78-
return {
79-
workspaceContext: undefined,
80-
logger,
81-
activate: () => activate(context),
82-
deactivate: async () => {
83-
await deactivate(context);
84-
},
85-
};
77+
// In order to select a toolchain we need to register the command first.
78+
const subscriptions = commands.registerToolchainCommands(undefined, logger, undefined);
79+
const chosenRemediation = await showToolchainError();
80+
subscriptions.forEach(sub => sub.dispose());
81+
82+
// If they tried to fix the improperly configured toolchain, re-initialize the extension.
83+
if (chosenRemediation) {
84+
return activate(context);
85+
} else {
86+
return {
87+
workspaceContext: undefined,
88+
logger,
89+
activate: () => activate(context),
90+
deactivate: async () => {
91+
await deactivate(context);
92+
},
93+
};
94+
}
8695
}
8796

8897
const workspaceContext = new WorkspaceContext(context, contextKeys, logger, toolchain);

src/ui/ToolchainSelection.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import configuration from "../configuration";
2020
import { Commands } from "../commands";
2121
import { Swiftly } from "../toolchain/swiftly";
2222
import { SwiftLogger } from "../logging/SwiftLogger";
23+
import { FolderContext } from "../FolderContext";
2324

2425
/**
2526
* Open the installation page on Swift.org
@@ -71,27 +72,32 @@ export async function selectToolchainFolder() {
7172

7273
/**
7374
* Displays an error notification to the user that toolchain discovery failed.
75+
* @returns true if the user made a selection (and potentially updated toolchain settings), false if they dismissed the dialog
7476
*/
75-
export async function showToolchainError(): Promise<void> {
77+
export async function showToolchainError(folder?: vscode.Uri): Promise<boolean> {
7678
let selected: "Remove From Settings" | "Select Toolchain" | undefined;
79+
const folderName = folder ? `${FolderContext.uriName(folder)}: ` : "";
7780
if (configuration.path) {
7881
selected = await vscode.window.showErrorMessage(
79-
`The Swift executable at "${configuration.path}" either could not be found or failed to launch. Please select a new toolchain.`,
82+
`${folderName}The Swift executable at "${configuration.path}" either could not be found or failed to launch. Please select a new toolchain.`,
8083
"Remove From Settings",
8184
"Select Toolchain"
8285
);
8386
} else {
8487
selected = await vscode.window.showErrorMessage(
85-
"Unable to automatically discover your Swift toolchain. Either install a toolchain from Swift.org or provide the path to an existing toolchain.",
88+
`${folderName}Unable to automatically discover your Swift toolchain. Either install a toolchain from Swift.org or provide the path to an existing toolchain.`,
8689
"Select Toolchain"
8790
);
8891
}
8992

9093
if (selected === "Remove From Settings") {
9194
await removeToolchainPath();
95+
return true;
9296
} else if (selected === "Select Toolchain") {
9397
await selectToolchain();
98+
return true;
9499
}
100+
return false;
95101
}
96102

97103
export async function selectToolchain() {
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2021-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 * as assert from "assert";
16+
import * as toolchain from "../../src/ui/ToolchainSelection";
17+
import { afterEach } from "mocha";
18+
import { stub, restore } from "sinon";
19+
import { testAssetUri } from "../fixtures";
20+
import { WorkspaceContext } from "../../src/WorkspaceContext";
21+
import { FolderContext } from "../../src/FolderContext";
22+
import { SwiftToolchain } from "../../src/toolchain/toolchain";
23+
import { activateExtensionForSuite, getRootWorkspaceFolder } from "./utilities/testutilities";
24+
import { MockedFunction, mockGlobalValue } from "../MockUtils";
25+
26+
suite("FolderContext Error Handling Test Suite", () => {
27+
let workspaceContext: WorkspaceContext;
28+
let swiftToolchainCreateStub: MockedFunction<typeof SwiftToolchain.create>;
29+
const showToolchainError = mockGlobalValue(toolchain, "showToolchainError");
30+
31+
activateExtensionForSuite({
32+
async setup(ctx) {
33+
workspaceContext = ctx;
34+
this.timeout(60000);
35+
},
36+
testAssets: ["defaultPackage"],
37+
});
38+
39+
afterEach(() => {
40+
restore();
41+
});
42+
43+
test("handles SwiftToolchain.create failure gracefully with user dismissal", async () => {
44+
const mockError = new Error("Mock toolchain failure");
45+
swiftToolchainCreateStub = stub(SwiftToolchain, "create").throws(mockError);
46+
47+
// Mock showToolchainError to return false (user dismissed dialog)
48+
const showToolchainErrorStub = stub().resolves(false);
49+
showToolchainError.setValue(showToolchainErrorStub);
50+
51+
const workspaceFolder = getRootWorkspaceFolder();
52+
const testFolder = testAssetUri("package2");
53+
54+
const folderContext = await FolderContext.create(
55+
testFolder,
56+
workspaceFolder,
57+
workspaceContext
58+
);
59+
60+
assert.ok(folderContext, "FolderContext should be created despite toolchain failure");
61+
assert.strictEqual(
62+
folderContext.toolchain,
63+
workspaceContext.globalToolchain,
64+
"Should fallback to global toolchain when user dismisses dialog"
65+
);
66+
67+
const errorLogs = workspaceContext.logger.logs.filter(
68+
log =>
69+
log.includes("Failed to discover Swift toolchain") &&
70+
log.includes("package2") &&
71+
log.includes("Mock toolchain failure")
72+
);
73+
assert.ok(errorLogs.length > 0, "Should log error message with folder context");
74+
75+
assert.ok(
76+
swiftToolchainCreateStub.calledWith(testFolder),
77+
"Should attempt to create toolchain for specific folder"
78+
);
79+
assert.strictEqual(
80+
swiftToolchainCreateStub.callCount,
81+
1,
82+
"Should only call SwiftToolchain.create once when user dismisses"
83+
);
84+
});
85+
86+
test("retries toolchain creation when user makes selection and succeeds", async () => {
87+
const workspaceFolder = getRootWorkspaceFolder();
88+
const testFolder = testAssetUri("package2");
89+
90+
// Arrange: Mock SwiftToolchain.create to fail first time, succeed second time
91+
swiftToolchainCreateStub = stub(SwiftToolchain, "create");
92+
swiftToolchainCreateStub.onFirstCall().throws(new Error("Initial toolchain failure"));
93+
swiftToolchainCreateStub
94+
.onSecondCall()
95+
.returns(Promise.resolve(workspaceContext.globalToolchain));
96+
97+
// Mock showToolchainError to return true (user made selection)
98+
const showToolchainErrorStub = stub().resolves(true);
99+
showToolchainError.setValue(showToolchainErrorStub);
100+
101+
const folderContext = await FolderContext.create(
102+
testFolder,
103+
workspaceFolder,
104+
workspaceContext
105+
);
106+
107+
// Assert: FolderContext should be created successfully
108+
assert.ok(folderContext, "FolderContext should be created after retry");
109+
assert.strictEqual(
110+
folderContext.toolchain,
111+
workspaceContext.globalToolchain,
112+
"Should use successfully created toolchain after retry"
113+
);
114+
115+
// Assert: SwiftToolchain.create should be called twice (initial + retry)
116+
assert.strictEqual(
117+
swiftToolchainCreateStub.callCount,
118+
2,
119+
"Should retry toolchain creation after user selection"
120+
);
121+
122+
// Assert: Should log both failure and success
123+
const failureLogs = workspaceContext.logger.logs.filter(log =>
124+
log.includes("Failed to discover Swift toolchain for package2")
125+
);
126+
const successLogs = workspaceContext.logger.logs.filter(log =>
127+
log.includes("Successfully created toolchain for package2 after user selection")
128+
);
129+
130+
assert.ok(failureLogs.length > 0, "Should log initial failure");
131+
assert.ok(successLogs.length > 0, "Should log success after retry");
132+
});
133+
134+
test("retries toolchain creation when user makes selection but still fails", async () => {
135+
const workspaceFolder = getRootWorkspaceFolder();
136+
const testFolder = testAssetUri("package2");
137+
138+
const initialError = new Error("Initial toolchain failure");
139+
const retryError = new Error("Retry toolchain failure");
140+
swiftToolchainCreateStub = stub(SwiftToolchain, "create");
141+
swiftToolchainCreateStub.onFirstCall().throws(initialError);
142+
swiftToolchainCreateStub.onSecondCall().throws(retryError);
143+
144+
// Mock showToolchainError to return true (user made selection)
145+
const showToolchainErrorStub = stub().resolves(true);
146+
showToolchainError.setValue(showToolchainErrorStub);
147+
148+
const folderContext = await FolderContext.create(
149+
testFolder,
150+
workspaceFolder,
151+
workspaceContext
152+
);
153+
154+
assert.ok(
155+
folderContext,
156+
"FolderContext should be created with fallback after retry failure"
157+
);
158+
assert.strictEqual(
159+
folderContext.toolchain,
160+
workspaceContext.globalToolchain,
161+
"Should fallback to global toolchain when retry also fails"
162+
);
163+
164+
assert.strictEqual(
165+
swiftToolchainCreateStub.callCount,
166+
2,
167+
"Should retry toolchain creation after user selection"
168+
);
169+
170+
const initialFailureLogs = workspaceContext.logger.logs.filter(log =>
171+
log.includes(
172+
"Failed to discover Swift toolchain for package2: Error: Initial toolchain failure"
173+
)
174+
);
175+
const retryFailureLogs = workspaceContext.logger.logs.filter(log =>
176+
log.includes(
177+
"Failed to create toolchain for package2 even after user selection: Error: Retry toolchain failure"
178+
)
179+
);
180+
181+
assert.ok(initialFailureLogs.length > 0, "Should log initial failure");
182+
assert.ok(retryFailureLogs.length > 0, "Should log retry failure");
183+
});
184+
});

0 commit comments

Comments
 (0)