← UnityCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Unity
Snapshot Sep 30, 2026 · 23:16 UTC · version 0.1.6-beta
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Use when adding, removing, upgrading, or discovering Unity (UPM) packages programmatically from outside the Editor — headless or CI package installs via the C# UnityEditor.PackageManager.Client API, verifying package ids/versions against the Unity registry, or choosing which packages a game needs by genre, platform, and monetization. The Unity CLI does not manage UPM packages, so this skill covers that gap. Triggers on \"install a Unity package\", \"add com.unity.*\", \"set up packages headless/CI\", \"which packages for a [genre] game\".",
"included_files": [
{
"relative_path": "references/select-packages.md",
"size_in_bytes": 5868
}
],
"name": "unity-package-management",
"skill_md_contents": "---\nname: unity-package-management\ndescription: Use when adding, removing, upgrading, or discovering Unity (UPM) packages programmatically from outside the Editor — headless or CI package installs via the C# UnityEditor.PackageManager.Client API, verifying package ids/versions against the Unity registry, or choosing which packages a game needs by genre, platform, and monetization. The Unity CLI does not manage UPM packages, so this skill covers that gap. Triggers on \"install a Unity package\", \"add com.unity.*\", \"set up packages headless/CI\", \"which packages for a [genre] game\".\nallowed-tools:\n - Bash\n - Read\n - Write\n - Edit\n---\n\n# Unity Package Management (headless, via the C# Client API)\n\nAdd, remove, upgrade, and discover UPM (Unity Package Manager) packages programmatically with\n`UnityEditor.PackageManager.Client`, driven headless from the terminal or CI. Do **not**\nhand-edit `Packages/manifest.json` — the Client API resolves dependencies and compatible\nversions correctly, whereas manual edits routinely break resolution.\n\nThis complements the **`unity-cli`** skill (editor install, project creation, build/test): the\nCLI has **no** package-management command, so all package work goes through the Editor's C# API.\n\n## When to use\n\n- Add / remove / upgrade one or more packages in an existing or freshly-created project.\n- Set up a project's packages non-interactively in CI.\n- Verify a package id exists, or find its available versions, before depending on it.\n- Decide which packages a game actually needs — see\n [references/select-packages.md](references/select-packages.md).\n\n## Choosing what to install\n\nInstall what the project actually needs, not everything; prefer packages the chosen template\nalready provides (URP templates already include the render pipeline, Input System, etc.). The\ngenre / look / platform / monetization → package mapping, plus how to search the registry, is\nin [references/select-packages.md](references/select-packages.md). Produce a **deduplicated\nlist of package ids** and read it back to the user before installing.\n\n## The `-quit` problem — why NOT `unity run` for installs\n\n`Client.Add` / `Client.AddAndRemove` are **asynchronous**: they return a `Request` that only\ncompletes on later `EditorApplication.update` ticks (the UPM child process marshals its result\nback on the Editor's main-loop pump, so a blocking `while (!req.IsCompleted)` busy-wait\ndeadlocks it). The Editor must **stay alive** after `-executeMethod` returns, until the request\nfinishes.\n\n`unity run` **cannot** be used for the installer: its default path injects `-quit` (see the reserved\nflags in the **`unity-cli`** skill). With `-quit`, the Editor quits the instant the method\nreturns — before UPM resolves — so packages never install and the callback never runs.\n\n**Solution:** launch the **Editor binary directly** in `-batchmode` **without** `-quit`. The\nEditor stays alive, `EditorApplication.update` keeps ticking, the poll callback runs, and it\ncalls `EditorApplication.Exit(code)` itself when done — which both quits and sets the process\nexit code.\n\n## The installer script\n\nWrite this to `Assets/Editor/ProjectBootstrap/PackageInstaller.cs`. It must live under an\n`Editor/` folder (or an Editor-only assembly) because it uses `UnityEditor`.\n\n```csharp\nusing System.Linq;\nusing UnityEditor;\nusing UnityEditor.PackageManager;\nusing UnityEditor.PackageManager.Requests;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n // Installs (and optionally removes) a fixed set of packages via the PackageManager\n // Client API, headless-safe.\n public static class PackageInstaller\n {\n // EDIT this list to match the package selection (see references/select-packages.md).\n static readonly string[] PackagesToAdd =\n {\n \"com.unity.inputsystem\",\n \"com.unity.cinemachine\",\n \"com.unity.render-pipelines.universal\",\n // \"com.unity.package@1.2.3\" // pin a version with @ when a minimum is required\n };\n\n // Optionally drop packages in the same resolution pass (e.g. a template default you don't want).\n static readonly string[] PackagesToRemove = { };\n\n const double TimeoutSeconds = 600; // UPM resolution + downloads can be slow\n\n static AddAndRemoveRequest _request;\n static double _deadline;\n\n // Invoke with: -executeMethod ProjectBootstrap.PackageInstaller.Install (NO -quit)\n public static void Install()\n {\n if (PackagesToAdd.Length == 0 && PackagesToRemove.Length == 0)\n {\n Debug.Log(\"[PackageInstaller] Nothing to do.\");\n EditorApplication.Exit(0);\n return;\n }\n\n Debug.Log($\"[PackageInstaller] Adding: {string.Join(\", \", PackagesToAdd)}\");\n _request = Client.AddAndRemove(packagesToAdd: PackagesToAdd, packagesToRemove: PackagesToRemove);\n _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;\n EditorApplication.update += Poll;\n }\n\n static void Poll()\n {\n if (_request == null) return;\n\n if (!_request.IsCompleted)\n {\n if (EditorApplication.timeSinceStartup > _deadline)\n {\n EditorApplication.update -= Poll;\n Debug.LogError(\"[PackageInstaller] Timed out waiting for UPM.\");\n EditorApplication.Exit(2);\n }\n return;\n }\n\n EditorApplication.update -= Poll;\n\n if (_request.Status == StatusCode.Success)\n {\n var names = _request.Result.Select(p => $\"{p.name}@{p.version}\");\n Debug.Log($\"[PackageInstaller] Resolved: {string.Join(\", \", names)}\");\n EditorApplication.Exit(0);\n }\n else\n {\n Debug.LogError($\"[PackageInstaller] Failed: {_request.Error?.message}\");\n EditorApplication.Exit(1);\n }\n }\n }\n}\n```\n\n`AddAndRemove` installs the whole set in a single UPM resolution pass — faster and less\nerror-prone than one `Client.Add` per package.\n\n**Add / remove / upgrade with one script:**\n- **Add**: list the id in `PackagesToAdd`.\n- **Remove**: list the id in `PackagesToRemove`.\n- **Upgrade / pin**: add the id with `@<version>` (e.g. `com.unity.cinemachine@2.9.7`). Without\n a version, resolution picks the latest compatible release.\n\n## Discovering / verifying packages\n\nTo confirm an id exists or list its versions before adding it, search the registry. The\nin-Editor `Client.SearchAll()` / `Client.Search(\"<id>\")` calls are also async, so they use the\n**same poll-and-`Exit` pattern and the same headless run** as the installer. Write\n`Assets/Editor/ProjectBootstrap/PackageSearch.cs`:\n\n```csharp\nusing System.Linq;\nusing UnityEditor;\nusing UnityEditor.PackageManager;\nusing UnityEditor.PackageManager.Requests;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n public static class PackageSearch\n {\n const double TimeoutSeconds = 120;\n static SearchRequest _request;\n static double _deadline;\n\n // Invoke with: -executeMethod ProjectBootstrap.PackageSearch.SearchAll (NO -quit)\n public static void SearchAll()\n {\n _request = Client.SearchAll(); // or Client.Search(\"com.unity.cinemachine\")\n _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;\n EditorApplication.update += Poll;\n }\n\n static void Poll()\n {\n if (_request == null) return;\n if (!_request.IsCompleted)\n {\n if (EditorApplication.timeSinceStartup > _deadline)\n {\n EditorApplication.update -= Poll;\n Debug.LogError(\"[PackageSearch] Timed out.\");\n EditorApplication.Exit(2);\n }\n return;\n }\n EditorApplication.update -= Poll;\n\n if (_request.Status == StatusCode.Success)\n {\n foreach (var p in _request.Result.OrderBy(p => p.name))\n Debug.Log($\"[PackageSearch] {p.name}@{p.versions.latestCompatible} {p.displayName}\");\n Debug.Log($\"[PackageSearch] {_request.Result.Length} packages found.\");\n EditorApplication.Exit(0);\n }\n else\n {\n Debug.LogError($\"[PackageSearch] Failed: {_request.Error?.message}\");\n EditorApplication.Exit(1);\n }\n }\n }\n}\n```\n\n`_request.Result` is a `PackageInfo[]`; each entry exposes `name`, `displayName`, `description`,\nand `versions` (`.latest`, `.latestCompatible`, `.all`). For a terminal-only check without the\nEditor (a **known** id, not free-text search), query the registry directly — see\n[references/select-packages.md](references/select-packages.md#discovering-and-verifying-packages).\n\n## Run it headless (direct Editor invocation, no `-quit`)\n\nResolve the Editor binary from the version, then run it in batch mode. The script owns quitting\nvia `EditorApplication.Exit`, so do **not** pass `-quit`:\n\n```bash\nVERSION=\"<version>\" # e.g. 6000.0.47f1 (or an installed version)\nPROJECT=\"<project-path>\"\nMETHOD=\"ProjectBootstrap.PackageInstaller.Install\" # or ...PackageSearch.SearchAll\n\n# Install directory of that editor (Hub layout), via the unity CLI\nED=$(unity editors path \"$VERSION\" --format json | python3 -c \"import sys,json;print(json.load(sys.stdin)['data']['path'])\")\n\n# Resolve the executable per-OS (handles both \"dir containing Unity.app\" and the \".app\" itself)\ncase \"$(uname)\" in\n Darwin) if [ -d \"$ED/Unity.app\" ]; then UNITY_BIN=\"$ED/Unity.app/Contents/MacOS/Unity\";\n elif [[ \"$ED\" == *.app ]]; then UNITY_BIN=\"$ED/Contents/MacOS/Unity\";\n else UNITY_BIN=\"$ED/Unity\"; fi ;;\n Linux) UNITY_BIN=\"$ED/Editor/Unity\" ;;\n *) UNITY_BIN=\"$ED/Editor/Unity.exe\" ;; # Windows (Git Bash / MSYS); use Editor\\Unity.exe in PowerShell\nesac\n\n\"$UNITY_BIN\" -batchmode -projectPath \"$PROJECT\" -executeMethod \"$METHOD\" -logFile -\necho \"Exit code: $?\" # 0 = success, 1 = UPM error, 2 = timeout\n```\n\n`-logFile -` streams the Editor log (including the `[PackageInstaller]` / `[PackageSearch]`\nlines) to stdout so you can watch resolution progress and read any UPM error. If\n`unity editors path` output shape differs on your build, get the directory from\n`unity editors --installed --format json` instead.\n\n## Verify\n\n```bash\n# Every requested id should appear as a dependency\ncat \"<project-path>/Packages/manifest.json\"\n```\n\nConfirm the run exited `0` and each package from the list is present in `manifest.json`. If a\npackage fails to resolve, `_request.Error.message` is logged; read it and check the id/version\nagainst the registry. The Editor's own log (including the `[PackageInstaller]` lines) is the\nstdout you streamed with `-logFile -` above — read it there, not via `unity logs` (which shows\nthe CLI's own log, not the Editor's).\n\n## Import & save headlessly (generate `.meta` files)\n\nAfter a script or tool writes new `.cs`/asset files, Unity must **import** them so it generates\nthe `.meta` file each asset needs — and every `.cs`/asset MUST be committed together with its\n`.meta`. Merely opening the project once (`unity open \"<project-path>\"`) imports and generates\nthem; use this method when you need it **headless** (in a script or CI).\n\nUnlike the package installer, this is **synchronous** — it finishes before returning — so it's\nsafe to run via `unity run` (its injected `-quit` is harmless; the method also calls\n`EditorApplication.Exit` for a clean exit code). Write\n`Assets/Editor/ProjectBootstrap/ProjectSaver.cs`:\n\n```csharp\nusing UnityEditor;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n public static class ProjectSaver\n {\n // Invoke with: -executeMethod ProjectBootstrap.ProjectSaver.SaveAll\n public static void SaveAll()\n {\n AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);\n AssetDatabase.SaveAssets();\n Debug.Log(\"[ProjectSaver] Assets imported and saved.\");\n EditorApplication.Exit(0);\n }\n }\n}\n```\n\n```bash\nunity run \"<project-path>\" --editor-version <version> \\\n -- -executeMethod ProjectBootstrap.ProjectSaver.SaveAll\n```\n\n## Notes\n\n- These editor scripts are a bootstrap convenience. Leave them in\n `Assets/Editor/ProjectBootstrap/` (they do nothing unless invoked) or delete them after\n setup — your call; mention it to the user.\n- All scripts live under `Editor/` because they use `UnityEditor`; they never ship in a build.\n- Monetization / backend packages (`com.unity.purchasing`, `com.unity.services.levelplay`, the\n UGS packages) install through this same mechanism, but do the actual **integration** via the\n dedicated skills: **implement-in-app-purchases**, **levelplay-unity-integration**,\n **build-live-game**.\n"
}SHA-256 of public snapshot: b5c012ea34009978c30438634e6b4c5289675ae4472acf95e95241523207844d