rust+nix: load static assets at runtime, drop build.rs (#555)

Cuts every `include_bytes!`/`include_str!` of a non-rust path in
the workspace over to runtime file loads from `$HIVE_ASSETS_DIR`
(the `hyperhive-assets` derivation introduced in the previous
commit). After this commit the rust derivation has no compile-time
dependency on `branding/*` or `hive-ag3nt/prompts/*` anymore.

Call-site flips:

- `hive-c0re/src/forge.rs::CORE_AVATAR_PNG` /
  `CONFIG_ORG_AVATAR_PNG`: were `include_bytes!` of
  `branding/hyperhive.png` and `$OUT_DIR/agent-configs.png`. Now
  `ensure_core_avatar` / `ensure_config_org_avatar` `tokio::fs::read`
  via `hive_sh4re::assets::{core_avatar_png, config_org_avatar_png}`
  at startup. The `agent-configs.png` is now rendered by the
  `hyperhive-assets` derivation's rsvg-convert step (was
  `hive-c0re/build.rs` + librsvg on the rust derivation's
  nativeBuildInputs — both gone in the next commit).
- `hive-ag3nt/src/prompt.rs::TEMPLATE`: `render` now takes the
  template as an argument; `write_system_prompt` reads it once from
  `$HIVE_ASSETS_DIR/prompts/system.md` before calling render. The
  test module still `include_str!`s the production template so
  `cargo test --workspace` doesn't need `HIVE_ASSETS_DIR` set —
  this is the only remaining compile-time reference to the file
  from the rust workspace, gated to `#[cfg(test)]`.
- `hive-ag3nt/src/turn.rs::CLAUDE_SETTINGS`: was `include_str!`'d
  and written via `tokio::fs::write`; now `tokio::fs::copy` from
  `$HIVE_ASSETS_DIR/prompts/claude-settings.json` into the
  per-agent socket dir.
- `hive-ag3nt/src/web_ui.rs::DEFAULT_ICON`: was `include_str!`'d;
  now read on-demand from `$HIVE_ASSETS_DIR/branding/hyperhive.svg`
  inside `serve_icon`. Falls back to an empty body if missing so
  the endpoint never panics on a misconfigured container (matches
  the existing "per-agent icon.svg override" fallthrough).

`HIVE_ASSETS_DIR` wiring:

- Inside containers: `nix/templates/harness-base.nix`
  `environment.variables` sets it to
  `${pkgs.hyperhive-assets}/share/hyperhive` (resolved through
  the default overlay applied in `mkContainer`). Verified by
  building `agent-base-toplevel` and grepping the resulting
  `/etc/set-environment`.
- Host-side: `nix/modules/hive-c0re.nix` adds an `assets` option
  defaulting to `hyperhive.packages.${system}.assets`, threaded
  in from the flake's nixosModules wiring, and sets the same env
  var on the `hive-c0re` systemd unit so the daemon's
  `forge::ensure_*_avatar` startup hooks find the PNGs.

`hive-c0re/build.rs` deleted entirely; `[package].build` removed
from `hive-c0re/Cargo.toml`; rsvg-convert dependency lives in the
assets derivation only.

Validated: `nix build .#default .#checks.x86_64-linux.clippy
.#agent-base-toplevel .#manager-toplevel --fallback` all succeed.
`/etc/set-environment` in the toplevel shows
`HIVE_ASSETS_DIR="/nix/store/.../hyperhive-assets-0.1.0/share/hyperhive"`.
This commit is contained in:
iris 2026-05-29 02:41:29 +02:00 committed by Mara
commit 73684fb00a
11 changed files with 207 additions and 105 deletions

View file

@ -2,13 +2,6 @@
name = "hive-c0re"
edition.workspace = true
version.workspace = true
# Render branding/agent-configs.svg → $OUT_DIR/agent-configs.png at
# compile time (#424). build.rs shells out to `rsvg-convert`
# (librsvg, pulled in via flake.nix' crane nativeBuildInputs); the
# baked PNG is included via include_bytes! from forge.rs so no
# raster gets checked into git.
build = "build.rs"
[lints]
workspace = true

View file

