Skip to content

Fixed JSON parsing error while selecting toolchain #1743

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

Merged
merged 4 commits into from
Jul 25, 2025
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
5 changes: 3 additions & 2 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,15 @@ import { generateSourcekitConfiguration } from "./commands/generateSourcekitConf
export type WorkspaceContextWithToolchain = WorkspaceContext & { toolchain: SwiftToolchain };

export function registerToolchainCommands(
toolchain: SwiftToolchain | undefined
toolchain: SwiftToolchain | undefined,
cwd?: vscode.Uri
): vscode.Disposable[] {
return [
vscode.commands.registerCommand("swift.createNewProject", () =>
createNewProject(toolchain)
),
vscode.commands.registerCommand("swift.selectToolchain", () =>
showToolchainSelectionQuickPick(toolchain)
showToolchainSelectionQuickPick(toolchain, cwd)
),
vscode.commands.registerCommand("swift.pickProcess", configuration =>
pickProcess(configuration)
Expand Down
4 changes: 3 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {

context.subscriptions.push(new SwiftEnvironmentVariablesManager(context));
context.subscriptions.push(SwiftTerminalProfileProvider.register());
context.subscriptions.push(...commands.registerToolchainCommands(toolchain));
context.subscriptions.push(
...commands.registerToolchainCommands(toolchain, workspaceContext.currentFolder?.folder)
);

// Watch for configuration changes the trigger a reload of the extension if necessary.
context.subscriptions.push(
Expand Down
80 changes: 64 additions & 16 deletions src/toolchain/swiftly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,36 @@ import * as vscode from "vscode";
import { Version } from "../utilities/version";
import { z } from "zod";

const ListAvailableResult = z.object({
const ListResult = 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(),
major: z.number().optional(),
minor: z.number().optional(),
patch: z.number().optional(),
name: z.string(),
type: z.literal("stable"),
}),
z.object({
major: z.number(),
minor: z.number(),
major: z.number().optional(),
minor: z.number().optional(),
branch: z.string(),
date: z.string(),

name: z.string(),
type: z.literal("snapshot"),
}),
]),
})
),
});

const InUseVersionResult = z.object({
version: z.string(),
});

