unity-package-management

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unity Package Management (headless, via the C# Client API)

Unity 包管理(无头模式,基于C# Client API)

Add, remove, upgrade, and discover UPM (Unity Package Manager) packages programmatically with
UnityEditor.PackageManager.Client
, driven headless from the terminal or CI. Do not hand-edit
Packages/manifest.json
— the Client API resolves dependencies and compatible versions correctly, whereas manual edits routinely break resolution.
This complements the
unity-cli
skill (editor install, project creation, build/test): the CLI has no package-management command, so all package work goes through the Editor's C# API.
通过
UnityEditor.PackageManager.Client
以编程方式添加、移除、升级和发现UPM(Unity Package Manager)包,可从终端或CI环境以无头模式运行。请勿手动编辑
Packages/manifest.json
——Client API能正确解析依赖项和兼容版本,而手动编辑通常会破坏解析流程。
本技能是对**
unity-cli
**技能(编辑器安装、项目创建、构建/测试)的补充:CLI没有包管理命令,因此所有包操作都需通过编辑器的C# API完成。

When to use

使用场景

  • Add / remove / upgrade one or more packages in an existing or freshly-created project.
  • Set up a project's packages non-interactively in CI.
  • Verify a package id exists, or find its available versions, before depending on it.
  • Decide which packages a game actually needs — see references/select-packages.md.
  • 在现有或新建项目中添加/移除/升级一个或多个包。
  • 在CI环境中以非交互方式设置项目的包。
  • 在依赖某个包之前,验证其ID是否存在,或查找其可用版本。
  • 确定游戏实际需要哪些包——详见references/select-packages.md

Choosing what to install

选择要安装的包

Install what the project actually needs, not everything; prefer packages the chosen template already provides (URP templates already include the render pipeline, Input System, etc.). The genre / look / platform / monetization → package mapping, plus how to search the registry, is in references/select-packages.md. Produce a deduplicated list of package ids and read it back to the user before installing.
仅安装项目实际需要的包,而非全部;优先选择所选模板已提供的包(URP模板已包含渲染管线、Input System等)。游戏类型/风格/平台/商业化模式与包的映射关系,以及如何搜索注册表的方法,均在references/select-packages.md中。生成一份去重后的包ID列表,并在安装前告知用户。

The
-quit
problem — why NOT
unity run
for installs

-quit
问题——为何不能用
unity run
进行安装

Client.Add
/
Client.AddAndRemove
are asynchronous: they return a
Request
that only completes on later
EditorApplication.update
ticks (the UPM child process marshals its result back on the Editor's main-loop pump, so a blocking
while (!req.IsCompleted)
busy-wait deadlocks it). The Editor must stay alive after
-executeMethod
returns, until the request finishes.
unity run
cannot be used for the installer: it always injects
-quit
(see the reserved flags in the
unity-cli
skill). With
-quit
, the Editor quits the instant the method returns — before UPM resolves — so packages never install and the callback never runs.
Solution: launch the Editor binary directly in
-batchmode
without
-quit
. The Editor stays alive,
EditorApplication.update
keeps ticking, the poll callback runs, and it calls
EditorApplication.Exit(code)
itself when done — which both quits and sets the process exit code.
Client.Add
/
Client.AddAndRemove
异步操作:它们返回一个
Request
,仅在后续的
EditorApplication.update
周期中才会完成(UPM子进程会在编辑器的主循环中返回结果,因此阻塞式的
while (!req.IsCompleted)
忙等待会导致死锁)。编辑器在
-executeMethod
返回后必须保持运行,直到请求完成。
unity run
不能用于安装操作:它总会自动添加
-quit
参数(详见**
unity-cli
**技能中的保留标志)。使用
-quit
时,编辑器会在方法返回的瞬间退出——此时UPM尚未完成解析,因此包永远不会安装,回调也不会执行。
解决方案:直接启动编辑器二进制文件,使用
-batchmode
模式且不添加
-quit
参数。编辑器会保持运行,
EditorApplication.update
持续执行,轮询回调会运行,完成后会自行调用
EditorApplication.Exit(code)
——这既会退出编辑器,也会设置进程退出码。

The installer script

安装脚本

Write this to
Assets/Editor/ProjectBootstrap/PackageInstaller.cs
. It must live under an
Editor/
folder (or an Editor-only assembly) because it uses
UnityEditor
.
csharp
using System.Linq;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;

namespace ProjectBootstrap
{
    // Installs (and optionally removes) a fixed set of packages via the PackageManager
    // Client API, headless-safe.
    public static class PackageInstaller
    {
        // EDIT this list to match the package selection (see references/select-packages.md).
        static readonly string[] PackagesToAdd =
        {
            "com.unity.inputsystem",
            "com.unity.cinemachine",
            "com.unity.render-pipelines.universal",
            // "com.unity.package@1.2.3"  // pin a version with @ when a minimum is required
        };

        // Optionally drop packages in the same resolution pass (e.g. a template default you don't want).
        static readonly string[] PackagesToRemove = { };

        const double TimeoutSeconds = 600; // UPM resolution + downloads can be slow

        static AddAndRemoveRequest _request;
        static double _deadline;

        // Invoke with: -executeMethod ProjectBootstrap.PackageInstaller.Install  (NO -quit)
        public static void Install()
        {
            if (PackagesToAdd.Length == 0 && PackagesToRemove.Length == 0)
            {
                Debug.Log("[PackageInstaller] Nothing to do.");
                EditorApplication.Exit(0);
                return;
            }

            Debug.Log($"[PackageInstaller] Adding: {string.Join(", ", PackagesToAdd)}");
            _request = Client.AddAndRemove(packagesToAdd: PackagesToAdd, packagesToRemove: PackagesToRemove);
            _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;
            EditorApplication.update += Poll;
        }

        static void Poll()
        {
            if (_request == null) return;

            if (!_request.IsCompleted)
            {
                if (EditorApplication.timeSinceStartup > _deadline)
                {
                    EditorApplication.update -= Poll;
                    Debug.LogError("[PackageInstaller] Timed out waiting for UPM.");
                    EditorApplication.Exit(2);
                }
                return;
            }

            EditorApplication.update -= Poll;

            if (_request.Status == StatusCode.Success)
            {
                var names = _request.Result.Select(p => $"{p.name}@{p.version}");
                Debug.Log($"[PackageInstaller] Resolved: {string.Join(", ", names)}");
                EditorApplication.Exit(0);
            }
            else
            {
                Debug.LogError($"[PackageInstaller] Failed: {_request.Error?.message}");
                EditorApplication.Exit(1);
            }
        }
    }
}
AddAndRemove
installs the whole set in a single UPM resolution pass — faster and less error-prone than one
Client.Add
per package.
Add / remove / upgrade with one script:
  • Add: list the id in
    PackagesToAdd
    .
  • Remove: list the id in
    PackagesToRemove
    .
  • Upgrade / pin: add the id with
    @<version>
    (e.g.
    com.unity.cinemachine@2.9.7
    ). Without a version, resolution picks the latest compatible release.
将以下代码写入
Assets/Editor/ProjectBootstrap/PackageInstaller.cs
。该脚本必须放在
Editor/
文件夹下(或仅编辑器程序集),因为它使用了
UnityEditor
csharp
using System.Linq;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;

namespace ProjectBootstrap
{
    // 通过PackageManager Client API安装(可选移除)一组固定包,支持无头模式。
    public static class PackageInstaller
    {
        // 编辑此列表以匹配所选包(详见references/select-packages.md)。
        static readonly string[] PackagesToAdd =
        {
            "com.unity.inputsystem",
            "com.unity.cinemachine",
            "com.unity.render-pipelines.universal",
            // "com.unity.package@1.2.3"  // 当需要最低版本时,使用@固定版本
        };

        // 可选在同一解析过程中移除包(例如不需要的模板默认包)。
        static readonly string[] PackagesToRemove = { };

        const double TimeoutSeconds = 600; // UPM解析和下载可能较慢

        static AddAndRemoveRequest _request;
        static double _deadline;

        // 调用方式:-executeMethod ProjectBootstrap.PackageInstaller.Install (请勿加-quit)
        public static void Install()
        {
            if (PackagesToAdd.Length == 0 && PackagesToRemove.Length == 0)
            {
                Debug.Log("[PackageInstaller] 无操作可执行。");
                EditorApplication.Exit(0);
                return;
            }

            Debug.Log($"[PackageInstaller] 正在添加:{string.Join(", ", PackagesToAdd)}");
            _request = Client.AddAndRemove(packagesToAdd: PackagesToAdd, packagesToRemove: PackagesToRemove);
            _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;
            EditorApplication.update += Poll;
        }

        static void Poll()
        {
            if (_request == null) return;

            if (!_request.IsCompleted)
            {
                if (EditorApplication.timeSinceStartup > _deadline)
                {
                    EditorApplication.update -= Poll;
                    Debug.LogError("[PackageInstaller] 等待UPM超时。");
                    EditorApplication.Exit(2);
                }
                return;
            }

            EditorApplication.update -= Poll;

            if (_request.Status == StatusCode.Success)
            {
                var names = _request.Result.Select(p => $"{p.name}@{p.version}");
                Debug.Log($"[PackageInstaller] 解析完成:{string.Join(", ", names)}");
                EditorApplication.Exit(0);
            }
            else
            {
                Debug.LogError($"[PackageInstaller] 失败:{_request.Error?.message}");
                EditorApplication.Exit(1);
            }
        }
    }
}
AddAndRemove
会在单次UPM解析过程中安装整个包集合——比逐个调用
Client.Add
更快且更不易出错。
通过一个脚本实现添加/移除/升级:
  • 添加:将ID列入
    PackagesToAdd
  • 移除:将ID列入
    PackagesToRemove
  • 升级/固定版本:添加带
    @<version>
    的ID(例如
    com.unity.cinemachine@2.9.7
    )。如果不指定版本,解析过程会选择最新的兼容版本。

Discovering / verifying packages

发现/验证包

To confirm an id exists or list its versions before adding it, search the registry. The in-Editor
Client.SearchAll()
/
Client.Search("<id>")
calls are also async, so they use the same poll-and-
Exit
pattern and the same headless run
as the installer. Write
Assets/Editor/ProjectBootstrap/PackageSearch.cs
:
csharp
using System.Linq;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;

namespace ProjectBootstrap
{
    public static class PackageSearch
    {
        const double TimeoutSeconds = 120;
        static SearchRequest _request;
        static double _deadline;

        // Invoke with: -executeMethod ProjectBootstrap.PackageSearch.SearchAll  (NO -quit)
        public static void SearchAll()
        {
            _request = Client.SearchAll();                 // or Client.Search("com.unity.cinemachine")
            _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;
            EditorApplication.update += Poll;
        }

        static void Poll()
        {
            if (_request == null) return;
            if (!_request.IsCompleted)
            {
                if (EditorApplication.timeSinceStartup > _deadline)
                {
                    EditorApplication.update -= Poll;
                    Debug.LogError("[PackageSearch] Timed out.");
                    EditorApplication.Exit(2);
                }
                return;
            }
            EditorApplication.update -= Poll;

            if (_request.Status == StatusCode.Success)
            {
                foreach (var p in _request.Result.OrderBy(p => p.name))
                    Debug.Log($"[PackageSearch] {p.name}@{p.versions.latestCompatible}  {p.displayName}");
                Debug.Log($"[PackageSearch] {_request.Result.Length} packages found.");
                EditorApplication.Exit(0);
            }
            else
            {
                Debug.LogError($"[PackageSearch] Failed: {_request.Error?.message}");
                EditorApplication.Exit(1);
            }
        }
    }
}
_request.Result
is a
PackageInfo[]
; each entry exposes
name
,
displayName
,
description
, and
versions
(
.latest
,
.latestCompatible
,
.all
). For a terminal-only check without the Editor (a known id, not free-text search), query the registry directly — see references/select-packages.md.
在添加包之前确认其ID是否存在或列出其版本,可搜索注册表。编辑器内的
Client.SearchAll()
/
Client.Search("<id>")
调用同样是异步的,因此它们使用与安装程序相同的轮询和
Exit
模式,以及相同的无头运行方式
。编写
Assets/Editor/ProjectBootstrap/PackageSearch.cs
csharp
using System.Linq;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;

namespace ProjectBootstrap
{
    public static class PackageSearch
    {
        const double TimeoutSeconds = 120;
        static SearchRequest _request;
        static double _deadline;

        // 调用方式:-executeMethod ProjectBootstrap.PackageSearch.SearchAll (请勿加-quit)
        public static void SearchAll()
        {
            _request = Client.SearchAll();                 // 或Client.Search("com.unity.cinemachine")
            _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;
            EditorApplication.update += Poll;
        }

        static void Poll()
        {
            if (_request == null) return;
            if (!_request.IsCompleted)
            {
                if (EditorApplication.timeSinceStartup > _deadline)
                {
                    EditorApplication.update -= Poll;
                    Debug.LogError("[PackageSearch] 超时。");
                    EditorApplication.Exit(2);
                }
                return;
            }
            EditorApplication.update -= Poll;

            if (_request.Status == StatusCode.Success)
            {
                foreach (var p in _request.Result.OrderBy(p => p.name))
                    Debug.Log($"[PackageSearch] {p.name}@{p.versions.latestCompatible}  {p.displayName}");
                Debug.Log($"[PackageSearch] 找到{_request.Result.Length}个包。");
                EditorApplication.Exit(0);
            }
            else
            {
                Debug.LogError($"[PackageSearch] 失败:{_request.Error?.message}");
                EditorApplication.Exit(1);
            }
        }
    }
}
_request.Result
PackageInfo[]
数组;每个条目包含
name
displayName
description
versions
.latest
.latestCompatible
.all
)。如果仅需在终端中检查已知ID(无需自由文本搜索)而不启动编辑器,可直接查询注册表——详见references/select-packages.md

Run it headless (direct Editor invocation, no
-quit
)

无头模式运行(直接调用编辑器,不加
-quit

Resolve the Editor binary from the version, then run it in batch mode. The script owns quitting via
EditorApplication.Exit
, so do not pass
-quit
:
bash
VERSION="<version>"          # e.g. 6000.0.47f1 (or an installed version)
PROJECT="<project-path>"
METHOD="ProjectBootstrap.PackageInstaller.Install"   # or ...PackageSearch.SearchAll
根据版本找到编辑器二进制文件,然后以批处理模式运行。脚本会通过
EditorApplication.Exit
自行退出,因此请勿传递
-quit
参数:
bash
VERSION="<version>"          # 例如6000.0.47f1(或已安装的版本)
PROJECT="<project-path>"
METHOD="ProjectBootstrap.PackageInstaller.Install"   # 或...PackageSearch.SearchAll

Install directory of that editor (Hub layout), via the unity CLI

通过unity CLI获取该编辑器的安装目录(Hub布局)

ED=$(unity editors path "$VERSION" --format json | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['path'])")
ED=$(unity editors path "$VERSION" --format json | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['path'])")

Resolve the executable per-OS (handles both "dir containing Unity.app" and the ".app" itself)

根据操作系统解析可执行文件(处理包含Unity.app的目录和.app本身)

case "$(uname)" in Darwin) if [ -d "$ED/Unity.app" ]; then UNITY_BIN="$ED/Unity.app/Contents/MacOS/Unity"; elif [[ "$ED" == *.app ]]; then UNITY_BIN="$ED/Contents/MacOS/Unity"; else UNITY_BIN="$ED/Unity"; fi ;; Linux) UNITY_BIN="$ED/Unity" ;; *) UNITY_BIN="$ED/Unity.exe" ;; # Windows (Git Bash / MSYS); use Unity.exe in PowerShell esac
"$UNITY_BIN" -batchmode -projectPath "$PROJECT" -executeMethod "$METHOD" -logFile - echo "Exit code: $?" # 0 = success, 1 = UPM error, 2 = timeout

