Skip to content

Commit aedf8eb

Browse files
combine results from mdfind and xcode-select when searching for available Xcode installations (#1532)
1 parent 2d69748 commit aedf8eb

File tree

3 files changed

+89
-11
lines changed

3 files changed

+89
-11
lines changed

src/toolchain/toolchain.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -218,21 +218,32 @@ export class SwiftToolchain {
218218
}
219219

220220
/**
221-
* Get list of Xcode versions intalled on mac
222-
* @returns Folders for each Xcode install
221+
* Get the list of Xcode applications installed on macOS.
222+
*
223+
* Note: this uses a combination of xcode-select and the Spotlight index and may not contain
224+
* all Xcode installations depending on the user's macOS settings.
225+
*
226+
* @returns an array of Xcode installations in no particular order.
223227
*/
224-
public static async getXcodeInstalls(): Promise<string[]> {
228+
public static async findXcodeInstalls(): Promise<string[]> {
225229
if (process.platform !== "darwin") {
226230
return [];
227231
}
228-
const { stdout: xcodes } = await execFile("mdfind", [
229-
`kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'`,
232+
233+
// Use the Spotlight index and xcode-select to find available Xcode installations
234+
const [{ stdout: mdfindOutput }, xcodeDeveloperDir] = await Promise.all([
235+
execFile("mdfind", [`kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'`]),
236+
this.getXcodeDeveloperDir(),
230237
]);
231-
// An empty string means no Xcodes are installed.
232-
if (xcodes.length === 0) {
233-
return [];
238+
const spotlightXcodes = mdfindOutput.length > 0 ? mdfindOutput.trimEnd().split("\n") : [];
239+
const selectedXcode = this.getXcodeDirectory(xcodeDeveloperDir);
240+
241+
// Combine the results from both commands
242+
const result = spotlightXcodes;
243+
if (selectedXcode && spotlightXcodes.find(xcode => xcode === selectedXcode) === undefined) {
244+
result.push(selectedXcode);
234245
}
235-
return xcodes.trimEnd().split("\n");
246+
return result;
236247
}
237248

238249
/**
@@ -405,13 +416,19 @@ export class SwiftToolchain {
405416
return path.join(toolchainPath, "bin", executable + executableSuffix);
406417
}
407418

419+
/**
420+
* Returns the path to the Xcode application given a toolchain path. Returns undefined
421+
* if no application could be found.
422+
* @param toolchainPath The toolchain path.
423+
* @returns The path to the Xcode application or undefined if none.
424+
*/
408425
private static getXcodeDirectory(toolchainPath: string): string | undefined {
409426
let xcodeDirectory = toolchainPath;
410427
while (path.extname(xcodeDirectory) !== ".app") {
411-
xcodeDirectory = path.dirname(xcodeDirectory);
412428
if (path.parse(xcodeDirectory).base === "") {
413429
return undefined;
414430
}
431+
xcodeDirectory = path.dirname(xcodeDirectory);
415432
}
416433
return xcodeDirectory;
417434
}

src/ui/ToolchainSelection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ async function getQuickPickItems(
143143
activeToolchain: SwiftToolchain | undefined
144144
): Promise<SelectToolchainItem[]> {
145145
// Find any Xcode installations on the system
146-
const xcodes = (await SwiftToolchain.getXcodeInstalls())
146+
const xcodes = (await SwiftToolchain.findXcodeInstalls())
147147
.reverse()
148148
.map<SwiftToolchainItem>(xcodePath => {
149149
const toolchainPath = path.join(

test/unit-tests/toolchain/toolchain.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,65 @@ suite("SwiftToolchain Unit Test Suite", () => {
235235
});
236236
});
237237
});
238+
239+
suite("findXcodeInstalls()", () => {
240+
test("returns the list of Xcode installations found in the Spotlight index on macOS", async () => {
241+
mockedPlatform.setValue("darwin");
242+
mockedUtilities.execFile.withArgs("mdfind").resolves({
243+
stdout: "/Applications/Xcode.app\n/Applications/Xcode-beta.app\n",
244+
stderr: "",
245+
});
246+
mockedUtilities.execFile
247+
.withArgs("xcode-select", ["-p"])
248+
.resolves({ stdout: "", stderr: "" });
249+
250+
const sortedXcodeInstalls = (await SwiftToolchain.findXcodeInstalls()).sort();
251+
expect(sortedXcodeInstalls).to.deep.equal([
252+
"/Applications/Xcode-beta.app",
253+
"/Applications/Xcode.app",
254+
]);
255+
});
256+
257+
test("includes the currently selected Xcode installation on macOS", async () => {
258+
mockedPlatform.setValue("darwin");
259+
mockedUtilities.execFile.withArgs("mdfind").resolves({
260+
stdout: "/Applications/Xcode-beta.app\n",
261+
stderr: "",
262+
});
263+
mockedUtilities.execFile
264+
.withArgs("xcode-select", ["-p"])
265+
.resolves({ stdout: "/Applications/Xcode.app\n", stderr: "" });
266+
267+
const sortedXcodeInstalls = (await SwiftToolchain.findXcodeInstalls()).sort();
268+
expect(sortedXcodeInstalls).to.deep.equal([
269+
"/Applications/Xcode-beta.app",
270+
"/Applications/Xcode.app",
271+
]);
272+
});
273+
274+
test("does not duplicate the currently selected Xcode installation on macOS", async () => {
275+
mockedPlatform.setValue("darwin");
276+
mockedUtilities.execFile.withArgs("mdfind").resolves({
277+
stdout: "/Applications/Xcode.app\n/Applications/Xcode-beta.app\n",
278+
stderr: "",
279+
});
280+
mockedUtilities.execFile
281+
.withArgs("xcode-select", ["-p"])
282+
.resolves({ stdout: "/Applications/Xcode.app\n", stderr: "" });
283+
284+
const sortedXcodeInstalls = (await SwiftToolchain.findXcodeInstalls()).sort();
285+
expect(sortedXcodeInstalls).to.deep.equal([
286+
"/Applications/Xcode-beta.app",
287+
"/Applications/Xcode.app",
288+
]);
289+
});
290+
291+
test("returns an empty array on non-macOS platforms", async () => {
292+
mockedPlatform.setValue("linux");
293+
await expect(SwiftToolchain.findXcodeInstalls()).to.eventually.be.empty;
294+
295+
mockedPlatform.setValue("win32");
296+
await expect(SwiftToolchain.findXcodeInstalls()).to.eventually.be.empty;
297+
});
298+
});
238299
});

0 commit comments

Comments
 (0)