export class Swiftly {
/**
* Finds the version of Swiftly installed on the system.
Expand All @@ -69,6 +72,27 @@ export class Swiftly {
}
}

/**
* Checks if the installed version of Swiftly supports JSON output.
*
* @returns `true` if JSON output is supported, `false` otherwise.
*/
private static async supportsJsonOutput(
outputChannel?: vscode.OutputChannel
): Promise<boolean> {
if (!Swiftly.isSupported()) {
return false;
}
try {
const { stdout } = await execFile("swiftly", ["--version"]);
const version = Version.fromString(stdout.trim());
return version?.isGreaterThanOrEqual(new Version(1, 1, 0)) ?? false;
} catch (error) {
outputChannel?.appendLine(`Failed to check Swiftly JSON support: ${error}`);
return false;
}
}

/**
* Finds the list of toolchains managed by Swiftly.
*
Expand All @@ -86,7 +110,7 @@ export class Swiftly {
return [];
}

if (version.isLessThan(new Version(1, 1, 0))) {
if (!(await Swiftly.supportsJsonOutput(outputChannel))) {
return await Swiftly.getToolchainInstallLegacy(outputChannel);
}

Expand All @@ -97,14 +121,12 @@ export class Swiftly {
outputChannel?: vscode.OutputChannel
): Promise<string[]> {
try {
const { stdout } = await execFile("swiftly", ["list-available", "--format=json"]);
const response = ListAvailableResult.parse(JSON.parse(stdout));
return response.toolchains.map(t => t.name);
const { stdout } = await execFile("swiftly", ["list", "--format=json"]);
const response = ListResult.parse(JSON.parse(stdout));
return response.toolchains.map(t => t.version.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}`
);
return [];
}
}

Expand Down Expand Up @@ -137,13 +159,39 @@ export class Swiftly {
return process.platform === "linux" || process.platform === "darwin";
}

public static async inUseLocation(swiftlyPath: string, cwd?: vscode.Uri) {
public static async inUseLocation(swiftlyPath: string = "swiftly", cwd?: vscode.Uri) {
const { stdout: inUse } = await execFile(swiftlyPath, ["use", "--print-location"], {
cwd: cwd?.fsPath,
});
return inUse.trimEnd();
}

public static async inUseVersion(
swiftlyPath: string = "swiftly",
cwd?: vscode.Uri
): Promise<string | undefined> {
if (!this.isSupported()) {
throw new Error("Swiftly is not supported on this platform");
}

if (!(await Swiftly.supportsJsonOutput())) {
return undefined;
}

const { stdout } = await execFile(swiftlyPath, ["use", "--format=json"], {
cwd: cwd?.fsPath,
});
const result = InUseVersionResult.parse(JSON.parse(stdout));
return result.version;
}

public static async use(version: string): Promise<void> {
if (!this.isSupported()) {
throw new Error("Swiftly is not supported on this platform");
}
await execFile("swiftly", ["use", version]);
}

/**
* Determine if Swiftly is being used to manage the active toolchain and if so, return
* the path to the active toolchain.
Expand Down
21 changes: 16 additions & 5 deletions src/toolchain/toolchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ export class SwiftToolchain {
public customSDK?: string,
public xcTestPath?: string,
public swiftTestingPath?: string,
public swiftPMTestingHelperPath?: string
public swiftPMTestingHelperPath?: string,
public isSwiftlyManaged: boolean = false // true if this toolchain is managed by Swiftly
) {
this.swiftVersionString = targetInfo.compilerVersion;
}
Expand All @@ -121,7 +122,10 @@ export class SwiftToolchain {
folder?: vscode.Uri,
outputChannel?: vscode.OutputChannel
): Promise<SwiftToolchain> {
const swiftFolderPath = await this.getSwiftFolderPath(folder, outputChannel);
const { path: swiftFolderPath, isSwiftlyManaged } = await this.getSwiftFolderPath(
folder,
outputChannel
);
const toolchainPath = await this.getToolchainPath(swiftFolderPath, folder, outputChannel);
const targetInfo = await this.getSwiftTargetInfo(
this._getToolchainExecutable(toolchainPath, "swift")
Expand Down Expand Up @@ -159,7 +163,8 @@ export class SwiftToolchain {
customSDK,
xcTestPath,
swiftTestingPath,
swiftPMTestingHelperPath
swiftPMTestingHelperPath,
isSwiftlyManaged
);
}

Expand Down Expand Up @@ -518,7 +523,7 @@ export class SwiftToolchain {
private static async getSwiftFolderPath(
cwd?: vscode.Uri,
outputChannel?: vscode.OutputChannel
): Promise<string> {
): Promise<{ path: string; isSwiftlyManaged: boolean }> {
try {
let swift: string;
if (configuration.path !== "") {
Expand Down Expand Up @@ -564,18 +569,24 @@ export class SwiftToolchain {
}
// swift may be a symbolic link
let realSwift = await fs.realpath(swift);
let isSwiftlyManaged = false;

if (path.basename(realSwift) === "swiftly") {
try {
const inUse = await Swiftly.inUseLocation(realSwift, cwd);
if (inUse) {
realSwift = path.join(inUse, "usr", "bin", "swift");
isSwiftlyManaged = true;
}
} catch {
// Ignore, will fall back to original path
}
}
const swiftPath = expandFilePathTilde(path.dirname(realSwift));
return await this.getSwiftEnvPath(swiftPath);
return {
path: await this.getSwiftEnvPath(swiftPath),
isSwiftlyManaged,
};
} catch (error) {
outputChannel?.appendLine(`Failed to find swift executable: ${error}`);
throw Error("Failed to find swift executable");
Expand Down
81 changes: 66 additions & 15 deletions src/ui/ToolchainSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,25 +98,27 @@ export async function selectToolchain() {
}

/** A {@link vscode.QuickPickItem} that contains the path to an installed Swift toolchain */
type SwiftToolchainItem = PublicSwiftToolchainItem | XcodeToolchainItem;
type SwiftToolchainItem = PublicSwiftToolchainItem | XcodeToolchainItem | SwiftlyToolchainItem;

/** Common properties for a {@link vscode.QuickPickItem} that represents a Swift toolchain */
interface BaseSwiftToolchainItem extends vscode.QuickPickItem {
type: "toolchain";
toolchainPath: string;
swiftFolderPath: string;
onDidSelect?(): Promise<void>;
}

/** A {@link vscode.QuickPickItem} for a Swift toolchain that has been installed manually */
interface PublicSwiftToolchainItem extends BaseSwiftToolchainItem {
category: "public" | "swiftly";
category: "public";
toolchainPath: string;
swiftFolderPath: string;
}

/** A {@link vscode.QuickPickItem} for a Swift toolchain provided by an installed Xcode application */
interface XcodeToolchainItem extends BaseSwiftToolchainItem {
category: "xcode";
xcodePath: string;
toolchainPath: string;
swiftFolderPath: string;
}

/** A {@link vscode.QuickPickItem} that performs an action for the user */
Expand All @@ -125,6 +127,11 @@ interface ActionItem extends vscode.QuickPickItem {
run(): Promise<void>;
}

interface SwiftlyToolchainItem extends BaseSwiftToolchainItem {
category: "swiftly";
version: string;
}

/** A {@link vscode.QuickPickItem} that separates items in the UI */
class SeparatorItem implements vscode.QuickPickItem {
readonly type = "separator";
Expand All @@ -146,7 +153,8 @@ type SelectToolchainItem = SwiftToolchainItem | ActionItem | SeparatorItem;
* @returns an array of {@link SelectToolchainItem}
*/
async function getQuickPickItems(
activeToolchain: SwiftToolchain | undefined
activeToolchain: SwiftToolchain | undefined,
cwd?: vscode.Uri
): Promise<SelectToolchainItem[]> {
// Find any Xcode installations on the system
const xcodes = (await SwiftToolchain.findXcodeInstalls())
Expand Down Expand Up @@ -195,18 +203,43 @@ async function getQuickPickItems(
// Find any Swift toolchains installed via Swiftly
const swiftlyToolchains = (await Swiftly.listAvailableToolchains())
.reverse()
.map<SwiftToolchainItem>(toolchainPath => ({
.map<SwiftlyToolchainItem>(toolchainPath => ({
type: "toolchain",
category: "swiftly",
label: path.basename(toolchainPath),
detail: toolchainPath,
toolchainPath: path.join(toolchainPath, "usr"),
swiftFolderPath: path.join(toolchainPath, "usr", "bin"),
category: "swiftly",
version: path.basename(toolchainPath),
onDidSelect: async () => {
try {
await Swiftly.use(toolchainPath);
void showReloadExtensionNotification(
"Changing the Swift path requires Visual Studio Code be reloaded."
);
} catch (error) {
void vscode.window.showErrorMessage(
`Failed to switch Swiftly toolchain: ${error}`
);
}
},
}));
// Mark which toolchain is being actively used
if (activeToolchain) {
const currentSwiftlyVersion = activeToolchain.isSwiftlyManaged
? await Swiftly.inUseVersion("swiftly", cwd)
: undefined;
const toolchainInUse = [...xcodes, ...toolchains, ...swiftlyToolchains].find(toolchain => {
return toolchain.toolchainPath === activeToolchain.toolchainPath;
if (currentSwiftlyVersion) {
if (toolchain.category !== "swiftly") {
return false;
}

// For Swiftly toolchains, check if the label matches the active toolchain version
return currentSwiftlyVersion === toolchain.label;
}
// For non-Swiftly toolchains, check if the toolchain path matches
return (
(toolchain as PublicSwiftToolchainItem | XcodeToolchainItem).toolchainPath ===
activeToolchain.toolchainPath
);
});
if (toolchainInUse) {
toolchainInUse.description = "$(check) in use";
Expand Down Expand Up @@ -262,10 +295,13 @@ async function getQuickPickItems(
*
* @param activeToolchain the {@link WorkspaceContext}
*/
export async function showToolchainSelectionQuickPick(activeToolchain: SwiftToolchain | undefined) {
export async function showToolchainSelectionQuickPick(
activeToolchain: SwiftToolchain | undefined,
cwd?: vscode.Uri
) {
let xcodePaths: string[] = [];
const selected = await vscode.window.showQuickPick<SelectToolchainItem>(
getQuickPickItems(activeToolchain).then(result => {
getQuickPickItems(activeToolchain, cwd).then(result => {
xcodePaths = result
.filter((i): i is XcodeToolchainItem => "category" in i && i.category === "xcode")
.map(xcode => xcode.xcodePath);
Expand Down Expand Up @@ -303,8 +339,23 @@ export async function showToolchainSelectionQuickPick(activeToolchain: SwiftTool
});
}
}
// Update the toolchain path
const isUpdated = await setToolchainPath(selected.swiftFolderPath, developerDir);
// Update the toolchain path`
let swiftPath: string | undefined;

// Handle Swiftly toolchains specially
if (selected.category === "swiftly") {
try {
swiftPath = undefined;
} catch (error) {
void vscode.window.showErrorMessage(`Failed to switch Swiftly toolchain: ${error}`);
return;
}
} else {
// For non-Swiftly toolchains, use the swiftFolderPath
swiftPath = selected.swiftFolderPath;
}

const isUpdated = await setToolchainPath(swiftPath, developerDir);
if (isUpdated && selected.onDidSelect) {
await selected.onDidSelect();
}
Expand Down
Loading