`-logFile -` streams the Editor log (including the `[PackageInstaller]` / `[PackageSearch]`
lines) to stdout so you can watch resolution progress and read any UPM error. If
`unity editors path` output shape differs on your build, get the directory from
`unity editors --installed --format json` instead.
case "$(uname)" in Darwin) if [ -d "$ED/Unity.app" ]; then UNITY_BIN="$ED/Unity.app/Contents/MacOS/Unity"; elif [[ "$ED" == *.app ]]; then UNITY_BIN="$ED/Contents/MacOS/Unity"; else UNITY_BIN="$ED/Unity"; fi ;; Linux) UNITY_BIN="$ED/Unity" ;; *) UNITY_BIN="$ED/Unity.exe" ;; # Windows(Git Bash / MSYS);在PowerShell中使用Unity.exe esac
"$UNITY_BIN" -batchmode -projectPath "$PROJECT" -executeMethod "$METHOD" -logFile - echo "退出码: $?" # 0 = 成功, 1 = UPM错误, 2 = 超时

`-logFile -`会将编辑器日志(包括`[PackageInstaller]` / `[PackageSearch]`行)输出到标准输出,以便你查看解析进度并读取任何UPM错误。如果`unity editors path`的输出格式在你的构建版本中有所不同,可从`unity editors --installed --format json`获取目录。

