Add xcodeVersionFromXcodeBuild

This commit is contained in:
Michael B. Gale
2026-10-30 16:14:50 +00:00
parent 8f2596eca1
commit efea0445d3
5 changed files with 233 additions and 7 deletions

60
lib/entry-points.js generated
View File

@@ -183478,6 +183478,64 @@ function xcodeVersionFromLink(logger2) {
return void 0;
}
}
async function xcodeVersionFromXcodeBuild(logger2) {
try {
const xcrun = await io4.which("xcrun", true);
let stdout = "";
let stderr = "";
const exitCode = await new toolrunner3.ToolRunner(
xcrun,
["xcodebuild", "-version"],
{
silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
stdout += data.toString("utf-8");
},
stderr: (data) => {
stderr += data.toString("utf-8");
}
}
}
).exec();
if (exitCode !== 0) {
logger2.debug(`xcodebuild returned exit code ${exitCode}`);
logger2.debug(`stdout:
${stdout}`);
logger2.debug(`stderr:
${stderr}`);
return void 0;
}
const match2 = stdout.match(/^Xcode (\d+(\.\d+)?)/);
if (match2 === null) {
logger2.debug(
`Output doesn't appear to contain the Xcode product version.`
);
logger2.debug(`stdout:
${stdout}`);
logger2.debug(`stderr:
${stderr}`);
return void 0;
}
const version = semver6.coerce(match2[1].trim());
if (version === null) {
logger2.debug(
`xcodebuild's output couldn't be parsed as a version:
${stdout}`
);
logger2.debug(`stderr:
${stderr}`);
return void 0;
}
return version;
} catch (err) {
logger2.debug(
`Unable to determine Xcode version from xcodebuild: ${getErrorMessage(err)}`
);
return void 0;
}
}
async function xcodeVersionFromSdk(logger2) {
try {
const xcrun = await io4.which("xcrun", true);
@@ -194063,7 +194121,7 @@ async function checkRunnerSupportsTracedAnalysis(action, config, codeql) {
);
return;
}
const xcodeVer = xcodeVersionFromLink(action.logger) ?? await xcodeVersionFromSdk(action.logger);
const xcodeVer = xcodeVersionFromLink(action.logger) ?? await xcodeVersionFromXcodeBuild(action.logger) ?? await xcodeVersionFromSdk(action.logger);
if (xcodeVer !== void 0 && xcodeVer.major >= UNSUPPORTED_XCODE_FOR_TRACED) {
handleUnsupportedVersion(
action.logger,

View File

@@ -1,5 +1,6 @@
import fs from "fs";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as io from "@actions/io";
import test from "ava";
import * as sinon from "sinon";
@@ -428,7 +429,7 @@ test.serial(
);
test.serial(
"checkRunnerSupportsTracedAnalysis uses Xcode SDK version as fallback",
"checkRunnerSupportsTracedAnalysis attempts fallbacks if needed",
async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
@@ -439,7 +440,16 @@ test.serial(
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.throws(new Error("Not found"));
const which = sinon.stub(io, "which").throws(new Error("Tool not found"));
const which = sinon.stub(io, "which").resolves("/path/to/xcrun");
const exec = sinon.stub<[], Promise<number>>().callsFake(async () => {
throw new Error("Error to force next fallback");
});
const runner = sinon.createStubInstance(toolrunner.ToolRunner, { exec });
const toolrunnerStub = sinon
.stub(toolrunner, "ToolRunner")
.callsFake(() => {
return runner;
});
const codeql = await getCodeQLWithTracedStub(
"codeql-for-testing",
@@ -468,11 +478,27 @@ test.serial(
t.is(readlinkSync.callCount, 1);
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
t.is(which.callCount, 1);
t.is(which.callCount, 2);
t.deepEqual(which.args[0], ["xcrun", true]);
t.deepEqual(which.args[1], ["xcrun", true]);
t.is(toolrunnerStub.callCount, 2);
t.is(exec.callCount, 2);
t.is(toolrunnerStub.args[0][0], "/path/to/xcrun");
t.deepEqual(toolrunnerStub.args[0][1], ["xcodebuild", "-version"]);
t.is(toolrunnerStub.args[1][0], "/path/to/xcrun");
t.deepEqual(toolrunnerStub.args[1][1], [
"--sdk",
"macosx",
"--show-sdk-version",
]);
checkExpectedLogMessages(t, logger.messages, [
"Unable to determine Xcode version: Tool not found",
"Unable to determine Xcode version: Not found",
"Unable to determine Xcode version from xcodebuild: Error to force next fallback",
"Unable to determine Xcode version: Error to force next fallback",
]);
checkUnexpectedLogMessages(t, logger.messages, [
"Traced analysis is not supported on Xcode 27",

View File

@@ -11,6 +11,7 @@ import {
darwinVersion,
xcodeVersionFromLink,
xcodeVersionFromSdk,
xcodeVersionFromXcodeBuild,
} from "../platform";
import { ConfigurationError } from "../util";
@@ -201,10 +202,12 @@ export async function checkRunnerSupportsTracedAnalysis(
// Determining whether the Xcode version is supported only makes sense on macOS, so we only do it
// after determining that we are running on macOS. We first try to obtain the version of Xcode itself
// starting with the cheapest approach. If that doesn't work, we obtain the version from the SDK instead.
// Since either Xcode or the SDK at version 27 or above is incompatible, the major version of either is fine.
// starting with the cheapest approach and then call out to an external program. If neither works, we try
// to obtain the version from the SDK instead. Since either Xcode or the SDK at version 27 or above is
// incompatible, either is fine as we only check the major version below.
const xcodeVer =
xcodeVersionFromLink(action.logger) ??
(await xcodeVersionFromXcodeBuild(action.logger)) ??
(await xcodeVersionFromSdk(action.logger));
if (

View File

@@ -13,6 +13,7 @@ import {
XCODE_SELECT_LINK_PATH,
xcodeVersionFromLink,
xcodeVersionFromSdk,
xcodeVersionFromXcodeBuild,
} from "./platform";
import {
checkExpectedLogMessages,
@@ -228,6 +229,74 @@ test.serial(
},
);
test.serial(
"xcodeVersionFromXcodeBuild returns undefined for non-zero exit code",
async (t) => {
const logger = new RecordingLogger();
const which = sinon.stub(io, "which").resolves("/path/to/xcrun");
const exec = sinon.stub<[], Promise<number>>().resolves(1);
const runner = sinon.createStubInstance(toolrunner.ToolRunner, { exec });
const toolrunnerStub = sinon.stub(toolrunner, "ToolRunner").returns(runner);
await t.notThrowsAsync(async () => {
const result = await xcodeVersionFromXcodeBuild(logger);
t.is(result, undefined);
t.is(which.callCount, 1);
t.deepEqual(which.args[0], ["xcrun", true]);
t.is(toolrunnerStub.callCount, 1);
t.is(exec.callCount, 1);
checkExpectedLogMessages(t, logger.messages, [
"xcodebuild returned exit code 1",
]);
});
},
);
test.serial(
"xcodeVersionFromXcodeBuild extracts version from xcrun stdout",
async (t) => {
const logger = new RecordingLogger();
const which = sinon.stub(io, "which").resolves("/path/to/xcrun");
let stdoutListener: ((data: Buffer) => void) | undefined = undefined;
const exec = sinon.stub<[], Promise<number>>().callsFake(async () => {
if (stdoutListener !== undefined) {
stdoutListener(Buffer.from("Xcode 26.5"));
stdoutListener(Buffer.from("Build version 26F9"));
}
return 0;
});
const runner = sinon.createStubInstance(toolrunner.ToolRunner, { exec });
const toolrunnerStub = sinon
.stub(toolrunner, "ToolRunner")
.callsFake((_cmd, _args, opts: ExecOptions) => {
stdoutListener = opts.listeners?.stdout;
return runner;
});
await t.notThrowsAsync(async () => {
const result = await xcodeVersionFromXcodeBuild(logger);
t.not(result, undefined);
t.is(result?.major, 26);
t.is(result?.minor, 5);
t.is(which.callCount, 1);
t.deepEqual(which.args[0], ["xcrun", true]);
t.is(toolrunnerStub.callCount, 1);
t.is(exec.callCount, 1);
});
},
);
test.serial(
"xcodeVersionFromSdk returns undefined for non-zero exit code",
async (t) => {

View File

@@ -115,6 +115,76 @@ export function xcodeVersionFromLink(
}
}
/**
* Tries to determine the version of Xcode that is installed by running `xcrun xcodebuild -version`.
*
* @param logger The logger to use.
* @returns The Xcode version or `undefined` if it couldn't be determined.
*/
export async function xcodeVersionFromXcodeBuild(
logger: Logger,
): Promise<semver.SemVer | undefined> {
try {
const xcrun = await io.which("xcrun", true);
let stdout = "";
let stderr = "";
const exitCode = await new toolrunner.ToolRunner(
xcrun,
["xcodebuild", "-version"],
{
silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data: Buffer) => {
stdout += data.toString("utf-8");
},
stderr: (data: Buffer) => {
stderr += data.toString("utf-8");
},
},
},
).exec();
if (exitCode !== 0) {
logger.debug(`xcodebuild returned exit code ${exitCode}`);
logger.debug(`stdout:\n${stdout}`);
logger.debug(`stderr:\n${stderr}`);
return undefined;
}
// The output contains a couple of lines, but we only care about the product version.
const match = stdout.match(/^Xcode (\d+(\.\d+)?)/);
if (match === null) {
logger.debug(
`Output doesn't appear to contain the Xcode product version.`,
);
logger.debug(`stdout:\n${stdout}`);
logger.debug(`stderr:\n${stderr}`);
return undefined;
}
// We expect the output to only be a version string. Try to parse it into a semver.
const version = semver.coerce(match[1].trim());
if (version === null) {
logger.debug(
`xcodebuild's output couldn't be parsed as a version:\n${stdout}`,
);
logger.debug(`stderr:\n${stderr}`);
return undefined;
}
return version;
} catch (err) {
logger.debug(
`Unable to determine Xcode version from xcodebuild: ${getErrorMessage(err)}`,
);
return undefined;
}
}
/**
* Tries to determine the version of Xcode that is installed by running `xcrun --sdk macosx --show-sdk-version`.
*