425 lines
23 KiB
Markdown
425 lines
23 KiB
Markdown
# Shared product builders
|
||
|
||
These modules compile frontends and assemble a runnable agent from prepared
|
||
inputs. They do not replace npm, uv, PM, or Nix.
|
||
[Shared bundle builds](../../docs/shared-bundle-builds.md) describes the
|
||
providers and distribution adapters.
|
||
|
||
This reference describes the current interfaces, not completed artifact
|
||
acceptance. A successful parser or helper test does not prove a distribution
|
||
build or a target-native runtime.
|
||
|
||
## Ownership
|
||
|
||
| Layer | Owns | Examples |
|
||
|---|---|---|
|
||
| Dependency provider | Tools, locked dependencies, native libraries and build environments | `node-deps.mjs`, PM Python operations, Nix `importNpmLock`/uv2nix, Termux wheelhouse |
|
||
| Product builder | Compilation or application assembly from prepared inputs | `tui.mjs`, `web.mjs`, `desktop.mjs`, `agent.py`, `../generate_icons.py` |
|
||
| Distribution adapter | Product selection, target preparation, package layout, signing and publication | `../bundles/`, `../../Dockerfile`, `../../nix/`, `../termux/` |
|
||
|
||
`node-deps.mjs` and `pm.build_environment` prepare dependencies. Unlike the product
|
||
builders, they can access package registries. There is no universal installer,
|
||
all-products dispatcher, or cross-platform Python environment.
|
||
|
||
## Complete desktop preparation
|
||
|
||
`../bundles/desktop.py` composes these providers for local and CI packaging:
|
||
|
||
```sh
|
||
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"
|
||
```
|
||
|
||
Start from a clean checkout at that tag (or use `--commit FULL_SHA`). A host
|
||
Python and Git bootstrap preparation; PM selects the pinned tools. Native
|
||
compiler/SDK prerequisites are still platform-specific. Omitting `--prepare-only`
|
||
runs both phases. Defaults use the same `.build/desktop-job` and
|
||
`.cache/desktop-inputs` roots. Do not pre-create the work directory: preparation
|
||
claims it and publishes its job-local path selection only after success.
|
||
|
||
Preparation covers the exact workspace union, icon Python environment, runtime
|
||
and independent PM dependencies, Electron-native bindings, Electron archive and
|
||
selected packaging utilities. Light omits the runtime and TUI/web union.
|
||
Compilation and packaging consume those inputs, refusing missing/stale inputs
|
||
rather than acquiring replacements. The result is tied to this source revision,
|
||
target and absolute paths; it must not be restored as an authoritative CI cache.
|
||
Stable bundled/Store variants can share it. Product compilation still reruns.
|
||
|
||
The desktop cache action derives reusable paths from provider declarations and
|
||
saves them after preparation, before compilation/signing. It excludes job-local
|
||
environments and products. Native wheel reuse additionally depends on measured
|
||
compiler/SDK inputs. Signing-result caches remain separate. Strict dependency
|
||
consumption is not offline signing: timestamps, notarization and publication can
|
||
still require network access. Native unsigned network-denied packaging and final
|
||
signed launch acceptance are distinct verification gates.
|
||
|
||
## Prepared JavaScript workspace
|
||
|
||
Run commands from the repository root. Replace the absolute example paths with
|
||
build-owned paths.
|
||
|
||
```sh
|
||
node scripts/build/node-deps.mjs --source /work/source \
|
||
--workspace ui-tui --workspace web
|
||
```
|
||
|
||
`--source` and at least one `--workspace` are required. A workspace can be its
|
||
locked path or package name. The provider deduplicates the selection and runs
|
||
one root `npm ci` operation. It includes root dependencies, development
|
||
dependencies, and optional dependencies. It checks Node/npm against the root
|
||
`engines` declarations. npm lifecycle scripts remain enabled.
|
||
|
||
The prepared source needs these inputs:
|
||
|
||
- Root `package.json` and `package-lock.json`.
|
||
- Selected workspace manifests and their required `file:` dependency sources.
|
||
- Source files and configuration for the selected product.
|
||
- Resolved dependencies in workspace-local or root `node_modules`.
|
||
|
||
Request the complete workspace union once. A later, narrower `npm ci` can
|
||
remove dependencies that another product needs. This provider modifies the
|
||
prepared workspace. It does not build a frontend.
|
||
Nix supplies dependencies through `importNpmLock` instead of this command.
|
||
|
||
`--reuse` opts into reusing a completed dependency install. Desktop bundles use
|
||
this with CI's cached `node_modules` tree. The receipt lives inside that tree
|
||
and matches the lockfile, local package manifests, project npm configuration,
|
||
Node/npm versions, OS/architecture, and exact workspace union. It also checks
|
||
npm's installed-tree lock and the presence of its recorded package directories.
|
||
A missing or mismatched receipt runs a clean `npm ci`; failed installs cannot
|
||
leave a reusable receipt. Omit `--reuse` to force a clean dependency install.
|
||
Source launchers add `--no-install` when PM's lazy-install policy is disabled.
|
||
That mode still reuses a matching completed receipt, but rejects stale or missing
|
||
dependencies before mutating the tree. Explicit build/update operations may install.
|
||
The receipt does not validate arbitrary edits inside installed packages and
|
||
never skips product compilation. CI saves the prepared tree before packaging
|
||
can mutate it, and before unrelated build/signing failures can discard it.
|
||
|
||
## Frontend products
|
||
|
||
```sh
|
||
node scripts/build/tui.mjs --source /work/source --out /work/products/tui
|
||
node scripts/build/web.mjs --source /work/source \
|
||
--icons /work/products/icons --out /work/products/web
|
||
node scripts/build/desktop.mjs --source /work/source \
|
||
--icons /work/products/icons --stamp /work/install-stamp.json \
|
||
--native-deps /work/native-deps --out /work/products/desktop
|
||
```
|
||
|
||
| Builder | Required CLI arguments | Additional CLI arguments | Product contents |
|
||
|---|---|---|---|
|
||
| `tui.mjs` | `--source`, `--out` | None | `dist/entry.js` and `package.json` with `type: module` |
|
||
| `web.mjs` | `--source`, `--out`, `--icons` | None | `index.html`, Vite assets, and public assets |
|
||
| `desktop.mjs` | `--source`, `--out`, `--icons`, `--stamp`, `--native-deps` | `--typecheck`, `--platform` | Renderer assets, `electron-main.mjs`, `electron-preload.js`, and native `node_modules` |
|
||
|
||
Each output is the product directory itself. The desktop output is a `dist`
|
||
directory, not an application package. The exported functions are `buildTui`,
|
||
`buildWeb`, and `buildDesktop`. They return output paths and publish the build-input
|
||
receipt described below.
|
||
|
||
Each compiler publishes `hermes-build.json` inside its output (inside `dist/`
|
||
for TUI). `freshness.mjs` owns this receipt and all source input selection.
|
||
TUI inputs are its source tree, the Ink source alias, shared sources, their
|
||
manifests and TypeScript configuration, dependency locks, and its compiler and
|
||
shared compiler helpers. Tests, workspace documentation, dependency-provider
|
||
recipes, and other products' compiler recipes do not invalidate the TUI.
|
||
It records product/host identity, content hashes of workspace/shared sources and
|
||
build inputs, and the exact supplied icon directory, desktop install stamp, and
|
||
native-dependency tree. Inputs are checked again before publication: a concurrent
|
||
input change fails the build and preserves the previous output. Output validation
|
||
checks renderer/main/preload/public bytes and the native file inventory; native
|
||
bytes may change through signing after compilation. Native ABI verification remains
|
||
with the native provider and desktop compiler.
|
||
|
||
Source launchers query this owner without provisioning tools:
|
||
|
||
```sh
|
||
node scripts/build/freshness.mjs --source /work/source --product web --out /work/products/web
|
||
node scripts/build/freshness.mjs --source /work/source --product tui --out /work/products/tui/dist
|
||
```
|
||
|
||
The result is a JSON boolean. Missing receipts, changed inputs (including supplied
|
||
inputs outside source), missing prepared trees, or damaged outputs are stale.
|
||
Receipts describe a source build, not a portable dependency cache; immutable
|
||
distributions use their existing prebuilt launch path instead. They replace the
|
||
old Python per-profile hashes and TUI mtime lists, not PM's dependency receipts.
|
||
|
||
The compilers resolve modules from the supplied workspace. They do not run
|
||
npm, uv, PM installation, or icon preparation. TypeScript/Vite scratch files
|
||
stay outside source inputs. Compilation uses a private output directory next
|
||
to the destination. A successful compile replaces the destination. A failed
|
||
compile leaves the previous product in place and reports failure. Its presence
|
||
alone does not prove that the latest build succeeded.
|
||
|
||
Existing arbitrary output directories require the builder's `.hermes-product`
|
||
marker. Files, symlinks, and source directories are rejected. The exact npm
|
||
destinations (`ui-tui/dist`, `hermes_cli/web_dist`, `apps/desktop/dist`, and
|
||
`apps/desktop/build/native-deps`) remain rebuildable without a prior marker.
|
||
Other in-tree products live beneath `.build/` or `apps/desktop/build/products/`.
|
||
`frontend-common.mjs` classifies these destinations independently of which source
|
||
children already exist, so a warm desktop rebuild uses the same rule as a fresh
|
||
build. Explicit stamp, icon, native-tree and dependency inputs remain protected,
|
||
even when they live beneath a generated destination.
|
||
|
||
### Icons and native inputs
|
||
|
||
`--icons` names the generator's output root, not a directory of loose icons.
|
||
The web builder reads `web/public/` beneath it and requires `favicon.ico`.
|
||
The desktop builder reads `apps/desktop/public/` beneath it and requires
|
||
`apple-touch-icon.png`.
|
||
|
||
Run the generator with a Hermes runtime Python (Pillow and resvg-py are core
|
||
dependencies):
|
||
|
||
```sh
|
||
python scripts/generate_icons.py --source /work/source --out /work/products/icons
|
||
python scripts/generate_icons.py --source /work/source --out /work/products/icons --check
|
||
```
|
||
|
||
The generator reads artwork from `SOURCE/assets` and writes its declared paths
|
||
beneath `OUT`. These paths also include desktop packaging, website, and
|
||
bootstrap-installer assets. `--check` regenerates targets in memory and checks
|
||
output image properties. It does not compare output bytes with regenerated bytes.
|
||
The generator writes targets directly, not through the frontend
|
||
publication helper.
|
||
|
||
The convenience wrapper runs the same generator on `HERMES_PYTHON` (else
|
||
`python` on PATH) and never installs anything:
|
||
|
||
```sh
|
||
node scripts/generate-icons.mjs --source /work/source --out /work/products/icons
|
||
```
|
||
|
||
Both icon commands accept `--check`. Without explicit paths, they use the
|
||
source checkout as the output root. Builders without a runtime environment
|
||
prepare one with `scripts/build/icon_environment.py`.
|
||
|
||
The desktop native tree contains prepared packages, including `node-pty` with
|
||
its compiled binding. macOS also requires `get-windows/main`. The provider owns
|
||
the architecture and Electron ABI match. `--platform` defaults to the running
|
||
Node platform and controls native-file checks. It does not cross-compile a
|
||
binding. `--typecheck` enables the desktop renderer TypeScript check and defaults
|
||
to false. The web builder always runs its TypeScript project check.
|
||
|
||
The supplied install stamp controls the desktop main/preload build identity.
|
||
The compiler does not create an install stamp, build the dashboard, package
|
||
Electron, or sign native files.
|
||
|
||
### Source-development entrypoints
|
||
|
||
Existing npm commands use these recipes:
|
||
|
||
```sh
|
||
npm run build --workspace ui-tui
|
||
npm run build --workspace web
|
||
npm run build --workspace apps/desktop
|
||
```
|
||
|
||
| Command | Output | Preparation outside the product compiler |
|
||
|---|---|---|
|
||
| TUI build | `ui-tui/dist/entry.js` | Existing installed workspace dependencies |
|
||
| Web build | `hermes_cli/web_dist/` | npm `prebuild` prepares icons |
|
||
| Desktop build | `apps/desktop/dist/` | Icons, root-install assertion, install stamp, and native-dependency staging |
|
||
|
||
Compositions prepare icons once and pass `npm run build -- --icons /prepared/root`
|
||
to the desktop's source-development driver. It copies prepared packaging artwork
|
||
and passes the same root to the compiler. A standalone `npm run build` still
|
||
prepares its own icons. Source desktop launch runs the already-prepared Electron
|
||
binary directly; `--skip-build` does not provision Node, npm, or Electron.
|
||
|
||
TUI and web scripts support a no-argument development mode. Explicit product
|
||
mode requires the arguments in the earlier table. The desktop product script
|
||
has no no-argument mode. The Node product parsers expose no `--help` flag.
|
||
|
||
## Python dependency provider
|
||
|
||
```sh
|
||
python -m pm.build_env --source /work/source \
|
||
--python /work/tools/python --out /work/venv --sealed \
|
||
--extra all --extra messaging
|
||
```
|
||
|
||
| Argument | Contract |
|
||
|---|---|
|
||
| `--source`, `--out` | Required prepared source and fresh environment destination |
|
||
| `--python` | Optional build interpreter; otherwise PM selects its pinned Python |
|
||
| `--cache` | Optional build cache directory; otherwise PM selects its cache |
|
||
| `--group` | Repeatable build/test dependency-group selection |
|
||
| `--sealed` | Prune build-time editable and virtualenv marker `.pth` files |
|
||
| `--extra` | Repeatable extra selection |
|
||
| `--all-extras` | Select all extras instead of `--extra` |
|
||
| `--no-install-project` | Exclude the root application install, but retain workspace-member installation |
|
||
| `--offline` | Prohibit uv network access. Required artifacts must already be available |
|
||
|
||
`OUT` must not exist. Failure removes this invocation's environment, not a
|
||
pre-existing environment. Success prints its Python executable. The CLI is an
|
||
explicit build request. The Python
|
||
function `pm.build_environment` accepts the same semantic inputs and an optional
|
||
explicit build `env` mapping. It returns the same executable as a `Path`.
|
||
|
||
PM owns pinned installer acquisition, environment creation, frozen workspace
|
||
sync, dependency checks, and failure cleanup. Callers never resolve or pass a uv
|
||
executable. It preserves project policy, uses the supplied interpreter, and
|
||
disables interpreter downloads. It does not discover user plugins or publish a
|
||
live PM selection. Nix retains its declarative dependency provider. Termux builds
|
||
native wheels separately, then uses PM's requirements-environment operation with
|
||
an explicit bionic interpreter and an offline wheelhouse. The build cache remains
|
||
available after this call.
|
||
|
||
Other build adapters use the same command with `--requirements FILE` (or repeated
|
||
`--requirement SPEC`) for a caller-owned dependency list, `--manager-runtime` for
|
||
the independent PM graph, `--check-lock` for non-mutating CI lock validation, and
|
||
`--export-requirements FILE` for marker-preserving frozen export. Cache teardown
|
||
uses `python -m pm.build_env --prune-cache --cache PATH`; add `--ci` only when the
|
||
cache will not be packaged for offline installation.
|
||
|
||
Native bundle staging keeps its HOME and PM state temporary, but not its uv
|
||
cache. `scripts.bundles.stage --cache PATH` (or `hermes pm bundle --cache PATH`)
|
||
selects the persistent cache explicitly. Direct staging also accepts the
|
||
provider's `UV_CACHE_DIR`; otherwise it uses the output parent's `.uv-cache`.
|
||
The PM runtime and application dependency builds receive this same cache.
|
||
General PM staging uses `setup-pm` and `save-pm-cache` to restore and save it
|
||
after the build, including failures, under its v2 namespace. Desktop composition
|
||
instead prepares the full dependency set before its single dependency snapshot
|
||
save. The packaged `uv-cache/` is a copy, not the writable build cache.
|
||
|
||
### Windows ARM64 build prerequisites
|
||
|
||
`scripts/windows-build-deps.ps1` owns Visual Studio ARM64, Clang, Rust, and
|
||
static OpenSSL preparation. PM calls it through `pm/native_build.py` before any
|
||
dependency build from a checkout, so every source install path gets it. Native
|
||
build adapters use the same `scripts/build/windows-deps.ps1` entrypoint through
|
||
`pm.native_build`, before
|
||
isolating HOME or compiling Node/Python dependencies. CI uses the same script
|
||
through `setup-windows-build-deps`, with an OpenSSL cache outside the product.
|
||
The product compilers and assembler do not install these prerequisites.
|
||
|
||
The PowerShell entrypoint accepts `-StateRoot` for persistent build-tool state
|
||
and `-EnvironmentFile` for its prepared environment. The Python adapter passes
|
||
that environment only to build children. Rust's original toolchain homes stay
|
||
explicit, so temporary HOME isolation cannot hide an initialized toolchain.
|
||
General CI setup exports that compiler environment to later steps. Desktop
|
||
preparation keeps it child-scoped and records native cache identity there.
|
||
Warm OpenSSL reuse validates both static libraries and its development header.
|
||
|
||
## Runnable agent assembly
|
||
|
||
```sh
|
||
python -m scripts.build.agent --inputs /work/agent-inputs.json --out /work/agent
|
||
```
|
||
|
||
The only builder arguments are `--inputs` and `--out`. Python entrypoints also
|
||
accept argparse's `-h`/`--help`. The library interface is
|
||
`assemble(AgentInputs(...), out)`. The input JSON rejects unknown fields.
|
||
|
||
### Input fields
|
||
|
||
All supplied filesystem input paths must be absolute and exist. `repo` and
|
||
`bin_dir` are output-relative names, not filesystem inputs.
|
||
|
||
| Field | Required | Meaning |
|
||
|---|---|---|
|
||
| `project` | Yes | `pyproject.toml` with static project metadata and `[project.scripts]` |
|
||
| `code` | Yes | Prepared source tree, or installed code root for reference placement |
|
||
| `repo` | Yes | Code/resource location beneath `OUT`, such as `hermes-agent`, `.`, or `share/hermes-agent` |
|
||
| `placement` | Yes | `contained`, `fixed`, or `references` |
|
||
| `target` | Yes | `linux-x64`, `linux-arm64`, `darwin-x64`, `darwin-arm64`, `win32-x64`, `win32-arm64`, or `linux-arm64-bionic` |
|
||
| `python` | Yes | Prepared target interpreter file |
|
||
| `site_packages` | Yes | Prepared application dependency directory |
|
||
| `environment` | Yes | Prepared application environment root |
|
||
| `pm_runtime` | Yes | Independent PM runtime directory with `pm-runtime.json` |
|
||
| `bin_dir` | No | One output-relative directory name. Default: `bin` |
|
||
| `tools` | No | Prepared runtime tool directory. The manifest defaults to `tools` if omitted |
|
||
| `command_dir` | For `references` | Directory of prepared commands declared by the project |
|
||
| `resources` | No | Resource-name to directory mapping |
|
||
| `frontends` | No | `tui` and/or `web` product paths |
|
||
| `ref` | No | Source identity copied to the completion manifest |
|
||
| `stamp` | No | Prepared install stamp copied to `OUT/repo/install-stamp.json` |
|
||
| `features` | No | Prepared feature inventory copied to `OUT/enabled-features.json` |
|
||
| `env` | No | Additional environment values in the reference-placement command map |
|
||
|
||
Recognized resource names are `skills`, `optional-skills`, `plugins`, `locales`,
|
||
and `optional-mcps`. Distribution adapters supply the required resources.
|
||
The generic assembler does not infer a missing resource mapping. TUI inputs
|
||
require `dist/entry.js` and `package.json`. Web inputs require `index.html`.
|
||
|
||
The PM marker supplies `python` and `sitePackages` paths relative to its runtime
|
||
directory, or absolute store references. These paths must resolve to real
|
||
inputs. If a supplied stamp declares `nix` or `docker`, its `pmRuntime` must
|
||
match the supplied PM runtime. The assembler does not create that environment.
|
||
|
||
### Placement and output
|
||
|
||
- **`contained`:** The provider prepares the interpreter, dependencies, PM
|
||
runtime, and tools inside `OUT`. Assembly copies source/resources, plants
|
||
frontends, and calls the portable link helper.
|
||
- **`fixed`:** The provider owns final-prefix preparation. Assembly copies or
|
||
reuses source/resources and generates launchers without portable relocation.
|
||
Docker and Termux use this placement.
|
||
- **`references`:** Assembly references installed code and commands without
|
||
copying Python code or replacing wheel metadata. It links explicit resources
|
||
and frontends, then emits `command-map.json`. Nix creates its native wrappers
|
||
from this map.
|
||
|
||
Source-layout placement writes project distribution metadata without building
|
||
a Hermes wheel. It also writes `site_packages/hermes-agent.pth` with a relative
|
||
code path. Source-layout placement therefore requires an output-owned dependency
|
||
directory, including in `fixed` mode. Reference placement does not write this file.
|
||
|
||
Commands derive from `[project.scripts]`, not a second command
|
||
list. POSIX launchers use the supplied runtime paths. Windows launcher minting
|
||
runs the target interpreter and therefore needs a runnable native environment.
|
||
A target label alone does not prove ABI compatibility.
|
||
|
||
For copied frontends, the assembler places TUI files at
|
||
`OUT/repo/hermes_cli/tui_dist/` and web files at
|
||
`OUT/repo/hermes_cli/web_dist/`. Reference placement links TUI at `OUT/ui-tui`
|
||
and web at `OUT/repo/web_dist` and records their environment bindings.
|
||
|
||
After structural assembly, `manifest.json` records `schema`, `target`, `repo`,
|
||
`venv`, `store`, `launchers`, and `runtime`, plus `ref` when supplied. Its
|
||
`runtime` contains `repoDir`, `toolsDir`, `storePython`, `sitePackages`, and
|
||
`commands`. Reference placement also emits command sources, destinations,
|
||
entrypoints, and environment values in `command-map.json`.
|
||
|
||
Agent assembly modifies its output in place. It removes old completion records
|
||
before work and writes the manifest last. This is not the frontend builder's
|
||
atomic-directory publication contract. A failed assembly can leave partial
|
||
files. The manifest is not evidence of a target-native launch or signed-package
|
||
acceptance.
|
||
|
||
## Implementation references
|
||
|
||
These locations define the interfaces described here:
|
||
|
||
| Contract | Source |
|
||
|---|---|
|
||
| Frontend arguments and publication | `frontend-common.mjs:31–79` |
|
||
| TUI product and development output | `tui.mjs:25–103` |
|
||
| Web inputs and TypeScript check | `web.mjs:8–71` |
|
||
| Desktop inputs and native checks | `desktop.mjs:11–69` |
|
||
| Locked workspace union | `node-deps.mjs:31–68` |
|
||
| Python provider | `../../pm/operations.py`, `../../pm/environment.py` |
|
||
| Agent input fields and checks | `inputs.py:30–115` |
|
||
| Agent assembly and outputs | `agent.py:76–168` |
|
||
| Launcher implementation | `launchers.py`, `launcher_wrapper.py`, `mint_launchers.py` |
|
||
|
||
## Verification still required
|
||
|
||
Parser checks and source inspection establish the documented call shapes.
|
||
They do not establish offline compilation, cache reuse, or runtime success.
|
||
Artifact acceptance still needs these checks:
|
||
|
||
- Real frontend builds from immutable prepared inputs without network access.
|
||
- Standalone TUI interaction and dashboard assets/backend behavior.
|
||
- Native Electron bindings under the packaged Electron version.
|
||
- Agent CLI, ACP, plugins, and catalogs from an unrelated working directory.
|
||
- Native payload relocation and offline mutable-environment reconstruction.
|
||
- Docker runtime probes as its non-root user and layer-content inspection.
|
||
- Actual Nix builds and commands through store-reference wrappers.
|
||
- Fresh network-disabled bionic installation and Android device acceptance.
|
||
|
||
Docker/Nix build execution belongs to the distribution verification work, not
|
||
this documentation pass. No build-pass claim follows from this reference.
|