Skip to content

Commit 8456249

Browse files
Prompt to update DEVELOPER_DIR for macOS toolchain selection (#938)
Co-authored-by: Adam Fowler <[email protected]>
1 parent 060daf4 commit 8456249

File tree

2 files changed

+177
-44
lines changed

2 files changed

+177
-44
lines changed

src/configuration.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,6 @@ const configuration = {
198198
.getConfiguration("swift")
199199
.get<{ [key: string]: string }>("swiftEnvironmentVariables", {});
200200
},
201-
set swiftEnvironmentVariables(vars: { [key: string]: string }) {
202-
vscode.workspace.getConfiguration("swift").update("swiftEnvironmentVariables", vars);
203-
},
204201
/** include build errors in problems view */
205202
get diagnosticsCollection(): DiagnosticCollectionOptions {
206203
return vscode.workspace

src/ui/ToolchainSelection.ts

Lines changed: 177 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export async function selectToolchainFolder() {
6363
if (!selected || selected.length !== 1) {
6464
return;
6565
}
66-
await setToolchainPath(selected[0].fsPath, "prompt");
66+
await setToolchainPath(selected[0].fsPath, "public");
6767
}
6868

6969
/**
@@ -91,14 +91,28 @@ export async function showToolchainError(): Promise<void> {
9191
}
9292
}
9393

94-
/** A {@link vscode.QuickPickItem} that contains the path to an installed swift toolchain */
95-
interface SwiftToolchainItem extends vscode.QuickPickItem {
94+
/** A {@link vscode.QuickPickItem} that contains the path to an installed Swift toolchain */
95+
type SwiftToolchainItem = PublicSwiftToolchainItem | XcodeToolchainItem;
96+
97+
/** Common properties for a {@link vscode.QuickPickItem} that represents a Swift toolchain */
98+
interface BaseSwiftToolchainItem extends vscode.QuickPickItem {
9699
type: "toolchain";
97100
toolchainPath: string;
98101
swiftFolderPath: string;
99102
onDidSelect?(): Promise<void>;
100103
}
101104

105+
/** A {@link vscode.QuickPickItem} for a Swift toolchain that has been installed manually */
106+
interface PublicSwiftToolchainItem extends BaseSwiftToolchainItem {
107+
category: "public" | "swiftly";
108+
}
109+
110+
/** A {@link vscode.QuickPickItem} for a Swift toolchain provided by an installed Xcode application */
111+
interface XcodeToolchainItem extends BaseSwiftToolchainItem {
112+
category: "xcode";
113+
xcodePath: string;
114+
}
115+
102116
/** A {@link vscode.QuickPickItem} that performs an action for the user */
103117
interface ActionItem extends vscode.QuickPickItem {
104118
type: "action";
@@ -128,6 +142,7 @@ type SelectToolchainItem = SwiftToolchainItem | ActionItem | SeparatorItem;
128142
async function getQuickPickItems(
129143
activeToolchain: SwiftToolchain | undefined
130144
): Promise<SelectToolchainItem[]> {
145+
// Find any Xcode installations on the system
131146
const xcodes = (await SwiftToolchain.getXcodeInstalls())
132147
.reverse()
133148
.map<SwiftToolchainItem>(xcodePath => {
@@ -141,17 +156,21 @@ async function getQuickPickItems(
141156
);
142157
return {
143158
type: "toolchain",
159+
category: "xcode",
144160
label: path.basename(xcodePath, ".app"),
145161
detail: xcodePath,
162+
xcodePath,
146163
toolchainPath,
147164
swiftFolderPath: path.join(toolchainPath, "bin"),
148165
};
149166
});
167+
// Find any public Swift toolchains on the system
150168
const toolchains = (await SwiftToolchain.getToolchainInstalls())
151169
.reverse()
152170
.map<SwiftToolchainItem>(toolchainPath => {
153171
const result: SwiftToolchainItem = {
154172
type: "toolchain",
173+
category: "public",
155174
label: path.basename(toolchainPath, ".xctoolchain"),
156175
detail: toolchainPath,
157176
toolchainPath: path.join(toolchainPath, "usr"),
@@ -167,15 +186,18 @@ async function getQuickPickItems(
167186
}
168187
return result;
169188
});
189+
// Find any Swift toolchains installed via Swiftly
170190
const swiftlyToolchains = (await SwiftToolchain.getSwiftlyToolchainInstalls())
171191
.reverse()
172192
.map<SwiftToolchainItem>(toolchainPath => ({
173193
type: "toolchain",
194+
category: "swiftly",
174195
label: path.basename(toolchainPath),
175196
detail: toolchainPath,
176197
toolchainPath: path.join(toolchainPath, "usr"),
177198
swiftFolderPath: path.join(toolchainPath, "usr", "bin"),
178199
}));
200+
// Mark which toolchain is being actively used
179201
if (activeToolchain) {
180202
const toolchainInUse = [...xcodes, ...toolchains, ...swiftlyToolchains].find(toolchain => {
181203
return toolchain.toolchainPath === activeToolchain.toolchainPath;
@@ -185,6 +207,7 @@ async function getQuickPickItems(
185207
} else {
186208
toolchains.splice(0, 0, {
187209
type: "toolchain",
210+
category: "public",
188211
label: `Swift ${activeToolchain.swiftVersion.toString()}`,
189212
description: "$(check) in use",
190213
detail: activeToolchain.toolchainPath,
@@ -193,6 +216,7 @@ async function getQuickPickItems(
193216
});
194217
}
195218
}
219+
// Various actions that the user can perform (e.g. to install new toolchains)
196220
const actionItems: ActionItem[] = [];
197221
if (process.platform === "linux") {
198222
actionItems.push({
@@ -232,69 +256,171 @@ async function getQuickPickItems(
232256
* @param activeToolchain the {@link WorkspaceContext}
233257
*/
234258
export async function showToolchainSelectionQuickPick(activeToolchain: SwiftToolchain | undefined) {
259+
let xcodePaths: string[] = [];
235260
const selected = await vscode.window.showQuickPick<SelectToolchainItem>(
236-
getQuickPickItems(activeToolchain),
261+
getQuickPickItems(activeToolchain).then(result => {
262+
xcodePaths = result
263+
.filter((i): i is XcodeToolchainItem => "category" in i && i.category === "xcode")
264+
.map(xcode => xcode.xcodePath);
265+
return result;
266+
}),
237267
{
238268
title: "Select the Swift toolchain",
239269
placeHolder: "Pick a Swift toolchain that VS Code will use",
240270
canPickMany: false,
241271
}
242272
);
243273
if (selected?.type === "action") {
244-
await selected.run();
245-
} else if (selected?.type === "toolchain") {
246-
const isUpdated = await setToolchainPath(selected.swiftFolderPath, "prompt");
274+
return await selected.run();
275+
}
276+
if (selected?.type === "toolchain") {
277+
// Select an Xcode to build with
278+
let developerDir: string | undefined;
279+
if (selected.category === "xcode") {
280+
developerDir = selected.xcodePath;
281+
} else if (xcodePaths.length === 1) {
282+
developerDir = xcodePaths[0];
283+
} else if (process.platform === "darwin" && xcodePaths.length > 1) {
284+
developerDir = await showDeveloperDirQuickPick(xcodePaths);
285+
if (!developerDir) {
286+
return;
287+
}
288+
}
289+
// Update the toolchain path
290+
const isUpdated = await setToolchainPath(selected.swiftFolderPath, developerDir);
247291
if (isUpdated && selected.onDidSelect) {
248292
await selected.onDidSelect();
249293
}
294+
return;
250295
}
251296
}
252297

298+
/**
299+
* Prompt the user to choose a value for the DEVELOPER_DIR environment variable.
300+
*
301+
* @param xcodePaths An array of paths to available Xcode installations on the system
302+
* @returns The selected DEVELOPER_DIR or undefined if the user cancelled selection
303+
*/
304+
async function showDeveloperDirQuickPick(xcodePaths: string[]): Promise<string | undefined> {
305+
const selected = await vscode.window.showQuickPick<vscode.QuickPickItem>(
306+
SwiftToolchain.getXcodeDeveloperDir(configuration.swiftEnvironmentVariables).then(
307+
existingDeveloperDir => {
308+
return xcodePaths
309+
.map(xcodePath => {
310+
const result: vscode.QuickPickItem = {
311+
label: path.basename(xcodePath, ".app"),
312+
detail: xcodePath,
313+
};
314+
if (existingDeveloperDir.startsWith(xcodePath)) {
315+
result.description = "$(check) in use";
316+
}
317+
return result;
318+
})
319+
.sort((a, b) => {
320+
// Bring the active Xcode to the top
321+
if (existingDeveloperDir.startsWith(a.detail ?? "")) {
322+
return -1;
323+
} else if (existingDeveloperDir.startsWith(b.detail ?? "")) {
324+
return 1;
325+
}
326+
// Otherwise sort by name
327+
return a.label.localeCompare(b.label);
328+
});
329+
}
330+
),
331+
{
332+
title: "Select a developer directory",
333+
placeHolder:
334+
"Pick an Xcode installation to use as the developer directory and for the macOS SDK",
335+
canPickMany: false,
336+
}
337+
);
338+
return selected?.detail;
339+
}
340+
253341
/**
254342
* Delete all set Swift path settings.
255343
*/
256344
async function removeToolchainPath() {
257345
const swiftSettings = vscode.workspace.getConfiguration("swift");
346+
const swiftEnvironmentSettings = swiftSettings.inspect("swiftEnvironmentVariables");
347+
if (swiftEnvironmentSettings?.globalValue) {
348+
await swiftSettings.update(
349+
"swiftEnvironmentVariables",
350+
{
351+
...swiftEnvironmentSettings?.globalValue,
352+
DEVELOPER_DIR: undefined,
353+
},
354+
vscode.ConfigurationTarget.Global
355+
);
356+
}
258357
await swiftSettings.update("path", undefined, vscode.ConfigurationTarget.Global);
358+
if (swiftEnvironmentSettings?.workspaceValue) {
359+
await swiftSettings.update(
360+
"swiftEnvironmentVariables",
361+
{
362+
...swiftEnvironmentSettings?.workspaceValue,
363+
DEVELOPER_DIR: undefined,
364+
},
365+
vscode.ConfigurationTarget.Workspace
366+
);
367+
}
259368
await swiftSettings.update("path", undefined, vscode.ConfigurationTarget.Workspace);
260369
}
261370

371+
/**
372+
* Update the toolchain path
373+
* @param swiftFolderPath
374+
* @param developerDir
375+
* @returns
376+
*/
262377
async function setToolchainPath(
263-
value: string | undefined,
264-
target?: vscode.ConfigurationTarget | "prompt"
378+
swiftFolderPath: string | undefined,
379+
developerDir: string | undefined
265380
): Promise<boolean> {
266-
if (target === "prompt") {
267-
const items: (vscode.QuickPickItem & {
268-
target?: vscode.ConfigurationTarget;
269-
})[] = [];
270-
if (vscode.workspace.workspaceFolders) {
271-
items.push({
272-
label: "Workspace Configuration",
273-
description: "(Recommended)",
274-
detail: "Add to VS Code workspace configuration",
275-
target: vscode.ConfigurationTarget.Workspace,
276-
});
277-
}
381+
let target: vscode.ConfigurationTarget | undefined;
382+
const items: (vscode.QuickPickItem & {
383+
target?: vscode.ConfigurationTarget;
384+
})[] = [];
385+
if (vscode.workspace.workspaceFolders) {
278386
items.push({
279-
label: "User Configuration",
280-
detail: "Add to VS Code user configuration.",
281-
target: vscode.ConfigurationTarget.Global,
387+
label: "Workspace Configuration",
388+
description: "(Recommended)",
389+
detail: "Add to VS Code workspace configuration",
390+
target: vscode.ConfigurationTarget.Workspace,
282391
});
283-
if (items.length > 1) {
284-
const selected = await vscode.window.showQuickPick(items, {
285-
title: "Toolchain Configuration",
286-
placeHolder: "Select a location to update the toolchain selection",
287-
canPickMany: false,
288-
});
289-
if (!selected) {
290-
return false;
291-
}
292-
target = selected.target;
293-
} else {
294-
target = vscode.ConfigurationTarget.Global; // Global scope by default
392+
}
393+
items.push({
394+
label: "User Configuration",
395+
detail: "Add to VS Code user configuration.",
396+
target: vscode.ConfigurationTarget.Global,
397+
});
398+
if (items.length > 1) {
399+
const selected = await vscode.window.showQuickPick(items, {
400+
title: "Toolchain Configuration",
401+
placeHolder: "Select a location to update the toolchain selection",
402+
canPickMany: false,
403+
});
404+
if (!selected) {
405+
return false;
295406
}
407+
target = selected.target;
408+
} else {
409+
target = vscode.ConfigurationTarget.Global; // Global scope by default
410+
}
411+
const swiftConfiguration = vscode.workspace.getConfiguration("swift");
412+
await swiftConfiguration.update("path", swiftFolderPath, target);
413+
if (developerDir) {
414+
const swiftEnv = configuration.swiftEnvironmentVariables;
415+
await swiftConfiguration.update(
416+
"swiftEnvironmentVariables",
417+
{
418+
...swiftEnv,
419+
DEVELOPER_DIR: developerDir,
420+
},
421+
target
422+
);
296423
}
297-
await vscode.workspace.getConfiguration("swift").update("path", value, target);
298424
await checkAndRemoveWorkspaceSetting(target);
299425
return true;
300426
}
@@ -307,15 +433,25 @@ async function checkAndRemoveWorkspaceSetting(target: vscode.ConfigurationTarget
307433
const inspect = vscode.workspace.getConfiguration("swift").inspect<string>("path");
308434
if (inspect?.workspaceValue) {
309435
const confirmation = await vscode.window.showWarningMessage(
310-
"You already have the Swift path configured in Workspace Settings which takes precedence over User Settings." +
436+
"You already have a Swift path configured in Workspace Settings which takes precedence over User Settings." +
311437
" Would you like to remove the setting from your workspace and use the User Settings instead?",
312438
"Remove Workspace Setting"
313439
);
314440
if (confirmation !== "Remove Workspace Setting") {
315441
return;
316442
}
317-
await vscode.workspace
318-
.getConfiguration("swift")
319-
.update("path", undefined, vscode.ConfigurationTarget.Workspace);
443+
const swiftSettings = vscode.workspace.getConfiguration("swift");
444+
const swiftEnvironmentSettings = swiftSettings.inspect("swiftEnvironmentVariables");
445+
if (swiftEnvironmentSettings?.workspaceValue) {
446+
await swiftSettings.update(
447+
"swiftEnvironmentVariables",
448+
{
449+
...swiftEnvironmentSettings?.workspaceValue,
450+
DEVELOPER_DIR: undefined,
451+
},
452+
vscode.ConfigurationTarget.Workspace
453+
);
454+
}
455+
await swiftSettings.update("path", undefined, vscode.ConfigurationTarget.Workspace);
320456
}
321457
}

0 commit comments

Comments
 (0)