From a5389d0e5dd58538d48518bb4405267ff16a6d41 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 02:48:55 -0400 Subject: [PATCH 01/26] unity-cli@v1.3.4 - fix elevated permissions check on windows - fix java exceptions on linux android installation --- .github/workflows/unity-build.yml | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/android-sdk.ts | 7 ++++--- src/utilities.ts | 10 +++++----- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 23fc12a..a7dc450 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -42,6 +42,18 @@ jobs: run: | unity-cli hub-install --auto-update unity-cli setup-unity --unity-version "${{ matrix.unity-version }}" --build-targets "${{ matrix.build-target }}" --json + - name: Clean Disk Space + if: ${{ matrix.os == 'ubuntu-latest' }} + shell: bash + run: | + echo "Disk space before cleanup:" + df -h + sudo rm -rf /usr/local/.ghcup + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo rm -rf /usr/local/lib/android/sdk/ndk + sudo rm -rf /opt/ghc + echo "Disk space after cleanup:" + df -h - name: Verify UNITY_HUB_PATH and UNITY_EDITOR_PATH variables shell: bash run: | diff --git a/package-lock.json b/package-lock.json index 0f39c7a..4e464f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.3.3", + "version": "1.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.3.3", + "version": "1.3.4", "license": "MIT", "dependencies": { "@electron/asar": "^4.0.1", diff --git a/package.json b/package.json index a92d061..de3cc09 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.3.3", + "version": "1.3.4", "description": "A command line utility for the Unity Game Engine.", "author": "RageAgainstThePixel", "license": "MIT", diff --git a/src/android-sdk.ts b/src/android-sdk.ts index 4b9f48b..10f9543 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -86,6 +86,7 @@ async function getSdkManager(rootEditorPath: string): Promise { default: throw new Error(`Unsupported platform: ${process.platform}`); } + const sdkmanagerPath = await ResolveGlobToPath(globPath); if (!sdkmanagerPath) { @@ -103,7 +104,6 @@ async function getAndroidSdkPath(rootEditorPath: string, androidTargetSdk: numbe try { sdkPath = await ResolveGlobToPath([rootEditorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); - await fs.promises.access(sdkPath, fs.constants.R_OK); } catch (error) { logger.debug(`android-${androidTargetSdk} not installed`); return undefined; @@ -124,12 +124,12 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st } try { - exitCode = await new Promise((resolve, reject) => { + exitCode = await new Promise(async (resolve, reject) => { let cmd = sdkManagerPath; let cmdArgs = args; if (process.platform === 'win32') { - if (!isProcessElevated()) { + if (!await isProcessElevated()) { throw new Error('Android SDK installation requires elevated (administrator) privileges. Please rerun as Administrator.'); } @@ -140,6 +140,7 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st const child = spawn(cmd, cmdArgs, { stdio: ['pipe', 'pipe', 'pipe'], env: { + ...process.env, JAVA_HOME: process.platform === 'win32' ? `"${javaPath}"` : javaPath } }); diff --git a/src/utilities.ts b/src/utilities.ts index 647837d..710f795 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -578,11 +578,11 @@ export async function KillChildProcesses(procInfo: ProcInfo): Promise { * Checks if the current process is running with elevated (administrator) privileges. * @returns True if the process is elevated, false otherwise. */ -export function isProcessElevated(): boolean { +export async function isProcessElevated(): Promise { if (process.platform !== 'win32') { return true; } // We can sudo easily on non-windows platforms - const probe = spawnSync('powershell.exe', [ + const output = await Exec('powershell', [ '-NoLogo', '-NoProfile', '-Command', - "[Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent().IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)" - ], { encoding: 'utf8' }); - return probe.status === 0 && probe.stdout.trim().toLowerCase() === 'true'; + "(New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)" + ], { silent: true, showCommand: false }); + return output.trim().toLowerCase() === 'true'; } \ No newline at end of file From 4108daff55d9582920ffeed0e942ba67433fe44d Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 03:00:41 -0400 Subject: [PATCH 02/26] move it outside --- src/android-sdk.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index 10f9543..b9af5da 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -123,16 +123,16 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st fs.accessSync(sdkManagerPath, fs.constants.R_OK | fs.constants.X_OK); } + if (process.platform === 'win32' && !await isProcessElevated()) { + throw new Error('Android SDK installation requires elevated (administrator) privileges. Please rerun as Administrator.'); + } + try { exitCode = await new Promise(async (resolve, reject) => { let cmd = sdkManagerPath; let cmdArgs = args; if (process.platform === 'win32') { - if (!await isProcessElevated()) { - throw new Error('Android SDK installation requires elevated (administrator) privileges. Please rerun as Administrator.'); - } - cmd = 'cmd.exe'; cmdArgs = ['/c', sdkManagerPath, ...args]; } From 1070a506422a4c3fe0498a0e8fad7c7b53f6d642 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 03:00:53 -0400 Subject: [PATCH 03/26] free disk space for linux --- .github/workflows/unity-build.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index a7dc450..bc8825a 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -42,18 +42,13 @@ jobs: run: | unity-cli hub-install --auto-update unity-cli setup-unity --unity-version "${{ matrix.unity-version }}" --build-targets "${{ matrix.build-target }}" --json - - name: Clean Disk Space + - name: Free Disk Space if: ${{ matrix.os == 'ubuntu-latest' }} - shell: bash - run: | - echo "Disk space before cleanup:" - df -h - sudo rm -rf /usr/local/.ghcup - sudo rm -rf /opt/hostedtoolcache/CodeQL - sudo rm -rf /usr/local/lib/android/sdk/ndk - sudo rm -rf /opt/ghc - echo "Disk space after cleanup:" - df -h + uses: endersonmenezes/free-disk-space@v2 + with: + remove_android: true + remove_dotnet: false + remove_tool_cache: false - name: Verify UNITY_HUB_PATH and UNITY_EDITOR_PATH variables shell: bash run: | From f00ec6ad5c0ab4d472e18886653a562ceae4bca3 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 04:16:55 -0400 Subject: [PATCH 04/26] test setting target android sdk --- .github/workflows/unity-build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index bc8825a..6e91e43 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -103,6 +103,12 @@ jobs: npm install -g openupm-cli cd "${UNITY_PROJECT_PATH}" openupm add com.utilities.buildpipeline + - name: Update Android Target Sdk Version + if: ${{ matrix.build-target == 'Android' }} + shell: bash + run: | + # update targetSdkVersion to 32 in ProjectSettings/PlayerSettings.asset + sed -i 's/targetSdkVersion: [0-9]*/targetSdkVersion: 32/' "${UNITY_PROJECT_PATH}/ProjectSettings/PlayerSettings.asset" - name: Build Project if: ${{ env.RUN_BUILD == 'true' }} timeout-minutes: 60 From 14fdfce5d0ac860ffb5ade4d2861ad9df960109d Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 04:17:39 -0400 Subject: [PATCH 05/26] test android on windows runner --- .github/workflows/build-options.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-options.json b/.github/workflows/build-options.json index 7065201..25bd767 100644 --- a/.github/workflows/build-options.json +++ b/.github/workflows/build-options.json @@ -35,6 +35,10 @@ "os": "windows-latest", "build-target": "StandaloneWindows64" }, + { + "os": "windows-latest", + "build-target": "Android" + }, { "os": "macos-latest", "build-target": "StandaloneOSX" From b54be1b74233b124d565cd851a7a2cecd2307b71 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 04:18:28 -0400 Subject: [PATCH 06/26] run free disk space first --- .github/workflows/unity-build.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 6e91e43..67fde78 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -25,6 +25,13 @@ jobs: UNITY_PROJECT_PATH: '' # Set from create-project step RUN_BUILD: '' # Set to true if the build pipeline package can be installed and used steps: + - name: Free Disk Space + if: ${{ matrix.os == 'ubuntu-latest' }} + uses: endersonmenezes/free-disk-space@v2 + with: + remove_android: true + remove_dotnet: false + remove_tool_cache: false - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: @@ -42,13 +49,6 @@ jobs: run: | unity-cli hub-install --auto-update unity-cli setup-unity --unity-version "${{ matrix.unity-version }}" --build-targets "${{ matrix.build-target }}" --json - - name: Free Disk Space - if: ${{ matrix.os == 'ubuntu-latest' }} - uses: endersonmenezes/free-disk-space@v2 - with: - remove_android: true - remove_dotnet: false - remove_tool_cache: false - name: Verify UNITY_HUB_PATH and UNITY_EDITOR_PATH variables shell: bash run: | From 43aa196b83a1dbf6427782e76f13f9d655c45136 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 04:20:45 -0400 Subject: [PATCH 07/26] update build options --- .github/workflows/build-options.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/build-options.json b/.github/workflows/build-options.json index 25bd767..4086875 100644 --- a/.github/workflows/build-options.json +++ b/.github/workflows/build-options.json @@ -57,6 +57,11 @@ "os": "macos-latest", "unity-version": "4.7.2" }, + { + "os": "windows-latest", + "build-target": "Android", + "unity-version": "4.7.2" + }, { "os": "ubuntu-latest", "unity-version": "5.6.7f1 (e80cc3114ac1)" @@ -65,6 +70,11 @@ "os": "macos-latest", "build-target": "iOS", "unity-version": "5.6.7f1 (e80cc3114ac1)" + }, + { + "os": "windows-latest", + "build-target": "Android", + "unity-version": "5.6.7f1 (e80cc3114ac1)" } ] } \ No newline at end of file From 4a30a4a02aef64ca1454a375ac7381fa5d69f1d0 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 05:38:10 -0400 Subject: [PATCH 08/26] fix AndroidTargetSdkVersion set --- .github/workflows/unity-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 67fde78..7a04221 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -107,8 +107,8 @@ jobs: if: ${{ matrix.build-target == 'Android' }} shell: bash run: | - # update targetSdkVersion to 32 in ProjectSettings/PlayerSettings.asset - sed -i 's/targetSdkVersion: [0-9]*/targetSdkVersion: 32/' "${UNITY_PROJECT_PATH}/ProjectSettings/PlayerSettings.asset" + # update AndroidTargetSdkVersion to 32 in ProjectSettings/ProjectSettings.asset + sed -i 's/AndroidTargetSdkVersion: [0-9]*/AndroidTargetSdkVersion: 32/' "${UNITY_PROJECT_PATH}/ProjectSettings/ProjectSettings.asset" - name: Build Project if: ${{ env.RUN_BUILD == 'true' }} timeout-minutes: 60 From 115f280f5b47822e2ebb6838b0bf5f608d796902 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 10:35:31 -0400 Subject: [PATCH 09/26] run unity setup again after changing the target android sdk version --- .github/workflows/unity-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 7a04221..f16a9ff 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -109,6 +109,7 @@ jobs: run: | # update AndroidTargetSdkVersion to 32 in ProjectSettings/ProjectSettings.asset sed -i 's/AndroidTargetSdkVersion: [0-9]*/AndroidTargetSdkVersion: 32/' "${UNITY_PROJECT_PATH}/ProjectSettings/ProjectSettings.asset" + unity-cli setup-unity -e "${UNITY_EDITOR_PATH}" -p "${UNITY_PROJECT_PATH}" -m android - name: Build Project if: ${{ env.RUN_BUILD == 'true' }} timeout-minutes: 60 From a499dfa8fba12c95b7fc9df11e21b1e40558c0ac Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Sat, 18 Oct 2025 11:19:50 -0400 Subject: [PATCH 10/26] fix setup args --- .github/workflows/unity-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index f16a9ff..d9f2f1f 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -109,7 +109,7 @@ jobs: run: | # update AndroidTargetSdkVersion to 32 in ProjectSettings/ProjectSettings.asset sed -i 's/AndroidTargetSdkVersion: [0-9]*/AndroidTargetSdkVersion: 32/' "${UNITY_PROJECT_PATH}/ProjectSettings/ProjectSettings.asset" - unity-cli setup-unity -e "${UNITY_EDITOR_PATH}" -p "${UNITY_PROJECT_PATH}" -m android + unity-cli setup-unity -u ${{ matrix.unity-version }} -p "${UNITY_PROJECT_PATH}" -m android - name: Build Project if: ${{ env.RUN_BUILD == 'true' }} timeout-minutes: 60 From a24bcfefd6c31e7470fb7fd96fc63e48283e65ef Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 12:00:26 -0400 Subject: [PATCH 11/26] Update unity-cli setup command in workflow Removed Unity version specification from setup command. --- .github/workflows/unity-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index d9f2f1f..873d28d 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -109,7 +109,7 @@ jobs: run: | # update AndroidTargetSdkVersion to 32 in ProjectSettings/ProjectSettings.asset sed -i 's/AndroidTargetSdkVersion: [0-9]*/AndroidTargetSdkVersion: 32/' "${UNITY_PROJECT_PATH}/ProjectSettings/ProjectSettings.asset" - unity-cli setup-unity -u ${{ matrix.unity-version }} -p "${UNITY_PROJECT_PATH}" -m android + unity-cli setup-unity -p "${UNITY_PROJECT_PATH}" -m android - name: Build Project if: ${{ env.RUN_BUILD == 'true' }} timeout-minutes: 60 From 1567daa29b67cdcdc36633a1a98a3f33b9589ad9 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Sat, 18 Oct 2025 12:23:20 -0400 Subject: [PATCH 12/26] print project version text content --- src/unity-project.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/unity-project.ts b/src/unity-project.ts index 288096a..7f4696d 100644 --- a/src/unity-project.ts +++ b/src/unity-project.ts @@ -66,15 +66,15 @@ export class UnityProject { const match = versionText.match(/m_EditorVersionWithRevision: (?(?:(?\d+)\.)?(?:(?\d+)\.)?(?:(?\d+[abcfpx]\d+)\b))\s?(?:\((?\w+)\))?/); if (!match) { - throw Error(`No version match found!`); + throw Error(`No version match found!\nProjectVersion.txt content:\n${versionText}`); } if (!match.groups?.version) { - throw Error(`No version group found!`); + throw Error(`No version group found!\nProjectVersion.txt content:\n${versionText}`); } if (!match.groups?.changeset) { - throw Error(`No changeset group found!`); + throw Error(`No changeset group found!\nProjectVersion.txt content:\n${versionText}`); } this.version = new UnityVersion(match.groups.version, match.groups.changeset, undefined); From c9d5896b2d31bdd4dc6d8a29fb7c7f7ebb3aa0c2 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Sat, 18 Oct 2025 12:23:44 -0400 Subject: [PATCH 13/26] comment --- .github/workflows/unity-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 873d28d..eb23edf 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -109,6 +109,7 @@ jobs: run: | # update AndroidTargetSdkVersion to 32 in ProjectSettings/ProjectSettings.asset sed -i 's/AndroidTargetSdkVersion: [0-9]*/AndroidTargetSdkVersion: 32/' "${UNITY_PROJECT_PATH}/ProjectSettings/ProjectSettings.asset" + # ensure android dependencies are installed unity-cli setup-unity -p "${UNITY_PROJECT_PATH}" -m android - name: Build Project if: ${{ env.RUN_BUILD == 'true' }} From 36cc2b5ba50f8e96465a763537ddf91d0ab38b22 Mon Sep 17 00:00:00 2001 From: StephenHodgson Date: Sat, 18 Oct 2025 12:44:39 -0400 Subject: [PATCH 14/26] fix project version text file parsing --- src/unity-project.ts | 8 ++------ src/utilities.ts | 3 +-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/unity-project.ts b/src/unity-project.ts index 7f4696d..8d30451 100644 --- a/src/unity-project.ts +++ b/src/unity-project.ts @@ -63,7 +63,7 @@ export class UnityProject { this.projectVersionPath = path.join(this.projectPath, 'ProjectSettings', 'ProjectVersion.txt'); fs.accessSync(this.projectVersionPath, fs.constants.R_OK); const versionText = fs.readFileSync(this.projectVersionPath, 'utf-8'); - const match = versionText.match(/m_EditorVersionWithRevision: (?(?:(?\d+)\.)?(?:(?\d+)\.)?(?:(?\d+[abcfpx]\d+)\b))\s?(?:\((?\w+)\))?/); + const match = versionText.match(/(?:m_EditorVersion|m_EditorVersionWithRevision): (?(?:(?\d+)\.)?(?:(?\d+)\.)?(?:(?\d+[abcfpx]\d+)\b))\s?(?:\((?\w+)\))?/); if (!match) { throw Error(`No version match found!\nProjectVersion.txt content:\n${versionText}`); @@ -73,11 +73,7 @@ export class UnityProject { throw Error(`No version group found!\nProjectVersion.txt content:\n${versionText}`); } - if (!match.groups?.changeset) { - throw Error(`No changeset group found!\nProjectVersion.txt content:\n${versionText}`); - } - - this.version = new UnityVersion(match.groups.version, match.groups.changeset, undefined); + this.version = new UnityVersion(match.groups!.version!, match.groups?.changeset, undefined); } /** diff --git a/src/utilities.ts b/src/utilities.ts index 710f795..52ae82d 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -43,9 +43,8 @@ export async function PromptForSecretInput(prompt: string): Promise { // mask the previous line with asterisks in place of each character readline.moveCursor(process.stdout, 0, -1); readline.clearLine(process.stdout, 0); - process.stdout.write(prompt + '*'.repeat(input.length) + '\n'); + process.stdout.write(`${prompt + '*'.repeat(input.length)}\n`); rl.close(); - console.log(); // Don't use logger. Move to next line after input. resolve(input); }); }); From f3645898e1b02a73603c30b01baea6546e7d1a82 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 13:57:11 -0400 Subject: [PATCH 15/26] fix artifact logs --- .github/workflows/unity-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index eb23edf..1155f9d 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -135,7 +135,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: ${{ github.run_id }}.${{ github.run_attempt }} ${{ matrix.unity-version }} ${{ matrix.build-target }} logs + name: ${{ github.run_id }}.${{ github.run_attempt }} ${{ matrix.os }} ${{ matrix.unity-version }} ${{ matrix.build-target }} logs retention-days: 1 path: | ${{ github.workspace }}/**/*.log From 1f11a4f3cdddf7b8565987aad0942c4f9281219d Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 14:32:49 -0400 Subject: [PATCH 16/26] fix module validation --- src/unity-hub.ts | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index 479683f..bf56219 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -92,17 +92,18 @@ export class UnityHub { 'Unexpected error attempting to determine if executable file exists', 'dri3 extension not supported', 'Failed to connect to the bus:', + 'Error: No modules found to install.', 'Checking for beta autoupdate feature for deb/rpm distributions', 'Found package-type: deb', 'XPC error for connection com.apple.backupd.sandbox.xpc: Connection invalid', - 'Error: No modules found to install.', 'Failed to execute the command due the following, please see \'-- --headless help\' for assistance.', - 'Invalid key: The GraphQL query at the field at', - 'You have to request `id` or `_id` fields for all selection sets or create a custom `keys` config for `UnityReleaseLabel`.', + 'Unable to move the cache: Access is denied.', 'Entities without keys will be embedded directly on the parent entity. If this is intentional, create a `keys` config for `UnityReleaseLabel` that always returns null.', 'https://bit.ly/2XbVrpR#15', 'Interaction is not allowed with the Security Server." (-25308)', 'Network service crashed, restarting service.', + 'Invalid key: The GraphQL query at the field at', + 'You have to request `id` or `_id` fields for all selection sets or create a custom `keys` config for `UnityReleaseLabel`.', ]; try { @@ -131,8 +132,7 @@ export class UnityHub { try { const chunk = data.toString(); const fullChunk = lineBuffer + chunk; - const lines = fullChunk.split('\n') // split by newline - .map(line => line.replace(/\r$/, '')) // remove trailing carriage return + const lines = fullChunk.split(/\r?\n/) // split by newline .filter(line => line.length > 0); // filter out empty lines if (!chunk.endsWith('\n')) { @@ -141,9 +141,7 @@ export class UnityHub { lineBuffer = ''; } - const outputLines = lines.filter(line => !ignoredLines.some(ignored => line.includes(ignored))); - - if (outputLines.includes(tasksCompleteMessage)) { + if (lines.includes(tasksCompleteMessage)) { isHubTaskComplete = true; if (child?.pid) { @@ -167,10 +165,10 @@ export class UnityHub { } } - for (const line of outputLines) { + for (const line of lines) { output += `${line}\n`; - if (!options.silent) { + if (!options.silent && !ignoredLines.some(ignored => line.includes(ignored))) { process.stdout.write(`${line}\n`); } } @@ -184,8 +182,7 @@ export class UnityHub { function flushOutput(): void { try { if (lineBuffer.length > 0) { - const lines = lineBuffer.split('\n') // split by newline - .map(line => line.replace(/\r$/, '')) // remove trailing carriage return + const lines = lineBuffer.split(/\r?\n/) // split by newline .filter(line => line.length > 0); // filter out empty lines lineBuffer = ''; const outputLines = lines.filter(line => !ignoredLines.some(ignored => line.includes(ignored))); @@ -254,7 +251,7 @@ export class UnityHub { case 'No modules found to install.': break; default: - throw new Error(`Failed to execute Unity Hub (exit code: ${exitCode}) ${errorMessage}`); + throw new Error(`Failed to execute Unity Hub (exit code: ${exitCode}) ${errorMessage}\nOutput:\n${output}`); } } @@ -606,7 +603,7 @@ chmod -R 777 "$hubPath"`]); } try { - this.logger.info(`Checking installed modules for Unity ${unityVersion.toString()}...`); + this.logger.info(`Validating installed modules for Unity ${unityVersion.toString()}...`); const [installedModules, additionalModules] = await this.checkEditorModules(editorPath, unityVersion, modules); if (installedModules && installedModules.length > 0) { @@ -616,6 +613,7 @@ chmod -R 777 "$hubPath"`]); this.logger.info(` > ${module}`); } } + if (additionalModules && additionalModules.length > 0) { this.logger.info(`Additional Modules:`); @@ -627,6 +625,8 @@ chmod -R 777 "$hubPath"`]); if (error.message.includes(`No modules found`)) { await DeleteDirectory(editorPath); await this.GetEditor(unityVersion, modules); + } else { + throw error; } } @@ -931,8 +931,7 @@ done const editorRootPath = UnityEditor.GetEditorRootPath(editorPath); const modulesPath = path.join(editorRootPath, 'modules.json'); this.logger.debug(`Editor Modules Manifest:\n > "${modulesPath}"`); - - const output = await this.Exec([...args, '--cm']); + const output = await this.Exec([...args, '--cm'], { showCommand: true, silent: false }); const moduleMatches = output.matchAll(/Omitting module (?.+) because it's already installed/g); if (moduleMatches) { From 463203ae7d3212f9dbd82bc345bf1f812779f9a8 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 18:04:49 -0400 Subject: [PATCH 17/26] refactor android sdk installation --- .github/workflows/unity-build.yml | 2 +- src/android-sdk.ts | 123 +++++++++++++++++++++--------- src/cli.ts | 11 ++- src/unity-editor.ts | 19 +++++ 4 files changed, 117 insertions(+), 38 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 1155f9d..aaf9062 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -26,7 +26,7 @@ jobs: RUN_BUILD: '' # Set to true if the build pipeline package can be installed and used steps: - name: Free Disk Space - if: ${{ matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.os == 'ubuntu-latest' && matrix.unity-version == '6000.2' }} uses: endersonmenezes/free-disk-space@v2 with: remove_android: true diff --git a/src/android-sdk.ts b/src/android-sdk.ts index b9af5da..88c94f4 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -14,15 +14,14 @@ const logger = Logger.instance; /** * Checks if the required Android SDK is installed for the given Unity Editor and Project. - * @param editorPath The path to the Unity Editor executable. + * @param editor The UnityEditor instance. * @param projectPath The path to the Unity project. * @returns A promise that resolves when the check is complete. */ -export async function CheckAndroidSdkInstalled(editorPath: string, projectPath: string): Promise { - logger.ci(`Checking Android SDK installation for:\n > Editor: ${editorPath}\n > Project: ${projectPath}`); +export async function CheckAndroidSdkInstalled(editor: UnityEditor, projectPath: string): Promise { + logger.ci(`Checking Android SDK installation for:\n > Editor: ${editor.editorPath}\n > Project: ${projectPath}`); let sdkPath = undefined; await createRepositoryCfg(); - const rootEditorPath = UnityEditor.GetEditorRootPath(editorPath); const projectSettingsPath = path.join(projectPath, 'ProjectSettings/ProjectSettings.asset'); const projectSettingsContent = await ReadFileContents(projectSettingsPath); const matchResult = projectSettingsContent.match(/(?<=AndroidTargetSdkVersion: )\d+/); @@ -31,7 +30,7 @@ export async function CheckAndroidSdkInstalled(editorPath: string, projectPath: if (androidTargetSdk === undefined || androidTargetSdk === 0) { return; } - sdkPath = await getAndroidSdkPath(rootEditorPath, androidTargetSdk); + sdkPath = await getAndroidSdkPath(editor, androidTargetSdk); if (sdkPath) { logger.ci(`Target Android SDK android-${androidTargetSdk} Installed in:\n > "${sdkPath}"`); @@ -39,15 +38,15 @@ export async function CheckAndroidSdkInstalled(editorPath: string, projectPath: } logger.info(`Installing Android Target SDK:\n > android-${androidTargetSdk}`); - const sdkManagerPath = await getSdkManager(rootEditorPath); - const javaSdk = await getJDKPath(rootEditorPath); + const sdkManagerPath = await getSdkManager(editor); + const javaSdk = await getJDKPath(editor); await execSdkManager(sdkManagerPath, javaSdk, ['--licenses']); await execSdkManager(sdkManagerPath, javaSdk, ['--update']); await execSdkManager(sdkManagerPath, javaSdk, ['platform-tools', `platforms;android-${androidTargetSdk}`]); - sdkPath = await getAndroidSdkPath(rootEditorPath, androidTargetSdk); + sdkPath = await getAndroidSdkPath(editor, androidTargetSdk); if (!sdkPath) { - throw new Error(`Failed to install android-${androidTargetSdk} in ${rootEditorPath}`); + throw new Error(`Failed to install android-${androidTargetSdk} in ${editor.editorRootPath}`); } logger.ci(`Target Android SDK Installed in:\n > "${sdkPath}"`); @@ -61,11 +60,24 @@ async function createRepositoryCfg(): Promise { await fileHandle.close(); } -async function getJDKPath(rootEditorPath: string): Promise { - const jdkPath = await ResolveGlobToPath([rootEditorPath, '**', 'AndroidPlayer', 'OpenJDK']); +async function getJDKPath(editor: UnityEditor): Promise { + let jdkPath: string | undefined = undefined; - if (!jdkPath) { - throw new Error(`Failed to resolve OpenJDK in ${rootEditorPath}`); + if (editor.version.satisfies('>=2019')) { + logger.info('Using JDK bundled with Unity 2019+'); + jdkPath = await ResolveGlobToPath([editor.editorRootPath, '**', 'AndroidPlayer', 'OpenJDK']); + + if (!jdkPath) { + throw new Error(`Failed to resolve OpenJDK in ${editor.editorRootPath}`); + } + } else { + logger.info('Using system JDK for Unity versions prior to 2019'); + // use system JDK + jdkPath = process.env.JAVA_HOME || process.env.JDK_HOME; + + if (!jdkPath) { + throw new Error('JDK installation not found: No system JAVA_HOME or JDK_HOME defined'); + } } await fs.promises.access(jdkPath, fs.constants.R_OK); @@ -73,18 +85,40 @@ async function getJDKPath(rootEditorPath: string): Promise { return jdkPath; } -async function getSdkManager(rootEditorPath: string): Promise { +async function getSdkManager(editor: UnityEditor): Promise { let globPath: string[] = []; - switch (process.platform) { - case 'darwin': - case 'linux': - globPath = [rootEditorPath, '**', 'AndroidPlayer', '**', 'sdkmanager']; - break; - case 'win32': - globPath = [rootEditorPath, '**', 'AndroidPlayer', '**', 'sdkmanager.bat']; - break; - default: - throw new Error(`Unsupported platform: ${process.platform}`); + if (editor.version.satisfies('>=2019')) { + logger.info('Using sdkmanager bundled with Unity 2019+'); + switch (process.platform) { + case 'darwin': + case 'linux': + globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'sdkmanager']; + break; + case 'win32': + globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'sdkmanager.bat']; + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + } else { + logger.info('Using system sdkmanager for Unity versions prior to 2019'); + const systemSdkPath = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME; + + if (!systemSdkPath) { + throw new Error('Android installation not found: No system ANDROID_SDK_ROOT or ANDROID_HOME defined'); + } + + switch (process.platform) { + case 'darwin': + case 'linux': + globPath = [systemSdkPath, 'cmdline-tools', 'latest', 'bin', 'sdkmanager']; + break; + case 'win32': + globPath = [systemSdkPath, 'cmdline-tools', 'latest', 'bin', 'sdkmanager.bat']; + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } } const sdkmanagerPath = await ResolveGlobToPath(globPath); @@ -98,15 +132,32 @@ async function getSdkManager(rootEditorPath: string): Promise { return sdkmanagerPath; } -async function getAndroidSdkPath(rootEditorPath: string, androidTargetSdk: number): Promise { - logger.ci(`Attempting to locate Android SDK Path...\n > editorPath: ${rootEditorPath}\n > androidTargetSdk: ${androidTargetSdk}`); +async function getAndroidSdkPath(unityEditor: UnityEditor, androidTargetSdk: number): Promise { + logger.ci(`Attempting to locate Android SDK Path...\n > editorPath: ${unityEditor.editorPath}\n > androidTargetSdk: ${androidTargetSdk}`); let sdkPath: string; - try { - sdkPath = await ResolveGlobToPath([rootEditorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); - } catch (error) { - logger.debug(`android-${androidTargetSdk} not installed`); - return undefined; + // if 2019+ test editor path, else use system android installation + if (unityEditor.version.satisfies('>=2019')) { + try { + sdkPath = await ResolveGlobToPath([unityEditor.editorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); + } catch (error) { + logger.debug(`android-${androidTargetSdk} not installed`); + return undefined; + } + } else { // fall back to system android installation + try { + const systemSdkPath = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME; + + if (!systemSdkPath) { + logger.debug('Android installation not found: No system ANDROID_SDK_ROOT or ANDROID_HOME defined'); + return undefined; + } + + sdkPath = await ResolveGlobToPath([systemSdkPath, 'platforms', `android-${androidTargetSdk}/`]); + } catch (error) { + logger.debug(`android-${androidTargetSdk} not installed`); + return undefined; + } } logger.ci(`Android sdkPath:\n > "${sdkPath}"`); @@ -129,6 +180,11 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st try { exitCode = await new Promise(async (resolve, reject) => { + let cmdEnv = { ...process.env }; + cmdEnv.JAVA_HOME = process.platform === 'win32' ? `"${javaPath}"` : javaPath; + cmdEnv.JDK_HOME = process.platform === 'win32' ? `"${javaPath}"` : javaPath; + cmdEnv.SKIP_JDK_VERSION_CHECK = 'true'; + cmdEnv.JAVA_TOOL_OPTIONS = '--enable-native-access=ALL-UNNAMED'; let cmd = sdkManagerPath; let cmdArgs = args; @@ -139,10 +195,7 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st const child = spawn(cmd, cmdArgs, { stdio: ['pipe', 'pipe', 'pipe'], - env: { - ...process.env, - JAVA_HOME: process.platform === 'win32' ? `"${javaPath}"` : javaPath - } + env: cmdEnv }); const sigintHandler = () => child.kill('SIGINT'); const sigtermHandler = () => child.kill('SIGTERM'); diff --git a/src/cli.ts b/src/cli.ts index 4d0edc1..95479e9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -225,7 +225,14 @@ program.command('setup-unity') process.exit(1); } - const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset, options.arch); + let unityVersion: UnityVersion; + + if (options.unityVersion) { + unityVersion = new UnityVersion(options.unityVersion, options.changeset, options.arch); + } else { + unityVersion = unityProject!.version; + } + const modules: string[] = options.modules ? options.modules.split(/[ ,]+/).filter(Boolean) : []; const buildTargets: string[] = options.buildTargets ? options.buildTargets.split(/[ ,]+/).filter(Boolean) : []; const moduleBuildTargetMap = UnityHub.GetPlatformTargetModuleMap(); @@ -262,7 +269,7 @@ program.command('setup-unity') output['UNITY_PROJECT_PATH'] = unityProject.projectPath; if (modules.includes('android')) { - await CheckAndroidSdkInstalled(unityEditor.editorPath, unityProject.projectPath); + await CheckAndroidSdkInstalled(unityEditor, unityProject.projectPath); } } diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 7bdbc33..ef9de66 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -68,6 +68,25 @@ export class UnityEditor { } this.autoAddNoGraphics = this.version.satisfies('>2018.0.0'); + + const hubMetaDataPath = path.join(this.editorRootPath, 'metadata.hub.json'); + if (!fs.existsSync(hubMetaDataPath)) { + const metadata = { + productName: `Unity ${this.version.version.toString()}`, + entitlements: [], + releaseStream: '', + isLTS: null + }; + fs.writeFileSync(hubMetaDataPath, JSON.stringify(metadata), { encoding: 'utf-8' }); + } else { + const metadataContent = fs.readFileSync(hubMetaDataPath, { encoding: 'utf-8' }); + const metadata = JSON.parse(metadataContent); + + if (!metadata.productName) { + metadata.productName = `Unity ${this.version.version.toString()}`; + fs.writeFileSync(hubMetaDataPath, JSON.stringify(metadata), { encoding: 'utf-8' }); + } + } } /** From 644eb53a9c261cb360392b5054cc52a60c5515d4 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 18:20:22 -0400 Subject: [PATCH 18/26] fix semver check --- src/android-sdk.ts | 13 +++++++------ src/unity-editor.ts | 11 +++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index 88c94f4..30aa3f1 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -9,6 +9,7 @@ import { ReadFileContents, ResolveGlobToPath } from './utilities'; +import { satisfies } from 'semver'; const logger = Logger.instance; @@ -63,7 +64,7 @@ async function createRepositoryCfg(): Promise { async function getJDKPath(editor: UnityEditor): Promise { let jdkPath: string | undefined = undefined; - if (editor.version.satisfies('>=2019')) { + if (satisfies(editor.version.version, '>=2019.0.0')) { logger.info('Using JDK bundled with Unity 2019+'); jdkPath = await ResolveGlobToPath([editor.editorRootPath, '**', 'AndroidPlayer', 'OpenJDK']); @@ -87,7 +88,7 @@ async function getJDKPath(editor: UnityEditor): Promise { async function getSdkManager(editor: UnityEditor): Promise { let globPath: string[] = []; - if (editor.version.satisfies('>=2019')) { + if (satisfies(editor.version.version, '>=2019.0.0')) { logger.info('Using sdkmanager bundled with Unity 2019+'); switch (process.platform) { case 'darwin': @@ -132,14 +133,14 @@ async function getSdkManager(editor: UnityEditor): Promise { return sdkmanagerPath; } -async function getAndroidSdkPath(unityEditor: UnityEditor, androidTargetSdk: number): Promise { - logger.ci(`Attempting to locate Android SDK Path...\n > editorPath: ${unityEditor.editorPath}\n > androidTargetSdk: ${androidTargetSdk}`); +async function getAndroidSdkPath(editor: UnityEditor, androidTargetSdk: number): Promise { + logger.ci(`Attempting to locate Android SDK Path...\n > editorPath: ${editor.editorPath}\n > androidTargetSdk: ${androidTargetSdk}`); let sdkPath: string; // if 2019+ test editor path, else use system android installation - if (unityEditor.version.satisfies('>=2019')) { + if (satisfies(editor.version.version, '>=2019.0.0')) { try { - sdkPath = await ResolveGlobToPath([unityEditor.editorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); + sdkPath = await ResolveGlobToPath([editor.editorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); } catch (error) { logger.debug(`android-${androidTargetSdk} not installed`); return undefined; diff --git a/src/unity-editor.ts b/src/unity-editor.ts index ef9de66..f4eea9b 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -83,8 +83,15 @@ export class UnityEditor { const metadata = JSON.parse(metadataContent); if (!metadata.productName) { - metadata.productName = `Unity ${this.version.version.toString()}`; - fs.writeFileSync(hubMetaDataPath, JSON.stringify(metadata), { encoding: 'utf-8' }); + // projectName must be the first property + const newMetadata: any = { + productName: `Unity ${this.version.version.toString()}` + }; + Object.keys(metadata).forEach(key => { + if (key === 'productName') { return; } + newMetadata[key] = metadata[key]; + }); + fs.writeFileSync(hubMetaDataPath, JSON.stringify(newMetadata), { encoding: 'utf-8' }); } } } From 4afe82b202a8fb199c84e99218b8d463602b9b34 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 18:40:20 -0400 Subject: [PATCH 19/26] add gt/ge/lt/le functions to unity version improved version checking --- src/android-sdk.ts | 8 +++----- src/unity-editor.ts | 2 +- src/unity-hub.ts | 2 +- src/unity-version.ts | 26 ++++++++++++++++++++------ 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index 30aa3f1..c7f3279 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -64,7 +64,7 @@ async function createRepositoryCfg(): Promise { async function getJDKPath(editor: UnityEditor): Promise { let jdkPath: string | undefined = undefined; - if (satisfies(editor.version.version, '>=2019.0.0')) { + if (editor.version.isGreaterThanOrEqualTo('2019.0.0')) { logger.info('Using JDK bundled with Unity 2019+'); jdkPath = await ResolveGlobToPath([editor.editorRootPath, '**', 'AndroidPlayer', 'OpenJDK']); @@ -73,7 +73,6 @@ async function getJDKPath(editor: UnityEditor): Promise { } } else { logger.info('Using system JDK for Unity versions prior to 2019'); - // use system JDK jdkPath = process.env.JAVA_HOME || process.env.JDK_HOME; if (!jdkPath) { @@ -88,7 +87,7 @@ async function getJDKPath(editor: UnityEditor): Promise { async function getSdkManager(editor: UnityEditor): Promise { let globPath: string[] = []; - if (satisfies(editor.version.version, '>=2019.0.0')) { + if (editor.version.isGreaterThanOrEqualTo('2019.0.0')) { logger.info('Using sdkmanager bundled with Unity 2019+'); switch (process.platform) { case 'darwin': @@ -138,7 +137,7 @@ async function getAndroidSdkPath(editor: UnityEditor, androidTargetSdk: number): let sdkPath: string; // if 2019+ test editor path, else use system android installation - if (satisfies(editor.version.version, '>=2019.0.0')) { + if (editor.version.isGreaterThanOrEqualTo('2019.0.0')) { try { sdkPath = await ResolveGlobToPath([editor.editorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); } catch (error) { @@ -185,7 +184,6 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st cmdEnv.JAVA_HOME = process.platform === 'win32' ? `"${javaPath}"` : javaPath; cmdEnv.JDK_HOME = process.platform === 'win32' ? `"${javaPath}"` : javaPath; cmdEnv.SKIP_JDK_VERSION_CHECK = 'true'; - cmdEnv.JAVA_TOOL_OPTIONS = '--enable-native-access=ALL-UNNAMED'; let cmd = sdkManagerPath; let cmdArgs = args; diff --git a/src/unity-editor.ts b/src/unity-editor.ts index f4eea9b..23c8ea5 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -67,7 +67,7 @@ export class UnityEditor { this.version = version; } - this.autoAddNoGraphics = this.version.satisfies('>2018.0.0'); + this.autoAddNoGraphics = this.version.isGreaterThan('2018.0.0'); const hubMetaDataPath = path.join(this.editorRootPath, 'metadata.hub.json'); if (!fs.existsSync(hubMetaDataPath)) { diff --git a/src/unity-hub.ts b/src/unity-hub.ts index bf56219..d444719 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -705,7 +705,7 @@ chmod -R 777 "$hubPath"`]); editorPath = exactEditor.editorPath; } else if (allowPartialMatches) { // Fallback: semver satisfies - const versionEditors = editors.filter(e => e.version && unityVersion.satisfies(e.version.version)); + const versionEditors = editors.filter(e => e.version && unityVersion.satisfies(e.version)); if (versionEditors.length === 0) { return undefined; diff --git a/src/unity-version.ts b/src/unity-version.ts index 9ad9081..1039748 100644 --- a/src/unity-version.ts +++ b/src/unity-version.ts @@ -103,14 +103,28 @@ export class UnityVersion { return this; } - satisfies(version: string): boolean { - const coercedVersion = coerce(version); + satisfies(version: UnityVersion): boolean { + return satisfies(version.semVer, `^${this.semVer.version}`); + } - if (!coercedVersion) { - throw new Error(`Invalid version to check against: ${version}`); - } + isGreaterThan(other: string | UnityVersion): boolean { + const otherVersion = other instanceof UnityVersion ? other : new UnityVersion(other); + return UnityVersion.compare(this, otherVersion) > 0; + } + + isGreaterThanOrEqualTo(other: string | UnityVersion): boolean { + const otherVersion = other instanceof UnityVersion ? other : new UnityVersion(other); + return UnityVersion.compare(this, otherVersion) >= 0; + } + + isLessThan(other: string | UnityVersion): boolean { + const otherVersion = other instanceof UnityVersion ? other : new UnityVersion(other); + return UnityVersion.compare(this, otherVersion) < 0; + } - return satisfies(coercedVersion, `^${this.semVer.version}`); + isLessThanOrEqualTo(other: string | UnityVersion): boolean { + const otherVersion = other instanceof UnityVersion ? other : new UnityVersion(other); + return UnityVersion.compare(this, otherVersion) <= 0; } equals(other: UnityVersion): boolean { From 98032dfb3e5f73233e15c05210ef36dc4dae8335 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 18:46:54 -0400 Subject: [PATCH 20/26] base compare without build metadata --- src/android-sdk.ts | 2 ++ src/unity-version.ts | 14 +++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index c7f3279..a34185e 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -138,6 +138,7 @@ async function getAndroidSdkPath(editor: UnityEditor, androidTargetSdk: number): // if 2019+ test editor path, else use system android installation if (editor.version.isGreaterThanOrEqualTo('2019.0.0')) { + logger.info('Using Android SDK bundled with Unity 2019+'); try { sdkPath = await ResolveGlobToPath([editor.editorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); } catch (error) { @@ -145,6 +146,7 @@ async function getAndroidSdkPath(editor: UnityEditor, androidTargetSdk: number): return undefined; } } else { // fall back to system android installation + logger.info('Using system Android SDK for Unity versions prior to 2019'); try { const systemSdkPath = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME; diff --git a/src/unity-version.ts b/src/unity-version.ts index 1039748..0107d66 100644 --- a/src/unity-version.ts +++ b/src/unity-version.ts @@ -42,7 +42,7 @@ export class UnityVersion { } static compare(a: UnityVersion, b: UnityVersion): number { - const baseComparison = compare(a.semVer, b.semVer, true); + const baseComparison = UnityVersion.baseCompare(a, b); if (baseComparison !== 0) { return baseComparison; @@ -51,6 +51,10 @@ export class UnityVersion { return UnityVersion.compareBuildMetadata(a.semVer, b.semVer); } + static baseCompare(a: UnityVersion, b: UnityVersion): number { + return compare(a.semVer, b.semVer, true); + } + toString(): string { return this.changeset ? `${this.version} (${this.changeset})` : this.version; } @@ -109,22 +113,22 @@ export class UnityVersion { isGreaterThan(other: string | UnityVersion): boolean { const otherVersion = other instanceof UnityVersion ? other : new UnityVersion(other); - return UnityVersion.compare(this, otherVersion) > 0; + return UnityVersion.baseCompare(this, otherVersion) > 0; } isGreaterThanOrEqualTo(other: string | UnityVersion): boolean { const otherVersion = other instanceof UnityVersion ? other : new UnityVersion(other); - return UnityVersion.compare(this, otherVersion) >= 0; + return UnityVersion.baseCompare(this, otherVersion) >= 0; } isLessThan(other: string | UnityVersion): boolean { const otherVersion = other instanceof UnityVersion ? other : new UnityVersion(other); - return UnityVersion.compare(this, otherVersion) < 0; + return UnityVersion.baseCompare(this, otherVersion) < 0; } isLessThanOrEqualTo(other: string | UnityVersion): boolean { const otherVersion = other instanceof UnityVersion ? other : new UnityVersion(other); - return UnityVersion.compare(this, otherVersion) <= 0; + return UnityVersion.baseCompare(this, otherVersion) <= 0; } equals(other: UnityVersion): boolean { From 94c0b0fdfc9dc11cea30b81a49e96728d08f64c1 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 19:02:12 -0400 Subject: [PATCH 21/26] update logging --- src/android-sdk.ts | 21 ++++++++++----------- src/utilities.ts | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index a34185e..a73ed97 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -20,7 +20,7 @@ const logger = Logger.instance; * @returns A promise that resolves when the check is complete. */ export async function CheckAndroidSdkInstalled(editor: UnityEditor, projectPath: string): Promise { - logger.ci(`Checking Android SDK installation for:\n > Editor: ${editor.editorPath}\n > Project: ${projectPath}`); + logger.ci(`Checking Android SDK installation for:\n > Editor: ${editor.editorRootPath}\n > Project: ${projectPath}`); let sdkPath = undefined; await createRepositoryCfg(); const projectSettingsPath = path.join(projectPath, 'ProjectSettings/ProjectSettings.asset'); @@ -65,14 +65,14 @@ async function getJDKPath(editor: UnityEditor): Promise { let jdkPath: string | undefined = undefined; if (editor.version.isGreaterThanOrEqualTo('2019.0.0')) { - logger.info('Using JDK bundled with Unity 2019+'); - jdkPath = await ResolveGlobToPath([editor.editorRootPath, '**', 'AndroidPlayer', 'OpenJDK']); + logger.debug('Using JDK bundled with Unity 2019+'); + jdkPath = await ResolveGlobToPath([editor.editorRootPath, '**', 'AndroidPlayer', 'OpenJDK/']); if (!jdkPath) { throw new Error(`Failed to resolve OpenJDK in ${editor.editorRootPath}`); } } else { - logger.info('Using system JDK for Unity versions prior to 2019'); + logger.debug('Using system JDK for Unity versions prior to 2019'); jdkPath = process.env.JAVA_HOME || process.env.JDK_HOME; if (!jdkPath) { @@ -88,7 +88,7 @@ async function getJDKPath(editor: UnityEditor): Promise { async function getSdkManager(editor: UnityEditor): Promise { let globPath: string[] = []; if (editor.version.isGreaterThanOrEqualTo('2019.0.0')) { - logger.info('Using sdkmanager bundled with Unity 2019+'); + logger.debug('Using sdkmanager bundled with Unity 2019+'); switch (process.platform) { case 'darwin': case 'linux': @@ -101,7 +101,7 @@ async function getSdkManager(editor: UnityEditor): Promise { throw new Error(`Unsupported platform: ${process.platform}`); } } else { - logger.info('Using system sdkmanager for Unity versions prior to 2019'); + logger.debug('Using system sdkmanager for Unity versions prior to 2019'); const systemSdkPath = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME; if (!systemSdkPath) { @@ -133,20 +133,20 @@ async function getSdkManager(editor: UnityEditor): Promise { } async function getAndroidSdkPath(editor: UnityEditor, androidTargetSdk: number): Promise { - logger.ci(`Attempting to locate Android SDK Path...\n > editorPath: ${editor.editorPath}\n > androidTargetSdk: ${androidTargetSdk}`); + logger.ci(`Attempting to locate Android SDK Path...\n > editorRootPath: ${editor.editorRootPath}\n > androidTargetSdk: ${androidTargetSdk}`); let sdkPath: string; // if 2019+ test editor path, else use system android installation if (editor.version.isGreaterThanOrEqualTo('2019.0.0')) { - logger.info('Using Android SDK bundled with Unity 2019+'); + logger.debug('Using Android SDK bundled with Unity 2019+'); try { - sdkPath = await ResolveGlobToPath([editor.editorPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); + sdkPath = await ResolveGlobToPath([editor.editorRootPath, '**', 'PlaybackEngines', 'AndroidPlayer', 'SDK', 'platforms', `android-${androidTargetSdk}/`]); } catch (error) { logger.debug(`android-${androidTargetSdk} not installed`); return undefined; } } else { // fall back to system android installation - logger.info('Using system Android SDK for Unity versions prior to 2019'); + logger.debug('Using system Android SDK for Unity versions prior to 2019'); try { const systemSdkPath = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME; @@ -162,7 +162,6 @@ async function getAndroidSdkPath(editor: UnityEditor, androidTargetSdk: number): } } - logger.ci(`Android sdkPath:\n > "${sdkPath}"`); return sdkPath; } diff --git a/src/utilities.ts b/src/utilities.ts index 52ae82d..ae12fb4 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import * as https from 'https'; import * as readline from 'readline'; import { glob } from 'glob'; -import { spawn, spawnSync } from 'child_process'; +import { spawn } from 'child_process'; import { Logger, LogLevel } from './logging'; const logger = Logger.instance; From b02734134242d0bc21f7098baca305fcc218ee53 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sat, 18 Oct 2025 22:28:20 -0400 Subject: [PATCH 22/26] update sdkmanager path --- src/android-sdk.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index a73ed97..e618512 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -92,10 +92,10 @@ async function getSdkManager(editor: UnityEditor): Promise { switch (process.platform) { case 'darwin': case 'linux': - globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'sdkmanager']; + globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'cmdline-tools', '**', 'sdkmanager']; break; case 'win32': - globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'sdkmanager.bat']; + globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'cmdline-tools', '**', 'sdkmanager.bat']; break; default: throw new Error(`Unsupported platform: ${process.platform}`); @@ -182,8 +182,8 @@ async function execSdkManager(sdkManagerPath: string, javaPath: string, args: st try { exitCode = await new Promise(async (resolve, reject) => { let cmdEnv = { ...process.env }; - cmdEnv.JAVA_HOME = process.platform === 'win32' ? `"${javaPath}"` : javaPath; - cmdEnv.JDK_HOME = process.platform === 'win32' ? `"${javaPath}"` : javaPath; + cmdEnv.JAVA_HOME = javaPath; + cmdEnv.JDK_HOME = javaPath; cmdEnv.SKIP_JDK_VERSION_CHECK = 'true'; let cmd = sdkManagerPath; let cmdArgs = args; From e5d0ea1226fed718a3f09e863e49e4d16740b935 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 19 Oct 2025 10:19:45 -0400 Subject: [PATCH 23/26] fix sdkmanager for 2019 & 2020 fix template lookups for versions prior to 2019 added UnityVersion.range helper function added additional unit tests --- src/android-sdk.ts | 19 ++++++++++++++++--- src/unity-editor.ts | 32 +++++++++++++++++++------------- src/unity-version.ts | 6 ++++++ tests/unity-editor.test.ts | 4 ++++ tests/unity-hub.test.ts | 1 - tests/unity-version.test.ts | 30 +++++++++++++++++++++++++++++- 6 files changed, 74 insertions(+), 18 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index e618512..db80127 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -87,8 +87,21 @@ async function getJDKPath(editor: UnityEditor): Promise { async function getSdkManager(editor: UnityEditor): Promise { let globPath: string[] = []; - if (editor.version.isGreaterThanOrEqualTo('2019.0.0')) { - logger.debug('Using sdkmanager bundled with Unity 2019+'); + if (editor.version.range('>=2019.0.0')) { + logger.debug('Using sdkmanager bundled with Unity 2019 && 2020'); + switch (process.platform) { + case 'darwin': + case 'linux': + globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'sdkmanager']; + break; + case 'win32': + globPath = [editor.editorRootPath, '**', 'AndroidPlayer', '**', 'sdkmanager.bat']; + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } + } else if (editor.version.range('>2021.0.0')) { + logger.debug('Using cmdline-tools sdkmanager bundled with Unity 2021+'); switch (process.platform) { case 'darwin': case 'linux': @@ -101,7 +114,7 @@ async function getSdkManager(editor: UnityEditor): Promise { throw new Error(`Unsupported platform: ${process.platform}`); } } else { - logger.debug('Using system sdkmanager for Unity versions prior to 2019'); + logger.debug('Using system sdkmanager'); const systemSdkPath = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME; if (!systemSdkPath) { diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 23c8ea5..408a17e 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -110,12 +110,12 @@ export class UnityEditor { return undefined; } - // Build a regex to match the template name and version - // e.g., com.unity.template.3d(-cross-platform)?.*[0-9]+\.[0-9]+\.[0-9]+\.tgz - // Accepts either a full regex or a simple string + // Build a regex to match the template name and optional version suffix + // e.g., com.unity.template.3d(-cross-platform)?.* + // Supports files (.tgz / .tar.gz) and legacy folder templates without a suffix. let regex: RegExp; try { - regex = new RegExp(`^${template}.*[0-9]+\\.[0-9]+\\.[0-9]+\\.tgz$`); + regex = new RegExp(`^${template}(?:[-.].*)?(?:\.tgz|\.tar\.gz)?$`); } catch (e) { throw new Error(`Invalid template regex: ${template}`); } @@ -145,11 +145,14 @@ export class UnityEditor { * @returns An array of available template file names. */ public GetAvailableTemplates(): string[] { + if (this.version.isLessThan('2018.0.0')) { + this.logger.warn(`Unity version ${this.version.toString()} does not support project templates.`); + return []; + } + let templateDir: string; let editorRoot = path.dirname(this.editorPath); - const templates: string[] = []; - if (process.platform === 'darwin') { templateDir = path.join(path.dirname(editorRoot), 'Resources', 'PackageManager', 'ProjectTemplates'); } else { @@ -158,17 +161,20 @@ export class UnityEditor { this.logger.debug(`Looking for templates in: ${templateDir}`); - // Check if the template directory exists if (!fs.existsSync(templateDir) || !fs.statSync(templateDir).isDirectory()) { - return templates; + return []; + } + + const templates: string[] = []; + const entries = fs.readdirSync(templateDir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isFile() && (entry.name.endsWith('.tgz') || entry.name.endsWith('.tar.gz'))) { + templates.push(path.join(templateDir, entry.name)); + } } - // Find all .tgz packages in the template directory - const packages = fs.readdirSync(templateDir) - .filter(f => f.endsWith('.tgz')) - .map(f => path.join(templateDir, f)); - templates.push(...packages); this.logger.debug(`Found ${templates.length} templates:\n${templates.map(t => ` - ${t}`).join('\n')}`); return templates; } diff --git a/src/unity-version.ts b/src/unity-version.ts index 0107d66..d67766a 100644 --- a/src/unity-version.ts +++ b/src/unity-version.ts @@ -1,6 +1,8 @@ import * as os from 'os'; import { Logger } from './logging'; import { + Range, + RangeOptions, SemVer, coerce, compare, @@ -131,6 +133,10 @@ export class UnityVersion { return UnityVersion.baseCompare(this, otherVersion) <= 0; } + range(string: string | Range, options: RangeOptions | undefined = undefined): boolean { + return satisfies(this.semVer, string, options); + } + equals(other: UnityVersion): boolean { return UnityVersion.compare(this, other) === 0; } diff --git a/tests/unity-editor.test.ts b/tests/unity-editor.test.ts index 86e016c..c07052b 100644 --- a/tests/unity-editor.test.ts +++ b/tests/unity-editor.test.ts @@ -30,6 +30,10 @@ describe('UnityEditor', () => { expect(Array.isArray(editors)).toBe(true); for (const editor of editors) { + if (editor.version.isLessThan('2018.0.0')) { + continue; // Skip versions that do not support templates + } + const template = editor.GetTemplatePath(pattern); expect(template).toBeDefined(); } diff --git a/tests/unity-hub.test.ts b/tests/unity-hub.test.ts index b46cd57..bd0c349 100644 --- a/tests/unity-hub.test.ts +++ b/tests/unity-hub.test.ts @@ -1,7 +1,6 @@ import { UnityRelease } from '@rage-against-the-pixel/unity-releases-api'; import { UnityHub } from '../src/unity-hub'; import { UnityVersion } from '../src/unity-version'; -import { Logger, LogLevel } from '../src/logging'; jest.setTimeout(30000); // UnityHub operations can be slow diff --git a/tests/unity-version.test.ts b/tests/unity-version.test.ts index e868532..baba947 100644 --- a/tests/unity-version.test.ts +++ b/tests/unity-version.test.ts @@ -1,4 +1,3 @@ -import { UnityHub } from '../src/unity-hub'; import { UnityVersion } from '../src/unity-version'; describe('UnityVersion', () => { @@ -62,4 +61,33 @@ describe('UnityVersion', () => { const older = new UnityVersion('2021.3.2a1'); expect(UnityVersion.compare(match, older)).toBeGreaterThan(0); }); + + it('evaluates caret compatibility with satisfies', () => { + const baseline = new UnityVersion('2021.3.5f1'); + const compatible = new UnityVersion('2021.4.0f1'); + const incompatible = new UnityVersion('2022.1.0f1'); + + expect(baseline.satisfies(compatible)).toBe(true); + expect(baseline.satisfies(incompatible)).toBe(false); + }); + + it('evaluates semantic version ranges using range()', () => { + const version = new UnityVersion('2021.3.5f1'); + + expect(version.range('>=2021.3.0 <2021.4.0')).toBe(true); + expect(version.range('<2021.3.0')).toBe(false); + }); + + it('compares versions with helper predicates', () => { + const older = new UnityVersion('2021.3.5f1'); + const newer = new UnityVersion('2021.3.6f1'); + + expect(newer.isGreaterThan(older)).toBe(true); + expect(newer.isGreaterThan('2021.3.5f1')).toBe(true); + expect(newer.isGreaterThanOrEqualTo('2021.3.6f1')).toBe(true); + expect(older.isGreaterThanOrEqualTo(newer)).toBe(false); + expect(older.isLessThan(newer)).toBe(true); + expect(older.isLessThanOrEqualTo('2021.3.5f1')).toBe(true); + expect(newer.isLessThanOrEqualTo(older)).toBe(false); + }); }); From 2966894ad25341b7ea144510786c6b26ff3b363d Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 19 Oct 2025 10:20:18 -0400 Subject: [PATCH 24/26] bump version to 1.4.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e464f9..0e9cf7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.3.4", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.3.4", + "version": "1.4.0", "license": "MIT", "dependencies": { "@electron/asar": "^4.0.1", diff --git a/package.json b/package.json index de3cc09..3fc8821 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.3.4", + "version": "1.4.0", "description": "A command line utility for the Unity Game Engine.", "author": "RageAgainstThePixel", "license": "MIT", From 34df58174f3dd1e4d5a28360232ddf56003381aa Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 19 Oct 2025 12:23:50 -0400 Subject: [PATCH 25/26] fix android sdk range --- src/android-sdk.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/android-sdk.ts b/src/android-sdk.ts index db80127..ebf70d9 100644 --- a/src/android-sdk.ts +++ b/src/android-sdk.ts @@ -87,8 +87,8 @@ async function getJDKPath(editor: UnityEditor): Promise { async function getSdkManager(editor: UnityEditor): Promise { let globPath: string[] = []; - if (editor.version.range('>=2019.0.0')) { - logger.debug('Using sdkmanager bundled with Unity 2019 && 2020'); + if (editor.version.range('>=2019.0.0 <2021.0.0')) { + logger.debug('Using sdkmanager bundled with Unity 2019 and 2020'); switch (process.platform) { case 'darwin': case 'linux': @@ -100,7 +100,7 @@ async function getSdkManager(editor: UnityEditor): Promise { default: throw new Error(`Unsupported platform: ${process.platform}`); } - } else if (editor.version.range('>2021.0.0')) { + } else if (editor.version.range('>=2021.0.0')) { logger.debug('Using cmdline-tools sdkmanager bundled with Unity 2021+'); switch (process.platform) { case 'darwin': From a423922924e20975185531012d38d25b4521e01f Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Sun, 19 Oct 2025 15:53:08 -0400 Subject: [PATCH 26/26] add transient network retry when fetching version release info add early check for fully qualified name before falling back to legacy lookup --- src/unity-hub.ts | 73 +++++++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/src/unity-hub.ts b/src/unity-hub.ts index d444719..987c416 100644 --- a/src/unity-hub.ts +++ b/src/unity-hub.ts @@ -27,6 +27,7 @@ import { UnityRelease, Release } from '@rage-against-the-pixel/unity-releases-api'; +import { get } from 'http'; interface ReleaseInfo { unityRelease: UnityRelease; @@ -251,7 +252,8 @@ export class UnityHub { case 'No modules found to install.': break; default: - throw new Error(`Failed to execute Unity Hub (exit code: ${exitCode}) ${errorMessage}\nOutput:\n${output}`); + this.logger.debug(output); + throw new Error(`Failed to execute Unity Hub (exit code: ${exitCode}) ${errorMessage}`); } } @@ -812,6 +814,7 @@ done // - otherwise "YYYY" const fullUnityVersionPattern = /^\d{1,4}\.\d+\.\d+[abcfpx]\d+$/; let version: string; + if (fullUnityVersionPattern.test(unityVersion.version)) { version = unityVersion.version; } else { @@ -851,40 +854,58 @@ done }; this.logger.debug(`Get Unity Release: ${JSON.stringify(request, null, 2)}`); - const { data, error } = await releasesClient.api.Release.getUnityReleases(request); - if (error) { - throw new Error(`Failed to get Unity releases: ${JSON.stringify(error, null, 2)}`); - } + async function getRelease() { + const { data, error } = await releasesClient.api.Release.getUnityReleases(request); - if (!data || !data.results || data.results.length === 0) { - throw new Error(`No Unity releases found for version: ${version}`); - } + if (error) { + throw new Error(`Failed to get Unity releases: ${JSON.stringify(error, null, 2)}`); + } + + if (!data || !data.results || data.results.length === 0) { + throw new Error(`No Unity releases found for version: ${version}`); + } + + // Filter to stable 'f' releases only unless the user explicitly asked for a pre-release + const isExplicitPrerelease = /[abcpx]$/.test(unityVersion.version) || /[abcpx]/.test(unityVersion.version); + const releases: ReleaseInfo[] = (data.results || []) + .filter(release => isExplicitPrerelease || release.version.includes('f')) + .map(release => ({ + unityRelease: release, + unityVersion: new UnityVersion(release.version, release.shortRevision, unityVersion.architecture) + })); + + if (releases.length === 0) { + throw new Error(`No suitable Unity releases (stable) found for version: ${version}`); + } + + releases.sort((a, b) => UnityVersion.compare(b.unityVersion, a.unityVersion)); - // Filter to stable 'f' releases only unless the user explicitly asked for a pre-release - const isExplicitPrerelease = /[abcpx]$/.test(unityVersion.version) || /[abcpx]/.test(unityVersion.version); - const releases: ReleaseInfo[] = (data.results || []) - .filter(release => isExplicitPrerelease || release.version.includes('f')) - .map(release => ({ - unityRelease: release, - unityVersion: new UnityVersion(release.version, release.shortRevision, unityVersion.architecture) - })); - - if (releases.length === 0) { - throw new Error(`No suitable Unity releases (stable) found for version: ${version}`); + Logger.instance.debug(`Found ${releases.length} matching Unity releases for version: ${version}`); + releases.forEach(release => { + Logger.instance.debug(` - ${release.unityRelease.version} (${release.unityRelease.shortRevision}) - ${release.unityRelease.recommended}`); + }); + const latest = releases[0]!.unityRelease!; + return latest; } - releases.sort((a, b) => UnityVersion.compare(b.unityVersion, a.unityVersion)); + try { + return await getRelease(); + } catch (error) { + if (error instanceof Error && error.message.includes('fetch failed')) { + // Transient network error, retry once + return await getRelease(); + } - this.logger.debug(`Found ${releases.length} matching Unity releases for version: ${version}`); - releases.forEach(release => { - this.logger.debug(` - ${release.unityRelease.version} (${release.unityRelease.shortRevision}) - ${release.unityRelease.recommended}`); - }); - const latest = releases[0]!.unityRelease!; - return latest; + throw new Error(`Failed to get Unity releases: ${error}`); + } } private async fallbackVersionLookup(unityVersion: UnityVersion): Promise { + if (!unityVersion.isFullyQualified()) { + throw new Error(`Cannot lookup changeset for non-fully-qualified Unity version: ${unityVersion.toString()}`); + } + const url = `https://unity.com/releases/editor/whats-new/${unityVersion.version}`; this.logger.debug(`Fetching release page: "${url}"`); let response: Response;