@ -1,49 +0,0 @@
//! Render `branding/agent-configs.svg` → `$OUT_DIR/agent-configs.png`
//! at compile time so the daemon can `include_bytes!` the PNG without
//! checking the raster into git (#424 mara: "generate png on the fly
//! or in build"). The SVG is the source of truth; the PNG is a build
//! artifact.
//!
//! Uses `rsvg-convert` from PATH (librsvg, already available in
//! nixpkgs and added to the crane derivation's `nativeBuildInputs`
//! in `flake.nix`). For dev builds outside Nix, install librsvg via
//! your system package manager (Debian/Ubuntu: `librsvg2-bin`,
//! macOS: `brew install librsvg`).
use std::env;
use std::path::PathBuf;
use std::process::Command;
const SVG_PATH: &str = "../branding/agent-configs.svg";
const PNG_NAME: &str = "agent-configs.png";
// 300×300 to match the existing branding/hyperhive.png, which the
// Forgejo avatar endpoint accepts without resizing on upload.
const PX: &str = "300";
fn main() {
// Re-run the build script when either the SVG itself or this
// script change. We deliberately don't watch every file in
// `branding/` — only the one PNG we generate.
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={SVG_PATH}");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR set by cargo"));
let png_path = out_dir.join(PNG_NAME);
let status = Command::new("rsvg-convert")
.args(["--width", PX, "--height", PX, "-o"])
.arg(&png_path)
.arg(SVG_PATH)
.status();
match status {
Ok(s) if s.success() => {}
Ok(s) => panic!("rsvg-convert exited with {s} rendering {SVG_PATH}"),
Err(e) => panic!(
"failed to invoke rsvg-convert: {e}\n\
install librsvg (Debian/Ubuntu: librsvg2-bin, macOS: brew install librsvg, \
NixOS: pkgs.librsvg). The Nix derivation already pulls it in via \
flake.nix craneLib.buildPackage.nativeBuildInputs.",
),
}
}

View file

@ -41,20 +41,14 @@ const CORE_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-core-avatar-set";
/// Sibling marker for the `agent-configs` org avatar (#424). Same one-
/// shot semantics — delete to force the upload to re-run.
const CONFIG_ORG_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-agent-configs-avatar-set";
/// Hyperhive logo bytes, baked into the daemon. Uploaded once via the
/// admin avatar API so the `core` Forgejo user shows the project mark
/// next to commits in `agent-configs/*`, `core/meta`, etc. instead of
/// the default hash identicon.
const CORE_AVATAR_PNG: &[u8] = include_bytes!("../../branding/hyperhive.png");
/// `agent-configs` org logo bytes (#424). Sibling visual to the main
/// hyperhive mark — same dark base + outer ring + corner brackets,
/// with a stacked-config-files glyph in the centre so the operator
/// can distinguish the agent-configs namespace from the main
/// `hyperhive` org at a glance. Source-of-truth is
/// `branding/agent-configs.svg`; `hive-c0re/build.rs` renders it
/// into `$OUT_DIR/agent-configs.png` at compile time via
/// `rsvg-convert` so the raster never gets checked into git.
const CONFIG_ORG_AVATAR_PNG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/agent-configs.png"));
// Avatar PNGs are loaded at runtime from
// `$HIVE_ASSETS_DIR/branding/{hyperhive,agent-configs}.png` via the
// helpers in `hive_sh4re::assets` (#555 — was `include_bytes!` of an
// in-source path and an OUT_DIR-rendered sibling, both of which
// invalidated the crane src cache on any branding edit). The
// `agent-configs.png` is rendered from its SVG during the
// `hyperhive-assets` derivation's build (was `hive-c0re/build.rs`
// + `rsvg-convert` on PATH; both gone now).
/// Forgejo org grouping every agent's applied config repo. Core is a
/// site admin and reads + writes every repo here; agents are NOT
/// members and the repos are private, so no agent — not even the one
@ -298,9 +292,13 @@ async fn ensure_core_avatar(token: &str) -> Result<()> {
if marker.exists() {
return Ok(());
}
let png_path = hive_sh4re::assets::core_avatar_png();
let png_bytes = tokio::fs::read(&png_path)
.await
.with_context(|| format!("read core avatar PNG from {}", png_path.display()))?;
let body = format!(
r#"{{"image":"{}"}}"#,
base64::engine::general_purpose::STANDARD.encode(CORE_AVATAR_PNG),
base64::engine::general_purpose::STANDARD.encode(&png_bytes),
);
let url = format!("{FORGE_HTTP}/api/v1/admin/users/core/avatar");
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
@ -325,9 +323,13 @@ async fn ensure_config_org_avatar(token: &str) -> Result<()> {
if marker.exists() {
return Ok(());
}
let png_path = hive_sh4re::assets::config_org_avatar_png();
let png_bytes = tokio::fs::read(&png_path)
.await
.with_context(|| format!("read {CONFIG_ORG} avatar PNG from {}", png_path.display()))?;
let body = format!(
r#"{{"image":"{}"}}"#,
base64::engine::general_purpose::STANDARD.encode(CONFIG_ORG_AVATAR_PNG),
base64::engine::general_purpose::STANDARD.encode(&png_bytes),
);
let url = format!("{FORGE_HTTP}/api/v1/orgs/{CONFIG_ORG}/avatar");
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;