共享 bundle 构建
Hermes 把依赖准备、产品构建和分发包打包分开。编译器和智能体组装接口位于
scripts/build/README.md。这些是当前接口,并不证明每个分发都通过了原生验收。
提供商、产品与分发 {#providers-products-and-distributions}
| 层 | 职责 | 实现 |
|---|---|---|
| 依赖提供商 | 准备工具、Python 环境、JavaScript 依赖和原生绑定 | PM(pm.build_environment)、scripts/build/node-deps.mjs、Nix 和 Termux |
| 产品构建器 | 编译图标/TUI/web/桌面 UI,或从已准备输入组装一个可运行智能体 | scripts/generate_icons.py 和 scripts/build/ |
| 分发适配器 | 选择产品并为其目标打包 | scripts/bundles/、Dockerfile、nix/ 和 scripts/termux/ |
提供商保留各自的包管理器和锁。产品构建器不安装缺失依赖,也不下载输入。Node 依赖提供商使用一个锁定的工作区并集。Nix 提供自己的 npm 和 Python 输入。Termux 提供 bionic 工具和 wheel。共享配方并不意味着跨目标字节、依赖选择或 Python 环境相同。
| 产品 | 实现 | 输入 |
|---|---|---|
| 图标 | scripts/generate_icons.py | 美术资源和一个带运行时依赖的 Python |
| TUI | scripts/build/tui.mjs | 已准备的 TUI 工作区 |
| Dashboard | scripts/build/web.mjs | 已准备的 web 工作区和生成的图标 |
| 桌面 UI | scripts/build/desktop.mjs | 已准备的桌面工作区、图标、戳记和原生绑定 |
| 可运行智能体 | scripts/build/agent.py | 代码、解释器、依赖、独立 PM 运行时、资源和所选前端 |
桌面 UI 编译器不依赖 dashboard。一个 bundle 桌面选择带 TUI 和 dashboard 产品的智能体。一个轻量桌面不选择智能体载荷。
构建与打包入口 {#build-and-packaging-entrypoints}
源码更新和源码 UI 启动使用同一套依赖提供商和产品构建器。hermes_cli/source_build.py 选择工作区并集,准备一次,再调用共享配方。一次更新会构建 TUI 和 web,外加更新前存在的本地桌面应用(若有)。桌面打包仍属于本地桌面适配器。
当前 checkout 重试、拉取的 checkout 和 ZIP 更新路径都用 update_cmd_maint._prepare_updated_checkout:一次 PM 依赖同步,再在所选解释器上起一个新进程做前端构建。构建失败中止完成;一个既有的陈旧产品不算成功更新。没有更新器专属的 npm 缓存、回退安装、额外刷新或记忆提供商重装。PM 拥有完整的 Python 并集。
从发布标签处的干净 checkout 构建桌面分发。以一个能运行入口和 Git 的主机 Python 起步;准备阶段通过 PM 获取锁定的 Python、Node 和 npm。原生编译器/SDK 仍是主机输入:
python -m scripts.bundles.desktop --tag=vX.Y.Z
不带 --prepared 时,desktop.py 要求 --tag 或 --commit 恰好其一。commit 构建要求完整 commit SHA 和匹配的 checkout HEAD。--variant 接受 bundled、store 或 light,默认 bundled。--repo 选择 checkout。-- 之后的参数传给已准备的 Electron Builder 包装器;它们不能替换准入的目标、工具、输出或打包配置。
驱动先准备完整依赖集:托管工具、锁定的 Node 工作区并集、图标环境、Electron 原生绑定、打包工具,以及(light 除外)应用和独立 PM 环境。然后才编译产品、组装载荷并打包 Electron。MSIX 元数据、签名、公证和包格式仍是适配器工作。
拆分桌面准备与消费 {#split-desktop-preparation-and-consumption}
单命令构建和 CI 使用同一准备操作。要在产品编译和打包前停下:
python scripts/bundles/desktop.py --tag vX.Y.Z --variant bundled --prepare-only \
--work "$PWD/.build/desktop-job" --cache "$PWD/.cache/desktop-inputs"
python scripts/bundles/desktop.py --prepared "$PWD/.build/desktop-job/prepared.json"
--work 和 --cache 是独立的、归构建所有的目录。准备阶段占用工作目录本身;不要提前创建它。它在每个提供商成功后最后写入 prepared.json。该文件含本作业的绝对路径和源码身份,不是可移植的缓存回执。移动 checkout、更改源码或锁、或丢失输入后要重新准备。重复准备可能复用准入的依赖字节,同时重建绑定路径的环境。
--prepared 不接受新的 tag/commit/work/cache 参数。它校验干净 checkout 和已准备输入,然后在输入陈旧或缺失时失败,而非安装它们。bundled 和 Store 构建可为一个稳定标签消费同一份准备;light 和 commit 构建不能切换到 Store。产品构建每次仍运行。原生 staging 向该组合暴露 prepare_native 和 finish_native;hermes pm bundle 仍是完整的原生 staging 命令,不是桌面准备接口。
Windows 和 macOS 发布作业使用restore → prepare → save → build。cache 动作推导提供商路径并传输候选;它既不创建准备工作目录,也不宣告缓存命中有效。save 在准备成功后、产品编译或签名之前运行。失败的准备在本地留下已完成的提供商数据,但不保存整体快照。通用的 setup-pm 动作对其他工作流仍可用。
原生 wheel 缓存在 python/runtime 下使用紧凑的 128 位分区,按目标和观察到的编译器/SDK 输入为键。缺失身份仍获得新分区。uv 在该缓存内构建源码分发,因此分区名必须在 Windows MAX_PATH 之下为嵌套编译器输出留余地。自定义 --cache 根也要保持短;开启 OS 长路径并不会让每个原生编译器都具备长路径感知。该布局保留共享缓存传输和所有目标兼容的 extras,包括 Silk。
严格边界禁止的是消费期间获取依赖,而非一切网络访问:签名、时间戳、公证和发布保留其在线职责。仍需要一个断网的未签名原生构建,在每个发布目标上证明该边界。
staging 一个带两种前端产品、但不做 Electron 打包的原生智能体:
python -m scripts.bundles.stage --out /work/agent-payload --ref HEAD
若未提供产品,该命令会准备并构建 TUI/web 产品。它也同时接受 --tui 和 --web 作为已准备产品根。它拒绝只带其中一个参数的选择。独立产品在构建器 README 中有明确的输入/输出契约。
--ref 选择智能体源码快照。自动前端编译使用同一修订的临时快照。显式前端产品必须来自所选修订。
hermes pm bundle --out /work/agent-payload --ref HEAD staging 原生工具、应用环境和智能体。该 PM 命令不构建前端。npm run payload --workspace apps/desktop 使用包含它们的 staging 驱动。两条命令都不创建 Electron 安装器。
拆分 PM Bundle 准备与组装 {#split-pm-bundle-preparation-and-assembly}
PM Bundle 使用同一套隔离工具 bootstrap 和原生准备切片,但不安装桌面工作区、图标、Electron 绑定或打包器:
python -S -B scripts/bundles/native_build.py --source "$PWD" \
--work "$PWD/.build/payload-job" --cache "$PWD/.cache/payload-inputs" \
--out "$PWD/build/agent-payload" --ref HEAD --prepare-only
python -S -B scripts/bundles/native_build.py \
--prepared "$PWD/build/agent-payload.prepared.json"
使用所选修订处的干净 checkout。--ref 默认为 HEAD;--commit 接受精确完整 SHA。work、cache 和 output 必须分开;准备阶段占用一个此前不存在的工作目录。不带 --prepare-only 时,驱动还会组装。--prepared 不接受新的选择或路径参数,并在组装前校验原生回执;它绝不 bootstrap 或修复依赖。回执和锁是输出兄弟文件(agent-payload.prepared.json、agent-payload.prepare.lock),不随产物发布。组装如 hermes pm bundle 一样刻意不提供前端产品。
既有 cache 动作上的 payload-test 生产者只选 tools、python/runtime 和 native。它排除源码 node_modules、npm 缓存、图标环境和打包器。PM 的托管工具 bootstrap 仍含 Node 和 npm,但不运行桌面 npm ci。原生编译器身份和 Windows ARM64 前置条件复用同一归属方一次。缓存命中仍要经过提供商准入;原生 SDK/编译器前置条件仍属主机。PR 作业只 restore;save 要求在默认分支受信流水线准备成功,且无备选源码 ref。不新增签名或发布。
对 Termux,工具和 wheelhouse 是前置条件:
python scripts/termux/build.py --repo /work/source --payload /work/termux-payload \
--out /work/packages --tag vX.Y.Z
Termux 驱动接受 --tag 或 --commit 恰好其一。它准备 TUI 工作区,调用共享 TUI 构建器,并把该产品传给 build_deb.sh。它不添加 dashboard,也不替换 bionic 准备。
CLI 契约位于 scripts/bundles/desktop.py、scripts/bundles/stage.py、pm/cli.py 和 scripts/termux/build.py。
共享智能体与启动器契约 {#shared-agent-and-launcher-contract}
AgentInputs 提供显式路径和一种放置模式:
contained把运行时输入保留在原生载荷内。fixed保留 Docker 或 Termux 安装前缀。references把已安装代码和依赖保留在独立的 Nix store 路径中。
组装器从 [project.scripts] 推导控制台命令。它复制源码布局资源和所选前端,或链接显式引用输入。它在结构组装之后写完成清单。Nix 还为其原生包装器接收一个命令/环境映射。
启动器实现位于 scripts/build/launchers.py。launcher_wrapper.py 和 mint_launchers.py 在同一目录。scripts/bundles/payload.py 负责 git 快照、PM 工具事实、PM 运行时封缄和可移植链接处理。它不再负责前端放置或启动器生成。
桌面戳记携带声明的启动路径。Electron 消费该契约,而不接管或修复载荷。非 bundle 构建不携带占位智能体载荷。Store 包声明 microsoft-store 为其更新机制。侧载 bundle 声明 app-installer。
Windows 启动器铸造运行载荷解释器。POSIX 启动器使用显式的解释器、源码和依赖路径。结构组装不替代目标原生的启动测试。单凭一个清单不能证明一个被迁移或签名的产物可运行。
独立 PM 运行时 {#independent-pm-runtime}
PM 依赖来自 pm/pyproject.toml 和 pm/uv.lock,而非应用依赖图。应用环境和 pm-runtime 是智能体组装的独立输入。
原生和 Docker 准备使用 pm.runtime_stage.stage_runtime。Termux 用该函数加其离线 wheelhouse。Nix 使用独立的 nix/pm-runtime.nix derivation。原生封缄在 pm-runtime.json 中记录载荷解释器和仅 PM 的 site 目录。
驻留 PM worker 用 -I -S -B 运行声明的解释器。它们在 worker 脚本之前只添加声明的 PM 依赖目录。它们不借用应用依赖,也不把 PM 包加进调用方解释器。Docker 和 Nix 戳记指向其声明的 PM 运行时。这些契约位于 pm/runtime.py:55–88,171–182 和 scripts/bundles/payload.py:58–90。
普通原生 staging 不扫描用户插件树。它在一个带临时 home 和 PM 根、外加一个显式持久构建缓存的子进程中运行 provisioning。Windows ARM64 前置准备在 HOME 隔离之前运行,并使用与源码 setup 和 CI 相同的提供商。运行中的 PM 保留插件准入、世代选择和事务状态。两条路径都用 pm.environment.PythonEnvironment 做显式 uv 环境构建。
分发边界与输出路径 {#distribution-boundaries-and-output-paths}
| 分发 | 所选产品 | 当前输出布局 |
|---|---|---|
| 桌面 bundled/Store | 图标、TUI、web、智能体、桌面 UI | 产品:apps/desktop/build/products/。智能体:apps/desktop/build/agent-payload/。UI:apps/desktop/dist/。包:apps/desktop/release/ |
| 桌面 light | 图标和桌面 UI | apps/desktop/dist/ 和 apps/desktop/release/,不带智能体载荷 |
| Docker | 图标、TUI、web、智能体 | 智能体在 /opt/hermes,依赖在 .venv,PM 在 pm-runtime,生成的命令在 libexec |
| Nix TUI | TUI | $out/lib/hermes-tui/{dist/entry.js,package.json} |
| Nix web | web | $out/index.html 和资源 |
| Nix 智能体 | 带 TUI/web 引用的智能体 | $out/bin、$out/share/hermes-agent、$out/ui-tui、manifest.json 和 command-map.json |
| Nix 桌面 | 图标和桌面 UI,连同 Nix 智能体 | $out/share/hermes-desktop 和 $out/bin/hermes-desktop |
| Termux | TUI 和智能体 | OUT/hermes-agent_<version>_aarch64.deb,安装于 $PREFIX/lib/hermes-agent/ |
原生载荷含 hermes-agent、tools、venv、pm-runtime、bin、uv-cache 及其清单/事实。复制的前端资源位于 hermes-agent/hermes_cli/tui_dist/ 和 hermes-agent/hermes_cli/web_dist/。TUI 资源目录含 entry.js 和模块模式包元数据。
Docker 把既有 TUI 产品保留在 /opt/hermes/ui-tui,web 输出在 /opt/hermes/hermes_cli/web_dist。组装器还在 hermes_cli/tui_dist 下种植其共享 TUI 资源布局。venv 命令符号链接保留 s6 和降权垫片所用的路径。
Docker 前端阶段负责 npm 依赖和编译。运行时阶段只复制前端产品和锁定的 TypeScript 包用于运行时 lint。Node/npm 和 Photon 单独安装的 sidecar 依赖仍是运行时输入。Python 依赖准备先于应用源码复制。实际的缓存命中和层大小声明需要构建证据。
Nix 保留 importNpmLock、uv2nix、平台覆盖和 store 引用。其构建器在 derivation 构建期间运行,而非求值期间。按产品的源码过滤器把前端、Python 和资源输入分开。Nix 包装器消费组装器的命令映射,并保留各自的 PATH 和 extra-Python 冲突策略。
Nix wheel 策略: nix/python.nix:128–135 仅对 Hermes derivation 设置 HERMES_NIX_BUILD=1。setup.py:34–72 拒绝通用的 Hermes wheel/sdist 构建。共享组装器使用已安装的 Nix 代码,不再另存一份。其他提供商使用源码布局代码和元数据,而非公开的 Hermes wheel。
Termux 保留 bionic wheel 编译、离线安装、原生库路径和其固定前缀。其安装根含 app、tools、runtime-libs、venv、pm-runtime、bin 和清单/事实。APT 维护者钩子管理 $PREFIX/bin 下声明的 CLI 符号链接,并拒绝外来冲突。它们不在安装期间编译依赖。
stage_apt_repo.py 负责仓库元数据和签名。稳定和 canary 套件为 hermes-stable 和 hermes-canary。包文件先于签名元数据发布。稳定发布准入跨分发协调验收和发布。
公开产物交接 {#public-artifact-handoffs}
桌面发布工作流在全新原生 runner 上下载并安装每种受支持的 bundle 格式,然后运行与 install-e2e 相同的 composer/提供商/回复检查。Windows 通用组装在冒烟之前 staging 字节;其 canary feed 仅在两条原生冒烟矩阵都通过后单独发布。macOS feed 发布和稳定候选验收同样设门槛。所覆盖格式、检查点证据以及 Store/Linux/不上传限制,见安装与聊天验收。
scripts.releases.handoff fetch --public-base URL 无需 R2 凭据下载一个已 staging 的产物。提供 --tag TAG --commit SHA 或 --commit-build SHA 之一,外加生产者 --name、目的地 --root 和可选 --include 选择器。它使用与已认证交接相同的回执身份、路径、大小和 SHA-256 检查。带标签和仅 commit 的归档保持分开;该命令不解析可变的 latest feed。
公开读取要求 HTTPS,环回 fixture 服务器除外。它们拒绝 URL 凭据、不安全路径和重定向。不完整或损坏的下载不能替换已验证的既有目的地。回执文件仅在所有所选产物验证后才保存。选择某一种桌面格式时,仍必须在安装前拒绝缺失或歧义匹配。
缓存归属 {#cache-ownership}
PM 二进制下载归档和原生 uv wheel 缓存有不同的消费者。它们不是可互换的清理对象。
- PM 保留已完成的下载,直到包验证和发布。成功安装移除其确切的 fetch 归档。失败保留下载以备重试。清理不碰无关的半成品。
- 原生 staging 只在其归构建所有的工具存储中清理过时条目。它不清理用户机器范围的存储。
- 原生 staging 保留
uv-cache/用于离线可变环境重建。它复制解压的 wheel 和索引/修订元数据,但在复制前排除冗余的 wheel ZIP 和缓存的 sdistsrc/树(含 Rusttarget/输出)。提供商缓存不变;原生签名到达解压的二进制,而不扫描仅构建产物。 - PM-runtime 和应用构建共享提供商的持久 uv 缓存。CI restore/save 该缓存,而非临时构建 HOME。失败构建保留已完成的 wheel。通用 PM staging 用其 v2 缓存命名空间;桌面准备用自己的输入快照命名空间。普通
uv cache prune移除悬空条目,而不丢弃离线 wheel 输入。它不移除所有历史版本,也不强制大小上限。 - 源码构建在其运行时解释器上渲染图标。桌面准备和产品 staging 没有解释器,在作业工作目录下准备锁定的运行时依赖(不含应用);桌面的 wheel 缓存是
CACHE/python/build。该环境不进入随发布的运行时。 - 前端
node_modules是提供商输入,不是前端产品。Docker 的运行时 TypeScript 和 Photon 选择是单独的例外。
原生缓存行为见 scripts/bundles/native.py。从载荷中移除所有缓存可能破坏离线环境重建。
桌面传输通过 scripts/ci/desktop_build_cache.py 选择 PM 工具条目、Python 下载/wheel 缓存、npm 内容寻址下载、已准备工作区依赖和原生/打包工具输入。它不选择工作目录、虚拟环境、运行中的用户 home、签名 token 或产品。签名结果缓存是分开的。原生 wheel 分区依赖观察到的编译器/SDK/OpenSSL 身份;不完整的身份刻意放弃热复用。这不是完全锁定的主机 SDK。
缓存键是查找提示,不是授权。原生构建器有分开的发布和 commit 执行作业,分别字面上使用 cache-mode: write 和 cache-mode: read。GitHub 在范围缓存 token 上强制这些权限,因此 commit 构建即使从自己的脚本也不能保存缓存。两个分支通过 YAML 锚共享矩阵、环境和步骤。原始 build-win32 和 build-darwin ID 聚合这些分支:成功验证和输入归档后,恰好所选分支必须成功,另一个必须被跳过。所选构建失败、取消或意外跳过都会使聚合失败;它不能允许发布。既有发布依赖继续使用这些 ID。一次跳过的 save 或不同的键前缀本身不能阻止一个脚本污染写入者命名空间。
默认分支的 canary 调度保留默认分支缓存范围;更改 checkout SHA 不改变工作流 ref 的缓存范围。见 GitHub 的缓存访问契约。
锁定二进制输入 {#pinned-binary-inputs}
python -m scripts.ci.archive_inputs 保留 pm/lock.json 中所有跨目标的 HTTPS 产物,外加 Termux 运行时库和许可证锁定。pm/artifact-mirror.json 负责公开镜像位置。对象键为 upstream/sha256/HASH,与文件名和发布标签无关。
CI 先请求 R2。只有 404 才允许上游下载。它在一次不可变的 If-None-Match: * 上传前校验既有 SHA256,再下载并校验存储的对象。损坏字节、拒绝访问和上传失败都会中止构建。并发写入者可复用相同字节,但不能替换既有对象。
全目标工作流在 main 上锁变更、手动调度以及作为准入的发布前置时运行。它在锁定工具链可用前使用 runner Python。桌面作业依赖该前置,然后让 PM 获取已验证输入而无需归档写凭据;它们不需要第二条缓存播种流水线。其他受保护工作流可通过 setup-pm 的 archive-inputs 输入选择加入归档。定向归档可用 --target 和 --store 播种一次性 fetch 条目;Termux 还传 --payload 用于运行时库。缓存命中不跳过保留。桌面 R2 凭据范围限于上传步骤。不受信 PR 作业不获得发布凭据。
已安装的 PM 客户端、bootstrap 安装器和 Nix 锁消费者在下载不可用时使用主 URL,后跟公开镜像。锁定哈希始终有约束力;客户端不需要凭据或上传。PM 保留按源的断点续传状态并报告尝试过的 URL。对于上游已移除的 Termux 文件,CI 发布者在上游 404/410 后可从社区 Internet Archive 恢复精确字节。它绝不重新锁定到最新包。
该归档不得有过期生命周期规则。发布清理不覆盖其前缀。这覆盖 PM 二进制锁和 Termux 运行时输入,不覆盖未锁定的 apt 包、OCI 镜像、语言包注册表,或由其构建工具独立下载的原生 Electron/SDK 归档。
验证边界 {#verification-boundary}
下面的检查区分产品测试与分发验收。签名安装器和 Android 设备执行需要各自的原生 runner。
macOS dmgbuild 获取归属是打包准备的一部分,不是严格构建。原生 DMG 创建、分离诊断、签名和公证必须在工具提供商变更后演练。Windows x64/ARM64 和 macOS x64/ARM64 冷/热及签名产物验收与 Linux 辅助测试分开。Linux 桌面发布流水线仍禁用。
聚焦的辅助测试不能确立以下全部要求:
- 从已准备不可变输入做离线前端编译。
- 独立 TUI 交互和 dashboard 后端/资源行为。
- 每个目标上的原生 Electron 绑定和最终签名启动器。
- 原生载荷迁移后的 PM 隔离。
- 保留缓存的可变应用环境离线重建。
- Docker 非 root CLI/TUI/web/浏览器行为和每个镜像层的内容。
- 经引用包装器的真实 Nix 包执行。
- 全新断网 Termux 包安装和 Android 设备行为。
使用 tests/install/BUNDLED_UPDATES.md、tests/docker/、Nix 检查以及 scripts/termux/check_deb.sh 加 validate_installed.py 中既有的验收检查。容器验收不能替代 Android 设备验收。本文档不衍生任何构建通过声明。