diff --git a/lib/entry-points.js b/lib/entry-points.js index b2bb9c1835..7740535e88 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) { @@ -183448,7 +183450,7 @@ var XCODE_SELECT_LINK_PATH = "/private/var/db/xcode_select_link"; var XCODE_APP_FILENAME_PATTERN = new RegExp( /(?Xcode_(?\d+(\.\d+)?).*\.app)/ ); -function xcodeVersion(logger2) { +function xcodeVersionFromLink(logger2) { try { const xcodePath = import_fs.default.readlinkSync(XCODE_SELECT_LINK_PATH); if (xcodePath.trim() === "") { @@ -183476,6 +183478,111 @@ function xcodeVersion(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 xcodeSdkVersion(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 SDK version: ${getErrorMessage(err)}` + ); + return void 0; + } +} // src/per-language-bundles.ts var MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION = "2.27.1"; @@ -183659,13 +183766,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: { @@ -185154,7 +185261,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(); }, @@ -185175,7 +185282,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(); }, @@ -185204,13 +185311,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", @@ -186153,7 +186260,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.` @@ -187502,9 +187609,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( @@ -187617,7 +187724,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(); } @@ -190345,10 +190452,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]) { @@ -190358,16 +190465,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++; @@ -193856,7 +193963,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 @@ -193911,20 +194018,35 @@ async function checkSwiftCompatibility(config, codeql, darwinVersion2) { // src/languages/traced.ts var UNSUPPORTED_DARWIN_FOR_TRACED = 27; var UNSUPPORTED_XCODE_FOR_TRACED = 27; -function handleUnsupportedVersion(logger2, config, tracedLanguages, abortUnsupportedTracedAnalysis, product, version) { - const unsupportedVersion = product === "macOS" ? UNSUPPORTED_DARWIN_FOR_TRACED : UNSUPPORTED_XCODE_FOR_TRACED; +function diagnosticAttributeName(product) { + switch (product) { + case "macOS": + return "darwinVersion"; + case "Xcode": + return "xcodeVersion"; + case "Xcode SDK": + return "xcodeSdkVersion"; + } +} +function handleUnsupportedVersion(logger2, config, tracedLanguages, abortUnsupportedTracedAnalysis, productVersion) { + const { product, version } = productVersion; + const unsupportedVersion = product === "macOS" ? UNSUPPORTED_DARWIN_FOR_TRACED : ( + // We use the same constant for both Xcode and the Xcode SDK since we treat them + // as equivalent indicators for whether traced analysis is unsupported. + UNSUPPORTED_XCODE_FOR_TRACED + ); const baseMessage = [ `Traced analysis is not supported on ${product} ${unsupportedVersion} or above.`, `Configure your analysis to run on macOS ${UNSUPPORTED_DARWIN_FOR_TRACED - 1} or below`, `and Xcode ${UNSUPPORTED_XCODE_FOR_TRACED - 1} or below.` ].join(" "); - const attributeName = product === "macOS" ? "darwinVersion" : "xcodeVersion"; + const attributeName = diagnosticAttributeName(product); for (const language of tracedLanguages) { addDiagnostic( config, language, makeDiagnostic( - `codeql-action/unsupported-traced-analysis-${product.toLowerCase()}`, + `codeql-action/unsupported-traced-analysis-${product.toLowerCase().replaceAll(" ", "-")}`, `Traced analysis is not supported on this version of ${product}`, { attributes: { @@ -194005,28 +194127,27 @@ async function checkRunnerSupportsTracedAnalysis(action, config, codeql) { config, tracedLanguages, abortUnsupportedTracedAnalysis, - "macOS", - version + { product: "macOS", version } ); return; } - if (isSelfHostedRunner(action.env)) { - action.logger.debug( - "Skipping traced analysis compatibility check for Xcode because we are on a self-hosted runner." - ); - return; + const xcodeVer = xcodeVersionFromLink(action.logger) ?? await xcodeVersionFromXcodeBuild(action.logger); + let xcodeProduct = void 0; + if (xcodeVer !== void 0) { + xcodeProduct = { product: "Xcode", version: xcodeVer }; + } else { + const xcodeSdkVer = await xcodeSdkVersion(action.logger); + xcodeProduct = xcodeSdkVer !== void 0 ? { product: "Xcode SDK", version: xcodeSdkVer } : void 0; } - const xcodeVer = xcodeVersion(action.logger); - if (xcodeVer !== void 0 && xcodeVer.major >= UNSUPPORTED_XCODE_FOR_TRACED) { + if (xcodeProduct !== void 0 && xcodeProduct.version.major >= UNSUPPORTED_XCODE_FOR_TRACED) { handleUnsupportedVersion( action.logger, config, tracedLanguages, abortUnsupportedTracedAnalysis, - "Xcode", - xcodeVer + xcodeProduct ); - } else if (xcodeVer === void 0) { + } else if (xcodeProduct === void 0) { action.logger.info( "Unable to determine Xcode version. Skipping Xcode compatibility check." ); @@ -194644,7 +194765,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 */ @@ -195999,8 +196120,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)) { @@ -196090,9 +196211,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.test.ts b/src/languages/traced.test.ts index 8de9512a5d..e4b38761c6 100644 --- a/src/languages/traced.test.ts +++ b/src/languages/traced.test.ts @@ -1,5 +1,8 @@ import fs from "fs"; +import type { ExecOptions } from "@actions/exec"; +import * as toolrunner from "@actions/exec/lib/toolrunner"; +import * as io from "@actions/io"; import test from "ava"; import * as sinon from "sinon"; @@ -344,41 +347,58 @@ test.serial( }), ); -test("checkRunnerSupportsTracedAnalysis doesn't perform Xcode check on self-hosted runners", async (t) => - withTmpDir(async (tmpDir) => { - const logger = new RecordingLogger(); - const env = getTestEnv(); - env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); - env.set(ActionsEnvVars.RUNNER_ENVIRONMENT, "self-hosted"); - const features = createFeatures([Feature.CheckUnsupportedTracedAnalysis]); +// `addDiagnostic` changes global state and we must stub it, so this test must be serial. +test.serial( + "checkRunnerSupportsTracedAnalysis doesn't add a diagnostic if Xcode version is supported", + async (t) => + withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const env = getTestEnv(); + env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); + const features = createFeatures([Feature.CheckUnsupportedTracedAnalysis]); - const codeql = await getCodeQLWithTracedStub( - "codeql-for-testing", - logger, - env, - ); - sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0")); + const readlinkSync = sinon + .stub(fs, "readlinkSync") + .returns("/Applications/Xcode_26.0.app/Contents/Developer"); - await checkRunnerSupportsTracedAnalysis( - initAllState({ + const codeql = await getCodeQLWithTracedStub( + "codeql-for-testing", logger, - platform: "darwin", - osRelease: "26.0.0", env, - features, - }), - createTestConfig({ tempDir: tmpDir, languages: [BuiltInLanguage.go] }), - codeql, - ); + ); + sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0")); - checkExpectedLogMessages(t, logger.messages, [ - "Skipping traced analysis compatibility check for Xcode because we are on a self-hosted runner.", - ]); - })); + const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic"); + + const config = createTestConfig({ + tempDir: tmpDir, + languages: [BuiltInLanguage.swift], + }); + await checkRunnerSupportsTracedAnalysis( + initAllState({ + logger, + platform: "darwin", + osRelease: "26.0.0", + env, + features, + }), + config, + codeql, + ); + + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + + checkUnexpectedLogMessages(t, logger.messages, [ + "Traced analysis is not supported on Xcode 27", + ]); + + t.is(addDiagnostic.callCount, 0); + }), +); -// `addDiagnostic` changes global state and we must stub it, so this test must be serial. test.serial( - "checkRunnerSupportsTracedAnalysis doesn't add a diagnostic if Xcode version is supported", + "checkRunnerSupportsTracedAnalysis attempts fallbacks if needed", async (t) => withTmpDir(async (tmpDir) => { const logger = new RecordingLogger(); @@ -388,7 +408,17 @@ test.serial( const readlinkSync = sinon .stub(fs, "readlinkSync") - .returns("/Applications/Xcode_26.0.app/Contents/Developer"); + .throws(new Error("Not found")); + const which = sinon.stub(io, "which").resolves("/path/to/xcrun"); + const exec = sinon.stub<[], Promise>().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", @@ -417,7 +447,28 @@ test.serial( t.is(readlinkSync.callCount, 1); t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + 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: Not found", + "Unable to determine Xcode version from xcodebuild: Error to force next fallback", + "Unable to determine Xcode SDK version: Error to force next fallback", + ]); checkUnexpectedLogMessages(t, logger.messages, [ "Traced analysis is not supported on Xcode 27", ]); @@ -496,6 +547,104 @@ test.serial( }), ); +// `addDiagnostic` changes global state and we must stub it, so this test must be serial. +test.serial( + "checkRunnerSupportsTracedAnalysis logs and adds diagnostic if Xcode SDK version is unsupported", + async (t) => + withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const env = getTestEnv(); + env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir); + const features = createFeatures([Feature.CheckUnsupportedTracedAnalysis]); + + const readlinkSync = sinon + .stub(fs, "readlinkSync") + .throws(new Error("Not found")); + 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("Xcode 27.5")); + stdoutListener(Buffer.from("Build version 27F9")); + return 0; + } else { + throw new Error("Error to force next fallback"); + } + }); + const runner = sinon.createStubInstance(toolrunner.ToolRunner, { + exec, + }); + const toolrunnerStub = sinon + .stub(toolrunner, "ToolRunner") + .callsFake((_cmd, args: string[], opts: ExecOptions) => { + if (args[0] !== "xcodebuild") { + stdoutListener = opts.listeners?.stdout; + } + return runner; + }); + + const codeql = await getCodeQLWithTracedStub( + "codeql-for-testing", + logger, + env, + ); + sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0")); + + const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic"); + + const config = createTestConfig({ + tempDir: tmpDir, + languages: [BuiltInLanguage.swift], + }); + await checkRunnerSupportsTracedAnalysis( + initAllState({ + logger, + platform: "darwin", + osRelease: "26.0.0", + env, + features, + }), + config, + codeql, + ); + + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + + t.is(which.callCount, 2); + t.is(toolrunnerStub.callCount, 2); + t.is(exec.callCount, 2); + + checkExpectedLogMessages(t, logger.messages, [ + "Unable to determine Xcode version from xcodebuild", + "Traced analysis is not supported on Xcode SDK 27", + ]); + + t.is(addDiagnostic.callCount, 1); + t.like(addDiagnostic.args[0], [ + config, + BuiltInLanguage.swift, + { + attributes: { + languages: [BuiltInLanguage.swift], + xcodeSdkVersion: "27.5.0", + }, + severity: "warning", + source: { + id: "codeql-action/unsupported-traced-analysis-xcode-sdk", + name: "Traced analysis is not supported on this version of Xcode SDK", + }, + visibility: { + cliSummaryTable: true, + statusPage: true, + telemetry: true, + }, + } satisfies Partial, + ]); + }), +); + // `addDiagnostic` changes global state and we must stub it, so this test must be serial. test.serial( "checkRunnerSupportsTracedAnalysis throws if Xcode version is unsupported and FF is enabled", diff --git a/src/languages/traced.ts b/src/languages/traced.ts index 1e6442dd60..406afc9b35 100644 --- a/src/languages/traced.ts +++ b/src/languages/traced.ts @@ -1,13 +1,17 @@ import * as semver from "semver"; import type { ActionState, Logger } from "../action-common"; -import { isSelfHostedRunner } from "../actions-util"; import type { CodeQL } from "../codeql"; import type { ActionConfig } from "../config/action-config"; import { addDiagnostic, makeDiagnostic } from "../diagnostics"; import { DocUrl } from "../doc-url"; import { Feature } from "../feature-flags"; -import { darwinVersion, xcodeVersion } from "../platform"; +import { + darwinVersion, + xcodeVersionFromLink, + xcodeSdkVersion, + xcodeVersionFromXcodeBuild, +} from "../platform"; import { ConfigurationError } from "../util"; import { checkSwiftCompatibility } from "./swift"; @@ -18,6 +22,27 @@ export const UNSUPPORTED_DARWIN_FOR_TRACED = 27; /** Xcode 27 and above do not support traced extraction. */ export const UNSUPPORTED_XCODE_FOR_TRACED = 27; +/** Names of products that we check the versions of here. */ +type ProductName = "macOS" | "Xcode" | "Xcode SDK"; + +/** A version along with the product it relates to. */ +type ProductVersion = { + product: ProductName; + version: semver.SemVer; +}; + +/** Gets the appropriate attribute name for the diagnostic based on {@link product}. */ +function diagnosticAttributeName(product: ProductName): string { + switch (product) { + case "macOS": + return "darwinVersion"; + case "Xcode": + return "xcodeVersion"; + case "Xcode SDK": + return "xcodeSdkVersion"; + } +} + /** * Creates a diagnostic indicating that `version` of `product` is unsupported for traced analysis. * Depending on `skipUnsupportedTracedAnalysis`, this function then either throws a {@link ConfigurationError} @@ -27,28 +52,30 @@ export const UNSUPPORTED_XCODE_FOR_TRACED = 27; * @param config The CodeQL Action configuration. * @param tracedLanguages Traced languages that are configured for analysis. * @param abortUnsupportedTracedAnalysis Whether this is a fatal error. - * @param product The product that the version is unsupported of. - * @param version The unsupported version. + * @param productVersion A description of the product and its version that is unsupported. */ function handleUnsupportedVersion( logger: Logger, config: ActionConfig<"dbLocation" | "tempDir">, tracedLanguages: string[], abortUnsupportedTracedAnalysis: boolean, - product: "macOS" | "Xcode", - version: semver.SemVer, + productVersion: ProductVersion, ) { + const { product, version } = productVersion; const unsupportedVersion = product === "macOS" ? UNSUPPORTED_DARWIN_FOR_TRACED - : UNSUPPORTED_XCODE_FOR_TRACED; + : // We use the same constant for both Xcode and the Xcode SDK since we treat them + // as equivalent indicators for whether traced analysis is unsupported. + UNSUPPORTED_XCODE_FOR_TRACED; + const baseMessage = [ `Traced analysis is not supported on ${product} ${unsupportedVersion} or above.`, `Configure your analysis to run on macOS ${UNSUPPORTED_DARWIN_FOR_TRACED - 1} or below`, `and Xcode ${UNSUPPORTED_XCODE_FOR_TRACED - 1} or below.`, ].join(" "); - const attributeName = product === "macOS" ? "darwinVersion" : "xcodeVersion"; + const attributeName = diagnosticAttributeName(product); // Create a diagnostic that will show up on the TSP. for (const language of tracedLanguages) { @@ -56,7 +83,7 @@ function handleUnsupportedVersion( config, language, makeDiagnostic( - `codeql-action/unsupported-traced-analysis-${product.toLowerCase()}`, + `codeql-action/unsupported-traced-analysis-${product.toLowerCase().replaceAll(" ", "-")}`, `Traced analysis is not supported on this version of ${product}`, { attributes: { @@ -177,8 +204,7 @@ export async function checkRunnerSupportsTracedAnalysis( config, tracedLanguages, abortUnsupportedTracedAnalysis, - "macOS", - version, + { product: "macOS", version }, ); // Return early so that we don't end up producing more-or-less the same log message/diagnostic @@ -186,32 +212,39 @@ export async function checkRunnerSupportsTracedAnalysis( return; } - // Skip if we are on a self-hosted runner since the current Xcode version detection - // is only validated on hosted runners. - if (isSelfHostedRunner(action.env)) { - action.logger.debug( - "Skipping traced analysis compatibility check for Xcode because we are on a self-hosted runner.", - ); - return; - } - // 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); + // after determining that we are running on macOS. We first try to obtain the version of Xcode itself + // 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)); + let xcodeProduct: ProductVersion | undefined = undefined; + + if (xcodeVer !== undefined) { + xcodeProduct = { product: "Xcode", version: xcodeVer }; + } else { + // Fallback to checking the SDK version, which may be available even if a full Xcode installation is not. + const xcodeSdkVer = await xcodeSdkVersion(action.logger); + xcodeProduct = + xcodeSdkVer !== undefined + ? { product: "Xcode SDK", version: xcodeSdkVer } + : undefined; + } if ( - xcodeVer !== undefined && - xcodeVer.major >= UNSUPPORTED_XCODE_FOR_TRACED + xcodeProduct !== undefined && + xcodeProduct.version.major >= UNSUPPORTED_XCODE_FOR_TRACED ) { handleUnsupportedVersion( action.logger, config, tracedLanguages, abortUnsupportedTracedAnalysis, - "Xcode", - xcodeVer, + xcodeProduct, ); - } else if (xcodeVer === undefined) { + } else if (xcodeProduct === undefined) { action.logger.info( "Unable to determine Xcode version. Skipping Xcode compatibility check.", ); diff --git a/src/platform.test.ts b/src/platform.test.ts index 58c41408e5..e696142027 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"; @@ -8,7 +11,9 @@ import { getBundlePlatform, darwinVersion, XCODE_SELECT_LINK_PATH, - xcodeVersion, + xcodeVersionFromLink, + xcodeSdkVersion, + xcodeVersionFromXcodeBuild, } from "./platform"; import { checkExpectedLogMessages, @@ -71,46 +76,52 @@ test("darwinVersion returns semver if parsing succeeds", (t) => { } }); -test.serial("xcodeVersion returns undefined if symlink doesn't exist", (t) => { - const logger = new RecordingLogger(); +test.serial( + "xcodeVersionFromLink returns undefined if symlink doesn't exist", + (t) => { + const logger = new RecordingLogger(); - const readlinkSync = sinon - .stub(fs, "readlinkSync") - .throws(new Error("ENOENT: no such file or directory")); + const readlinkSync = sinon + .stub(fs, "readlinkSync") + .throws(new Error("ENOENT: no such file or directory")); - t.is(xcodeVersion(logger), undefined); - t.is(readlinkSync.callCount, 1); - t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + t.is(xcodeVersionFromLink(logger), undefined); + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); - checkExpectedLogMessages(t, logger.messages, [ - "Unable to determine Xcode version: ENOENT: no such file or directory", - ]); -}); + checkExpectedLogMessages(t, logger.messages, [ + "Unable to determine Xcode version: ENOENT: no such file or directory", + ]); + }, +); -test.serial("xcodeVersion returns undefined if file is not a symlink", (t) => { - const logger = new RecordingLogger(); +test.serial( + "xcodeVersionFromLink returns undefined if file is not a symlink", + (t) => { + const logger = new RecordingLogger(); - const readlinkSync = sinon - .stub(fs, "readlinkSync") - .throws(new Error("EINVAL: invalid argument")); + const readlinkSync = sinon + .stub(fs, "readlinkSync") + .throws(new Error("EINVAL: invalid argument")); - t.is(xcodeVersion(logger), undefined); - t.is(readlinkSync.callCount, 1); - t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + t.is(xcodeVersionFromLink(logger), undefined); + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); - checkExpectedLogMessages(t, logger.messages, [ - "Unable to determine Xcode version: EINVAL: invalid argument", - ]); -}); + checkExpectedLogMessages(t, logger.messages, [ + "Unable to determine Xcode version: EINVAL: invalid argument", + ]); + }, +); test.serial( - "xcodeVersion returns undefined if resolving the symlink returns nothing", + "xcodeVersionFromLink returns undefined if resolving the symlink returns nothing", (t) => { const logger = new RecordingLogger(); const readlinkSync = sinon.stub(fs, "readlinkSync").returns(""); - t.is(xcodeVersion(logger), undefined); + t.is(xcodeVersionFromLink(logger), undefined); t.is(readlinkSync.callCount, 1); t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); @@ -121,7 +132,7 @@ test.serial( ); test.serial( - "xcodeVersion returns undefined if resolved path doesn't include pattern", + "xcodeVersionFromLink returns undefined if resolved path doesn't include pattern", (t) => { const logger = new RecordingLogger(); @@ -129,7 +140,7 @@ test.serial( .stub(fs, "readlinkSync") .returns("/Applications/Xcode.app/Contents/Developer"); - t.is(xcodeVersion(logger), undefined); + t.is(xcodeVersionFromLink(logger), undefined); t.is(readlinkSync.callCount, 1); t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); @@ -139,30 +150,33 @@ test.serial( }, ); -test.serial("xcodeVersion returns undefined if match can't be parsed", (t) => { - const logger = new RecordingLogger(); +test.serial( + "xcodeVersionFromLink returns undefined if match can't be parsed", + (t) => { + const logger = new RecordingLogger(); - const readlinkSync = sinon - .stub(fs, "readlinkSync") - .returns("/Applications/Xcode_00.0.app/Contents/Developer"); + const readlinkSync = sinon + .stub(fs, "readlinkSync") + .returns("/Applications/Xcode_00.0.app/Contents/Developer"); - t.is(xcodeVersion(logger), undefined); - t.is(readlinkSync.callCount, 1); - t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + t.is(xcodeVersionFromLink(logger), undefined); + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); - checkExpectedLogMessages(t, logger.messages, [ - "Couldn't parse '00.0' as a semantic version.", - ]); -}); + checkExpectedLogMessages(t, logger.messages, [ + "Couldn't parse '00.0' as a semantic version.", + ]); + }, +); -test.serial("xcodeVersion returns version from resolved path", (t) => { +test.serial("xcodeVersionFromLink returns version from resolved path", (t) => { const logger = new RecordingLogger(); const readlinkSync = sinon .stub(fs, "readlinkSync") .returns("/Applications/Xcode_16.4.app/Contents/Developer"); - const result = xcodeVersion(logger); + const result = xcodeVersionFromLink(logger); if (t.truthy(result)) { t.is(result.major, 16); @@ -173,26 +187,29 @@ test.serial("xcodeVersion returns version from resolved path", (t) => { t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); }); -test.serial("xcodeVersion returns major version from resolved path", (t) => { - const logger = new RecordingLogger(); +test.serial( + "xcodeVersionFromLink returns major version from resolved path", + (t) => { + const logger = new RecordingLogger(); - const readlinkSync = sinon - .stub(fs, "readlinkSync") - .returns("/Applications/Xcode_16.app/Contents/Developer"); + const readlinkSync = sinon + .stub(fs, "readlinkSync") + .returns("/Applications/Xcode_16.app/Contents/Developer"); - const result = xcodeVersion(logger); + const result = xcodeVersionFromLink(logger); - if (t.truthy(result)) { - t.is(result.major, 16); - t.is(result.minor, 0); - } + if (t.truthy(result)) { + t.is(result.major, 16); + t.is(result.minor, 0); + } - t.is(readlinkSync.callCount, 1); - t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); -}); + t.is(readlinkSync.callCount, 1); + t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); + }, +); test.serial( - "xcodeVersion allows more version components in resolved path", + "xcodeVersionFromLink allows more version components in resolved path", (t) => { const logger = new RecordingLogger(); @@ -200,7 +217,7 @@ test.serial( .stub(fs, "readlinkSync") .returns("/Applications/Xcode_16.5.9.app/Contents/Developer"); - const result = xcodeVersion(logger); + const result = xcodeVersionFromLink(logger); if (t.truthy(result)) { t.is(result.major, 16); @@ -211,3 +228,135 @@ test.serial( t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); }, ); + +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>().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>().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( + "xcodeSdkVersion 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 xcodeSdkVersion(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("xcodeSdkVersion 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 xcodeSdkVersion(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); + }); +}); diff --git a/src/platform.ts b/src/platform.ts index e5c0e6ac63..283a789c0b 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"; @@ -68,12 +70,15 @@ export const XCODE_APP_FILENAME_PATTERN = new RegExp( ); /** - * Tries to determine the version of Xcode that is installed. + * Tries to determine the version of Xcode that is installed from the resolved symbolic link + * at {@link XCODE_SELECT_LINK_PATH}. * * @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 { +export function xcodeVersionFromLink( + logger: Logger, +): semver.SemVer | undefined { try { // Read what the symbolic link points to. This will throw if `XCODE_SELECT_LINK_PATH` does not // exist (`ENOENT`) or is not a symbolic link (`EINVAL`). @@ -109,3 +114,132 @@ export function xcodeVersion(logger: Logger): semver.SemVer | undefined { return undefined; } } + +/** + * 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 { + 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 the Xcode SDK that is installed by running + * `xcrun --sdk macosx --show-sdk-version`. + * + * @param logger The logger to use. + * @returns The Xcode SDK version or `undefined` if it couldn't be determined. + */ +export async function xcodeSdkVersion( + 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 SDK version: ${getErrorMessage(err)}`, + ); + return undefined; + } +}