Skip to content

feat(cli): unityLicensingToolset option + relocate platform-setup to engine scope#50

Merged
frostebite merged 2 commits into
mainfrom
fix/issue-739-license-server-toolset
May 7, 2026
Merged

feat(cli): unityLicensingToolset option + relocate platform-setup to engine scope#50
frostebite merged 2 commits into
mainfrom
fix/issue-739-license-server-toolset

Conversation

@frostebite
Copy link
Copy Markdown
Member

@frostebite frostebite commented May 7, 2026

Summary

Two coordinated changes addressing the same architectural concern:

  1. Add unityLicensingToolset option (-lt) for users on Unity floating-license servers that host multiple toolsets. Mirrors fix: license server toolset input + runAsHostUser HOME/USER (closes #739) unity-builder#838.
  2. Relocate Unity-specific platform-setup out of cli's engine-agnostic core into the Unity-scoped logic dir, so the new option lands in the correct location and the boundary cleanup that should have happened during the original cli extraction is no longer pending.

Tracking issue: #51 (cli's engine-agnostic boundary, current violations, phased plan).

Background — why both changes are in one PR

cli is structured around an explicit engine-agnostic / engine-scoped split:

Location Intent
src/command/, src/model/, src/logic/, src/middleware/ Engine-agnostic core
src/command-options/unity-options.ts, src/logic/unity/, src/model/unity/, src/command/build/unity-build-command.ts Unity-scoped

src/model/platform-setup.ts was a violation — engine-agnostic location but engine-specific content (writing Unity's services-config.json, dispatching to Unity Mac/Windows/Android setup). My initial commit added unityLicensingToolset injection inside that file, perpetuating the violation. Reviewer flagged this. The follow-up commit fixes the boundary properly: the file moves to src/logic/unity/platform-setup/platform-setup.ts, the toolset injection rides along into the correct location, and the single caller (itself Unity-scoped) is updated.

Net result: adding a new Unity field touches only Unity-scoped directories. The next licensing knob doesn't repeat this PR's mistake.

Commits

  • feat(cli): add unityLicensingToolset option for floating-license servers — adds the yargs option and the services-config.json toolset injection
  • refactor(cli): move Unity-specific platform-setup to logic/unity/ scope — relocates the file, updates imports, deletes the old engine-agnostic location, adds boundary doc-block

Files changed

File Change
src/command-options/unity-options.ts Add unityLicensingToolset (alias -lt) — Unity-scoped, correct location
src/logic/unity/platform-setup/platform-setup.ts New file: full PlatformSetup class + toolset injection. Doc-block explains the boundary intent
src/logic/unity/platform-setup/index.ts Re-export PlatformSetup
src/model/platform-setup.ts Deleted — was the boundary violation
src/command/build/unity-build-command.ts Import path updated to new location
dist/index.js Rebuilt

Are these opt-in / backwards compatible?

Concern Compat
Users not setting --unity-licensing-toolset Unchanged — services-config.json is byte-for-byte identical
Users using --unity-licensing-toolset New feature, no prior behavior to preserve
External consumers importing from '../../model/platform-setup' Breaking if any exist outside this repo. cli is a cli/library; if no public consumer relies on this internal path, no impact. The single internal caller is updated.

Out of scope — separate cleanups per #51

  • src/model/image-environment-factory.ts hardcodes UNITY_LICENSE, UNITY_LICENSE_FILE, UNITY_SERIAL, UNITY_LICENSING_SERVER. Engine module should supply env-var list.
  • src/model/docker.ts hardcodes --env UNITY_SERIAL in the docker invocation. Same.
  • Both are larger refactors that touch critical paths; bundling them here would balloon the diff and slow the user fix.

Test plan

  • bun test — 86 passed, 3 skipped, 0 failed
  • bun run build regenerates dist/index.js containing the new option
  • End-to-end verification on a real floating-license server (cannot reproduce locally — needs a reviewer with access)

🤖 Generated with Claude Code

Mirrors unity-builder#838 and orchestrator#24 — surfaces an optional
unityLicensingToolset / -lt flag for users on Unity floating-license
servers that host multiple toolsets. When set, written into
services-config.json so the Licensing Client requests entitlements from
the named toolset instead of relying on the server-side default.

Background: when a license server hosts multiple toolsets and the wrong
one (or no default) is selected, Unity falls through to a license that
lacks the requested build-target support and silently produces the wrong
artifact. See game-ci/unity-builder#739 for the original investigation.

Opt-in and backwards compatible — when unset, the rendered config is
byte-for-byte identical to before.

The runAsHostUser HOME/USER fix from unity-builder#838 does not apply
here: cli's entrypoint.sh has no runAsHostUser branch (runs as root
unconditionally).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds support for specifying a Unity floating-license toolset identifier via CLI option. The new unityLicensingToolset option (alias lt) is captured during CLI parsing and injected into the generated services configuration JSON when provided.

Changes

Unity Licensing Toolset Support

Layer / File(s) Summary
CLI Option Definition
src/command-options/unity-options.ts
New unityLicensingToolset CLI option with alias lt and default empty string.
Platform Setup Integration
src/model/platform-setup.ts
Option is destructured and conditionally applied to services config by parsing JSON, setting the toolset field, and re-serializing.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

A toolset flies through the CLI door,
Caught by options, forevermore,
JSON's parsed with gentle care,
Toolset injected here and there,
Config blooms, the setup's done! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title primarily references adding unityLicensingToolset option but also mentions relocating platform-setup to engine scope, which is only partially explained by the changes shown. The title mentions two distinct changes: the new option (clearly visible) and platform-setup relocation (not shown in raw summary). Clarify if the relocation is included in this PR or if the title should focus only on the licensing toolset option.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description comprehensively covers changes, rationale, architecture context, compatibility concerns, and test status against the repository template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-739-license-server-toolset

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/model/platform-setup.ts`:
- Around line 34-41: The code currently does a raw string replace of
unityLicensingServer into servicesConfig and then calls JSON.parse (in the
SetupShared flow using servicesConfigPathTemplate and unityLicensingToolset),
which can corrupt JSON and throw an uncaught SyntaxError; change the flow to
read and JSON.parse the template first inside a try/catch (wrap the JSON.parse
to throw a clear error if parsing fails), then set the URL property and
parsed.toolset property on the resulting object (instead of replacing in the raw
string), and finally JSON.stringify that object; if you cannot determine the URL
property name programmatically, at minimum wrap the existing JSON.parse in
try/catch and validate/escape unityLicensingServer for JSON-unsafe chars before
doing the string replace.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c19aa71-b66e-4d7f-a2ed-55182412feda

📥 Commits

Reviewing files that changed from the base of the PR and between c8305fb and 5e7d540.

⛔ Files ignored due to path filters (1)
  • dist/index.js is excluded by !**/dist/**
📒 Files selected for processing (2)
  • src/command-options/unity-options.ts
  • src/model/platform-setup.ts

Comment on lines 34 to +41
let servicesConfig = nodeFs.readFileSync(servicesConfigPathTemplate, 'utf-8');
servicesConfig = servicesConfig.replace('%URL%', unityLicensingServer);

if (unityLicensingToolset) {
const parsed = JSON.parse(servicesConfig);
parsed.toolset = unityLicensingToolset;
servicesConfig = JSON.stringify(parsed, undefined, 2);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

JSON.parse can throw an uncaught SyntaxError; URL with JSON-special chars silently corrupts the parse.

There are two related problems on the new unityLicensingToolset code path:

  1. Unhandled parse error. JSON.parse(servicesConfig) at line 38 is uncaught. SetupShared is synchronous and its caller (setup, line 12) does not wrap it, so a SyntaxError will propagate as an unhandled exception with a confusing stack trace rather than a clean error message.

  2. URL injection into raw JSON before parse. Line 35 splices unityLicensingServer verbatim into the raw JSON text with a string-replace. If the URL contains a " or \ (e.g. from a misconfigured value), the resulting string is no longer valid JSON and JSON.parse at line 38 will throw. This path is exercised only when unityLicensingToolset is also set, making it a silent regression for users using both options together.

The safest fix is to parse the unmodified template first, then assign both the URL field and the toolset field as object properties, which avoids raw-string surgery entirely:

🛡️ Proposed fix
-    let servicesConfig = nodeFs.readFileSync(servicesConfigPathTemplate, 'utf-8');
-    servicesConfig = servicesConfig.replace('%URL%', unityLicensingServer);
-
-    if (unityLicensingToolset) {
-      const parsed = JSON.parse(servicesConfig);
-      parsed.toolset = unityLicensingToolset;
-      servicesConfig = JSON.stringify(parsed, undefined, 2);
-    }
-
-    nodeFs.writeFileSync(servicesConfigPath, servicesConfig);
+    const templateText = nodeFs.readFileSync(servicesConfigPathTemplate, 'utf-8');
+
+    let servicesConfig: string;
+    if (unityLicensingToolset) {
+      try {
+        const parsed = JSON.parse(templateText);
+        // Assign both fields as typed properties instead of raw-string surgery
+        if (unityLicensingServer) {
+          // Replicate whatever property the template placeholder maps to
+          parsed.licensingServerURL = unityLicensingServer; // adjust key to match template schema
+        }
+        parsed.toolset = unityLicensingToolset;
+        servicesConfig = JSON.stringify(parsed, undefined, 2);
+      } catch (err) {
+        log.error(`Failed to parse services config template: ${err}`);
+        return;
+      }
+    } else {
+      servicesConfig = templateText.replace('%URL%', unityLicensingServer);
+    }
+
+    nodeFs.writeFileSync(servicesConfigPath, servicesConfig);

Note: The exact JSON property name that %URL% maps to in the template is not visible here. If you prefer to keep the string-replace approach for the URL (e.g. because the key name isn't easily determined programmatically), the minimum safe change is to wrap the existing JSON.parse in a try/catch and validate the URL for JSON-unsafe characters before substitution.

Verify the template's schema to confirm the right property key for the URL field, and check whether the %URL% → property mapping can be replaced cleanly:

#!/bin/bash
# Locate the services-config.json.template to inspect its schema
fd 'services-config.json.template' --exec cat {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/model/platform-setup.ts` around lines 34 - 41, The code currently does a
raw string replace of unityLicensingServer into servicesConfig and then calls
JSON.parse (in the SetupShared flow using servicesConfigPathTemplate and
unityLicensingToolset), which can corrupt JSON and throw an uncaught
SyntaxError; change the flow to read and JSON.parse the template first inside a
try/catch (wrap the JSON.parse to throw a clear error if parsing fails), then
set the URL property and parsed.toolset property on the resulting object
(instead of replacing in the raw string), and finally JSON.stringify that
object; if you cannot determine the URL property name programmatically, at
minimum wrap the existing JSON.parse in try/catch and validate/escape
unityLicensingServer for JSON-unsafe chars before doing the string replace.

Move src/model/platform-setup.ts → src/logic/unity/platform-setup/platform-setup.ts.
The class only writes Unity's services-config.json and dispatches to
Unity-specific Mac/Windows/Android setup modules — it is engine-specific
code that lived in cli's engine-agnostic core directory by accident,
carried over from the original unity-builder extraction.

cli's intended boundary:
  src/command/, src/model/, src/logic/, src/middleware/   ← engine-agnostic
  src/command-options/unity-options.ts, src/logic/unity/   ← Unity-scoped
  src/model/unity/, src/command/build/unity-build-command  ← Unity-scoped

The unityLicensingToolset support added in the previous commit lives
inside the SetupShared method that this commit relocates, so the
combined diff places the toolset injection in the correct (Unity-
scoped) location, not in engine-agnostic core.

Single caller (src/command/build/unity-build-command.ts — itself
Unity-scoped) updated to import from the new location.

Tracking issue: #51

Out of scope (separate phases per the tracking issue):
- src/model/image-environment-factory.ts hardcodes UNITY_LICENSE,
  UNITY_LICENSE_FILE, UNITY_SERIAL, UNITY_LICENSING_SERVER. Engine
  module should supply its env-var list.
- src/model/docker.ts hardcodes --env UNITY_SERIAL. Same.
- These are larger refactors that touch critical paths; bundling them
  with the toolset fix would balloon the PR.
@frostebite frostebite changed the title feat(cli): add unityLicensingToolset option for floating-license servers feat(cli): unityLicensingToolset option + relocate platform-setup to engine scope May 7, 2026
@frostebite frostebite merged commit f1d9632 into main May 7, 2026
8 checks passed
@frostebite frostebite deleted the fix/issue-739-license-server-toolset branch May 7, 2026 22:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants