Skip to content

Improve handling misconfigured Swift toolchain #1801

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
### Fixed

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

## 2.11.20250806 - 2025-08-06

Expand Down
36 changes: 35 additions & 1 deletion src/FolderContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { isPathInsidePath } from "./utilities/filesystem";
import { SwiftToolchain } from "./toolchain/toolchain";
import { SwiftLogger } from "./logging/SwiftLogger";
import { TestRunProxy } from "./TestExplorer/TestRunner";
import { showToolchainError } from "./ui/ToolchainSelection";

export class FolderContext implements vscode.Disposable {
public backgroundCompilation: BackgroundCompilation;
Expand Down Expand Up @@ -77,7 +78,40 @@ export class FolderContext implements vscode.Disposable {
const statusItemText = `Loading Package (${FolderContext.uriName(folder)})`;
workspaceContext.statusItem.start(statusItemText);

const toolchain = await SwiftToolchain.create(folder);
let toolchain: SwiftToolchain;
try {
toolchain = await SwiftToolchain.create(folder);
} catch (error) {
// This error case is quite hard for the user to get in to, but possible.
// Typically on startup the toolchain creation failure is going to happen in
// the extension activation in extension.ts. However if they incorrectly configure
// their path post activation, and add a new folder to the workspace, this failure can occur.
workspaceContext.logger.error(
`Failed to discover Swift toolchain for ${FolderContext.uriName(folder)}: ${error}`,
FolderContext.uriName(folder)
);
const userMadeSelection = await showToolchainError(folder);
if (userMadeSelection) {
// User updated toolchain settings, retry once
try {
toolchain = await SwiftToolchain.create(folder);
workspaceContext.logger.info(
`Successfully created toolchain for ${FolderContext.uriName(folder)} after user selection`,
FolderContext.uriName(folder)
);
} catch (retryError) {
workspaceContext.logger.error(
`Failed to create toolchain for ${FolderContext.uriName(folder)} even after user selection: ${retryError}`,
FolderContext.uriName(folder)
);
// Fall back to global toolchain
toolchain = workspaceContext.globalToolchain;
}
} else {
toolchain = workspaceContext.globalToolchain;
}
}

const { linuxMain, swiftPackage } =
await workspaceContext.statusItem.showStatusWhileRunning(statusItemText, async () => {
const linuxMain = await LinuxMain.create(folder);
Expand Down
27 changes: 18 additions & 9 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,24 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
// This can happen if the user has not installed Swift or if the toolchain is not
// properly configured.
if (!toolchain) {
void showToolchainError();
return {
workspaceContext: undefined,
logger,
activate: () => activate(context),
deactivate: async () => {
await deactivate(context);
},
};
// In order to select a toolchain we need to register the command first.
const subscriptions = commands.registerToolchainCommands(undefined, logger, undefined);
const chosenRemediation = await showToolchainError();
subscriptions.forEach(sub => sub.dispose());

// If they tried to fix the improperly configured toolchain, re-initialize the extension.
if (chosenRemediation) {
return activate(context);
} else {
return {
workspaceContext: undefined,
logger,
activate: () => activate(context),
deactivate: async () => {
await deactivate(context);
},
};
}
}

const workspaceContext = new WorkspaceContext(context, logger, toolchain);
Expand Down
12 changes: 9 additions & 3 deletions src/ui/ToolchainSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import configuration from "../configuration";
import { Commands } from "../commands";
import { Swiftly } from "../toolchain/swiftly";
import { SwiftLogger } from "../logging/SwiftLogger";
import { FolderContext } from "../FolderContext";

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

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

if (selected === "Remove From Settings") {
await removeToolchainPath();
return true;
} else if (selected === "Select Toolchain") {
await selectToolchain();
return true;
}
return false;
}

export async function selectToolchain() {
Expand Down
184 changes: 184 additions & 0 deletions test/integration-tests/FolderContext.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the VS Code Swift open source project
//
// Copyright (c) 2021-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 assert from "assert";
import * as toolchain from "../../src/ui/ToolchainSelection";
import { afterEach } from "mocha";
import { stub, restore } from "sinon";
import { testAssetUri } from "../fixtures";
import { WorkspaceContext } from "../../src/WorkspaceContext";
import { FolderContext } from "../../src/FolderContext";
import { SwiftToolchain } from "../../src/toolchain/toolchain";
import { activateExtensionForSuite, getRootWorkspaceFolder } from "./utilities/testutilities";
import { MockedFunction, mockGlobalValue } from "../MockUtils";

suite("FolderContext Error Handling Test Suite", () => {
let workspaceContext: WorkspaceContext;
let swiftToolchainCreateStub: MockedFunction<typeof SwiftToolchain.create>;
const showToolchainError = mockGlobalValue(toolchain, "showToolchainError");

activateExtensionForSuite({
async setup(ctx) {
workspaceContext = ctx;
this.timeout(60000);
},
testAssets: ["defaultPackage"],
});

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

test("handles SwiftToolchain.create failure gracefully with user dismissal", async () => {
const mockError = new Error("Mock toolchain failure");
swiftToolchainCreateStub = stub(SwiftToolchain, "create").throws(mockError);

// Mock showToolchainError to return false (user dismissed dialog)
const showToolchainErrorStub = stub().resolves(false);
showToolchainError.setValue(showToolchainErrorStub);

const workspaceFolder = getRootWorkspaceFolder();
const testFolder = testAssetUri("package2");

const folderContext = await FolderContext.create(
testFolder,
workspaceFolder,
workspaceContext
);

assert.ok(folderContext, "FolderContext should be created despite toolchain failure");
assert.strictEqual(
folderContext.toolchain,
workspaceContext.globalToolchain,
"Should fallback to global toolchain when user dismisses dialog"
);

const errorLogs = workspaceContext.logger.logs.filter(
log =>
log.includes("Failed to discover Swift toolchain") &&
log.includes("package2") &&
log.includes("Mock toolchain failure")
);
assert.ok(errorLogs.length > 0, "Should log error message with folder context");

assert.ok(
swiftToolchainCreateStub.calledWith(testFolder),
"Should attempt to create toolchain for specific folder"
);
assert.strictEqual(
swiftToolchainCreateStub.callCount,
1,
"Should only call SwiftToolchain.create once when user dismisses"
);
});

test("retries toolchain creation when user makes selection and succeeds", async () => {
const workspaceFolder = getRootWorkspaceFolder();
const testFolder = testAssetUri("package2");

// Arrange: Mock SwiftToolchain.create to fail first time, succeed second time
swiftToolchainCreateStub = stub(SwiftToolchain, "create");
swiftToolchainCreateStub.onFirstCall().throws(new Error("Initial toolchain failure"));
swiftToolchainCreateStub
.onSecondCall()
.returns(Promise.resolve(workspaceContext.globalToolchain));

// Mock showToolchainError to return true (user made selection)
const showToolchainErrorStub = stub().resolves(true);
showToolchainError.setValue(showToolchainErrorStub);

const folderContext = await FolderContext.create(
testFolder,
workspaceFolder,
workspaceContext
);

// Assert: FolderContext should be created successfully
assert.ok(folderContext, "FolderContext should be created after retry");
assert.strictEqual(
folderContext.toolchain,
workspaceContext.globalToolchain,
"Should use successfully created toolchain after retry"
);

// Assert: SwiftToolchain.create should be called twice (initial + retry)
assert.strictEqual(
swiftToolchainCreateStub.callCount,
2,
"Should retry toolchain creation after user selection"
);

// Assert: Should log both failure and success
const failureLogs = workspaceContext.logger.logs.filter(log =>
log.includes("Failed to discover Swift toolchain for package2")
);
const successLogs = workspaceContext.logger.logs.filter(log =>
log.includes("Successfully created toolchain for package2 after user selection")
);

assert.ok(failureLogs.length > 0, "Should log initial failure");
assert.ok(successLogs.length > 0, "Should log success after retry");
});

test("retries toolchain creation when user makes selection but still fails", async () => {
const workspaceFolder = getRootWorkspaceFolder();
const testFolder = testAssetUri("package2");

const initialError = new Error("Initial toolchain failure");
const retryError = new Error("Retry toolchain failure");
swiftToolchainCreateStub = stub(SwiftToolchain, "create");
swiftToolchainCreateStub.onFirstCall().throws(initialError);
swiftToolchainCreateStub.onSecondCall().throws(retryError);

// Mock showToolchainError to return true (user made selection)
const showToolchainErrorStub = stub().resolves(true);
showToolchainError.setValue(showToolchainErrorStub);

const folderContext = await FolderContext.create(
testFolder,
workspaceFolder,
workspaceContext
);

assert.ok(
folderContext,
"FolderContext should be created with fallback after retry failure"
);
assert.strictEqual(
folderContext.toolchain,
workspaceContext.globalToolchain,
"Should fallback to global toolchain when retry also fails"
);

assert.strictEqual(
swiftToolchainCreateStub.callCount,
2,
"Should retry toolchain creation after user selection"
);

const initialFailureLogs = workspaceContext.logger.logs.filter(log =>
log.includes(
"Failed to discover Swift toolchain for package2: Error: Initial toolchain failure"
)
);
const retryFailureLogs = workspaceContext.logger.logs.filter(log =>
log.includes(
"Failed to create toolchain for package2 even after user selection: Error: Retry toolchain failure"
)
);

assert.ok(initialFailureLogs.length > 0, "Should log initial failure");
assert.ok(retryFailureLogs.length > 0, "Should log retry failure");
});
});