diff --git a/.github/workflows/test-full.yml b/.github/workflows/test-full.yml index 187c8c3..8f2d4d3 100644 --- a/.github/workflows/test-full.yml +++ b/.github/workflows/test-full.yml @@ -1,83 +1,54 @@ +# Some of these test cases should fail, it is expected + name: Validate 'setup-xamarin' (all test cases) on: pull_request: types: [ labeled ] jobs: - partial-versions: - name: valid versions (should pass) + invalid-version-format: + name: invalid version format (should fail) runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@master - + uses: actions/checkout@v2 - name: setup-xamarin + id: test uses: ./ with: - mono-version: 6.6 - xamarin-ios-version: 13.8 - xamarin-mac-version: 6.6 - xamarin-android-version: 10.1 - - - name: Validate versions - run: ./__tests__/validate-versions.sh '6.6' '13.8' '6.6' '10.1' + xamarin-mac-version: 6_6_0 # this version has invalid format - latest-keyword: - name: latest keyword (should pass) + xamarin-version-not-found: + name: Xamarin.iOS version is not found (should fail) runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@master - + uses: actions/checkout@v2 - name: setup-xamarin + id: test uses: ./ with: - mono-version: latest - xamarin-ios-version: latest - xamarin-mac-version: latest - xamarin-android-version: latest + mono-version: 6.6 + xamarin-ios-version: 11.1 # this versino doesn't exist - full-versions: - name: valid full versions (should warn) + xcode-version-not-found: + name: Xcode version is not found (should fail) runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@master - - - name: setup-xamarin - uses: ./ - with: - xamarin-ios-version: 13.10.0.21 - xamarin-mac-version: 6.6.0.12 - - - name: Validate versions - run: ./__tests__/validate-versions.sh '6.6.0' '13.10.0.21' '6.6.0.12' '10.1.3' - - invalid-version-format: - name: invalid version format (should fail) - runs-on: macos-latest - steps: - - name: setup-xamarin - id: test - uses: ./ - with: - xamarin-mac-version: 6_6_0 # this version has invalid format + uses: actions/checkout@v2 - xamarin-version-not-found: - name: Xamarin.iOS version is not found (should fail) - runs-on: macos-latest - steps: - name: setup-xamarin - id: test uses: ./ with: - mono-version: 6.6 - xamarin-ios-version: 11.1 # this versino doesn't exist + xcode-version: 10.3 invalid-platform: name: invalid platform (should fail) runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v2 - name: setup-xamarin id: test uses: ./ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 174eef1..c16e72f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,12 +5,12 @@ on: - cron: 0 0 * * * jobs: - partial-versions: - name: valid versions (should pass) + xamarin-partial-versions: + name: xamarin - valid versions runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@master + uses: actions/checkout@v2 - name: setup-xamarin uses: ./ @@ -21,14 +21,14 @@ jobs: xamarin-android-version: 10.1 - name: Validate versions - run: ./__tests__/validate-versions.sh '6.6' '13.8' '6.6' '10.1' + run: pwsh ./__tests__/validate-xamarin-versions.ps1 "6.6" "13.8" "6.6" "10.1" - latest-keyword: - name: latest keyword (should pass) + xamarin-latest-keyword: + name: xamarin - latest keyword runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@master + uses: actions/checkout@v2 - name: setup-xamarin uses: ./ @@ -38,12 +38,12 @@ jobs: xamarin-mac-version: latest xamarin-android-version: latest - full-versions: - name: valid full versions (should warn) + xamarin-full-versions: + name: xamarin - valid full versions (should warn) runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@master + uses: actions/checkout@v2 - name: setup-xamarin uses: ./ @@ -52,4 +52,31 @@ jobs: xamarin-mac-version: 6.6.0.12 - name: Validate versions - run: ./__tests__/validate-versions.sh '6.6.0' '13.10.0.21' '6.6.0.12' '10.1.3' \ No newline at end of file + run: pwsh ./__tests__/validate-xamarin-versions.ps1 -XamarinIOSVersion "13.10.0.21" -XamarinMacVersion "6.6.0.12" + + xcode-full-version: + name: xcode - valid version + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: setup-xamarin + uses: ./ + with: + xcode-version: 11.4 + + - name: Validate versions + run: pwsh ./__tests__/validate-xcode-version.ps1 -XcodeVersion "11.4" + + xcode-wildcard-version: + name: xcode - wildcard version + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: setup-xamarin + uses: ./ + with: + xcode-version: 11.x \ No newline at end of file diff --git a/README.md b/README.md index c0ae35d..5019dc6 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ # setup-xamarin -This action is intended to switch between pre-installed versions Xamarin & Mono on macos-10.15 image in GitHub Actions. -The list of available versions can be found in [virtual-environments](https://github.com/actions/virtual-environments/blob/master/images/macos/macos-10.15-Readme.md#mono) repository. +This action is intended to switch between pre-installed versions of Xamarin and Mono for macOS images in GitHub Actions. + # Available parameters -| Argument | Required | Description | -|-------------------------|----------|--------------------------------------------------| -| mono-version | False | Specify the version of Mono to switch | -| xamarin-ios-version | False | Specify the version of Xamarin.iOS to switch | -| xamarin-mac-version | False | Specify the version of Xamarin.Mac to switch | -| xamarin-android-version | False | Specify the version of Xamarin.Android to switch | +| Argument | Required | Description | Available versions | +|-------------------------|----------|-----------------------------------------------------------|--------------------| +| mono-version | False | Specify the version of Mono to switch | [Link](https://github.com/actions/virtual-environments/blob/master/images/macos/macos-10.15-Readme.md#mono) | +| xamarin-ios-version | False | Specify the version of Xamarin.iOS to switch | [Link](https://github.com/actions/virtual-environments/blob/master/images/macos/macos-10.15-Readme.md#xamarinios) | +| xamarin-mac-version | False | Specify the version of Xamarin.Mac to switch | [Link](https://github.com/actions/virtual-environments/blob/master/images/macos/macos-10.15-Readme.md#xamarinmac) | +| xamarin-android-version | False | Specify the version of Xamarin.Android to switch | [Link](https://github.com/actions/virtual-environments/blob/master/images/macos/macos-10.15-Readme.md#xamarinandroid) | +| xcode-version | False | Specify the Xcode to use with Xamarin.iOS and Xamarin.Mac | [Link](https://github.com/actions/virtual-environments/blob/master/images/macos/macos-10.15-Readme.md#xcode) | -All fields support the following format: `latest`, `13`, `13.2`, `13.2.1.4` +- `mono-version`, `xamarin-ios-version`, `xamarin-mac-version`, `xamarin-android-version` parameters support the following format: `latest`, `13`, `13.2`, `13.2.1.4` +- `xcode-version` parameter supports the following format: `latest`, `11.4`, `11.x`, `11.2.1` # Usage ``` @@ -30,6 +32,7 @@ jobs: xamarin-ios-version: 13 # specify version in '' format xamarin-mac-version: latest # specify 'latest' keyword to pick up the latest available version xamarin-android-version: 10.1.3.7 # specify full version; it is not recomended option because your pipeline can be broken suddenly in future + xcode-version: 11.x # set the latest available Xcode 11 ``` # License diff --git a/__tests__/validate-versions.sh b/__tests__/validate-versions.sh deleted file mode 100755 index bc89c4c..0000000 --- a/__tests__/validate-versions.sh +++ /dev/null @@ -1,19 +0,0 @@ -set -e - -MONO_VERSION=$1 -XAMARIN_IOS_VERSION=$2 -XAMARIN_MAC_VERSION=$3 -XAMARIN_ANDROID_VERSION=$4 - -mono --version | grep "Mono JIT compiler version ${MONO_VERSION}" -printf "Mono " -cat /Library/Frameworks/Mono.framework/Versions/Current/Version | grep $MONO_VERSION - -printf "Xamarin.iOS " -cat /Library/Frameworks/Xamarin.iOS.framework/Versions/Current/Version | grep $XAMARIN_IOS_VERSION - -printf "Xamarin.Mac " -cat /Library/Frameworks/Xamarin.Mac.framework/Versions/Current/Version | grep $XAMARIN_MAC_VERSION - -printf "Xamarin.Android " -cat /Library/Frameworks/Xamarin.Android.framework/Versions/Current/Version | grep $XAMARIN_ANDROID_VERSION diff --git a/__tests__/validate-xamarin-versions.ps1 b/__tests__/validate-xamarin-versions.ps1 new file mode 100755 index 0000000..278a415 --- /dev/null +++ b/__tests__/validate-xamarin-versions.ps1 @@ -0,0 +1,42 @@ +param ( + [string]$MonoVersion, + [string]$XamarinIOSVersion, + [string]$XamarinMacVersion, + [string]$XamarinAndroidVersion +) + +function Test-ToolVersion { + param ( + [string]$ToolName, + [string]$ExpectedVersion + ) + + if ([string]::IsNullOrEmpty($ExpectedVersion)) { + return + } + + Write-Host "Check $ToolName Version..." + + $versionFilePath = "/Library/Frameworks/$ToolName.framework/Versions/Current/Version" + $actualVersion = Get-Content $versionFilePath + if (!$actualVersion.StartsWith($ExpectedVersion)) { + Write-Error("Incorrect $ToolName version: $actualVersion") + exit 1 + } + + Write-Host "Correct $ToolName version: $ExpectedVersion" +} + +if (![string]::IsNullOrEmpty($MonoVersion)) { + Write-Host "Check Mono Version..." + $actualVersion = & mono --version + if (!$actualVersion[0].StartsWith("Mono JIT compiler version $MonoVersion")) { + Write-Error("Incorrect Mono version: $actualVersion") + exit 1 + } +} + +Test-ToolVersion -ToolName "Mono" -ExpectedVersion $MonoVersion +Test-ToolVersion -ToolName "Xamarin.IOS" -ExpectedVersion $XamarinIOSVersion +Test-ToolVersion -ToolName "Xamarin.Mac" -ExpectedVersion $XamarinMacVersion +Test-ToolVersion -ToolName "Xamarin.Android" -ExpectedVersion $XamarinAndroidVersion \ No newline at end of file diff --git a/__tests__/validate-xcode-version.ps1 b/__tests__/validate-xcode-version.ps1 new file mode 100644 index 0000000..a2d3453 --- /dev/null +++ b/__tests__/validate-xcode-version.ps1 @@ -0,0 +1,19 @@ +param ( + [string]$XcodeVersion +) + +$expectedXcodePath = "/Applications/Xcode_$XcodeVersion.app" + +Write-Host "Check Xcode version" +$actualXcodePath = & xcode-select -p +if (!$actualXcodePath.StartsWith($expectedXcodePath)) { + Write-Error "Incorrect Xcode: $actualXcodePath" + exit 1 +} + +if ($env:MD_APPLE_SDK_ROOT -ne $expectedXcodePath) { + Write-Error "Incorrect Xcode: $($env:MD_APPLE_SDK_ROOT)" + exit 1 +} + +Write-Host "Correct Xcode: $XcodeVersion" \ No newline at end of file diff --git a/__tests__/version-utils.test.ts b/__tests__/version-utils.test.ts index 46ae450..06598d0 100644 --- a/__tests__/version-utils.test.ts +++ b/__tests__/version-utils.test.ts @@ -15,6 +15,38 @@ describe("VersionUtils", () => { }); }); + it("sortVersions", () => { + const actual = VersionUtils.sortVersions([ + "11.2", + "11.4", + "10.1", + "11.2.1", + "10.2" + ]); + expect(actual).toEqual([ + "11.4", + "11.2.1", + "11.2", + "10.2", + "10.1" + ]); + }); + + describe("isVersionsEqual", () => { + it.each([ + ["11.2", "11.2", true], + ["11.x", "11.2", true], + ["11.x.x", "11.2", true], + ["11.x.x", "11.2.1", true], + ["11", "11.2", false], + ["11", "11.2.1", false], + ["10", "11.2", false] + ])("'%s', '%s' -> %s", (firstVersion: string, secondVersion: string, expected: boolean) => { + const actual = VersionUtils.isVersionsEqual(firstVersion, secondVersion); + expect(actual).toBe(expected); + }); + }); + describe("normalizeVersion", () => { it.each([ ["5", "5.x.x.x"], diff --git a/__tests__/tool-selector.test.ts b/__tests__/xamarin-selector.test.ts similarity index 96% rename from __tests__/tool-selector.test.ts rename to __tests__/xamarin-selector.test.ts index 5150194..4cbd3be 100644 --- a/__tests__/tool-selector.test.ts +++ b/__tests__/xamarin-selector.test.ts @@ -1,13 +1,13 @@ import * as fs from "fs"; import * as path from "path"; import * as core from "@actions/core"; +import * as utils from "../src/utils"; import { MonoToolSelector } from "../src/mono-selector"; import { XamarinIosToolSelector } from "../src/xamarin-ios-selector"; import { XamarinMacToolSelector } from "../src/xamarin-mac-selector"; import { XamarinAndroidToolSelector } from "../src/xamarin-android-selector"; -import * as utils from "../src/utils"; -import compareVersions from "compare-versions"; import { ToolSelector } from "../src/tool-selector"; +import { VersionUtils } from "../src/version-utils"; jest.mock("fs"); jest.mock("@actions/core"); @@ -35,7 +35,7 @@ const fakeReadDirResults = [ buildFsDirentItem("Latest", { isSymbolicLink: false, isDirectory: false }) ]; -const fakeGetVersionsResult = [ +const fakeGetVersionsResult = VersionUtils.sortVersions([ "13.2.0.47", "13.4.0.2", "13.6.0.12", @@ -45,7 +45,7 @@ const fakeGetVersionsResult = [ "13.9.1.0", "13.10.0.21", "14.0.2.1" -].sort(compareVersions).reverse(); +]); describe.each([ MonoToolSelector, @@ -122,7 +122,7 @@ describe.each([ const sel = new selectorClass(); sel.setVersion("1.2.3.4"); expect(fsInvokeCommandSpy).toHaveBeenCalledTimes(1); - expect(fsInvokeCommandSpy).toHaveBeenCalledWith("ln", expect.any(Array), true); + expect(fsInvokeCommandSpy).toHaveBeenCalledWith("ln", expect.any(Array), expect.any(Object)); }); it("symlink is recreated", () => { @@ -130,8 +130,8 @@ describe.each([ const sel = new selectorClass(); sel.setVersion("1.2.3.4"); expect(fsInvokeCommandSpy).toHaveBeenCalledTimes(2); - expect(fsInvokeCommandSpy).toHaveBeenCalledWith("rm", expect.any(Array), true); - expect(fsInvokeCommandSpy).toHaveBeenCalledWith("ln", expect.any(Array), true); + expect(fsInvokeCommandSpy).toHaveBeenCalledWith("rm", expect.any(Array), expect.any(Object)); + expect(fsInvokeCommandSpy).toHaveBeenCalledWith("ln", expect.any(Array), expect.any(Object)); }); it("error is thrown if version doesn't exist", () => { diff --git a/__tests__/xcode-selector.test.ts b/__tests__/xcode-selector.test.ts new file mode 100644 index 0000000..c5b1072 --- /dev/null +++ b/__tests__/xcode-selector.test.ts @@ -0,0 +1,131 @@ +import * as fs from "fs"; +import * as core from "@actions/core"; +import * as utils from "../src/utils"; +import { XcodeSelector } from "../src/xcode-selector"; +import { VersionUtils } from "../src/version-utils"; + +jest.mock("fs"); +jest.mock("@actions/core"); +jest.mock("../src/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("third_party_folder", { isSymbolicLink: false, isDirectory: true }), +]; + +const fakeGetVersionsResult = VersionUtils.sortVersions([ + "10.3", + "11", + "11.2", + "11.2.1", + "11.4" +]); + +describe("XcodeSelector", () => { + describe("xcodeRegex", () => { + it.each([ + ["Xcode_11.app", "11"], + ["Xcode_11.2.app", "11.2"], + ["Xcode_11.2.1.app", "11.2.1"], + ["Xcode.app", null], + ["Xcode_11.2", null], + ["Xcode.11.2.app", null] + ])("'%s' -> '%s'", (input: string, expected: string | null) => { + // test private method + const actual = new XcodeSelector()["parseXcodeVersionFromFilename"](input); + expect(actual).toBe(expected); + }); + + }); + + describe("getAllVersions", () => { + beforeEach(() => { + jest.spyOn(fs, "readdirSync").mockImplementation(() => fakeReadDirResults); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + it("versions are filtered correctly", () => { + const sel = new XcodeSelector(); + const expectedVersions = [ + "11.4", + "11.2.1", + "11.1", + "11" + ]; + expect(sel.getAllVersions()).toEqual(expectedVersions); + }); + }); + + describe("findVersion", () => { + it.each([ + ["latest", "11.4", "latest is matched"], + ["11", "11", "one digit is matched"], + ["11.x", "11.4", "one digit is matched and latest version is selected"], + ["10", null, "one digit is not matched"], + ["11.2", "11.2", "two digits are matched"], + ["11.2.x", "11.2.1", "two digits are matched and latest version is selected"], + ["11.4.x", "11.4", "the latest patch version is matched"], + ["10.4", null, "two digits are not matched"], + ["9.x", null, "two digits are not matched"], + ["11.5.x", null, "three digits are not matched"], + ["11.2.1", "11.2.1", "full version is matched"] + ] as [string, string | null, string][])("'%s' -> '%s' (%s)", (versionSpec: string, expected: string | null) => { + const sel = new XcodeSelector(); + sel.getAllVersions = (): string[] => fakeGetVersionsResult; + const matchedVersion = sel.findVersion(versionSpec); + expect(matchedVersion).toBe(expected); + }); + }); + + describe("setVersion", () => { + let coreExportVariableSpy: jest.SpyInstance; + let fsExistsSpy: jest.SpyInstance; + let fsInvokeCommandSpy: jest.SpyInstance; + + beforeEach(() => { + coreExportVariableSpy = jest.spyOn(core, "exportVariable"); + fsExistsSpy = jest.spyOn(fs, "existsSync"); + fsInvokeCommandSpy = jest.spyOn(utils, "invokeCommandSync"); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + it("works correctly", () => { + fsExistsSpy.mockImplementation(() => true); + const sel = new XcodeSelector(); + sel.setVersion("11.4"); + expect(fsInvokeCommandSpy).toHaveBeenCalledWith("xcode-select", expect.any(Array), expect.any(Object)); + expect(coreExportVariableSpy).toHaveBeenCalledWith("MD_APPLE_SDK_ROOT", expect.any(String)); + }); + + it("error is thrown if version doesn't exist", () => { + fsExistsSpy.mockImplementation(() => false); + const sel = new XcodeSelector(); + expect(() => sel.setVersion("11.4")).toThrow(); + expect(fsInvokeCommandSpy).toHaveBeenCalledTimes(0); + expect(coreExportVariableSpy).toHaveBeenCalledTimes(0); + }); + }); +}); \ No newline at end of file diff --git a/action.yml b/action.yml index c190864..0810674 100644 --- a/action.yml +++ b/action.yml @@ -14,6 +14,12 @@ inputs: xamarin-android-version: description: 'Version of Xamarin.Android to select' required: false + xcode-version: + description: 'Version of Xcode to use with Xamarin.iOS and Xamarin.Mac' + required: false runs: using: 'node12' - main: 'dist/index.js' \ No newline at end of file + main: 'dist/index.js' +branding: + icon: 'code' + color: 'yellow' \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index 143280b..d92ffbb 100644 --- a/dist/index.js +++ b/dist/index.js @@ -56,8 +56,8 @@ module.exports = require("os"); "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const tool_selector_1 = __webpack_require__(136); -class XamarinMacToolSelector extends tool_selector_1.ToolSelector { +const xamarin_selector_1 = __webpack_require__(792); +class XamarinMacToolSelector extends xamarin_selector_1.XamarinSelector { get toolName() { return "Xamarin.Mac"; } @@ -75,79 +75,6 @@ exports.XamarinMacToolSelector = XamarinMacToolSelector; module.exports = require("child_process"); -/***/ }), - -/***/ 136: -/***/ (function(__unusedmodule, exports, __webpack_require__) { - -"use strict"; - -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; - result["default"] = mod; - return result; -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __importStar(__webpack_require__(747)); -const path = __importStar(__webpack_require__(622)); -const core = __importStar(__webpack_require__(470)); -const compare_versions_1 = __importDefault(__webpack_require__(247)); -const utils_1 = __webpack_require__(611); -const version_utils_1 = __webpack_require__(957); -class ToolSelector { - get versionsDirectoryPath() { - return path.join(this.basePath, "Versions"); - } - getVersionPath(version) { - return path.join(this.versionsDirectoryPath, version); - } - getAllVersions() { - const children = fs.readdirSync(this.versionsDirectoryPath, { encoding: "utf8", withFileTypes: true }); - // macOS image contains symlinks for full versions, like '13.2' -> '13.2.3.0' - // filter such symlinks and look for only real versions - let potentialVersions = children.filter(child => !child.isSymbolicLink() && child.isDirectory()).map(child => child.name); - potentialVersions = potentialVersions.filter(child => compare_versions_1.default.validate(child)); - // sort versions array by descending to make sure that the newest version will be picked up - return potentialVersions.sort(compare_versions_1.default).reverse(); - } - findVersion(versionSpec) { - var _a; - const availableVersions = this.getAllVersions(); - if (availableVersions.length === 0) { - return null; - } - if (version_utils_1.VersionUtils.isLatestVersionKeyword(versionSpec)) { - return availableVersions[0]; - } - const normalizedVersionSpec = version_utils_1.VersionUtils.normalizeVersion(versionSpec); - core.debug(`Semantic version spec of '${versionSpec}' is '${normalizedVersionSpec}'`); - return (_a = availableVersions.find(ver => compare_versions_1.default.compare(ver, normalizedVersionSpec, "="))) !== null && _a !== void 0 ? _a : null; - } - setVersion(version) { - const targetVersionDirectory = this.getVersionPath(version); - if (!fs.existsSync(targetVersionDirectory)) { - throw new Error(`Invalid version: Directory '${targetVersionDirectory}' doesn't exist`); - } - const currentVersionDirectory = path.join(this.versionsDirectoryPath, "Current"); - core.debug(`Creating symlink '${currentVersionDirectory}' -> '${targetVersionDirectory}'`); - if (fs.existsSync(currentVersionDirectory)) { - utils_1.invokeCommandSync("rm", ["-f", currentVersionDirectory], true); - } - utils_1.invokeCommandSync("ln", ["-s", targetVersionDirectory, currentVersionDirectory], true); - } - static toString() { - // show correct name for test suite - return this.name; - } -} -exports.ToolSelector = ToolSelector; - - /***/ }), /***/ 182: @@ -156,8 +83,8 @@ exports.ToolSelector = ToolSelector; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const tool_selector_1 = __webpack_require__(136); -class XamarinAndroidToolSelector extends tool_selector_1.ToolSelector { +const xamarin_selector_1 = __webpack_require__(792); +class XamarinAndroidToolSelector extends xamarin_selector_1.XamarinSelector { get toolName() { return "Xamarin.Android"; } @@ -186,9 +113,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); const fs = __importStar(__webpack_require__(747)); const path = __importStar(__webpack_require__(622)); const core = __importStar(__webpack_require__(470)); -const tool_selector_1 = __webpack_require__(136); +const xamarin_selector_1 = __webpack_require__(792); const version_utils_1 = __webpack_require__(957); -class MonoToolSelector extends tool_selector_1.ToolSelector { +class MonoToolSelector extends xamarin_selector_1.XamarinSelector { get basePath() { return "/Library/Frameworks/Mono.framework"; } @@ -369,6 +296,7 @@ const xamarin_mac_selector_1 = __webpack_require__(116); const xamarin_android_selector_1 = __webpack_require__(182); const os_1 = __webpack_require__(87); const version_utils_1 = __webpack_require__(957); +const xcode_selector_1 = __webpack_require__(670); let showVersionMajorMinorWarning = false; const invokeSelector = (variableName, toolSelector) => { const versionSpec = core.getInput(variableName, { required: false }); @@ -402,6 +330,7 @@ const run = () => { invokeSelector("xamarin-ios-version", xamarin_ios_selector_1.XamarinIosToolSelector); invokeSelector("xamarin-mac-version", xamarin_mac_selector_1.XamarinMacToolSelector); invokeSelector("xamarin-android-version", xamarin_android_selector_1.XamarinAndroidToolSelector); + invokeSelector("xcode-version", xcode_selector_1.XcodeSelector); if (showVersionMajorMinorWarning) { core.warning([ "It is recommended to specify only major and minor versions of tool (like '13' or '13.2').", @@ -726,8 +655,8 @@ exports.getState = getState; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const tool_selector_1 = __webpack_require__(136); -class XamarinIosToolSelector extends tool_selector_1.ToolSelector { +const xamarin_selector_1 = __webpack_require__(792); +class XamarinIosToolSelector extends xamarin_selector_1.XamarinSelector { get toolName() { return "Xamarin.iOS"; } @@ -755,17 +684,18 @@ var __importStar = (this && this.__importStar) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); const child = __importStar(__webpack_require__(129)); const os_1 = __webpack_require__(87); -exports.invokeCommandSync = (command, args, sudo) => { +exports.invokeCommandSync = (command, args, options) => { let execResult; - if (sudo) { + if (options.sudo) { execResult = child.spawnSync("sudo", [command, ...args]); } else { execResult = child.spawnSync(command, args); } if (execResult.status !== 0) { + const fullCommand = `${options.sudo ? "sudo " : ""}${command} ${args.join(" ")}`; throw new Error([ - `Error during run ${sudo ? "sudo " : ""}${command} ${args.join(" ")}`, + `Error during run '${fullCommand}'`, execResult.stderr, execResult.stdout ].join(os_1.EOL)); @@ -780,6 +710,81 @@ exports.invokeCommandSync = (command, args, sudo) => { module.exports = require("path"); +/***/ }), + +/***/ 670: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = __importStar(__webpack_require__(747)); +const path = __importStar(__webpack_require__(622)); +const core = __importStar(__webpack_require__(470)); +const version_utils_1 = __webpack_require__(957); +const utils = __importStar(__webpack_require__(611)); +class XcodeSelector { + constructor() { + this.xcodeDirectoryPath = "/Applications"; + this.xcodeFilenameRegex = /Xcode_([\d.]+)(_beta)?\.app/; + } + parseXcodeVersionFromFilename(filename) { + const match = filename.match(this.xcodeFilenameRegex); + if (!match || match.length < 2) { + return null; + } + return match[1]; + } + get toolName() { + return "Xcode"; + } + getVersionPath(version) { + return path.join(this.xcodeDirectoryPath, `Xcode_${version}.app`); + } + getAllVersions() { + const children = fs.readdirSync(this.xcodeDirectoryPath, { encoding: "utf8", withFileTypes: true }); + let potentialVersions = children.filter(child => !child.isSymbolicLink() && child.isDirectory()).map(child => child.name); + potentialVersions = potentialVersions.map(child => this.parseXcodeVersionFromFilename(child)).filter((child) => !!child); + const stableVersions = potentialVersions.filter(ver => version_utils_1.VersionUtils.isValidVersion(ver)); + const betaVersions = potentialVersions.filter(ver => ver.endsWith("_beta")).map(ver => { + var _a; + const verWithoutBeta = ver.substr(0, ver.length - 5); + return (_a = children.find(child => child.isSymbolicLink() && this.parseXcodeVersionFromFilename(child.name) === verWithoutBeta)) === null || _a === void 0 ? void 0 : _a.name; + }).filter(((ver) => !!ver && version_utils_1.VersionUtils.isValidVersion(ver))); + // sort versions array by descending to make sure that the newest version will be picked up + return version_utils_1.VersionUtils.sortVersions([...stableVersions, ...betaVersions]); + } + findVersion(versionSpec) { + var _a; + const availableVersions = this.getAllVersions(); + if (availableVersions.length === 0) { + return null; + } + if (version_utils_1.VersionUtils.isLatestVersionKeyword(versionSpec)) { + return availableVersions[0]; + } + return (_a = availableVersions.find(ver => version_utils_1.VersionUtils.isVersionsEqual(ver, versionSpec))) !== null && _a !== void 0 ? _a : null; + } + setVersion(version) { + const targetVersionDirectory = this.getVersionPath(version); + if (!fs.existsSync(targetVersionDirectory)) { + throw new Error(`Invalid version: Directory '${targetVersionDirectory}' doesn't exist`); + } + core.debug(`sudo xcode-select -s ${targetVersionDirectory}`); + utils.invokeCommandSync("xcode-select", ["-s", targetVersionDirectory], { sudo: true }); + core.exportVariable("MD_APPLE_SDK_ROOT", targetVersionDirectory); + } +} +exports.XcodeSelector = XcodeSelector; + + /***/ }), /***/ 747: @@ -787,6 +792,75 @@ module.exports = require("path"); module.exports = require("fs"); +/***/ }), + +/***/ 792: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = __importStar(__webpack_require__(747)); +const path = __importStar(__webpack_require__(622)); +const core = __importStar(__webpack_require__(470)); +const utils = __importStar(__webpack_require__(611)); +const version_utils_1 = __webpack_require__(957); +class XamarinSelector { + get versionsDirectoryPath() { + return path.join(this.basePath, "Versions"); + } + getVersionPath(version) { + return path.join(this.versionsDirectoryPath, version); + } + getAllVersions() { + const children = fs.readdirSync(this.versionsDirectoryPath, { encoding: "utf8", withFileTypes: true }); + // macOS image contains symlinks for full versions, like '13.2' -> '13.2.3.0' + // filter such symlinks and look for only real versions + let potentialVersions = children.filter(child => !child.isSymbolicLink() && child.isDirectory()).map(child => child.name); + potentialVersions = potentialVersions.filter(child => version_utils_1.VersionUtils.isValidVersion(child)); + // sort versions array by descending to make sure that the newest version will be picked up + return version_utils_1.VersionUtils.sortVersions(potentialVersions); + } + findVersion(versionSpec) { + var _a; + const availableVersions = this.getAllVersions(); + if (availableVersions.length === 0) { + return null; + } + if (version_utils_1.VersionUtils.isLatestVersionKeyword(versionSpec)) { + return availableVersions[0]; + } + const normalizedVersionSpec = version_utils_1.VersionUtils.normalizeVersion(versionSpec); + core.debug(`Semantic version spec of '${versionSpec}' is '${normalizedVersionSpec}'`); + return (_a = availableVersions.find(ver => version_utils_1.VersionUtils.isVersionsEqual(ver, normalizedVersionSpec))) !== null && _a !== void 0 ? _a : null; + } + setVersion(version) { + const targetVersionDirectory = this.getVersionPath(version); + if (!fs.existsSync(targetVersionDirectory)) { + throw new Error(`Invalid version: Directory '${targetVersionDirectory}' doesn't exist`); + } + const currentVersionDirectory = path.join(this.versionsDirectoryPath, "Current"); + core.debug(`Creating symlink '${currentVersionDirectory}' -> '${targetVersionDirectory}'`); + if (fs.existsSync(currentVersionDirectory)) { + utils.invokeCommandSync("rm", ["-f", currentVersionDirectory], { sudo: true }); + } + utils.invokeCommandSync("ln", ["-s", targetVersionDirectory, currentVersionDirectory], { sudo: true }); + } + static toString() { + // show correct name for test suite + return this.name; + } +} +exports.XamarinSelector = XamarinSelector; + + /***/ }), /***/ 957: @@ -808,6 +882,12 @@ VersionUtils.isValidVersion = (version) => { VersionUtils.isLatestVersionKeyword = (version) => { return version === "latest"; }; +VersionUtils.isVersionsEqual = (firstVersion, secondVersion) => { + return compare_versions_1.default.compare(firstVersion, secondVersion, "="); +}; +VersionUtils.sortVersions = (versions) => { + return [...versions].sort(compare_versions_1.default).reverse(); +}; VersionUtils.normalizeVersion = (version) => { const versionParts = VersionUtils.splitVersionToParts(version); while (versionParts.length < 4) { diff --git a/src/mono-selector.ts b/src/mono-selector.ts index 9b99801..ec07e10 100644 --- a/src/mono-selector.ts +++ b/src/mono-selector.ts @@ -1,10 +1,10 @@ import * as fs from "fs"; import * as path from "path"; import * as core from "@actions/core"; -import { ToolSelector } from "./tool-selector"; +import { XamarinSelector } from "./xamarin-selector"; import { VersionUtils } from "./version-utils"; -export class MonoToolSelector extends ToolSelector { +export class MonoToolSelector extends XamarinSelector { protected get basePath(): string { return "/Library/Frameworks/Mono.framework"; } diff --git a/src/setup-xamarin.ts b/src/setup-xamarin.ts index b49cfb1..aa03a40 100644 --- a/src/setup-xamarin.ts +++ b/src/setup-xamarin.ts @@ -6,6 +6,7 @@ import { XamarinAndroidToolSelector } from "./xamarin-android-selector"; import { EOL } from "os"; import { VersionUtils } from "./version-utils"; import { ToolSelector } from "./tool-selector"; +import { XcodeSelector } from "./xcode-selector"; let showVersionMajorMinorWarning = false; @@ -51,6 +52,7 @@ const run = (): void => { invokeSelector("xamarin-ios-version", XamarinIosToolSelector); invokeSelector("xamarin-mac-version", XamarinMacToolSelector); invokeSelector("xamarin-android-version", XamarinAndroidToolSelector); + invokeSelector("xcode-version", XcodeSelector); if (showVersionMajorMinorWarning) { core.warning( diff --git a/src/tool-selector.ts b/src/tool-selector.ts index fb654ac..e33cfef 100644 --- a/src/tool-selector.ts +++ b/src/tool-selector.ts @@ -1,68 +1,6 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as core from "@actions/core"; -import compareVersions from "compare-versions"; -import { invokeCommandSync } from "./utils"; -import { VersionUtils } from "./version-utils"; - -export abstract class ToolSelector { - public abstract get toolName(): string; - - protected abstract get basePath(): string; - - protected get versionsDirectoryPath(): string { - return path.join(this.basePath, "Versions"); - } - - protected getVersionPath(version: string): string { - return path.join(this.versionsDirectoryPath, version); - } - - public getAllVersions(): string[] { - const children = fs.readdirSync(this.versionsDirectoryPath, { encoding: "utf8", withFileTypes: true }); - - // macOS image contains symlinks for full versions, like '13.2' -> '13.2.3.0' - // filter such symlinks and look for only real versions - let potentialVersions = children.filter(child => !child.isSymbolicLink() && child.isDirectory()).map(child => child.name); - potentialVersions = potentialVersions.filter(child => compareVersions.validate(child)); - - // sort versions array by descending to make sure that the newest version will be picked up - return potentialVersions.sort(compareVersions).reverse(); - } - - public findVersion(versionSpec: string): string | null { - const availableVersions = this.getAllVersions(); - if (availableVersions.length === 0) { - return null; - } - - if (VersionUtils.isLatestVersionKeyword(versionSpec)) { - return availableVersions[0]; - } - - const normalizedVersionSpec = VersionUtils.normalizeVersion(versionSpec); - core.debug(`Semantic version spec of '${versionSpec}' is '${normalizedVersionSpec}'`); - - return availableVersions.find(ver => compareVersions.compare(ver, normalizedVersionSpec, "=")) ?? null; - } - - public setVersion(version: string): void { - const targetVersionDirectory = this.getVersionPath(version); - if (!fs.existsSync(targetVersionDirectory)) { - throw new Error(`Invalid version: Directory '${targetVersionDirectory}' doesn't exist`); - } - - const currentVersionDirectory = path.join(this.versionsDirectoryPath, "Current"); - core.debug(`Creating symlink '${currentVersionDirectory}' -> '${targetVersionDirectory}'`); - if (fs.existsSync(currentVersionDirectory)) { - invokeCommandSync("rm", ["-f", currentVersionDirectory], true); - } - - invokeCommandSync("ln", ["-s", targetVersionDirectory, currentVersionDirectory], true); - } - - public static toString(): string { - // show correct name for test suite - return this.name; - } -} +export interface ToolSelector { + readonly toolName: string; + getAllVersions(): string[]; + findVersion(versionSpec: string): string | null; + setVersion(version: string): void; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 104190c..cf0c36b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,19 +1,24 @@ import * as child from "child_process"; import { EOL } from "os"; -export const invokeCommandSync = (command: string, args: string[], sudo: boolean): void => { +interface InvokeCommandOptions { + sudo: boolean; +} + +export const invokeCommandSync = (command: string, args: string[], options: InvokeCommandOptions): void => { let execResult: child.SpawnSyncReturns; - if (sudo) { + if (options.sudo) { execResult = child.spawnSync("sudo", [command, ...args]); } else { execResult = child.spawnSync(command, args); } if (execResult.status !== 0) { + const fullCommand = `${options.sudo ? "sudo " : ""}${command} ${args.join(" ")}`; throw new Error( [ - `Error during run ${sudo ? "sudo " : ""}${command} ${args.join(" ")}`, // + `Error during run '${fullCommand}'`, execResult.stderr, execResult.stdout ].join(EOL) diff --git a/src/version-utils.ts b/src/version-utils.ts index f50e15f..c06a82f 100644 --- a/src/version-utils.ts +++ b/src/version-utils.ts @@ -9,6 +9,14 @@ export class VersionUtils { return version === "latest"; } + public static isVersionsEqual = (firstVersion: string, secondVersion: string): boolean => { + return compareVersions.compare(firstVersion, secondVersion, "="); + } + + public static sortVersions = (versions: string[]): string[] => { + return [...versions].sort(compareVersions).reverse(); + } + public static normalizeVersion = (version: string): string => { const versionParts = VersionUtils.splitVersionToParts(version); while (versionParts.length < 4) { diff --git a/src/xamarin-android-selector.ts b/src/xamarin-android-selector.ts index 39bbe48..04cbd2f 100644 --- a/src/xamarin-android-selector.ts +++ b/src/xamarin-android-selector.ts @@ -1,6 +1,6 @@ -import { ToolSelector } from "./tool-selector"; +import { XamarinSelector } from "./xamarin-selector"; -export class XamarinAndroidToolSelector extends ToolSelector { +export class XamarinAndroidToolSelector extends XamarinSelector { public get toolName(): string { return "Xamarin.Android"; } diff --git a/src/xamarin-ios-selector.ts b/src/xamarin-ios-selector.ts index 838acc2..7bd6470 100644 --- a/src/xamarin-ios-selector.ts +++ b/src/xamarin-ios-selector.ts @@ -1,6 +1,6 @@ -import { ToolSelector } from "./tool-selector"; +import { XamarinSelector } from "./xamarin-selector"; -export class XamarinIosToolSelector extends ToolSelector { +export class XamarinIosToolSelector extends XamarinSelector { public get toolName(): string { return "Xamarin.iOS"; } diff --git a/src/xamarin-mac-selector.ts b/src/xamarin-mac-selector.ts index b5f04a4..fe4daa0 100644 --- a/src/xamarin-mac-selector.ts +++ b/src/xamarin-mac-selector.ts @@ -1,6 +1,6 @@ -import { ToolSelector } from "./tool-selector"; +import { XamarinSelector } from "./xamarin-selector"; -export class XamarinMacToolSelector extends ToolSelector { +export class XamarinMacToolSelector extends XamarinSelector { public get toolName(): string { return "Xamarin.Mac"; } diff --git a/src/xamarin-selector.ts b/src/xamarin-selector.ts new file mode 100644 index 0000000..e2731df --- /dev/null +++ b/src/xamarin-selector.ts @@ -0,0 +1,68 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as core from "@actions/core"; +import * as utils from "./utils"; +import { VersionUtils } from "./version-utils"; +import { ToolSelector } from "./tool-selector"; + +export abstract class XamarinSelector implements ToolSelector { + public abstract get toolName(): string; + + protected abstract get basePath(): string; + + protected get versionsDirectoryPath(): string { + return path.join(this.basePath, "Versions"); + } + + protected getVersionPath(version: string): string { + return path.join(this.versionsDirectoryPath, version); + } + + public getAllVersions(): string[] { + const children = fs.readdirSync(this.versionsDirectoryPath, { encoding: "utf8", withFileTypes: true }); + + // macOS image contains symlinks for full versions, like '13.2' -> '13.2.3.0' + // filter such symlinks and look for only real versions + let potentialVersions = children.filter(child => !child.isSymbolicLink() && child.isDirectory()).map(child => child.name); + potentialVersions = potentialVersions.filter(child => VersionUtils.isValidVersion(child)); + + // sort versions array by descending to make sure that the newest version will be picked up + return VersionUtils.sortVersions(potentialVersions); + } + + public findVersion(versionSpec: string): string | null { + const availableVersions = this.getAllVersions(); + if (availableVersions.length === 0) { + return null; + } + + if (VersionUtils.isLatestVersionKeyword(versionSpec)) { + return availableVersions[0]; + } + + const normalizedVersionSpec = VersionUtils.normalizeVersion(versionSpec); + core.debug(`Semantic version spec of '${versionSpec}' is '${normalizedVersionSpec}'`); + + return availableVersions.find(ver => VersionUtils.isVersionsEqual(ver, normalizedVersionSpec)) ?? null; + } + + public setVersion(version: string): void { + const targetVersionDirectory = this.getVersionPath(version); + if (!fs.existsSync(targetVersionDirectory)) { + throw new Error(`Invalid version: Directory '${targetVersionDirectory}' doesn't exist`); + } + + const currentVersionDirectory = path.join(this.versionsDirectoryPath, "Current"); + core.debug(`Creating symlink '${currentVersionDirectory}' -> '${targetVersionDirectory}'`); + if (fs.existsSync(currentVersionDirectory)) { + utils.invokeCommandSync("rm", ["-f", currentVersionDirectory], { sudo: true }); + } + + utils.invokeCommandSync("ln", ["-s", targetVersionDirectory, currentVersionDirectory], { sudo: true }); + } + + public static toString(): string { + // show correct name for test suite + return this.name; + } +} diff --git a/src/xcode-selector.ts b/src/xcode-selector.ts new file mode 100644 index 0000000..d1c6ca2 --- /dev/null +++ b/src/xcode-selector.ts @@ -0,0 +1,70 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as core from "@actions/core"; +import { VersionUtils } from "./version-utils"; +import { ToolSelector } from "./tool-selector"; +import * as utils from "./utils"; + +export class XcodeSelector implements ToolSelector { + private readonly xcodeDirectoryPath = "/Applications"; + private readonly xcodeFilenameRegex = /Xcode_([\d.]+)(_beta)?\.app/; + + private parseXcodeVersionFromFilename(filename: string): string | null { + const match = filename.match(this.xcodeFilenameRegex); + if (!match || match.length < 2) { + return null; + } + + return match[1]; + } + + public get toolName(): string { + return "Xcode"; + } + + protected getVersionPath(version: string): string { + return path.join(this.xcodeDirectoryPath, `Xcode_${version}.app`); + } + + public getAllVersions(): string[] { + const children = fs.readdirSync(this.xcodeDirectoryPath, { encoding: "utf8", withFileTypes: true }); + + let potentialVersions = children.filter(child => !child.isSymbolicLink() && child.isDirectory()).map(child => child.name); + potentialVersions = potentialVersions.map(child => this.parseXcodeVersionFromFilename(child)).filter((child): child is string => !!child); + + const stableVersions = potentialVersions.filter(ver => VersionUtils.isValidVersion(ver)); + const betaVersions = potentialVersions.filter(ver => ver.endsWith("_beta")).map(ver => { + const verWithoutBeta = ver.substr(0, ver.length - 5); + return children.find(child => child.isSymbolicLink() && this.parseXcodeVersionFromFilename(child.name) === verWithoutBeta)?.name; + }).filter(((ver): ver is string => !!ver && VersionUtils.isValidVersion(ver))); + + // sort versions array by descending to make sure that the newest version will be picked up + return VersionUtils.sortVersions([...stableVersions, ...betaVersions]); + } + + findVersion(versionSpec: string): string | null { + const availableVersions = this.getAllVersions(); + if (availableVersions.length === 0) { + return null; + } + + if (VersionUtils.isLatestVersionKeyword(versionSpec)) { + return availableVersions[0]; + } + + return availableVersions.find(ver => VersionUtils.isVersionsEqual(ver, versionSpec)) ?? null; + } + + setVersion(version: string): void { + const targetVersionDirectory = this.getVersionPath(version); + if (!fs.existsSync(targetVersionDirectory)) { + throw new Error(`Invalid version: Directory '${targetVersionDirectory}' doesn't exist`); + } + + core.debug(`sudo xcode-select -s ${targetVersionDirectory}`); + utils.invokeCommandSync("xcode-select", ["-s", targetVersionDirectory], { sudo: true }); + + core.exportVariable("MD_APPLE_SDK_ROOT", targetVersionDirectory); + } + +}