Skip to content

Combine results from mdfind and xcode-select when searching for available Xcode applications #1532

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 1 commit into from
May 1, 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
37 changes: 27 additions & 10 deletions src/toolchain/toolchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,21 +218,32 @@ export class SwiftToolchain {
}

/**
* Get list of Xcode versions intalled on mac
* @returns Folders for each Xcode install
* Get the list of Xcode applications installed on macOS.
*
* Note: this uses a combination of xcode-select and the Spotlight index and may not contain
* all Xcode installations depending on the user's macOS settings.
*
* @returns an array of Xcode installations in no particular order.
*/
public static async getXcodeInstalls(): Promise<string[]> {
public static async findXcodeInstalls(): Promise<string[]> {
if (process.platform !== "darwin") {
return [];
}
const { stdout: xcodes } = await execFile("mdfind", [
`kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'`,

// Use the Spotlight index and xcode-select to find available Xcode installations
const [{ stdout: mdfindOutput }, xcodeDeveloperDir] = await Promise.all([
execFile("mdfind", [`kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode'`]),
this.getXcodeDeveloperDir(),
]);
// An empty string means no Xcodes are installed.
if (xcodes.length === 0) {
return [];
const spotlightXcodes = mdfindOutput.length > 0 ? mdfindOutput.trimEnd().split("\n") : [];
const selectedXcode = this.getXcodeDirectory(xcodeDeveloperDir);

// Combine the results from both commands
const result = spotlightXcodes;
if (selectedXcode && spotlightXcodes.find(xcode => xcode === selectedXcode) === undefined) {
result.push(selectedXcode);
}
return xcodes.trimEnd().split("\n");
return result;
}

/**
Expand Down Expand Up @@ -405,13 +416,19 @@ export class SwiftToolchain {
return path.join(toolchainPath, "bin", executable + executableSuffix);
}

/**
* Returns the path to the Xcode application given a toolchain path. Returns undefined
* if no application could be found.
* @param toolchainPath The toolchain path.
* @returns The path to the Xcode application or undefined if none.
*/
private static getXcodeDirectory(toolchainPath: string): string | undefined {
let xcodeDirectory = toolchainPath;
while (path.extname(xcodeDirectory) !== ".app") {
xcodeDirectory = path.dirname(xcodeDirectory);
if (path.parse(xcodeDirectory).base === "") {
return undefined;
}
xcodeDirectory = path.dirname(xcodeDirectory);
}
return xcodeDirectory;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ui/ToolchainSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ async function getQuickPickItems(
activeToolchain: SwiftToolchain | undefined
): Promise<SelectToolchainItem[]> {
// Find any Xcode installations on the system
const xcodes = (await SwiftToolchain.getXcodeInstalls())
const xcodes = (await SwiftToolchain.findXcodeInstalls())
.reverse()
.map<SwiftToolchainItem>(xcodePath => {
const toolchainPath = path.join(
Expand Down
61 changes: 61 additions & 0 deletions test/unit-tests/toolchain/toolchain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,65 @@ suite("SwiftToolchain Unit Test Suite", () => {
});
});
});

suite("findXcodeInstalls()", () => {
test("returns the list of Xcode installations found in the Spotlight index on macOS", async () => {
mockedPlatform.setValue("darwin");
mockedUtilities.execFile.withArgs("mdfind").resolves({
stdout: "/Applications/Xcode.app\n/Applications/Xcode-beta.app\n",
stderr: "",
});
mockedUtilities.execFile
.withArgs("xcode-select", ["-p"])
.resolves({ stdout: "", stderr: "" });

const sortedXcodeInstalls = (await SwiftToolchain.findXcodeInstalls()).sort();
expect(sortedXcodeInstalls).to.deep.equal([
"/Applications/Xcode-beta.app",
"/Applications/Xcode.app",
]);
});

test("includes the currently selected Xcode installation on macOS", async () => {
mockedPlatform.setValue("darwin");
mockedUtilities.execFile.withArgs("mdfind").resolves({
stdout: "/Applications/Xcode-beta.app\n",
stderr: "",
});
mockedUtilities.execFile
.withArgs("xcode-select", ["-p"])
.resolves({ stdout: "/Applications/Xcode.app\n", stderr: "" });

const sortedXcodeInstalls = (await SwiftToolchain.findXcodeInstalls()).sort();
expect(sortedXcodeInstalls).to.deep.equal([
"/Applications/Xcode-beta.app",
"/Applications/Xcode.app",
]);
});

test("does not duplicate the currently selected Xcode installation on macOS", async () => {
mockedPlatform.setValue("darwin");
mockedUtilities.execFile.withArgs("mdfind").resolves({
stdout: "/Applications/Xcode.app\n/Applications/Xcode-beta.app\n",
stderr: "",
});
mockedUtilities.execFile
.withArgs("xcode-select", ["-p"])
.resolves({ stdout: "/Applications/Xcode.app\n", stderr: "" });

const sortedXcodeInstalls = (await SwiftToolchain.findXcodeInstalls()).sort();
expect(sortedXcodeInstalls).to.deep.equal([
"/Applications/Xcode-beta.app",
"/Applications/Xcode.app",
]);
});

test("returns an empty array on non-macOS platforms", async () => {
mockedPlatform.setValue("linux");
await expect(SwiftToolchain.findXcodeInstalls()).to.eventually.be.empty;

mockedPlatform.setValue("win32");
await expect(SwiftToolchain.findXcodeInstalls()).to.eventually.be.empty;
});
});
});