Skip to content

Commit d2dc6df

Browse files
authored
Fixed JSON parsing error while selecting toolchain (#1743)
1 parent dca7546 commit d2dc6df

File tree

6 files changed

+160
-52
lines changed

6 files changed

+160
-52
lines changed

src/commands.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,15 @@ import { generateSourcekitConfiguration } from "./commands/generateSourcekitConf
6363
export type WorkspaceContextWithToolchain = WorkspaceContext & { toolchain: SwiftToolchain };
6464

6565
export function registerToolchainCommands(
66-
toolchain: SwiftToolchain | undefined
66+
toolchain: SwiftToolchain | undefined,
67+
cwd?: vscode.Uri
6768
): vscode.Disposable[] {
6869
return [
6970
vscode.commands.registerCommand("swift.createNewProject", () =>
7071
createNewProject(toolchain)
7172
),
7273
vscode.commands.registerCommand("swift.selectToolchain", () =>
73-
showToolchainSelectionQuickPick(toolchain)
74+
showToolchainSelectionQuickPick(toolchain, cwd)
7475
),
7576
vscode.commands.registerCommand("swift.pickProcess", configuration =>
7677
pickProcess(configuration)

src/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
8282

8383
context.subscriptions.push(new SwiftEnvironmentVariablesManager(context));
8484
context.subscriptions.push(SwiftTerminalProfileProvider.register());
85-
context.subscriptions.push(...commands.registerToolchainCommands(toolchain));
85+
context.subscriptions.push(
86+
...commands.registerToolchainCommands(toolchain, workspaceContext.currentFolder?.folder)
87+
);
8688

8789
// Watch for configuration changes the trigger a reload of the extension if necessary.
8890
context.subscriptions.push(

src/toolchain/swiftly.ts

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,36 @@ import * as vscode from "vscode";
2020
import { Version } from "../utilities/version";
2121
import { z } from "zod";
2222

23-
const ListAvailableResult = z.object({
23+
const ListResult = z.object({
2424
toolchains: z.array(
2525
z.object({
2626
inUse: z.boolean(),
27-
installed: z.boolean(),
2827
isDefault: z.boolean(),
29-
name: z.string(),
3028
version: z.discriminatedUnion("type", [
3129
z.object({
32-
major: z.number(),
33-
minor: z.number(),
30+
major: z.number().optional(),
31+
minor: z.number().optional(),
3432
patch: z.number().optional(),
33+
name: z.string(),
3534
type: z.literal("stable"),
3635
}),
3736
z.object({
38-
major: z.number(),
39-
minor: z.number(),
37+
major: z.number().optional(),
38+
minor: z.number().optional(),
4039
branch: z.string(),
4140
date: z.string(),
42-
41+
name: z.string(),
4342
type: z.literal("snapshot"),
4443
}),
4544
]),
4645
})
4746
),
4847
});
4948

49+
const InUseVersionResult = z.object({
50+
version: z.string(),
51+
});
52+
5053
export class Swiftly {
5154
/**
5255
* Finds the version of Swiftly installed on the system.
@@ -69,6 +72,27 @@ export class Swiftly {
6972
}
7073
}
7174

75+
/**
76+
* Checks if the installed version of Swiftly supports JSON output.
77+
*
78+
* @returns `true` if JSON output is supported, `false` otherwise.
79+
*/
80+
private static async supportsJsonOutput(
81+
outputChannel?: vscode.OutputChannel
82+
): Promise<boolean> {
83+
if (!Swiftly.isSupported()) {
84+
return false;
85+
}
86+
try {
87+
const { stdout } = await execFile("swiftly", ["--version"]);
88+
const version = Version.fromString(stdout.trim());
89+
return version?.isGreaterThanOrEqual(new Version(1, 1, 0)) ?? false;
90+
} catch (error) {
91+
outputChannel?.appendLine(`Failed to check Swiftly JSON support: ${error}`);
92+
return false;
93+
}
94+
}
95+
7296
/**
7397
* Finds the list of toolchains managed by Swiftly.
7498
*
@@ -86,7 +110,7 @@ export class Swiftly {
86110
return [];
87111
}
88112

89-
if (version.isLessThan(new Version(1, 1, 0))) {
113+
if (!(await Swiftly.supportsJsonOutput(outputChannel))) {
90114
return await Swiftly.getToolchainInstallLegacy(outputChannel);
91115
}
92116

@@ -97,14 +121,12 @@ export class Swiftly {
97121
outputChannel?: vscode.OutputChannel
98122
): Promise<string[]> {
99123
try {
100-
const { stdout } = await execFile("swiftly", ["list-available", "--format=json"]);
101-
const response = ListAvailableResult.parse(JSON.parse(stdout));
102-
return response.toolchains.map(t => t.name);
124+
const { stdout } = await execFile("swiftly", ["list", "--format=json"]);
125+
const response = ListResult.parse(JSON.parse(stdout));
126+
return response.toolchains.map(t => t.version.name);
103127
} catch (error) {
104128
outputChannel?.appendLine(`Failed to retrieve Swiftly installations: ${error}`);
105-
throw new Error(
106-
`Failed to retrieve Swiftly installations from disk: ${(error as Error).message}`
107-
);
129+
return [];
108130
}
109131
}
110132

@@ -137,13 +159,39 @@ export class Swiftly {
137159
return process.platform === "linux" || process.platform === "darwin";
138160
}
139161

140-
public static async inUseLocation(swiftlyPath: string, cwd?: vscode.Uri) {
162+
public static async inUseLocation(swiftlyPath: string = "swiftly", cwd?: vscode.Uri) {
141163
const { stdout: inUse } = await execFile(swiftlyPath, ["use", "--print-location"], {
142164
cwd: cwd?.fsPath,
143165
});
144166
return inUse.trimEnd();
145167
}
146168

169+
public static async inUseVersion(
170+
swiftlyPath: string = "swiftly",
171+
cwd?: vscode.Uri
172+
): Promise<string | undefined> {
173+
if (!this.isSupported()) {
174+
throw new Error("Swiftly is not supported on this platform");
175+
}
176+
177+
if (!(await Swiftly.supportsJsonOutput())) {
178+
return undefined;
179+
}
180+
181+
const { stdout } = await execFile(swiftlyPath, ["use", "--format=json"], {
182+
cwd: cwd?.fsPath,
183+
});
184+
const result = InUseVersionResult.parse(JSON.parse(stdout));
185+
return result.version;
186+
}
187+
188+
public static async use(version: string): Promise<void> {
189+
if (!this.isSupported()) {
190+
throw new Error("Swiftly is not supported on this platform");
191+
}
192+
await execFile("swiftly", ["use", version]);
193+
}
194+
147195
/**
148196
* Determine if Swiftly is being used to manage the active toolchain and if so, return
149197
* the path to the active toolchain.

src/toolchain/toolchain.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ export class SwiftToolchain {
112112
public customSDK?: string,
113113
public xcTestPath?: string,
114114
public swiftTestingPath?: string,
115-
public swiftPMTestingHelperPath?: string
115+
public swiftPMTestingHelperPath?: string,
116+
public isSwiftlyManaged: boolean = false // true if this toolchain is managed by Swiftly
116117
) {
117118
this.swiftVersionString = targetInfo.compilerVersion;
118119
}
@@ -121,7 +122,10 @@ export class SwiftToolchain {
121122
folder?: vscode.Uri,
122123
outputChannel?: vscode.OutputChannel
123124
): Promise<SwiftToolchain> {
124-
const swiftFolderPath = await this.getSwiftFolderPath(folder, outputChannel);
125+
const { path: swiftFolderPath, isSwiftlyManaged } = await this.getSwiftFolderPath(
126+
folder,
127+
outputChannel
128+
);
125129
const toolchainPath = await this.getToolchainPath(swiftFolderPath, folder, outputChannel);
126130
const targetInfo = await this.getSwiftTargetInfo(
127131
this._getToolchainExecutable(toolchainPath, "swift")
@@ -159,7 +163,8 @@ export class SwiftToolchain {
159163
customSDK,
160164
xcTestPath,
161165
swiftTestingPath,
162-
swiftPMTestingHelperPath
166+
swiftPMTestingHelperPath,
167+
isSwiftlyManaged
163168
);
164169
}
165170

@@ -518,7 +523,7 @@ export class SwiftToolchain {
518523
private static async getSwiftFolderPath(
519524
cwd?: vscode.Uri,
520525
outputChannel?: vscode.OutputChannel
521-
): Promise<string> {
526+
): Promise<{ path: string; isSwiftlyManaged: boolean }> {
522527
try {
523528
let swift: string;
524529
if (configuration.path !== "") {
@@ -564,18 +569,24 @@ export class SwiftToolchain {
564569
}
565570
// swift may be a symbolic link
566571
let realSwift = await fs.realpath(swift);
572+
let isSwiftlyManaged = false;
573+
567574
if (path.basename(realSwift) === "swiftly") {
568575
try {
569576
const inUse = await Swiftly.inUseLocation(realSwift, cwd);
570577
if (inUse) {
571578
realSwift = path.join(inUse, "usr", "bin", "swift");
579+
isSwiftlyManaged = true;
572580
}
573581
} catch {
574582
// Ignore, will fall back to original path
575583
}
576584
}
577585
const swiftPath = expandFilePathTilde(path.dirname(realSwift));
578-
return await this.getSwiftEnvPath(swiftPath);
586+
return {
587+
path: await this.getSwiftEnvPath(swiftPath),
588+
isSwiftlyManaged,
589+
};
579590
} catch (error) {
580591
outputChannel?.appendLine(`Failed to find swift executable: ${error}`);
581592
throw Error("Failed to find swift executable");

src/ui/ToolchainSelection.ts

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,25 +98,27 @@ export async function selectToolchain() {
9898
}
9999

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

103103
/** Common properties for a {@link vscode.QuickPickItem} that represents a Swift toolchain */
104104
interface BaseSwiftToolchainItem extends vscode.QuickPickItem {
105105
type: "toolchain";
106-
toolchainPath: string;
107-
swiftFolderPath: string;
108106
onDidSelect?(): Promise<void>;
109107
}
110108

111109
/** A {@link vscode.QuickPickItem} for a Swift toolchain that has been installed manually */
112110
interface PublicSwiftToolchainItem extends BaseSwiftToolchainItem {
113-
category: "public" | "swiftly";
111+
category: "public";
112+
toolchainPath: string;
113+
swiftFolderPath: string;
114114
}
115115

116116
/** A {@link vscode.QuickPickItem} for a Swift toolchain provided by an installed Xcode application */
117117
interface XcodeToolchainItem extends BaseSwiftToolchainItem {
118118
category: "xcode";
119119
xcodePath: string;
120+
toolchainPath: string;
121+
swiftFolderPath: string;
120122
}
121123

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

130+
interface SwiftlyToolchainItem extends BaseSwiftToolchainItem {
131+
category: "swiftly";
132+
version: string;
133+
}
134+
128135
/** A {@link vscode.QuickPickItem} that separates items in the UI */
129136
class SeparatorItem implements vscode.QuickPickItem {
130137
readonly type = "separator";
@@ -146,7 +153,8 @@ type SelectToolchainItem = SwiftToolchainItem | ActionItem | SeparatorItem;
146153
* @returns an array of {@link SelectToolchainItem}
147154
*/
148155
async function getQuickPickItems(
149-
activeToolchain: SwiftToolchain | undefined
156+
activeToolchain: SwiftToolchain | undefined,
157+
cwd?: vscode.Uri
150158
): Promise<SelectToolchainItem[]> {
151159
// Find any Xcode installations on the system
152160
const xcodes = (await SwiftToolchain.findXcodeInstalls())
@@ -195,18 +203,43 @@ async function getQuickPickItems(
195203
// Find any Swift toolchains installed via Swiftly
196204
const swiftlyToolchains = (await Swiftly.listAvailableToolchains())
197205
.reverse()
198-
.map<SwiftToolchainItem>(toolchainPath => ({
206+
.map<SwiftlyToolchainItem>(toolchainPath => ({
199207
type: "toolchain",
200-
category: "swiftly",
201208
label: path.basename(toolchainPath),
202-
detail: toolchainPath,
203-
toolchainPath: path.join(toolchainPath, "usr"),
204-
swiftFolderPath: path.join(toolchainPath, "usr", "bin"),
209+
category: "swiftly",
210+
version: path.basename(toolchainPath),
211+
onDidSelect: async () => {
212+
try {
213+
await Swiftly.use(toolchainPath);
214+
void showReloadExtensionNotification(
215+
"Changing the Swift path requires Visual Studio Code be reloaded."
216+
);
217+
} catch (error) {
218+
void vscode.window.showErrorMessage(
219+
`Failed to switch Swiftly toolchain: ${error}`
220+
);
221+
}
222+
},
205223
}));
206224
// Mark which toolchain is being actively used
207225
if (activeToolchain) {
226+
const currentSwiftlyVersion = activeToolchain.isSwiftlyManaged
227+
? await Swiftly.inUseVersion("swiftly", cwd)
228+
: undefined;
208229
const toolchainInUse = [...xcodes, ...toolchains, ...swiftlyToolchains].find(toolchain => {
209-
return toolchain.toolchainPath === activeToolchain.toolchainPath;
230+
if (currentSwiftlyVersion) {
231+
if (toolchain.category !== "swiftly") {
232+
return false;
233+
}
234+
235+
// For Swiftly toolchains, check if the label matches the active toolchain version
236+
return currentSwiftlyVersion === toolchain.label;
237+
}
238+
// For non-Swiftly toolchains, check if the toolchain path matches
239+
return (
240+
(toolchain as PublicSwiftToolchainItem | XcodeToolchainItem).toolchainPath ===
241+
activeToolchain.toolchainPath
242+
);
210243
});
211244
if (toolchainInUse) {
212245
toolchainInUse.description = "$(check) in use";
@@ -262,10 +295,13 @@ async function getQuickPickItems(
262295
*
263296
* @param activeToolchain the {@link WorkspaceContext}
264297
*/
265-
export async function showToolchainSelectionQuickPick(activeToolchain: SwiftToolchain | undefined) {
298+
export async function showToolchainSelectionQuickPick(
299+
activeToolchain: SwiftToolchain | undefined,
300+
cwd?: vscode.Uri
301+
) {
266302
let xcodePaths: string[] = [];
267303
const selected = await vscode.window.showQuickPick<SelectToolchainItem>(
268-
getQuickPickItems(activeToolchain).then(result => {
304+
getQuickPickItems(activeToolchain, cwd).then(result => {
269305
xcodePaths = result
270306
.filter((i): i is XcodeToolchainItem => "category" in i && i.category === "xcode")
271307
.map(xcode => xcode.xcodePath);
@@ -303,8 +339,23 @@ export async function showToolchainSelectionQuickPick(activeToolchain: SwiftTool
303339
});
304340
}
305341
}
306-
// Update the toolchain path
307-
const isUpdated = await setToolchainPath(selected.swiftFolderPath, developerDir);
342+
// Update the toolchain path`
343+
let swiftPath: string | undefined;
344+
345+
// Handle Swiftly toolchains specially
346+
if (selected.category === "swiftly") {
347+
try {
348+
swiftPath = undefined;
349+
} catch (error) {
350+
void vscode.window.showErrorMessage(`Failed to switch Swiftly toolchain: ${error}`);
351+
return;
352+
}
353+
} else {
354+
// For non-Swiftly toolchains, use the swiftFolderPath
355+
swiftPath = selected.swiftFolderPath;
356+
}
357+
358+
const isUpdated = await setToolchainPath(swiftPath, developerDir);
308359
if (isUpdated && selected.onDidSelect) {
309360
await selected.onDidSelect();
310361
}

0 commit comments

Comments
 (0)