From dfa1339579ec1d367f65b6005771be148c466c7d Mon Sep 17 00:00:00 2001 From: Maxim Lobanov Date: Thu, 15 Oct 2020 09:11:40 +0300 Subject: [PATCH] Improve version parsing - parse Xcode version from version plist (#10) * rework xcode version retrieving * debug logs * fix version parsing * rebuild task * more debug * more debug * Update test.yml * improve logging * Update test.yml --- .github/workflows/test.yml | 2 +- ...de-beta.plist => xcode-beta-license.plist} | 0 ...table.plist => xcode-stable-license.plist} | 0 __tests__/data/xcode-version.plist | 20 ++ __tests__/xcode-selector.test.ts | 104 ++++------ __tests__/xcode-utils.test.ts | 182 +++++++++++++++--- dist/index.js | 93 ++++----- src/setup-xcode.ts | 5 + src/xcode-selector.ts | 37 +--- src/xcode-utils.ts | 72 +++++-- 10 files changed, 315 insertions(+), 200 deletions(-) rename __tests__/data/{xcode-beta.plist => xcode-beta-license.plist} (100%) rename __tests__/data/{xcode-stable.plist => xcode-stable-license.plist} (100%) create mode 100644 __tests__/data/xcode-version.plist diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ecfeca..b857f59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: runs-on: macos-latest strategy: matrix: - xcode-version: ['10.3', '11', '11.2', '11.4.0', '11.4.1', '^11.4.0', latest, latest-stable] + xcode-version: ['10.3', '11', '11.2', '11.4.1', '11.7', '12', '12.0', '12.2', '^11.4.0', '~11.4.0', latest, latest-stable] fail-fast: false steps: - name: Checkout diff --git a/__tests__/data/xcode-beta.plist b/__tests__/data/xcode-beta-license.plist similarity index 100% rename from __tests__/data/xcode-beta.plist rename to __tests__/data/xcode-beta-license.plist diff --git a/__tests__/data/xcode-stable.plist b/__tests__/data/xcode-stable-license.plist similarity index 100% rename from __tests__/data/xcode-stable.plist rename to __tests__/data/xcode-stable-license.plist diff --git a/__tests__/data/xcode-version.plist b/__tests__/data/xcode-version.plist new file mode 100644 index 0000000..178b0f7 --- /dev/null +++ b/__tests__/data/xcode-version.plist @@ -0,0 +1,20 @@ + + + + + BuildAliasOf + IDEFrameworks + BuildVersion + 4 + CFBundleShortVersionString + 12.0.1 + CFBundleVersion + 17220 + ProductBuildVersion + 12A7300 + ProjectName + IDEFrameworks + SourceVersion + 17220000000000000 + + diff --git a/__tests__/xcode-selector.test.ts b/__tests__/xcode-selector.test.ts index b2915f3..263fe67 100644 --- a/__tests__/xcode-selector.test.ts +++ b/__tests__/xcode-selector.test.ts @@ -1,75 +1,42 @@ -import * as fs from "fs"; -import * as child from "child_process"; +import fs from "fs"; +import child from "child_process"; import * as core from "@actions/core"; -import { XcodeSelector, XcodeVersion } from "../src/xcode-selector"; +import { XcodeSelector } from "../src/xcode-selector"; import * as xcodeUtils from "../src/xcode-utils"; -jest.mock("fs"); jest.mock("child_process"); -jest.mock("@actions/core"); -jest.mock("../src/xcode-utils"); -const buildFsDirentItem = (name: string, opt: { isSymbolicLink: boolean; isDirectory: boolean }): fs.Dirent => { - return { - name, - isSymbolicLink: () => opt.isSymbolicLink, - isDirectory: () => opt.isDirectory - } as fs.Dirent; -}; - -const fakeReadDirResults = [ - buildFsDirentItem("Xcode.app", { isSymbolicLink: true, isDirectory: false }), - buildFsDirentItem("Xcode.app", { isSymbolicLink: false, isDirectory: true }), - buildFsDirentItem("Xcode_11.1.app", { isSymbolicLink: false, isDirectory: true }), - buildFsDirentItem("Xcode_11.1_beta.app", { isSymbolicLink: true, isDirectory: false }), - buildFsDirentItem("Xcode_11.2.1.app", { isSymbolicLink: false, isDirectory: true }), - buildFsDirentItem("Xcode_11.4.app", { isSymbolicLink: true, isDirectory: false }), - buildFsDirentItem("Xcode_11.4_beta.app", { isSymbolicLink: false, isDirectory: true }), - buildFsDirentItem("Xcode_11.app", { isSymbolicLink: false, isDirectory: true }), - buildFsDirentItem("Xcode_12_beta.app", { isSymbolicLink: false, isDirectory: true }), - buildFsDirentItem("third_party_folder", { isSymbolicLink: false, isDirectory: true }), +const fakeGetXcodeVersionInfoResult: xcodeUtils.XcodeVersion[] = [ + { version: "10.3.0", buildNumber: "", path: "/Applications/Xcode_10.3.app", releaseType: "GM", stable: true }, + { version: "12.0.0", buildNumber: "", path: "/Applications/Xcode_12_beta.app", releaseType: "Beta", stable: false }, + { version: "11.2.1", buildNumber: "", path: "/Applications/Xcode_11.2.1.app", releaseType: "GM", stable: true }, + { version: "11.4.0", buildNumber: "", path: "/Applications/Xcode_11.4.app", releaseType: "GM", stable: true }, + { version: "11.0.0", buildNumber: "", path: "/Applications/Xcode_11.app", releaseType: "GM", stable: true }, + { version: "11.2.0", buildNumber: "", path: "/Applications/Xcode_11.2.app", releaseType: "GM", stable: true }, ]; - -const fakeGetVersionsResult: XcodeVersion[] = [ - { version: "12.0.0", path: "", stable: false }, - { version: "11.4.0", path: "", stable: true }, - { version: "11.2.1", path: "", stable: true }, - { version: "11.2.0", path: "", stable: true }, - { version: "11.0.0", path: "", stable: true }, - { version: "10.3.0", path: "", stable: true } +const fakeGetInstalledXcodeAppsResult: string[] = [ + "/Applications/Xcode_10.3.app", + "/Applications/Xcode_12_beta.app", + "/Applications/Xcode_11.2.1.app", + "/Applications/Xcode_11.4.app", + "/Applications/Xcode_11.app", + "/Applications/Xcode_11.2.app", + "/Applications/Xcode_fake_path.app" +]; +const expectedGetAllVersionsResult: xcodeUtils.XcodeVersion[] = [ + { version: "12.0.0", buildNumber: "", path: "/Applications/Xcode_12_beta.app", releaseType: "Beta", stable: false }, + { version: "11.4.0", buildNumber: "", path: "/Applications/Xcode_11.4.app", releaseType: "GM", stable: true }, + { version: "11.2.1", buildNumber: "", path: "/Applications/Xcode_11.2.1.app", releaseType: "GM", stable: true }, + { version: "11.2.0", buildNumber: "", path: "/Applications/Xcode_11.2.app", releaseType: "GM", stable: true }, + { version: "11.0.0", buildNumber: "", path: "/Applications/Xcode_11.app", releaseType: "GM", stable: true }, + { version: "10.3.0", buildNumber: "", path: "/Applications/Xcode_10.3.app", releaseType: "GM", stable: true }, ]; describe("XcodeSelector", () => { - describe("getXcodeVersionFromAppPath", () => { - beforeEach(() => { - jest.spyOn(xcodeUtils, "getXcodeReleaseType").mockImplementation(() => xcodeUtils.XcodeReleaseType.GM); - }); - - afterEach(() => { - jest.resetAllMocks(); - jest.clearAllMocks(); - }); - - it.each([ - ["/temp/Xcode_11.app", { version: "11.0.0", path: "/temp/Xcode_11.app", stable: true }], - ["/temp/Xcode_11.2.app", { version: "11.2.0", path: "/temp/Xcode_11.2.app", stable: true }], - ["/temp/Xcode_11.2.1.app", { version: "11.2.1", path: "/temp/Xcode_11.2.1.app", stable: true }], - ["/temp/Xcode_11.2.1_beta.app", { version: "11.2.1", path: "/temp/Xcode_11.2.1_beta.app", stable: true }], - ["/temp/Xcode.app", null], - ["/temp/Xcode_11.2", null], - ["/temp/Xcode.11.2.app", null] - ])("'%s' -> '%s'", (input: string, expected: XcodeVersion | null) => { - // test private method - const actual = new XcodeSelector()["getXcodeVersionFromAppPath"](input); - expect(actual).toEqual(expected); - }); - - }); - describe("getAllVersions", () => { beforeEach(() => { - jest.spyOn(fs, "readdirSync").mockImplementation(() => fakeReadDirResults); - jest.spyOn(xcodeUtils, "getXcodeReleaseType").mockImplementation(() => xcodeUtils.XcodeReleaseType.GM); + jest.spyOn(xcodeUtils, "getInstalledXcodeApps").mockImplementation(() => fakeGetInstalledXcodeAppsResult); + jest.spyOn(xcodeUtils, "getXcodeVersionInfo").mockImplementation((path) => fakeGetXcodeVersionInfoResult.find(app => app.path === path) ?? null); }); afterEach(() => { @@ -79,14 +46,7 @@ describe("XcodeSelector", () => { it("versions are filtered correctly", () => { const sel = new XcodeSelector(); - const expectedVersions: XcodeVersion[] = [ - { version: "12.0.0", path: "/Applications/Xcode_12_beta.app", stable: true}, - { version: "11.4.0", path: "/Applications/Xcode_11.4_beta.app", stable: true }, - { version: "11.2.1", path: "/Applications/Xcode_11.2.1.app", stable: true }, - { version: "11.1.0", path: "/Applications/Xcode_11.1.app", stable: true }, - { version: "11.0.0", path: "/Applications/Xcode_11.app", stable: true }, - ]; - expect(sel.getAllVersions()).toEqual(expectedVersions); + expect(sel.getAllVersions()).toEqual(expectedGetAllVersionsResult); }); }); @@ -106,7 +66,7 @@ describe("XcodeSelector", () => { ["give me latest version", null] ] as [string, string | null][])("'%s' -> '%s'", (versionSpec: string, expected: string | null) => { const sel = new XcodeSelector(); - sel.getAllVersions = (): XcodeVersion[] => fakeGetVersionsResult; + sel.getAllVersions = (): xcodeUtils.XcodeVersion[] => expectedGetAllVersionsResult; const matchedVersion = sel.findVersion(versionSpec)?.version ?? null; expect(matchedVersion).toBe(expected); }); @@ -116,8 +76,10 @@ describe("XcodeSelector", () => { let coreExportVariableSpy: jest.SpyInstance; let fsExistsSpy: jest.SpyInstance; let fsSpawnSpy: jest.SpyInstance; - const xcodeVersion: XcodeVersion = { + const xcodeVersion: xcodeUtils.XcodeVersion = { version: "11.4", + buildNumber: "12A7300", + releaseType: "GM", path: "/Applications/Xcode_11.4.app", stable: true }; diff --git a/__tests__/xcode-utils.test.ts b/__tests__/xcode-utils.test.ts index c1e89de..afb472d 100644 --- a/__tests__/xcode-utils.test.ts +++ b/__tests__/xcode-utils.test.ts @@ -1,49 +1,173 @@ -import * as path from "path"; -import { getXcodeReleaseType, XcodeReleaseType } from "../src/xcode-utils"; +import fs from "fs"; +import path from "path"; +import * as xcodeUtils from "../src/xcode-utils"; -jest.mock("path"); +let pathJoinSpy: jest.SpyInstance; +let readdirSyncSpy: jest.SpyInstance; +let getXcodeReleaseTypeSpy: jest.SpyInstance; +let parsePlistFileSpy: jest.SpyInstance; -describe("getXcodeReleaseType", () => { - const buildPlistPath = (plistName: string) => { - return `${__dirname}/data/${plistName}`; - }; +const buildPlistPath = (plistName: string) => { + return `${__dirname}/data/${plistName}`; +}; + +const buildFsDirentItem = (name: string, opt: { isSymbolicLink: boolean; isDirectory: boolean }): fs.Dirent => { + return { + name, + isSymbolicLink: () => opt.isSymbolicLink, + isDirectory: () => opt.isDirectory + } as fs.Dirent; +}; - let pathJoinSpy: jest.SpyInstance; +const fakeReadDirResults = [ + buildFsDirentItem("Xcode_2.app", { isSymbolicLink: true, isDirectory: false }), + buildFsDirentItem("Xcode.app", { isSymbolicLink: false, isDirectory: true }), + buildFsDirentItem("Xcode_11.1.app", { isSymbolicLink: false, isDirectory: true }), + buildFsDirentItem("Xcode_11.1_beta.app", { isSymbolicLink: true, isDirectory: false }), + buildFsDirentItem("Xcode_11.2.1.app", { isSymbolicLink: false, isDirectory: true }), + buildFsDirentItem("Xcode_11.4.app", { isSymbolicLink: true, isDirectory: false }), + buildFsDirentItem("Xcode_11.4_beta.app", { isSymbolicLink: false, isDirectory: true }), + buildFsDirentItem("Xcode_11.app", { isSymbolicLink: false, isDirectory: true }), + buildFsDirentItem("Xcode_12_beta.app", { isSymbolicLink: false, isDirectory: true }), + buildFsDirentItem("third_party_folder", { isSymbolicLink: false, isDirectory: true }), +]; +describe("getInstalledXcodeApps", () => { beforeEach(() => { - pathJoinSpy = jest.spyOn(path, "join"); + readdirSyncSpy = jest.spyOn(fs, "readdirSync"); }); - it("stable release", () => { - const plistPath = buildPlistPath("xcode-stable.plist"); - pathJoinSpy.mockImplementation(() => plistPath); - const releaseType = getXcodeReleaseType(""); - expect(releaseType).toBe(XcodeReleaseType.GM); + it("versions are filtered correctly", () => { + readdirSyncSpy.mockImplementation(() => fakeReadDirResults); + const expectedVersions: string[] = [ + "/Applications/Xcode_11.1.app", + "/Applications/Xcode_11.2.1.app", + "/Applications/Xcode_11.4_beta.app", + "/Applications/Xcode_11.app", + "/Applications/Xcode_12_beta.app", + ]; + + const installedXcodeApps = xcodeUtils.getInstalledXcodeApps(); + expect(installedXcodeApps).toEqual(expectedVersions); }); - it("beta release", () => { - const plistPath = buildPlistPath("xcode-beta.plist"); - pathJoinSpy.mockImplementation(() => plistPath); - const releaseType = getXcodeReleaseType(""); - expect(releaseType).toBe(XcodeReleaseType.Beta); + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); }); +}); - it("unknown release", () => { - const plistPath = buildPlistPath("xcode-empty-license.plist"); - pathJoinSpy.mockImplementation(() => plistPath); - const releaseType = getXcodeReleaseType(""); - expect(releaseType).toBe(XcodeReleaseType.Unknown); +describe("getXcodeReleaseType", () => { + beforeEach(() => { + pathJoinSpy = jest.spyOn(path, "join"); }); - it("non-existent plist", () => { - const plistPath = buildPlistPath("xcode-fake.plist"); + it.each([ + ["xcode-stable-license.plist", "GM"], + ["xcode-beta-license.plist", "Beta"], + ["xcode-empty-license.plist", "Unknown"], + ["xcode-fake.plist", "Unknown"], + ] as [string, xcodeUtils.XcodeVersionReleaseType][])("%s -> %s", (plistName: string, expected: xcodeUtils.XcodeVersionReleaseType) => { + const plistPath = buildPlistPath(plistName); pathJoinSpy.mockImplementation(() => plistPath); - const releaseType = getXcodeReleaseType(""); - expect(releaseType).toBe(XcodeReleaseType.Unknown); + const releaseType = xcodeUtils.getXcodeReleaseType(""); + expect(releaseType).toBe(expected); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); +}); + +describe("getXcodeVersionInfo", () => { + beforeEach(() => { + pathJoinSpy = jest.spyOn(path, "join"); + getXcodeReleaseTypeSpy = jest.spyOn(xcodeUtils, "getXcodeReleaseType"); }); afterEach(() => { jest.resetAllMocks(); jest.clearAllMocks(); }); -}); \ No newline at end of file + + it("read version from plist", () => { + const plistPath = buildPlistPath("xcode-version.plist"); + pathJoinSpy.mockImplementation(() => plistPath); + getXcodeReleaseTypeSpy.mockImplementation(() => "GM"); + + const expected: xcodeUtils.XcodeVersion = { + version: "12.0.1", + buildNumber: "12A7300", + path: "fake_path", + releaseType: "GM", + stable: true + }; + + const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path"); + expect(xcodeInfo).toEqual(expected); + }); + + describe("'stable' property", () => { + it.each([ + ["GM", true], + ["Beta", false], + ["Unknown", false] + ])("%s -> %s", (releaseType: string, expected: boolean) => { + const plistPath = buildPlistPath("xcode-version.plist"); + pathJoinSpy.mockImplementation(() => plistPath); + getXcodeReleaseTypeSpy.mockImplementation(() => releaseType); + + const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path"); + expect(xcodeInfo).toBeTruthy(); + expect(xcodeInfo?.stable).toBe(expected); + }); + }); + + describe("coerce validation", () => { + beforeEach(() => { + parsePlistFileSpy = jest.spyOn(xcodeUtils, "parsePlistFile"); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + it("full version", () => { + parsePlistFileSpy.mockImplementation(() => { + return { + CFBundleShortVersionString: "12.0.1", ProductBuildVersion: "2FF" + }; + }); + getXcodeReleaseTypeSpy.mockImplementation(() => "GM"); + + const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path"); + expect(xcodeInfo?.version).toBe("12.0.1"); + }); + + it("partial version", () => { + parsePlistFileSpy.mockImplementation(() => { + return { + CFBundleShortVersionString: "10.3", ProductBuildVersion: "2FF" + }; + }); + getXcodeReleaseTypeSpy.mockImplementation(() => "GM"); + + const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path"); + expect(xcodeInfo?.version).toBe("10.3.0"); + }); + + it("invalid version", () => { + parsePlistFileSpy.mockImplementation(() => { + return { + CFBundleShortVersionString: "fake_version", ProductBuildVersion: "2FF" + }; + }); + getXcodeReleaseTypeSpy.mockImplementation(() => "GM"); + + const xcodeInfo = xcodeUtils.getXcodeVersionInfo("fake_path"); + expect(xcodeInfo).toBeNull(); + }); + }); +}); diff --git a/dist/index.js b/dist/index.js index ae9182c..4651859 100644 --- a/dist/index.js +++ b/dist/index.js @@ -38,6 +38,11 @@ const run = () => { const versionSpec = core.getInput("xcode-version", { required: false }); core.info(`Switching Xcode to version '${versionSpec}'...`); const selector = new xcode_selector_1.XcodeSelector(); + if (core.isDebug()) { + core.startGroup("Available Xcode versions:"); + core.debug(JSON.stringify(selector.getAllVersions(), null, 2)); + core.endGroup(); + } const targetVersion = selector.findVersion(versionSpec); if (!targetVersion) { throw new Error([ @@ -88,33 +93,12 @@ exports.XcodeSelector = void 0; const child = __importStar(__webpack_require__(3129)); const core = __importStar(__webpack_require__(2186)); const fs = __importStar(__webpack_require__(5747)); -const path = __importStar(__webpack_require__(5622)); const semver = __importStar(__webpack_require__(1383)); const xcode_utils_1 = __webpack_require__(1752); class XcodeSelector { - constructor() { - this.xcodeRootDirectory = "/Applications"; - this.xcodeFilenameRegex = /Xcode_([\d.]+)(_beta)?\.app/; - } - getXcodeVersionFromAppPath(appPath) { - const match = appPath.match(this.xcodeFilenameRegex); - if (!match || match.length < 2) { - return null; - } - const version = semver.coerce(match[1]); - if (!semver.valid(version) || !version) { - return null; - } - return { - version: version.version, - path: appPath, - stable: xcode_utils_1.getXcodeReleaseType(appPath) === xcode_utils_1.XcodeReleaseType.GM, - }; - } getAllVersions() { - const childrenAll = fs.readdirSync(this.xcodeRootDirectory, { encoding: "utf8", withFileTypes: true }); - const childrenReal = childrenAll.filter(child => !child.isSymbolicLink() && child.isDirectory()).map(child => path.join(this.xcodeRootDirectory, child.name)); - const xcodeVersions = childrenReal.map(appPath => this.getXcodeVersionFromAppPath(appPath)).filter((appItem) => !!appItem); + const potentialXcodeApps = xcode_utils_1.getInstalledXcodeApps().map(appPath => xcode_utils_1.getXcodeVersionInfo(appPath)); + const xcodeVersions = potentialXcodeApps.filter((app) => !!app); // sort versions array by descending to make sure that the newest version will be picked up return xcodeVersions.sort((first, second) => semver.compare(second.version, first.version)); } @@ -171,37 +155,56 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getXcodeReleaseType = exports.XcodeReleaseType = void 0; +exports.getXcodeVersionInfo = exports.getXcodeReleaseType = exports.getInstalledXcodeApps = exports.parsePlistFile = void 0; const path = __importStar(__webpack_require__(5622)); const fs = __importStar(__webpack_require__(5747)); const core = __importStar(__webpack_require__(2186)); const plist = __importStar(__webpack_require__(1933)); -var XcodeReleaseType; -(function (XcodeReleaseType) { - XcodeReleaseType["GM"] = "GM"; - XcodeReleaseType["Beta"] = "Beta"; - XcodeReleaseType["Unknown"] = "Unknown"; -})(XcodeReleaseType = exports.XcodeReleaseType || (exports.XcodeReleaseType = {})); -exports.getXcodeReleaseType = (xcodeRootPath) => { - const licenseInfoPlistPath = path.join(xcodeRootPath, "Contents", "Resources", "LicenseInfo.plist"); - if (!fs.existsSync(licenseInfoPlistPath)) { - // Every Xcode should contain license plist but it can be changed in future - core.debug("Unable to determine Xcode version type based on license plist"); - core.debug(`Xcode License plist doesn't on exist on path '${licenseInfoPlistPath}'`); - return XcodeReleaseType.Unknown; +const semver = __importStar(__webpack_require__(1383)); +exports.parsePlistFile = (plistPath) => { + if (!fs.existsSync(plistPath)) { + core.debug(`Unable to open plist file. File doesn't exist on path '${plistPath}'`); + return null; } - const licenseInfoRawContent = fs.readFileSync(licenseInfoPlistPath, "utf8"); - const licenseInfo = plist.parse(licenseInfoRawContent); - if (!licenseInfo.licenseType) { + const plistRawContent = fs.readFileSync(plistPath, "utf8"); + return plist.parse(plistRawContent); +}; +exports.getInstalledXcodeApps = () => { + const applicationsDirectory = "/Applications"; + const xcodeAppFilenameRegex = /Xcode_([\d.]+)(_beta)?\.app/; + const allApplicationsChildItems = fs.readdirSync(applicationsDirectory, { encoding: "utf8", withFileTypes: true }); + const allApplicationsRealItems = allApplicationsChildItems.filter(child => !child.isSymbolicLink() && child.isDirectory()); + const xcodeAppsItems = allApplicationsRealItems.filter(app => xcodeAppFilenameRegex.test(app.name)); + return xcodeAppsItems.map(child => path.join(applicationsDirectory, child.name)); +}; +exports.getXcodeReleaseType = (xcodeRootPath) => { + var _a, _b; + const licenseInfo = exports.parsePlistFile(path.join(xcodeRootPath, "Contents", "Resources", "LicenseInfo.plist")); + const licenseType = (_b = (_a = licenseInfo === null || licenseInfo === void 0 ? void 0 : licenseInfo.licenseType) === null || _a === void 0 ? void 0 : _a.toString()) === null || _b === void 0 ? void 0 : _b.toLowerCase(); + if (!licenseType) { core.debug("Unable to determine Xcode version type based on license plist"); core.debug("Xcode License plist doesn't contain 'licenseType' property"); - return XcodeReleaseType.Unknown; + return "Unknown"; } - const licenseType = licenseInfo.licenseType.toString().toLowerCase(); - if (licenseType.includes("beta")) { - return XcodeReleaseType.Beta; + return licenseType.includes("beta") ? "Beta" : "GM"; +}; +exports.getXcodeVersionInfo = (xcodeRootPath) => { + var _a, _b; + const versionInfo = exports.parsePlistFile(path.join(xcodeRootPath, "Contents", "version.plist")); + const xcodeVersion = semver.coerce((_a = versionInfo === null || versionInfo === void 0 ? void 0 : versionInfo.CFBundleShortVersionString) === null || _a === void 0 ? void 0 : _a.toString()); + const xcodeBuildNumber = (_b = versionInfo === null || versionInfo === void 0 ? void 0 : versionInfo.ProductBuildVersion) === null || _b === void 0 ? void 0 : _b.toString(); + if (!xcodeVersion || !semver.valid(xcodeVersion)) { + core.debug(`Unable to retrieve Xcode version info on path '${xcodeRootPath}'`); + return null; } - return XcodeReleaseType.GM; + const releaseType = exports.getXcodeReleaseType(xcodeRootPath); + return { + version: xcodeVersion.version, + buildNumber: xcodeBuildNumber, + releaseType: releaseType, + stable: releaseType === "GM", + path: xcodeRootPath, + }; }; diff --git a/src/setup-xcode.ts b/src/setup-xcode.ts index 1463d1f..bd749da 100644 --- a/src/setup-xcode.ts +++ b/src/setup-xcode.ts @@ -12,6 +12,11 @@ const run = (): void => { core.info(`Switching Xcode to version '${versionSpec}'...`); const selector = new XcodeSelector(); + if (core.isDebug()) { + core.startGroup("Available Xcode versions:"); + core.debug(JSON.stringify(selector.getAllVersions(), null, 2)); + core.endGroup(); + } const targetVersion = selector.findVersion(versionSpec); if (!targetVersion) { diff --git a/src/xcode-selector.ts b/src/xcode-selector.ts index 0b0cf24..6dbced3 100644 --- a/src/xcode-selector.ts +++ b/src/xcode-selector.ts @@ -1,48 +1,17 @@ import * as child from "child_process"; import * as core from "@actions/core"; import * as fs from "fs"; -import * as path from "path"; import * as semver from "semver"; -import { getXcodeReleaseType, XcodeReleaseType } from "./xcode-utils"; - -export interface XcodeVersion { - version: string; - path: string; - stable: boolean; -} +import { getInstalledXcodeApps, getXcodeVersionInfo, XcodeVersion } from "./xcode-utils"; export class XcodeSelector { - private xcodeRootDirectory = "/Applications"; - private xcodeFilenameRegex = /Xcode_([\d.]+)(_beta)?\.app/; - - private getXcodeVersionFromAppPath(appPath: string): XcodeVersion | null { - const match = appPath.match(this.xcodeFilenameRegex); - if (!match || match.length < 2) { - return null; - } - - const version = semver.coerce(match[1]); - if (!semver.valid(version) || !version) { - return null; - } - - return { - version: version.version, - path: appPath, - stable: getXcodeReleaseType(appPath) === XcodeReleaseType.GM, - }; - } - public getAllVersions(): XcodeVersion[] { - const childrenAll = fs.readdirSync(this.xcodeRootDirectory, { encoding: "utf8", withFileTypes: true }); - const childrenReal = childrenAll.filter(child => !child.isSymbolicLink() && child.isDirectory()).map(child => path.join(this.xcodeRootDirectory, child.name)); - - const xcodeVersions = childrenReal.map(appPath => this.getXcodeVersionFromAppPath(appPath)).filter((appItem): appItem is XcodeVersion => !!appItem); + const potentialXcodeApps = getInstalledXcodeApps().map(appPath => getXcodeVersionInfo(appPath)); + const xcodeVersions = potentialXcodeApps.filter((app): app is XcodeVersion => !!app); // sort versions array by descending to make sure that the newest version will be picked up return xcodeVersions.sort((first, second) => semver.compare(second.version, first.version)); } - public findVersion(versionSpec: string): XcodeVersion | null { const availableVersions = this.getAllVersions(); diff --git a/src/xcode-utils.ts b/src/xcode-utils.ts index 28ae6cb..4b4b4bc 100644 --- a/src/xcode-utils.ts +++ b/src/xcode-utils.ts @@ -2,35 +2,67 @@ import * as path from "path"; import * as fs from "fs"; import * as core from "@actions/core"; import * as plist from "plist"; +import * as semver from "semver"; -export enum XcodeReleaseType { - GM = "GM", - Beta = "Beta", - Unknown = "Unknown" +export type XcodeVersionReleaseType = "GM" | "Beta" | "Unknown"; + +export interface XcodeVersion { + version: string; + buildNumber: string; + path: string; + releaseType: XcodeVersionReleaseType; + stable: boolean; } -export const getXcodeReleaseType = (xcodeRootPath: string): XcodeReleaseType => { - const licenseInfoPlistPath = path.join(xcodeRootPath, "Contents", "Resources", "LicenseInfo.plist"); - if (!fs.existsSync(licenseInfoPlistPath)) { - // Every Xcode should contain license plist but it can be changed in future - core.debug("Unable to determine Xcode version type based on license plist"); - core.debug(`Xcode License plist doesn't on exist on path '${licenseInfoPlistPath}'`); - return XcodeReleaseType.Unknown; +export const parsePlistFile = (plistPath: string): plist.PlistObject | null => { + if (!fs.existsSync(plistPath)) { + core.debug(`Unable to open plist file. File doesn't exist on path '${plistPath}'`); + return null; } - const licenseInfoRawContent = fs.readFileSync(licenseInfoPlistPath, "utf8"); - const licenseInfo = plist.parse(licenseInfoRawContent) as plist.PlistObject; - if (!licenseInfo.licenseType) { + const plistRawContent = fs.readFileSync(plistPath, "utf8"); + return plist.parse(plistRawContent) as plist.PlistObject; +}; + +export const getInstalledXcodeApps = (): string[] => { + const applicationsDirectory = "/Applications"; + const xcodeAppFilenameRegex = /Xcode_([\d.]+)(_beta)?\.app/; + + const allApplicationsChildItems = fs.readdirSync(applicationsDirectory, { encoding: "utf8", withFileTypes: true }); + const allApplicationsRealItems = allApplicationsChildItems.filter(child => !child.isSymbolicLink() && child.isDirectory()); + const xcodeAppsItems = allApplicationsRealItems.filter(app => xcodeAppFilenameRegex.test(app.name)); + return xcodeAppsItems.map(child => path.join(applicationsDirectory, child.name)); +}; + +export const getXcodeReleaseType = (xcodeRootPath: string): XcodeVersionReleaseType => { + const licenseInfo = parsePlistFile(path.join(xcodeRootPath, "Contents", "Resources", "LicenseInfo.plist")); + const licenseType = licenseInfo?.licenseType?.toString()?.toLowerCase(); + if (!licenseType) { core.debug("Unable to determine Xcode version type based on license plist"); core.debug("Xcode License plist doesn't contain 'licenseType' property"); - return XcodeReleaseType.Unknown; + return "Unknown"; } - const licenseType = licenseInfo.licenseType.toString().toLowerCase(); + return licenseType.includes("beta") ? "Beta" : "GM"; +}; - if (licenseType.includes("beta")) { - return XcodeReleaseType.Beta; +export const getXcodeVersionInfo = (xcodeRootPath: string): XcodeVersion | null => { + const versionInfo = parsePlistFile(path.join(xcodeRootPath, "Contents", "version.plist")); + const xcodeVersion = semver.coerce(versionInfo?.CFBundleShortVersionString?.toString()); + const xcodeBuildNumber = versionInfo?.ProductBuildVersion?.toString(); + if (!xcodeVersion || !semver.valid(xcodeVersion)) { + core.debug(`Unable to retrieve Xcode version info on path '${xcodeRootPath}'`); + return null; } - - return XcodeReleaseType.GM; + + const releaseType = getXcodeReleaseType(xcodeRootPath); + + return { + version: xcodeVersion.version, + buildNumber: xcodeBuildNumber, + releaseType: releaseType, + stable: releaseType === "GM", + path: xcodeRootPath, + } as XcodeVersion; }; +