From 4a6767cdd0faaaa28ded2b054493fec5bafdb1a9 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 29 Oct 2026 16:41:01 +0000 Subject: [PATCH 1/2] Rename `xcodeVersion` to `xcodeVersionFromLink` --- lib/entry-points.js | 5 ++- src/platform.test.ts | 103 +++++++++++++++++++++++-------------------- src/platform.ts | 17 ++++++- 3 files changed, 75 insertions(+), 50 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index b2bb9c1835..8f903af15b 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -183448,7 +183448,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 +183476,9 @@ function xcodeVersion(logger2) { return void 0; } } +function xcodeVersion(logger2) { + return xcodeVersionFromLink(logger2); +} // src/per-language-bundles.ts var MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION = "2.27.1"; diff --git a/src/platform.test.ts b/src/platform.test.ts index 58c41408e5..d84ccb43d7 100644 --- a/src/platform.test.ts +++ b/src/platform.test.ts @@ -8,7 +8,7 @@ import { getBundlePlatform, darwinVersion, XCODE_SELECT_LINK_PATH, - xcodeVersion, + xcodeVersionFromLink, } from "./platform"; import { checkExpectedLogMessages, @@ -71,21 +71,24 @@ 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(); @@ -94,7 +97,7 @@ test.serial("xcodeVersion returns undefined if file is not a symlink", (t) => { .stub(fs, "readlinkSync") .throws(new Error("EINVAL: invalid argument")); - t.is(xcodeVersion(logger), undefined); + t.is(xcodeVersionFromLink(logger), undefined); t.is(readlinkSync.callCount, 1); t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]); @@ -104,13 +107,13 @@ test.serial("xcodeVersion returns undefined if file is not a symlink", (t) => { }); 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 +124,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 +132,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 +142,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 +179,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 +209,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); diff --git a/src/platform.ts b/src/platform.ts index e5c0e6ac63..37cd7fa11b 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -68,12 +68,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 +112,13 @@ export function xcodeVersion(logger: Logger): semver.SemVer | undefined { 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); +} From be54d791a11fa6fd2800a0353310bd0cebe58c38 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 29 Oct 2026 17:19:57 +0000 Subject: [PATCH 2/2] Add `xcrun`-based fallback for Xcode version detection --- lib/entry-points.js | 175 +++++++++++++++++++++++++--------------- src/languages/traced.ts | 2 +- src/platform.test.ts | 96 ++++++++++++++++++++++ src/platform.ts | 64 ++++++++++++++- 4 files changed, 270 insertions(+), 67 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 8f903af15b..81833ec2de 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 1e6442dd60..a84c3aa745 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 d84ccb43d7..22aca6b217 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 37cd7fa11b..fd1487b503 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)); }