Files
teknium1 a3f454a287 fix(build): keep scripts/build/inputs.py free of pm imports; accept musl targets in its grammar
The Nix agent derivation builds scripts/build/*.py from a fileset that
does not include pm/, so importing pm.store.ALL_TARGETS there failed
nix flake check with ModuleNotFoundError. Extend the local target
regex with linux-(x64|arm64)-musl instead.
2026-09-27 03:24:39 -07:00
..
…
…
…

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 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:

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.

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

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:

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):

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:

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:

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

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

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.