Verify

验证

bash
undefined
bash
undefined

Every requested id should appear as a dependency

每个请求的ID都应作为依赖项出现

cat "<project-path>/Packages/manifest.json"

Confirm the run exited `0` and each package from the list is present in `manifest.json`. If a
package fails to resolve, `_request.Error.message` is logged; read it and check the id/version
against the registry. The Editor's own log (including the `[PackageInstaller]` lines) is the
stdout you streamed with `-logFile -` above — read it there, not via `unity logs` (which shows
the CLI's own log, not the Editor's).
cat "<project-path>/Packages/manifest.json"

确认运行退出码为`0`,且列表中的每个包都存在于`manifest.json`中。如果某个包解析失败,`_request.Error.message`会被记录;请读取该信息并检查ID/版本是否符合注册表要求。编辑器的日志(包括`[PackageInstaller]`行)是你通过`-logFile -`输出的标准输出——请在此处查看,而非通过`unity logs`(后者显示的是CLI自身的日志,而非编辑器的日志)。

Import & save headlessly (generate
.meta
files)

无头模式导入并保存(生成
.meta
文件)

After a script or tool writes new
.cs
/asset files, Unity must import them so it generates the
.meta
file each asset needs — and every
.cs
/asset MUST be committed together with its
.meta
. Merely opening the project once (
unity open "<project-path>"
) imports and generates them; use this method when you need it headless (in a script or CI).
Unlike the package installer, this is synchronous — it finishes before returning — so it's safe to run via
unity run
(its injected
-quit
is harmless; the method also calls
EditorApplication.Exit
for a clean exit code). Write
Assets/Editor/ProjectBootstrap/ProjectSaver.cs
:
csharp
using UnityEditor;
using UnityEngine;

