From ed05a9b1fb452fd5964375df3a6527e3feb9fecb Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 00:10:50 -0400 Subject: [PATCH 01/13] unity-cli@v1.2.2 - fix macOS x86_64 command runs - added additional version checks --- .github/workflows/build-options.json | 9 +++++++++ README.md | 6 ++++-- package.json | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-options.json b/.github/workflows/build-options.json index d5847286..b81e1c7f 100644 --- a/.github/workflows/build-options.json +++ b/.github/workflows/build-options.json @@ -6,6 +6,11 @@ ], "unity-version": [ "4.7.2", + "5.6.7f1 (e80cc3114ac1)", + "2017.x", + "2018.x", + "2019.x", + "2020.x", "2021.x", "2022.x", "6000.0.x", @@ -34,6 +39,10 @@ { "os": "macos-latest", "unity-version": "4.7.2" + }, + { + "os": "ubuntu-latest", + "unity-version": "5.6.7f1 (e80cc3114ac1)" } ] } \ No newline at end of file diff --git a/README.md b/README.md index 161544aa..b8da86bd 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,8 @@ unity-cli activate-license --email --password --ser #### Create a New Project from a Template -> [!NOTE] Regex patterns are supported for the `--template` option. For example, to create a 3D project with either the standard or cross-platform template, you can use `com.unity.template.3d(-cross-platform)?`. +> [!NOTE] +> Regex patterns are supported for the `--template` option. For example, to create a 3D project with either the standard or cross-platform template, you can use `com.unity.template.3d(-cross-platform)?`. ```bash unity-cli create-project --name "MyGame" --template com.unity.template.3d(-cross-platform)? --unity-editor @@ -75,7 +76,8 @@ unity-cli create-project --name "MyGame" --template com.unity.template.3d(-cross #### Open a project from the command line -> [!NOTE] If you run this command in the same directory as your Unity project, you can omit the `--unity-project`, `--unity-version`, and `--unity-editor` options. +> [!TIP] +> If you run this command in the same directory as your Unity project, you can omit the `--unity-project`, `--unity-version`, and `--unity-editor` options. ```bash unity-cli open-project diff --git a/package.json b/package.json index 8b18f73a..94b1311a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rage-against-the-pixel/unity-cli", - "version": "1.2.1", + "version": "1.2.2", "description": "A command line utility for the Unity Game Engine.", "author": "RageAgainstThePixel", "license": "MIT", From 36bb640419eb98abb18e25e33ddc098ae5b01bef Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 08:33:40 -0400 Subject: [PATCH 02/13] add Rosetta 2 exec on mac fix licensing docs add support for floating license config as path AND base64 string fix occasional race condition when starting editor process exits faster than logs can be seen --- README.md | 4 +++- src/cli.ts | 4 ++-- src/license-client.ts | 13 ++++++++++++- src/unity-editor.ts | 17 +++++++++++++---- src/utilities.ts | 4 +++- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b8da86bd..a08e17a5 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,10 @@ unity-cli setup-unity --unity-version 2022.3.x --modules android,ios #### Activate a Unity License +Supports personal, professional, and floating licenses (using a license server configuration). + ```bash -unity-cli activate-license --email --password --serial +unity-cli activate-license --license personal --email --password ``` #### Create a New Project from a Template diff --git a/src/cli.ts b/src/cli.ts index 6070b207..fbb20b13 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -36,11 +36,11 @@ program.command('license-version') program.command('activate-license') .description('Activate a Unity license.') + .option('-l, --license ', 'License type (personal, professional, floating). Required.') .option('-e, --email ', 'Email associated with the Unity account. Required when activating a personal or professional license.') .option('-p, --password ', 'Password for the Unity account. Required when activating a personal or professional license.') .option('-s, --serial ', 'License serial number. Required when activating a professional license.') - .option('-l, --license ', 'License type (personal, professional, floating).') - .option('-c, --config ', 'Path to the configuration file. Required when activating a floating license.') + .option('-c, --config ', 'Path to the configuration file, or base64 encoded JSON string. Required when activating a floating license.') .option('--verbose', 'Enable verbose logging.') .action(async (options) => { if (options.verbose) { diff --git a/src/license-client.ts b/src/license-client.ts index fa7e3ee4..92411a54 100644 --- a/src/license-client.ts +++ b/src/license-client.ts @@ -354,8 +354,19 @@ export class LicensingClient { throw new Error(`Unsupported platform: ${process.platform}`); } + // Ensure the services directory exists + if (!fs.existsSync(servicesPath)) { + await fs.promises.mkdir(servicesPath, { recursive: true }); + } + const servicesConfigPath = path.join(servicesPath, 'services-config.json'); - await fs.promises.writeFile(servicesConfigPath, Buffer.from(options.servicesConfig, 'base64')); + + if (fs.existsSync(options.servicesConfig)) { + await fs.promises.copyFile(options.servicesConfig, servicesConfigPath); + } + else { + await fs.promises.writeFile(servicesConfigPath, Buffer.from(options.servicesConfig, 'base64')); + } break; } default: { // personal and professional license activation diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 4ec1d1c6..c988cf87 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -151,7 +151,7 @@ export class UnityEditor { */ public async Run(command: EditorCommand): Promise { let isCancelled = false; - let exitCode: number = 1; + let exitCode: number | undefined = undefined; let procInfo: ProcInfo | null = null; let logTail: LogTailResult | null = null; let unityProcess: ChildProcessByStdio; @@ -196,6 +196,7 @@ export class UnityEditor { } const logPath: string = GetArgumentValueAsString('-logFile', command.args); + logTail = TailLogFile(logPath); const commandStr = `\x1b[34m${this.editorPath} ${command.args.join(' ')}\x1b[0m`; this.logger.startGroup(commandStr); @@ -211,6 +212,13 @@ export class UnityEditor { } }); } else { + if (process.platform === 'darwin') { + if (this.version.architecture === 'X86_64' && process.arch === 'arm64') { + // Force the Unity Editor to run under Rosetta 2 on Apple Silicon Macs if the editor is x86_64 + command.args.unshift('arch', '-x86_64'); + } + } + unityProcess = spawn( this.editorPath, command.args, { @@ -230,14 +238,13 @@ export class UnityEditor { process.once('SIGTERM', onCancel); procInfo = { pid: unityProcess.pid, ppid: process.pid, name: this.editorPath }; this.logger.debug(`Unity process started with pid: ${procInfo.pid}`); - await WaitForFileToBeCreatedAndReadable(logPath, 10_000); - logTail = TailLogFile(logPath); exitCode = await new Promise((resolve, reject) => { unityProcess.on('close', (code) => { logTail?.stopLogTail(); resolve(code === null ? 1 : code); }); unityProcess.on('error', (error) => { + this.logger.error(`Unity process error: ${error}`); logTail?.stopLogTail(); reject(error); }); @@ -258,7 +265,9 @@ export class UnityEditor { if (!isCancelled) { await tryKillEditorProcesses(); - if (exitCode !== 0) { + if (exitCode === undefined) { + throw Error('Failed to start Unity!'); + } else if (exitCode > 0) { throw Error(`Unity failed with exit code ${exitCode}`); } } diff --git a/src/utilities.ts b/src/utilities.ts index 13d47aa5..3b8f868d 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -390,13 +390,15 @@ export function TailLogFile(logPath: string): LogTailResult { const tailPromise = new Promise((resolve, reject) => { (async () => { try { + await WaitForFileToBeCreatedAndReadable(logPath, 10_000); + while (!logEnded) { await Delay(logPollingInterval); await readNewLogContent(); } // Final read to capture any remaining content after tailing stops - await WaitForFileToBeUnlocked(logPath, 10000); + await WaitForFileToBeUnlocked(logPath, 10_000); await readNewLogContent(); try { From 42e6d41db6cf1007300a643cb777daaef77a563f Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 08:34:20 -0400 Subject: [PATCH 03/13] tweak command --- src/unity-editor.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/unity-editor.ts b/src/unity-editor.ts index c988cf87..29b26673 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -212,11 +212,9 @@ export class UnityEditor { } }); } else { - if (process.platform === 'darwin') { - if (this.version.architecture === 'X86_64' && process.arch === 'arm64') { - // Force the Unity Editor to run under Rosetta 2 on Apple Silicon Macs if the editor is x86_64 - command.args.unshift('arch', '-x86_64'); - } + if (process.platform === 'darwin' && this.version.architecture === 'X86_64' && process.arch === 'arm64') { + // Force the Unity Editor to run under Rosetta 2 on Apple Silicon Macs if the editor is x86_64 + command.args.unshift('arch', '-x86_64'); } unityProcess = spawn( From f26db6840651740ea71d7f88df179d9a01514e4f Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 08:47:19 -0400 Subject: [PATCH 04/13] rework mac editor spawn --- src/unity-editor.ts | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 29b26673..95963ee8 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -214,18 +214,27 @@ export class UnityEditor { } else { if (process.platform === 'darwin' && this.version.architecture === 'X86_64' && process.arch === 'arm64') { // Force the Unity Editor to run under Rosetta 2 on Apple Silicon Macs if the editor is x86_64 - command.args.unshift('arch', '-x86_64'); + command.args.unshift('-x86_64', this.editorPath); + unityProcess = spawn( + 'arch', + command.args, { + stdio: ['ignore', 'ignore', 'ignore'], + env: { + ...process.env, + UNITY_THISISABUILDMACHINE: '1' + } + }); + } else { + unityProcess = spawn( + this.editorPath, + command.args, { + stdio: ['ignore', 'ignore', 'ignore'], + env: { + ...process.env, + UNITY_THISISABUILDMACHINE: '1' + } + }); } - - unityProcess = spawn( - this.editorPath, - command.args, { - stdio: ['ignore', 'ignore', 'ignore'], - env: { - ...process.env, - UNITY_THISISABUILDMACHINE: '1' - } - }); } if (!unityProcess?.pid) { From efbb4b88b6681ae025b497656f61a41a7548eda2 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 08:48:14 -0400 Subject: [PATCH 05/13] revert error code check in case there are negative codes --- src/unity-editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 95963ee8..1a4df940 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -274,7 +274,7 @@ export class UnityEditor { if (exitCode === undefined) { throw Error('Failed to start Unity!'); - } else if (exitCode > 0) { + } else if (exitCode !== 0) { throw Error(`Unity failed with exit code ${exitCode}`); } } From fbe1056b5afd52012f73d8ea5181f9eec699d45e Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 08:51:20 -0400 Subject: [PATCH 06/13] format --- src/unity-editor.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 1a4df940..91b0195c 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -212,8 +212,10 @@ export class UnityEditor { } }); } else { - if (process.platform === 'darwin' && this.version.architecture === 'X86_64' && process.arch === 'arm64') { - // Force the Unity Editor to run under Rosetta 2 on Apple Silicon Macs if the editor is x86_64 + if (process.arch === 'arm64' && + process.platform === 'darwin' && + this.version.architecture === 'X86_64' + ) { // Force the Unity Editor to run under Rosetta 2 on Apple Silicon Macs if the editor is x86_64 command.args.unshift('-x86_64', this.editorPath); unityProcess = spawn( 'arch', From fa8d0062598f3ac232f560e85e639436ed84281e Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 08:55:32 -0400 Subject: [PATCH 07/13] formatting --- src/unity-editor.ts | 51 ++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 91b0195c..291981ce 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -200,7 +200,9 @@ export class UnityEditor { const commandStr = `\x1b[34m${this.editorPath} ${command.args.join(' ')}\x1b[0m`; this.logger.startGroup(commandStr); - if (process.platform === 'linux' && !command.args.includes('-nographics')) { + if (process.platform === 'linux' && + !command.args.includes('-nographics') + ) { unityProcess = spawn( 'xvfb-run', [this.editorPath, ...command.args], { @@ -211,32 +213,29 @@ export class UnityEditor { UNITY_THISISABUILDMACHINE: '1' } }); + } else if (process.arch === 'arm64' && + process.platform === 'darwin' && + this.version.architecture === 'X86_64' + ) { // Force the Unity Editor to run under Rosetta 2 on Apple Silicon Macs if the editor is x86_64 + unityProcess = spawn( + 'arch', + ['-x86_64', this.editorPath, ...command.args], { + stdio: ['ignore', 'ignore', 'ignore'], + env: { + ...process.env, + UNITY_THISISABUILDMACHINE: '1' + } + }); } else { - if (process.arch === 'arm64' && - process.platform === 'darwin' && - this.version.architecture === 'X86_64' - ) { // Force the Unity Editor to run under Rosetta 2 on Apple Silicon Macs if the editor is x86_64 - command.args.unshift('-x86_64', this.editorPath); - unityProcess = spawn( - 'arch', - command.args, { - stdio: ['ignore', 'ignore', 'ignore'], - env: { - ...process.env, - UNITY_THISISABUILDMACHINE: '1' - } - }); - } else { - unityProcess = spawn( - this.editorPath, - command.args, { - stdio: ['ignore', 'ignore', 'ignore'], - env: { - ...process.env, - UNITY_THISISABUILDMACHINE: '1' - } - }); - } + unityProcess = spawn( + this.editorPath, + command.args, { + stdio: ['ignore', 'ignore', 'ignore'], + env: { + ...process.env, + UNITY_THISISABUILDMACHINE: '1' + } + }); } if (!unityProcess?.pid) { From f3dd900609c34e5bfe44e2c8eefb4ea5249773ed Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 09:08:20 -0400 Subject: [PATCH 08/13] update template finding --- src/cli.ts | 5 ++++- src/unity-editor.ts | 15 ++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index fbb20b13..c782592c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -506,7 +506,10 @@ program.command('create-project') if (!unityEditor.version.isLegacy() && options.template && options.template.length > 0) { const templatePath = unityEditor.GetTemplatePath(options.template); - args.push('-cloneFromTemplate', templatePath); + + if (!templatePath) { + args.push('-cloneFromTemplate', templatePath); + } } await unityEditor.Run({ projectPath, args }); diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 291981ce..5801efd9 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -77,11 +77,12 @@ export class UnityEditor { * @returns The full path to the matching template file. * @throws If no templates are found, or no matching template is found. */ - public GetTemplatePath(template: string): string { + public GetTemplatePath(template: string): string | undefined { const templates: string[] = this.GetAvailableTemplates(); if (templates.length === 0) { - throw new Error('No Unity templates found!'); + this.logger.warn(`No Unity templates found for ${this.version.toString()}`); + return undefined; } // Build a regex to match the template name and version @@ -98,7 +99,8 @@ export class UnityEditor { const matches = templates.filter(t => regex.test(path.basename(t))); if (matches.length === 0) { - throw new Error(`${template} path not found!`); + this.logger.warn(`No matching template path found for ${template}`); + return undefined; } // Pick the longest match (as in the shell script: sort by length descending) @@ -106,7 +108,8 @@ export class UnityEditor { const templatePath = matches[0]; if (!templatePath) { - throw new Error('No matching template path found.'); + this.logger.warn(`No matching template path found for ${template}`); + return undefined; } return path.normalize(templatePath); @@ -128,7 +131,7 @@ export class UnityEditor { templateDir = path.join(editorRoot, 'Data', 'Resources', 'PackageManager', 'ProjectTemplates'); } - this.logger.debug(`Looking for templates in: ${templateDir}`); + this.logger.ci(`Looking for templates in: ${templateDir}`); // Check if the template directory exists if (!fs.existsSync(templateDir) || @@ -141,6 +144,8 @@ export class UnityEditor { .filter(f => f.endsWith('.tgz')) .map(f => path.join(templateDir, f)); templates.push(...packages); + this.logger.ci(`Found ${templates.length} templates:`); + templates.forEach(t => this.logger.ci(` - ${t}`)); return templates; } From 82b0d4c2fac0510603ddff7fec5671d283513328 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 09:08:34 -0400 Subject: [PATCH 09/13] fix inverted logic gate --- src/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index c782592c..04140d5b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -507,7 +507,7 @@ program.command('create-project') if (!unityEditor.version.isLegacy() && options.template && options.template.length > 0) { const templatePath = unityEditor.GetTemplatePath(options.template); - if (!templatePath) { + if (templatePath) { args.push('-cloneFromTemplate', templatePath); } } From a8557adab4a76713b5a2a482968e8ef1fb43ce25 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 09:29:09 -0400 Subject: [PATCH 10/13] update workflow build logic fixed open-project to search the current directory for project version text files --- .github/workflows/unity-build.yml | 20 ++++++++++++++++++-- src/cli.ts | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index 2e4e9419..bcf31604 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -24,6 +24,7 @@ jobs: timeout-minutes: 30 env: UNITY_PROJECT_PATH: '' # Set from create-project step + RUN_BUILD: '' # Set to true if the build pipeline package can be installed and used steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -73,15 +74,30 @@ jobs: echo "Error: UNITY_PROJECT_PATH is not set" exit 1 fi + # check if the project can be built. Only Unity 2019.4+ and newer majors support the build pipeline package + version="${{ matrix.unity-version }}" + # extract major and minor (minor may be empty if version is just '2019' etc.) + major=$(echo "$version" | cut -d'.' -f1) + minor=$(echo "$version" | cut -d'.' -f2) + if [ -z "$minor" ]; then + minor=0 + fi + # numeric comparison: enable build for major > 2019 or major == 2019 and minor >= 4 + if [ "$major" -gt 2019 ] || { [ "$major" -eq 2019 ] && [ "$minor" -ge 4 ]; }; then + echo "Proceeding with build for Unity version $version" + echo "RUN_BUILD=true" >> $GITHUB_ENV + else + echo "Skipping build: Unity version $version does not support the build pipeline package (requires 2019.4+)" + fi - name: Install OpenUPM and build pipeline package shell: bash - if: ${{ matrix.unity-version != '4.7.2' && matrix.unity-version != '5.6.7' }} + if: ${{ env.RUN_BUILD == 'true' }} run: | npm install -g openupm-cli cd "${UNITY_PROJECT_PATH}" openupm add com.utilities.buildpipeline - name: Build project - if: ${{ matrix.unity-version != '4.7.2' && matrix.unity-version != '5.6.7' }} + if: ${{ env.RUN_BUILD == 'true' }} shell: bash run: | unity-cli run --log-name Validate -quit -nographics -batchmode -executeMethod Utilities.Editor.BuildPipeline.UnityPlayerBuildTools.ValidateProject -importTMProEssentialsAsset diff --git a/src/cli.ts b/src/cli.ts index 04140d5b..ff193c6b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -332,7 +332,7 @@ program.command('open-project') } Logger.instance.debug(JSON.stringify(options)); - const projectPath = options.unityProject?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || process.cwd(); + const projectPath = options.unityProject?.toString()?.trim() || process.env.UNITY_PROJECT_PATH || undefined; const unityProject = await UnityProject.GetProject(projectPath); if (!unityProject) { From 9a75bfabdb2c7c1e34649824737fece4784974cb Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 09:47:44 -0400 Subject: [PATCH 11/13] explicit process exit codes for cli updated some logging and private function names --- README.md | 6 ++++- src/cli.ts | 61 ++++++++++++++++++++++++++++++++------------ src/unity-editor.ts | 5 ++-- src/unity-version.ts | 12 ++++----- 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index a08e17a5..73aa90fe 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,14 @@ npm install -g @rage-against-the-pixel/unity-cli ## Usage +In general, the command structure is: + ```bash -unity-cli [command] [options] +unity-cli [command] [options] ``` +With options always using double dashes (`--option`) and arguments passed directly to Unity or Unity Hub commands as they normally would with single dashes (`-arg`). Each option typically has a short alias using a single dash (`-o`), except for commands where we pass through arguments, as those get confused by the command parser. + ### Common Commands #### Auth diff --git a/src/cli.ts b/src/cli.ts index ff193c6b..39da881c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -32,6 +32,7 @@ program.command('license-version') .action(async () => { const client = new LicensingClient(); await client.Version(); + process.exit(0); }); program.command('activate-license') @@ -53,13 +54,15 @@ program.command('activate-license') const licenseStr: string = options.license?.toString()?.trim(); if (!licenseStr || licenseStr.length === 0) { - throw new Error('License type is required. Use -l or --license to specify it.'); + Logger.instance.error('License type is required. Use -l or --license to specify it.'); + process.exit(1); } const licenseType: LicenseType = options.license.toLowerCase() as LicenseType; if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { - throw new Error(`Invalid license type: ${licenseType}`); + Logger.instance.error(`Invalid license type: ${licenseType}`); + process.exit(1); } if (licenseType !== LicenseType.floating) { @@ -83,6 +86,7 @@ program.command('activate-license') username: options.email, password: options.password }); + process.exit(0); }); program.command('return-license') @@ -100,16 +104,19 @@ program.command('return-license') const licenseStr: string = options.license?.toString()?.trim(); if (!licenseStr || licenseStr.length === 0) { - throw new Error('License type is required. Use -l or --license to specify it.'); + Logger.instance.error('License type is required. Use -l or --license to specify it.'); + process.exit(1); } const licenseType: LicenseType = licenseStr.toLowerCase() as LicenseType; if (![LicenseType.personal, LicenseType.professional, LicenseType.floating].includes(licenseType)) { - throw new Error(`Invalid license type: ${licenseType}`); + Logger.instance.error(`Invalid license type: ${licenseType}`); + process.exit(1); } await client.Deactivate(licenseType); + process.exit(0); }); program.commandsGroup('Unity Hub:'); @@ -123,6 +130,8 @@ program.command('hub-version') process.stdout.write(`${version}\n`); } catch (error) { process.stdout.write(`${error}\n`); + } finally { + process.exit(0); } }); @@ -148,6 +157,8 @@ program.command('hub-install') } else { process.stdout.write(`${hubPath}\n`); } + + process.exit(0); }); program.command('hub-path') @@ -163,13 +174,15 @@ program.command('hub-path') } else { process.stdout.write(`${hub.executable}\n`); } + + process.exit(0); }); program.command('hub') .description('Run commands directly to the Unity Hub. (You need not to pass --headless or -- to this command).') + .allowUnknownOption(true) .argument('', 'Arguments to pass to the Unity Hub executable.') .option('--verbose', 'Enable verbose logging.') - .allowUnknownOption(true) .action(async (args: string[], options) => { if (options.verbose) { Logger.instance.logLevel = LogLevel.DEBUG; @@ -179,6 +192,7 @@ program.command('hub') const unityHub = new UnityHub(); await unityHub.Exec(args, { silent: false, showCommand: Logger.instance.logLevel === LogLevel.DEBUG }); + process.exit(0); }); program.command('setup-unity') @@ -206,7 +220,8 @@ program.command('setup-unity') } if (!options.unityVersion && !unityProject) { - throw new Error('You must specify a Unity version or project path with -u, --unity-version, -p, --unity-project.'); + Logger.instance.error('You must specify a Unity version or project path with -u, --unity-version, -p, --unity-project.'); + process.exit(1); } const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset); @@ -266,6 +281,8 @@ program.command('setup-unity') } } } + + process.exit(0); }); program.command('uninstall-unity') @@ -289,6 +306,7 @@ program.command('uninstall-unity') const unityVersion = new UnityVersion(unityVersionStr, options.changeset, options.arch); const unityHub = new UnityHub(); const installedEditors = await unityHub.ListInstalledEditors(); + if (unityVersion.isLegacy()) { const installPath = await unityHub.GetInstallPath(); unityEditor = new UnityEditor(path.join(installPath, `Unity ${unityVersion.toString()}`, 'Unity.exe')); @@ -299,7 +317,8 @@ program.command('uninstall-unity') const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR_PATH || undefined; if (!editorPath || editorPath.length === 0) { - throw new Error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.'); + Logger.instance.error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.'); + process.exit(1); } try { @@ -336,7 +355,8 @@ program.command('open-project') const unityProject = await UnityProject.GetProject(projectPath); if (!unityProject) { - throw new Error(`The specified path is not a valid Unity project: ${projectPath}`); + Logger.instance.error(`The specified path is not a valid Unity project: ${projectPath}`); + process.exit(1); } const unityVersion = unityProject?.version ?? new UnityVersion(options.unityVersion, options.changeset); @@ -380,7 +400,8 @@ program.command('run') const unityProject = await UnityProject.GetProject(projectPath); if (!unityProject) { - throw new Error(`The specified path is not a valid Unity project: ${projectPath}`); + Logger.instance.error(`The specified path is not a valid Unity project: ${projectPath}`); + process.exit(1); } if (!unityEditor) { @@ -389,7 +410,8 @@ program.command('run') } if (!unityEditor) { - throw new Error('The Unity Editor path was not specified. Use --unity-editor to specify it or set the UNITY_EDITOR_PATH environment variable.'); + Logger.instance.error('The Unity Editor path was not specified. Use --unity-editor to specify it or set the UNITY_EDITOR_PATH environment variable.'); + process.exit(1); } if (!args.includes('-logFile')) { @@ -400,6 +422,7 @@ program.command('run') await unityEditor.Run({ args: [...args] }); + process.exit(0); }); program.command('list-project-templates') @@ -418,7 +441,8 @@ program.command('list-project-templates') const unityVersionStr = options.unityVersion?.toString()?.trim(); if (!unityVersionStr && !options.unityEditor) { - throw new Error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.'); + Logger.instance.error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.'); + process.exit(1); } let unityEditor: UnityEditor; @@ -442,14 +466,13 @@ program.command('list-project-templates') if (options.json) { process.stdout.write(`\n${JSON.stringify({ templates })}\n`); } else { - process.stdout.write(`Available project templates:\n`); - for (const template of templates) { - process.stdout.write(` - ${path.basename(template)}\n`); - } + process.stdout.write(`Available project templates:\n${templates.map(t => ` - ${path.basename(t)}`).join('\n')}\n`); } } else { process.stdout.write('No project templates found for this Unity Editor.\n'); } + + process.exit(0); }); program.command('create-project') @@ -471,7 +494,8 @@ program.command('create-project') const unityVersionStr = options.unityVersion?.toString()?.trim(); if (!unityVersionStr && !options.unityEditor) { - throw new Error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.'); + Logger.instance.error('You must specify a Unity version or editor path with -u, --unity-version, -e, --unity-editor.'); + process.exit(1); } let unityEditor: UnityEditor; @@ -483,7 +507,8 @@ program.command('create-project') const editorPath = options.unityEditor?.toString()?.trim() || process.env.UNITY_EDITOR_PATH; if (!editorPath || editorPath.length === 0) { - throw new Error('The Unity Editor path was not specified. Use -e or --unity-editor to specify it, or set the UNITY_EDITOR_PATH environment variable.'); + Logger.instance.error('The Unity Editor path was not specified. Use -e or --unity-editor to specify it, or set the UNITY_EDITOR_PATH environment variable.'); + process.exit(1); } unityEditor = new UnityEditor(editorPath); @@ -521,6 +546,8 @@ program.command('create-project') } else { process.stdout.write(`Unity project created at: ${projectPath}\n`); } + + process.exit(0); }); program.parse(process.argv); diff --git a/src/unity-editor.ts b/src/unity-editor.ts index 5801efd9..fb6e0549 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -131,7 +131,7 @@ export class UnityEditor { templateDir = path.join(editorRoot, 'Data', 'Resources', 'PackageManager', 'ProjectTemplates'); } - this.logger.ci(`Looking for templates in: ${templateDir}`); + this.logger.debug(`Looking for templates in: ${templateDir}`); // Check if the template directory exists if (!fs.existsSync(templateDir) || @@ -144,8 +144,7 @@ export class UnityEditor { .filter(f => f.endsWith('.tgz')) .map(f => path.join(templateDir, f)); templates.push(...packages); - this.logger.ci(`Found ${templates.length} templates:`); - templates.forEach(t => this.logger.ci(` - ${t}`)); + 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 2e28853f..3bc223ed 100644 --- a/src/unity-version.ts +++ b/src/unity-version.ts @@ -78,10 +78,10 @@ export class UnityVersion { return new UnityVersion(exactMatch.version, this.changeset ?? null, this.architecture); } - if (UnityVersion.needsFallbackSearch(this.version)) { - const candidates = UnityVersion.resolveFallbackCandidates(this.version, releaseInfos); + if (UnityVersion.needsGlobSearch(this.version)) { + const candidates = UnityVersion.resolveVersionCandidates(this.version, releaseInfos); - this.logger.debug(`Searching for fallback match for ${this.version}:`); + this.logger.debug(`Searching for match for ${this.version}:`); candidates.forEach(release => { this.logger.debug(` > ${release.version}`); }); @@ -89,7 +89,7 @@ export class UnityVersion { const latest = candidates[0]; if (latest) { - this.logger.debug(`Found fallback Unity ${latest.version}`); + this.logger.debug(`Found Unity ${latest.version}`); return new UnityVersion(latest.version, null, this.architecture); } } @@ -215,11 +215,11 @@ export class UnityVersion { }; } - private static needsFallbackSearch(version: string): boolean { + private static needsGlobSearch(version: string): boolean { return /\.x($|[^\w])/.test(version) || /\.\*($|[^\w])/.test(version) || !UnityVersion.UNITY_RELEASE_PATTERN.test(version); } - private static resolveFallbackCandidates( + private static resolveVersionCandidates( version: string, releases: UnityReleaseInfo[] ): UnityReleaseInfo[] { From 0a01fb23f03d6f1f7419b649621277d07e4a23a8 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 10:05:47 -0400 Subject: [PATCH 12/13] change logging and timeouts --- src/unity-editor.ts | 6 +++++- src/utilities.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/unity-editor.ts b/src/unity-editor.ts index fb6e0549..27375ead 100644 --- a/src/unity-editor.ts +++ b/src/unity-editor.ts @@ -204,6 +204,10 @@ export class UnityEditor { const commandStr = `\x1b[34m${this.editorPath} ${command.args.join(' ')}\x1b[0m`; this.logger.startGroup(commandStr); + if (this.version.isLegacy() && process.platform === 'darwin' && process.arch === 'arm64') { + throw new Error(`Cannot execute Unity ${this.version.toString()} on Apple Silicon Macs.`); + } + if (process.platform === 'linux' && !command.args.includes('-nographics') ) { @@ -242,7 +246,7 @@ export class UnityEditor { }); } - if (!unityProcess?.pid) { + if (!unityProcess?.pid || unityProcess.killed) { throw new Error('Failed to start Unity process!'); } diff --git a/src/utilities.ts b/src/utilities.ts index 3b8f868d..839356f2 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -390,7 +390,7 @@ export function TailLogFile(logPath: string): LogTailResult { const tailPromise = new Promise((resolve, reject) => { (async () => { try { - await WaitForFileToBeCreatedAndReadable(logPath, 10_000); + await WaitForFileToBeCreatedAndReadable(logPath); while (!logEnded) { await Delay(logPollingInterval); From 412069a40a84d0f07252c23ff0a4d557ee51fe32 Mon Sep 17 00:00:00 2001 From: Stephen Hodgson Date: Tue, 7 Oct 2025 15:48:53 -0400 Subject: [PATCH 13/13] increase timeouts bump action deps --- .github/workflows/integration-tests.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/unity-build.yml | 2 +- src/utilities.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 602c702e..233a0bc0 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -16,7 +16,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: sparse-checkout: .github/ - uses: RageAgainstThePixel/job-builder@v1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7de2d605..e5d89cbd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 24.x diff --git a/.github/workflows/unity-build.yml b/.github/workflows/unity-build.yml index bcf31604..f429c1c5 100644 --- a/.github/workflows/unity-build.yml +++ b/.github/workflows/unity-build.yml @@ -26,7 +26,7 @@ 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: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: 24.x diff --git a/src/utilities.ts b/src/utilities.ts index 839356f2..02e5fca6 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -390,7 +390,7 @@ export function TailLogFile(logPath: string): LogTailResult { const tailPromise = new Promise((resolve, reject) => { (async () => { try { - await WaitForFileToBeCreatedAndReadable(logPath); + await WaitForFileToBeCreatedAndReadable(logPath, 60_000); while (!logEnded) { await Delay(logPollingInterval);