diff --git a/lib/entry-points.js b/lib/entry-points.js index 8f903af15..81833ec2d 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -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"], { diff --git a/src/languages/traced.ts b/src/languages/traced.ts index 1e6442dd6..a84c3aa74 100644 --- a/src/languages/traced.ts +++ b/src/languages/traced.ts @@ -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 && diff --git a/src/platform.test.ts b/src/platform.test.ts index d84ccb43d..22aca6b21 100644 --- a/src/platform.test.ts +++ b/src/platform.test.ts @@ -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>().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>().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", + ]); + }); +}); diff --git a/src/platform.ts b/src/platform.ts index 37cd7fa11..fd1487b50 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -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 { + 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 { + return xcodeVersionFromLink(logger) ?? (await xcodeVersionFromSdk(logger)); }