From b121c4490d388c0d72d5434f00f09b3c4a2d9ebb Mon Sep 17 00:00:00 2001 From: frostebite Date: Mon, 24 Aug 2026 15:14:50 +0100 Subject: [PATCH] fix: install Unity Hub as a Homebrew cask, not a versioned formula installUnityHub built `brew install unity-hub@`, treating Unity Hub as a versioned Homebrew formula. It's only distributed as a cask, so that command fails with "No available formula with the name...". Every Mac build in game-ci/unity-builder#844's CI has been failing at this exact step since the thin-wrapper rewrite; the last successful Mac run (pre-rewrite code) ran the plain, unversioned `brew install --cask unity-hub` successfully. Default (no unityHubVersionOnMac override) now installs the unversioned cask directly, matching the proven-working behavior, instead of round-tripping through `brew info` to resolve a "latest" version and then re-requesting it via non-existent formula syntax. An explicit unityHubVersionOnMac override still pins a version, now via the `--cask @` token Homebrew casks use. --- .../unity/platform-setup/setup-mac.test.ts | 62 +++++++++++++++++++ src/logic/unity/platform-setup/setup-mac.ts | 50 ++++++--------- 2 files changed, 80 insertions(+), 32 deletions(-) create mode 100644 src/logic/unity/platform-setup/setup-mac.test.ts diff --git a/src/logic/unity/platform-setup/setup-mac.test.ts b/src/logic/unity/platform-setup/setup-mac.test.ts new file mode 100644 index 00000000..476db4b2 --- /dev/null +++ b/src/logic/unity/platform-setup/setup-mac.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, mock, afterEach } from "bun:test"; +import { SetupMac } from "./setup-mac.ts"; +import { fsSync as fs } from "../../../dependencies.ts"; +import { System } from "../../../model/system/system.ts"; + +const originalExistsSync = fs.existsSync; +const originalSystemRun = System.run; + +afterEach(() => { + fs.existsSync = originalExistsSync; + System.run = originalSystemRun; +}); + +describe("SetupMac", () => { + // Regression test for a real bug: installUnityHub used to build + // `brew install unity-hub@`, treating Unity Hub as a versioned + // Homebrew formula. Unity Hub is only distributed as a cask, so that + // command fails with "No available formula with the name ...". Confirmed + // live in game-ci/unity-builder CI: every Mac build failed at Unity Hub + // install with exactly this error before this fix, while the previously + // working (pre-thin-wrapper) code path ran the plain, unversioned + // `brew install unity-hub` cask install successfully. + it("installs the unversioned unity-hub cask when no version is pinned", async () => { + // Only the Hub paths are missing; the Editor path exists so setup() doesn't also + // fall into installUnity, which is unrelated to this fix. + fs.existsSync = mock((path: string) => !path.includes("Hub.app")) as any; + let capturedCommand = ""; + const systemRunMock = mock((command: string) => { + capturedCommand = command; + + return Promise.resolve({ status: { code: 0 }, output: "" }); + }); + System.run = systemRunMock as any; + + await SetupMac.setup({ + isRunningLocally: false, + unityHubVersionOnMac: "", + engineVersion: "2021.3.16f1", + } as any); + + expect(capturedCommand).toBe("brew install --cask unity-hub"); + }); + + it("pins the cask version when unityHubVersionOnMac is explicitly set", async () => { + fs.existsSync = mock((path: string) => !path.includes("Hub.app")) as any; + let capturedCommand = ""; + const systemRunMock = mock((command: string) => { + capturedCommand = command; + + return Promise.resolve({ status: { code: 0 }, output: "" }); + }); + System.run = systemRunMock as any; + + await SetupMac.setup({ + isRunningLocally: false, + unityHubVersionOnMac: "3.19.5", + engineVersion: "2021.3.16f1", + } as any); + + expect(capturedCommand).toBe("brew install --cask unity-hub@3.19.5"); + }); +}); diff --git a/src/logic/unity/platform-setup/setup-mac.ts b/src/logic/unity/platform-setup/setup-mac.ts index a5f7d098..92f161ed 100644 --- a/src/logic/unity/platform-setup/setup-mac.ts +++ b/src/logic/unity/platform-setup/setup-mac.ts @@ -1,6 +1,6 @@ -import { fsSync as fs } from '../../../dependencies.ts'; -import type { Options } from '../../../dependencies.ts'; -import { System } from '../../../model/system/system.ts'; +import { fsSync as fs } from "../../../dependencies.ts"; +import type { Options } from "../../../dependencies.ts"; +import { System } from "../../../model/system/system.ts"; class SetupMac { static unityHubBasePath = `/Applications/"Unity Hub.app"`; @@ -23,7 +23,7 @@ class SetupMac { await SetupMac.installUnity(options); } else { throw new Error(String.dedent`Unity Editor ${options.engineVersion} is not installed at the default location. - Please install Unity Editor ${options.engineVersion} at the default location with the necessary modules and try again.`) + Please install Unity Editor ${options.engineVersion} at the default location with the necessary modules and try again.`); } } @@ -31,13 +31,12 @@ class SetupMac { } private static async installUnityHub(options: Options, silent = false) { - - const targetHubVersion = - options.unityHubVersionOnMac !== '' - ? options.unityHubVersionOnMac - : await SetupMac.getLatestUnityHubVersion(); - - const command = `brew install unity-hub@${targetHubVersion}`; + // Unity Hub is distributed on Homebrew as a cask, not a formula, so it has no `@version` + // formula-style pinning by default. Install the unversioned cask (always the latest available) + // unless the caller explicitly pinned a version, in which case we pass through the + // `@` token Homebrew uses for casks that publish versioned taps. + const versionSuffix = options.unityHubVersionOnMac !== "" ? `@${options.unityHubVersionOnMac}` : ""; + const command = `brew install --cask unity-hub${versionSuffix}`; if (!fs.existsSync(this.unityHubBasePath)) { try { @@ -48,36 +47,23 @@ class SetupMac { } } - /** - * Gets the latest version of Unity Hub available on brew - */ - private static async getLatestUnityHubVersion(): Promise { - const hubVersionCommand = `/bin/bash -c "brew info unity-hub | grep -o '[0-9]\\+\\.[0-9]\\+\\.[0-9]\\+'"`; - const result = await System.run(hubVersionCommand, undefined, { silent: true }); - if (result.status?.code === 0 && result.output !== '') { - return result.output; - } - - return ''; - } - private static getModuleParametersForTargetPlatform(targetPlatform: string): string { - let moduleArgument = ''; + let moduleArgument = ""; switch (targetPlatform) { - case 'iOS': + case "iOS": moduleArgument += `--module ios `; break; - case 'tvOS': - moduleArgument += '--module tvos '; + case "tvOS": + moduleArgument += "--module tvos "; break; - case 'StandaloneOSX': + case "StandaloneOSX": moduleArgument += `--module mac-il2cpp `; break; - case 'Android': + case "Android": moduleArgument += `--module android `; break; - case 'WebGL': - moduleArgument += '--module webgl '; + case "WebGL": + moduleArgument += "--module webgl "; break; default: throw new Error(`Unsupported module for target platform: ${targetPlatform}.`);