共享 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
TUIscripts/build/tui.mjs已准备的 TUI 工作区
Dashboardscripts/build/web.mjs已准备的 web 工作区和生成的图标
桌面 UIscripts/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图标和桌面 UIapps/desktop/dist/ 和 apps/desktop/release/,不带智能体载荷
Docker图标、TUI、web、智能体智能体在 /opt/hermes,依赖在 .venv,PM 在 pm-runtime,生成的命令在 libexec
Nix TUITUI$out/lib/hermes-tui/{dist/entry.js,package.json}
Nix webweb$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
TermuxTUI 和智能体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 和缓存的 sdist src/ 树(含 Rust target/ 输出)。提供商缓存不变;原生签名到达解压的二进制,而不扫描仅构建产物。
  • 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 设备验收。本文档不衍生任何构建通过声明。