namespace ProjectBootstrap
{
    public static class ProjectSaver
    {
        // Invoke with: -executeMethod ProjectBootstrap.ProjectSaver.SaveAll
        public static void SaveAll()
        {
            AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
            AssetDatabase.SaveAssets();
            Debug.Log("[ProjectSaver] Assets imported and saved.");
            EditorApplication.Exit(0);
        }
    }
}
bash
unity run "<project-path>" --editor-version <version> \
  -- -executeMethod ProjectBootstrap.ProjectSaver.SaveAll
当脚本或工具写入新的
.cs
/资源文件后,Unity必须导入这些文件以生成每个资源所需的
.meta
文件——且每个
.cs
/资源必须与其
.meta
文件一起提交。只需打开项目一次(
unity open "<project-path>"
)即可导入并生成这些文件;当你需要无头模式(在脚本或CI中)完成此操作时,可使用此方法。
与包安装程序不同,此操作是同步的——在返回前就会完成,因此可以安全地通过
unity run
运行(其自动添加的
-quit
参数无影响;方法也会调用
EditorApplication.Exit
以返回干净的退出码)。编写
Assets/Editor/ProjectBootstrap/ProjectSaver.cs
csharp
using UnityEditor;
using UnityEngine;

namespace ProjectBootstrap
{
    public static class ProjectSaver
    {
        // 调用方式:-executeMethod ProjectBootstrap.ProjectSaver.SaveAll
        public static void SaveAll()
        {
            AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
            AssetDatabase.SaveAssets();
            Debug.Log("[ProjectSaver] 资源已导入并保存。");
            EditorApplication.Exit(0);
        }
    }
}
bash
unity run "<project-path>" --editor-version <version> \
  -- -executeMethod ProjectBootstrap.ProjectSaver.SaveAll

Notes

注意事项

  • These editor scripts are a bootstrap convenience. Leave them in
    Assets/Editor/ProjectBootstrap/
    (they do nothing unless invoked) or delete them after setup — your call; mention it to the user.
  • All scripts live under
    Editor/
    because they use
    UnityEditor
    ; they never ship in a build.
  • Monetization / backend packages (
    com.unity.purchasing
    ,
    com.unity.services.levelplay
    , the UGS packages) install through this same mechanism, but do the actual integration via the dedicated skills: implement-in-app-purchases, levelplay-unity-integration, build-live-game.
  • 这些编辑器脚本是用于初始化的便捷工具。你可以选择将它们留在
    Assets/Editor/ProjectBootstrap/
    中(除非被调用,否则它们不会执行任何操作),或在设置完成后删除——由你决定;请告知用户这一点。
  • 所有脚本都位于
    Editor/
    下,因为它们使用了
    UnityEditor
    ;它们不会被打包到构建版本中。
  • 商业化/后端包(
    com.unity.purchasing
    com.unity.services.levelplay
    、UGS包)也通过相同机制安装,但实际集成需通过专门的技能:implement-in-app-purchaseslevelplay-unity-integrationbuild-live-game