Add xcrun-based fallback for Xcode version detection

This commit is contained in:
Michael B. Gale
2026-10-29 17:19:57 +00:00
parent 4a6767cdd0
commit be54d791a1
4 changed files with 270 additions and 67 deletions

175
lib/entry-points.js generated
View File

@@ -28347,7 +28347,7 @@ var require_io = __commonJS({
exports2.mv = mv;
exports2.rmRF = rmRF;
exports2.mkdirP = mkdirP;
exports2.which = which9;
exports2.which = which10;
exports2.findInPath = findInPath;
var assert_1 = require("assert");
var path30 = __importStar2(require("path"));
@@ -28423,13 +28423,13 @@ var require_io = __commonJS({
yield ioUtil.mkdir(fsPath, { recursive: true });
});
}
function which9(tool, check) {
function which10(tool, check) {
return __awaiter2(this, void 0, void 0, function* () {
if (!tool) {
throw new Error("parameter 'tool' is required");
}
if (check) {
const result = yield which9(tool, false);
const result = yield which10(tool, false);
if (!result) {
if (ioUtil.IS_WINDOWS) {
throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`);
@@ -28610,11 +28610,11 @@ var require_toolrunner = __commonJS({
var events = __importStar2(require("events"));
var child = __importStar2(require("child_process"));
var path30 = __importStar2(require("path"));
var io9 = __importStar2(require_io());
var io10 = __importStar2(require_io());
var ioUtil = __importStar2(require_io_util());
var timers_1 = require("timers");
var IS_WINDOWS = process.platform === "win32";
var ToolRunner7 = class extends events.EventEmitter {
var ToolRunner8 = class extends events.EventEmitter {
constructor(toolPath, args, options) {
super();
if (!toolPath) {
@@ -28826,7 +28826,7 @@ var require_toolrunner = __commonJS({
if (!ioUtil.isRooted(this.toolPath) && (this.toolPath.includes("/") || IS_WINDOWS && this.toolPath.includes("\\"))) {
this.toolPath = path30.resolve(process.cwd(), this.options.cwd || process.cwd(), this.toolPath);
}
this.toolPath = yield io9.which(this.toolPath, true);
this.toolPath = yield io10.which(this.toolPath, true);
return new Promise((resolve15, reject) => __awaiter2(this, void 0, void 0, function* () {
this._debug(`exec tool: ${this.toolPath}`);
this._debug("arguments:");
@@ -28923,7 +28923,7 @@ var require_toolrunner = __commonJS({
});
}
};
exports2.ToolRunner = ToolRunner7;
exports2.ToolRunner = ToolRunner8;
function argStringToArray(argString) {
const args = [];
let inQuotes = false;
@@ -42426,7 +42426,7 @@ var require_cacheUtils = __commonJS({
var core29 = __importStar2(require_core());
var exec3 = __importStar2(require_exec());
var glob2 = __importStar2(require_glob());
var io9 = __importStar2(require_io());
var io10 = __importStar2(require_io());
var crypto3 = __importStar2(require("crypto"));
var fs34 = __importStar2(require("fs"));
var path30 = __importStar2(require("path"));
@@ -42452,7 +42452,7 @@ var require_cacheUtils = __commonJS({
tempDirectory = path30.join(baseLocation, "actions", "temp");
}
const dest = path30.join(tempDirectory, crypto3.randomUUID());
yield io9.mkdirP(dest);
yield io10.mkdirP(dest);
return dest;
});
}
@@ -42541,7 +42541,7 @@ var require_cacheUtils = __commonJS({
return constants_1.GnuTarPathOnWindows;
}
const versionOutput = yield getVersion("tar");
return versionOutput.toLowerCase().includes("gnu tar") ? io9.which("tar") : "";
return versionOutput.toLowerCase().includes("gnu tar") ? io10.which("tar") : "";
});
}
function assertDefined(name, value) {
@@ -113305,7 +113305,7 @@ var require_tar = __commonJS({
exports2.createTar = createTar;
var core29 = __importStar2(require_core());
var exec_1 = require_exec();
var io9 = __importStar2(require_io());
var io10 = __importStar2(require_io());
var fs_1 = require("fs");
var path30 = __importStar2(require("path"));
var utils = __importStar2(require_cacheUtils());
@@ -113325,12 +113325,12 @@ var require_tar = __commonJS({
break;
}
case "darwin": {
const gnuTar = yield io9.which("gtar", false);
const gnuTar = yield io10.which("gtar", false);
if (gnuTar) {
return { path: gnuTar, type: constants_1.ArchiveToolType.GNU };
} else {
return {
path: yield io9.which("tar", true),
path: yield io10.which("tar", true),
type: constants_1.ArchiveToolType.BSD
};
}
@@ -113339,7 +113339,7 @@ var require_tar = __commonJS({
break;
}
return {
path: yield io9.which("tar", true),
path: yield io10.which("tar", true),
type: constants_1.ArchiveToolType.GNU
};
});
@@ -113468,7 +113468,7 @@ var require_tar = __commonJS({
} finally {
if (tempDirectory) {
try {
yield io9.rmRF(tempDirectory);
yield io10.rmRF(tempDirectory);
} catch (error3) {
core29.debug(`Failed to delete temporary tar directory: ${error3}`);
}
@@ -113484,7 +113484,7 @@ var require_tar = __commonJS({
function extractTar3(archivePath, compressionMethod) {
return __awaiter2(this, void 0, void 0, function* () {
const workingDirectory = getWorkingDirectory();
yield io9.mkdirP(workingDirectory);
yield io10.mkdirP(workingDirectory);
yield execReadCommands(archivePath, compressionMethod, "extract");
});
}
@@ -114348,7 +114348,7 @@ var require_tool_cache = __commonJS({
exports2.isExplicitVersion = isExplicitVersion;
exports2.evaluateVersions = evaluateVersions;
var core29 = __importStar2(require_core());
var io9 = __importStar2(require_io());
var io10 = __importStar2(require_io());
var crypto3 = __importStar2(require("crypto"));
var fs34 = __importStar2(require("fs"));
var mm = __importStar2(require_manifest());
@@ -114375,7 +114375,7 @@ var require_tool_cache = __commonJS({
function downloadTool3(url2, dest, auth2, headers) {
return __awaiter2(this, void 0, void 0, function* () {
dest = dest || path30.join(_getTempDirectory(), crypto3.randomUUID());
yield io9.mkdirP(path30.dirname(dest));
yield io10.mkdirP(path30.dirname(dest));
core29.debug(`Downloading ${url2}`);
core29.debug(`Destination ${dest}`);
const maxAttempts = 3;
@@ -114428,7 +114428,7 @@ var require_tool_cache = __commonJS({
if (!succeeded) {
core29.debug("download failed");
try {
yield io9.rmRF(dest);
yield io10.rmRF(dest);
} catch (err) {
core29.debug(`Failed to delete '${dest}'. ${err.message}`);
}
@@ -114483,7 +114483,7 @@ var require_tool_cache = __commonJS({
silent: true
};
try {
const powershellPath = yield io9.which("powershell", true);
const powershellPath = yield io10.which("powershell", true);
yield (0, exec_1.exec)(`"${powershellPath}"`, args, options);
} finally {
process.chdir(originalCwd);
@@ -114550,7 +114550,7 @@ var require_tool_cache = __commonJS({
if (core29.isDebug()) {
args.push("-v");
}
const xarPath = yield io9.which("xar", true);
const xarPath = yield io10.which("xar", true);
yield (0, exec_1.exec)(`"${xarPath}"`, _unique(args));
return dest;
});
@@ -114573,7 +114573,7 @@ var require_tool_cache = __commonJS({
return __awaiter2(this, void 0, void 0, function* () {
const escapedFile = file.replace(/'/g, "''").replace(/"|\n|\r/g, "");
const escapedDest = dest.replace(/'/g, "''").replace(/"|\n|\r/g, "");
const pwshPath = yield io9.which("pwsh", false);
const pwshPath = yield io10.which("pwsh", false);
if (pwshPath) {
const pwshCommand = [
`$ErrorActionPreference = 'Stop' ;`,
@@ -114609,7 +114609,7 @@ var require_tool_cache = __commonJS({
"-Command",
powershellCommand
];
const powershellPath = yield io9.which("powershell", true);
const powershellPath = yield io10.which("powershell", true);
core29.debug(`Using powershell at path: ${powershellPath}`);
yield (0, exec_1.exec)(`"${powershellPath}"`, args);
}
@@ -114617,7 +114617,7 @@ var require_tool_cache = __commonJS({
}
function extractZipNix(file, dest) {
return __awaiter2(this, void 0, void 0, function* () {
const unzipPath = yield io9.which("unzip", true);
const unzipPath = yield io10.which("unzip", true);
const args = [file];
if (!core29.isDebug()) {
args.unshift("-q");
@@ -114638,7 +114638,7 @@ var require_tool_cache = __commonJS({
const destPath = yield _createToolPath(tool, version, arch2);
for (const itemName of fs34.readdirSync(sourceDir)) {
const s = path30.join(sourceDir, itemName);
yield io9.cp(s, destPath, { recursive: true });
yield io10.cp(s, destPath, { recursive: true });
}
_completeToolPath(tool, version, arch2);
return destPath;
@@ -114656,7 +114656,7 @@ var require_tool_cache = __commonJS({
const destFolder = yield _createToolPath(tool, version, arch2);
const destPath = path30.join(destFolder, targetFile);
core29.debug(`destination file ${destPath}`);
yield io9.cp(sourceFile, destPath);
yield io10.cp(sourceFile, destPath);
_completeToolPath(tool, version, arch2);
return destFolder;
});
@@ -114750,7 +114750,7 @@ var require_tool_cache = __commonJS({
if (!dest) {
dest = path30.join(_getTempDirectory(), crypto3.randomUUID());
}
yield io9.mkdirP(dest);
yield io10.mkdirP(dest);
return dest;
});
}
@@ -114759,9 +114759,9 @@ var require_tool_cache = __commonJS({
const folderPath = path30.join(_getCacheDirectory(), tool, semver12.clean(version) || version, arch2 || "");
core29.debug(`destination ${folderPath}`);
const markerPath = `${folderPath}.complete`;
yield io9.rmRF(folderPath);
yield io9.rmRF(markerPath);
yield io9.mkdirP(folderPath);
yield io10.rmRF(folderPath);
yield io10.rmRF(markerPath);
yield io10.mkdirP(folderPath);
return folderPath;
});
}
@@ -152391,13 +152391,13 @@ var require_io2 = __commonJS({
});
}
exports2.mkdirP = mkdirP;
function which9(tool, check) {
function which10(tool, check) {
return __awaiter2(this, void 0, void 0, function* () {
if (!tool) {
throw new Error("parameter 'tool' is required");
}
if (check) {
const result = yield which9(tool, false);
const result = yield which10(tool, false);
if (!result) {
if (ioUtil.IS_WINDOWS) {
throw new Error(`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`);
@@ -152414,7 +152414,7 @@ var require_io2 = __commonJS({
return "";
});
}
exports2.which = which9;
exports2.which = which10;
function findInPath(tool) {
return __awaiter2(this, void 0, void 0, function* () {
if (!tool) {
@@ -152565,11 +152565,11 @@ var require_toolrunner2 = __commonJS({
var events = __importStar2(require("events"));
var child = __importStar2(require("child_process"));
var path30 = __importStar2(require("path"));
var io9 = __importStar2(require_io2());
var io10 = __importStar2(require_io2());
var ioUtil = __importStar2(require_io_util2());
var timers_1 = require("timers");
var IS_WINDOWS = process.platform === "win32";
var ToolRunner7 = class extends events.EventEmitter {
var ToolRunner8 = class extends events.EventEmitter {
constructor(toolPath, args, options) {
super();
if (!toolPath) {
@@ -152781,7 +152781,7 @@ var require_toolrunner2 = __commonJS({
if (!ioUtil.isRooted(this.toolPath) && (this.toolPath.includes("/") || IS_WINDOWS && this.toolPath.includes("\\"))) {
this.toolPath = path30.resolve(process.cwd(), this.options.cwd || process.cwd(), this.toolPath);
}
this.toolPath = yield io9.which(this.toolPath, true);
this.toolPath = yield io10.which(this.toolPath, true);
return new Promise((resolve15, reject) => __awaiter2(this, void 0, void 0, function* () {
this._debug(`exec tool: ${this.toolPath}`);
this._debug("arguments:");
@@ -152878,7 +152878,7 @@ var require_toolrunner2 = __commonJS({
});
}
};
exports2.ToolRunner = ToolRunner7;
exports2.ToolRunner = ToolRunner8;
function argStringToArray(argString) {
const args = [];
let inQuotes = false;
@@ -180446,13 +180446,13 @@ var SarifScanOrder = [
var fs19 = __toESM(require("fs"));
var path16 = __toESM(require("path"));
var import_perf_hooks5 = require("perf_hooks");
var io5 = __toESM(require_io());
var io6 = __toESM(require_io());
// src/codeql.ts
var fs18 = __toESM(require("fs"));
var path15 = __toESM(require("path"));
var core12 = __toESM(require_core());
var toolrunner3 = __toESM(require_toolrunner());
var toolrunner4 = __toESM(require_toolrunner());
// src/cli-errors.ts
var SUPPORTED_PLATFORMS = [
@@ -183421,6 +183421,8 @@ var semver7 = __toESM(require_semver2());
// src/platform.ts
var import_fs = __toESM(require("fs"));
var toolrunner3 = __toESM(require_toolrunner());
var io4 = __toESM(require_io());
var semver6 = __toESM(require_semver2());
function getBundlePlatform(platform2 = process.platform, arch2 = process.arch) {
switch (platform2) {
@@ -183476,8 +183478,53 @@ function xcodeVersionFromLink(logger2) {
return void 0;
}
}
function xcodeVersion(logger2) {
return xcodeVersionFromLink(logger2);
async function xcodeVersionFromSdk(logger2) {
try {
const xcrun = await io4.which("xcrun", true);
let stdout = "";
let stderr = "";
const exitCode = await new toolrunner3.ToolRunner(
xcrun,
["--sdk", "macosx", "--show-sdk-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(`xcrun returned exit code ${exitCode}`);
logger2.debug(`stdout:
${stdout}`);
logger2.debug(`stderr:
${stderr}`);
return void 0;
}
const version = semver6.coerce(stdout.trim());
if (version === null) {
logger2.debug(
`xcrun'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: ${getErrorMessage(err)}`);
return void 0;
}
}
async function xcodeVersion(logger2) {
return xcodeVersionFromLink(logger2) ?? await xcodeVersionFromSdk(logger2);
}
// src/per-language-bundles.ts
@@ -183662,13 +183709,13 @@ var import_child_process = require("child_process");
var fs14 = __toESM(require("fs"));
var stream = __toESM(require("stream"));
var import_toolrunner = __toESM(require_toolrunner());
var io4 = __toESM(require_io());
var io5 = __toESM(require_io());
var toolcache = __toESM(require_tool_cache());
var semver8 = __toESM(require_semver2());
var MIN_REQUIRED_BSD_TAR_VERSION = "3.4.3";
var MIN_REQUIRED_GNU_TAR_VERSION = "1.31";
async function getTarVersion() {
const tar = await io4.which("tar", true);
const tar = await io5.which("tar", true);
let stdout = "";
const exitCode = await new import_toolrunner.ToolRunner(tar, ["--version"], {
listeners: {
@@ -185157,7 +185204,7 @@ async function getCodeQLForCmd(logger2, cmd, checkVersion, env = getEnv()) {
])
);
}
await new toolrunner3.ToolRunner(cmd, args, {
await new toolrunner4.ToolRunner(cmd, args, {
env: env.getDefined()
}).exec();
},
@@ -185178,7 +185225,7 @@ async function getCodeQLForCmd(logger2, cmd, checkVersion, env = getEnv()) {
if (automationDetailsId !== void 0) {
args.push("--sarif-category", automationDetailsId);
}
await new toolrunner3.ToolRunner(cmd, args, {
await new toolrunner4.ToolRunner(cmd, args, {
env: env.getDefined()
}).exec();
},
@@ -185207,13 +185254,13 @@ async function getCodeQLForCmd(logger2, cmd, checkVersion, env = getEnv()) {
if (automationDetailsId !== void 0) {
args.push("--sarif-category", automationDetailsId);
}
await new toolrunner3.ToolRunner(cmd, args, {
await new toolrunner4.ToolRunner(cmd, args, {
env: env.getDefined()
}).exec();
},
async resolveExtractor(language) {
let extractorPath = "";
await new toolrunner3.ToolRunner(
await new toolrunner4.ToolRunner(
cmd,
[
"resolve",
@@ -186156,7 +186203,7 @@ async function runFinalize(features, outputDir, threadsFlag, memoryFlag, codeql,
async function warnIfGoInstalledAfterInit(config, logger2) {
const goInitPath = process.env["CODEQL_ACTION_GO_BINARY" /* GO_BINARY_LOCATION */];
if (process.env["CODEQL_ACTION_DID_AUTOBUILD_GOLANG" /* DID_AUTOBUILD_GOLANG */] !== "true" && goInitPath !== void 0) {
const goBinaryPath = await io5.which("go", true);
const goBinaryPath = await io6.which("go", true);
if (goInitPath !== goBinaryPath) {
logger2.warning(
`Expected \`which go\` to return ${goInitPath}, but got ${goBinaryPath}: please ensure that the correct version of Go is installed before the \`codeql-action/init\` Action is used.`
@@ -187505,9 +187552,9 @@ async function addFingerprints(sarifLog, sourceRoot, logger2) {
// src/init.ts
var fs22 = __toESM(require("fs"));
var path18 = __toESM(require("path"));
var toolrunner4 = __toESM(require_toolrunner());
var toolrunner5 = __toESM(require_toolrunner());
var github3 = __toESM(require_github());
var io6 = __toESM(require_io());
var io7 = __toESM(require_io());
async function initCodeQL(toolsInput, apiDetails, tempDir, variant, defaultCliVersion, rawLanguages, otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, features, logger2) {
logger2.startGroup("Setup CodeQL tools");
const { codeql, toolsDownloadStatusReport, toolsSource, toolsVersion } = await setupCodeQL(
@@ -187620,7 +187667,7 @@ async function checkInstallPython311(languages, codeql) {
"../python-setup",
"check_python12.ps1"
);
await new toolrunner4.ToolRunner(await io6.which("powershell", true), [
await new toolrunner5.ToolRunner(await io7.which("powershell", true), [
script
]).exec();
}
@@ -190348,10 +190395,10 @@ var Minimatch = class {
let ai = 0;
let bi = 0;
let result = [];
let which9 = "";
let which10 = "";
while (ai < a.length && bi < b.length) {
if (a[ai] === b[bi]) {
result.push(which9 === "b" ? b[bi] : a[ai]);
result.push(which10 === "b" ? b[bi] : a[ai]);
ai++;
bi++;
} else if (emptyGSMatch && a[ai] === "**" && b[bi] === a[ai + 1]) {
@@ -190361,16 +190408,16 @@ var Minimatch = class {
result.push(b[bi]);
bi++;
} else if (a[ai] === "*" && b[bi] && (this.options.dot || !b[bi].startsWith(".")) && b[bi] !== "**") {
if (which9 === "b")
if (which10 === "b")
return false;
which9 = "a";
which10 = "a";
result.push(a[ai]);
ai++;
bi++;
} else if (b[bi] === "*" && a[ai] && (this.options.dot || !a[ai].startsWith(".")) && a[ai] !== "**") {
if (which9 === "a")
if (which10 === "a")
return false;
which9 = "b";
which10 = "b";
result.push(b[bi]);
ai++;
bi++;
@@ -193859,7 +193906,7 @@ async function runWrapper3() {
var fs31 = __toESM(require("fs"));
var path25 = __toESM(require("path"));
var core19 = __toESM(require_core());
var io7 = __toESM(require_io());
var io8 = __toESM(require_io());
var semver11 = __toESM(require_semver2());
// src/config/inputs.ts
@@ -194019,7 +194066,7 @@ async function checkRunnerSupportsTracedAnalysis(action, config, codeql) {
);
return;
}
const xcodeVer = xcodeVersion(action.logger);
const xcodeVer = await xcodeVersion(action.logger);
if (xcodeVer !== void 0 && xcodeVer.major >= UNSUPPORTED_XCODE_FOR_TRACED) {
handleUnsupportedVersion(
action.logger,
@@ -194647,7 +194694,7 @@ async function run3(actionState) {
}
if (config.languages.includes("go" /* go */) && process.platform === "linux") {
try {
const goBinaryPath = await io7.which("go", true);
const goBinaryPath = await io8.which("go", true);
const fileOutput = await getFileType(goBinaryPath);
if (fileOutput.includes("statically linked") && !await codeql.supportsFeature(
"indirectTracingSupportsStaticBinaries" /* IndirectTracingSupportsStaticBinaries */
@@ -196002,8 +196049,8 @@ function generateCertificateAuthority() {
// src/start-proxy/environment.ts
var fs33 = __toESM(require("fs"));
var path28 = __toESM(require("path"));
var toolrunner5 = __toESM(require_toolrunner());
var io8 = __toESM(require_io());
var toolrunner6 = __toESM(require_toolrunner());
var io9 = __toESM(require_io());
function checkEnvVar(logger2, name) {
const value = process.env[name];
if (isDefined2(value)) {
@@ -196093,9 +196140,9 @@ function checkJdkSettings(logger2, jdkHome) {
}
async function showJavaSettings(logger2) {
try {
const java = await io8.which("java", true);
const java = await io9.which("java", true);
let output = "";
await new toolrunner5.ToolRunner(
await new toolrunner6.ToolRunner(
java,
["-XshowSettings:all", "-XshowSettings:security:all", "-version"],
{

View File

@@ -197,7 +197,7 @@ 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.
const xcodeVer = xcodeVersion(action.logger);
const xcodeVer = await xcodeVersion(action.logger);
if (
xcodeVer !== undefined &&

View File

@@ -1,5 +1,8 @@
import fs from "fs";
import type { ExecOptions } from "@actions/exec/lib/interfaces";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as io from "@actions/io";
import test from "ava";
import * as sinon from "sinon";
@@ -9,6 +12,8 @@ import {
darwinVersion,
XCODE_SELECT_LINK_PATH,
xcodeVersionFromLink,
xcodeVersion,
xcodeVersionFromSdk,
} from "./platform";
import {
checkExpectedLogMessages,
@@ -220,3 +225,94 @@ test.serial(
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
},
);
test.serial(
"xcodeVersionFromSdk 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 xcodeVersionFromSdk(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, [
"xcrun returned exit code 1",
]);
});
},
);
test.serial(
"xcodeVersionFromSdk 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("26.5"));
}
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 xcodeVersionFromSdk(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("xcodeVersion falls back to xcodeVersionFromSdk", async (t) => {
const logger = new RecordingLogger();
const readlinkSync = sinon
.stub(fs, "readlinkSync")
.throws(new Error("ENOENT: no such file or directory"));
const which = sinon.stub(io, "which").throws(new Error("No such tool"));
await t.notThrowsAsync(async () => {
const result = await xcodeVersion(logger);
t.is(result, undefined);
t.is(readlinkSync.callCount, 1);
t.is(which.callCount, 1);
t.deepEqual(which.args[0], ["xcrun", true]);
checkExpectedLogMessages(t, logger.messages, [
"Unable to determine Xcode version: ENOENT: no such file or directory",
"Unable to determine Xcode version: No such tool",
]);
});
});

View File

@@ -1,5 +1,7 @@
import fs from "fs";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as io from "@actions/io";
import * as semver from "semver";
import type { ActionState, Logger } from "./action-common";
@@ -113,12 +115,70 @@ export function xcodeVersionFromLink(
}
}
/**
* Tries to determine the version of Xcode that is installed by running `xcrun --sdk macosx --show-sdk-version`.
*
* @param logger The logger to use.
* @returns The Xcode version or `undefined` if it couldn't be determined.
*/
export async function xcodeVersionFromSdk(
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,
["--sdk", "macosx", "--show-sdk-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(`xcrun returned exit code ${exitCode}`);
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(stdout.trim());
if (version === null) {
logger.debug(
`xcrun'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: ${getErrorMessage(err)}`);
return undefined;
}
}
/**
* Tries to determine the version of Xcode that is installed.
*
* @param logger The logger to use.
* @returns The Xcode version or `undefined` if it couldn't be determined.
*/
export function xcodeVersion(logger: Logger): semver.SemVer | undefined {
return xcodeVersionFromLink(logger);
export async function xcodeVersion(
logger: Logger,
): Promise<semver.SemVer | undefined> {
return xcodeVersionFromLink(logger) ?? (await xcodeVersionFromSdk(logger));
}