mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-10 08:32:09 +00:00
Hermes-Setup.exe is a small signed Rust+Tauri binary that drives
scripts/install.ps1 stage-by-stage with a native UI matching the
desktop's design language. Replaces the chicken-and-egg pattern of
shipping a 200MB Electron app whose first launch existed only to
run install.ps1.
The architecture:
Rust backend (src-tauri/):
bootstrap.rs orchestrator -- Tauri commands, stage iteration
install_script.rs resolve install.ps1 (dev checkout, cache, GitHub raw)
powershell.rs spawn powershell, line-stream stdout/stderr, parse JSON
events.rs BootstrapEvent types -- mirror bootstrap-runner.cjs
paths.rs HERMES_HOME resolution + tracing log setup
build.rs bakes BUILD_PIN_COMMIT / BUILD_PIN_BRANCH from
'git rev-parse HEAD' at compile time
React frontend (src/):
Tauri webview rendering 4 screens (welcome / progress / success /
failure), driven by nanostores subscribing to the Rust event stream.
Visual layer reuses the desktop's styles.css wholesale via @import
so the installer and desktop never drift visually.
Distribution:
targets = ['app', 'dmg', 'appimage'] -- no NSIS/MSI wrapper. The
raw target/release/Hermes-Setup.exe IS the artifact on Windows;
.dmg + .app on macOS; AppImage on Linux. One file, double-click,
no installer-installing-an-installer pattern.
Compile-time pinning:
build.rs reads 'git rev-parse HEAD' and emits
cargo:rustc-env=BUILD_PIN_COMMIT=<sha> + BUILD_PIN_BRANCH=<branch>.
bootstrap.rs's option_env!() picks these up so the binary fetches
install.ps1 from the exact SHA it was tested against. CI / release
builds can override via HERMES_BUILD_PIN_COMMIT env var.
Windows manifest:
hermes-setup.manifest declares level='asInvoker' so the
productName 'Hermes Setup' doesn't trip Windows's installer-
detection heuristic and refuse to launch without elevation.
Also declares PerMonitorV2 DPI + UTF-8 active code page + Common
Controls v6.
Limitations of this initial version:
* No code signing -- Windows SmartScreen will warn once on Hermes-Setup.exe
('More info -> Run anyway'). The downstream binaries it produces
(Hermes.exe in win-unpacked/, the hermes CLI) are locally-built and
therefore don't carry MOTW, so they launch without SmartScreen
intervention. Cert procurement tracked separately.
* macOS and Linux build paths defined but untested -- Windows-only V1.
150 lines
5.5 KiB
Rust
150 lines
5.5 KiB
Rust
use std::process::Command;
|
|
|
|
fn main() {
|
|
// -----------------------------------------------------------------
|
|
// Bake the install.ps1 pin into the binary at compile time.
|
|
//
|
|
// BUILD_PIN_COMMIT and BUILD_PIN_BRANCH are read by bootstrap.rs's
|
|
// `option_env!()` macro to default the install-script reference.
|
|
// Precedence (matches install.ps1's own arg precedence): commit > branch.
|
|
//
|
|
// Resolution order:
|
|
// 1. Env var override at build time (HERMES_BUILD_PIN_COMMIT, etc.).
|
|
// Useful for CI builds that want to pin to a tagged release SHA
|
|
// rather than whatever the checkout's HEAD happens to be.
|
|
// 2. `git rev-parse HEAD` + `git rev-parse --abbrev-ref HEAD` against
|
|
// the repo this build.rs lives in. Default for `cargo tauri build`
|
|
// from a dev machine — pins the produced .exe to your current
|
|
// checkout state.
|
|
// 3. Last-resort fallback: hardcoded `main` branch, no commit. The
|
|
// installer will fetch HEAD-of-main at runtime. Used when the
|
|
// build is happening outside a git checkout (e.g. cargo install
|
|
// from a packaged crate, unlikely for this binary but defensive).
|
|
//
|
|
// Build script reruns on git HEAD change so a new commit triggers
|
|
// a rebuild without `cargo clean`.
|
|
// -----------------------------------------------------------------
|
|
|
|
let commit = resolve_commit_pin();
|
|
let branch = resolve_branch_pin();
|
|
|
|
if let Some(c) = &commit {
|
|
println!("cargo:rustc-env=BUILD_PIN_COMMIT={c}");
|
|
println!("cargo:warning=hermes-bootstrap: pinning to commit {}", short(c));
|
|
}
|
|
if let Some(b) = &branch {
|
|
println!("cargo:rustc-env=BUILD_PIN_BRANCH={b}");
|
|
println!("cargo:warning=hermes-bootstrap: pinning to branch {b}");
|
|
}
|
|
if commit.is_none() && branch.is_none() {
|
|
// Fail loudly rather than silently produce a binary that errors
|
|
// at runtime with "no install-script pin supplied". A build that
|
|
// can't resolve a pin almost certainly indicates a misconfigured
|
|
// build environment.
|
|
println!(
|
|
"cargo:warning=hermes-bootstrap: no pin resolved at build time; binary will fail at runtime without HERMES_SETUP_DEV_REPO_ROOT or runtime args"
|
|
);
|
|
}
|
|
|
|
// Rerun build.rs when HEAD moves so successive builds pick up new
|
|
// commits without needing `cargo clean`. .git/HEAD changes on every
|
|
// commit / branch switch / rebase.
|
|
let git_dir = locate_git_dir();
|
|
if let Some(gd) = &git_dir {
|
|
println!("cargo:rerun-if-changed={}/HEAD", gd.display());
|
|
// .git/HEAD often points at a ref (e.g. `ref: refs/heads/bb/gui`);
|
|
// also watch the ref itself so a new commit on the same branch
|
|
// re-triggers.
|
|
if let Ok(head) = std::fs::read_to_string(gd.join("HEAD")) {
|
|
if let Some(rest) = head.trim().strip_prefix("ref: ") {
|
|
println!("cargo:rerun-if-changed={}/{}", gd.display(), rest);
|
|
}
|
|
}
|
|
}
|
|
println!("cargo:rerun-if-env-changed=HERMES_BUILD_PIN_COMMIT");
|
|
println!("cargo:rerun-if-env-changed=HERMES_BUILD_PIN_BRANCH");
|
|
|
|
// -----------------------------------------------------------------
|
|
// Tauri windows manifest. See hermes-setup.manifest for rationale —
|
|
// declares level="asInvoker" so Windows's installer-detection
|
|
// heuristic doesn't refuse to launch us without UAC elevation.
|
|
// -----------------------------------------------------------------
|
|
#[cfg(target_os = "windows")]
|
|
let attrs = {
|
|
let manifest = include_str!("hermes-setup.manifest");
|
|
let win = tauri_build::WindowsAttributes::new().app_manifest(manifest);
|
|
tauri_build::Attributes::new().windows_attributes(win)
|
|
};
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
let attrs = tauri_build::Attributes::new();
|
|
|
|
tauri_build::try_build(attrs).expect("failed to run tauri-build");
|
|
}
|
|
|
|
fn resolve_commit_pin() -> Option<String> {
|
|
if let Ok(v) = std::env::var("HERMES_BUILD_PIN_COMMIT") {
|
|
if !v.trim().is_empty() {
|
|
return Some(v.trim().to_string());
|
|
}
|
|
}
|
|
let out = Command::new("git")
|
|
.args(["rev-parse", "HEAD"])
|
|
.output()
|
|
.ok()?;
|
|
if !out.status.success() {
|
|
return None;
|
|
}
|
|
let s = String::from_utf8(out.stdout).ok()?.trim().to_string();
|
|
if s.is_empty() {
|
|
None
|
|
} else {
|
|
Some(s)
|
|
}
|
|
}
|
|
|
|
fn resolve_branch_pin() -> Option<String> {
|
|
if let Ok(v) = std::env::var("HERMES_BUILD_PIN_BRANCH") {
|
|
if !v.trim().is_empty() {
|
|
return Some(v.trim().to_string());
|
|
}
|
|
}
|
|
let out = Command::new("git")
|
|
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
|
.output()
|
|
.ok()?;
|
|
if !out.status.success() {
|
|
return None;
|
|
}
|
|
let s = String::from_utf8(out.stdout).ok()?.trim().to_string();
|
|
// "HEAD" is what you get on a detached checkout — no meaningful branch
|
|
// to pin to. The commit pin still applies; just don't emit a branch.
|
|
if s.is_empty() || s == "HEAD" {
|
|
None
|
|
} else {
|
|
Some(s)
|
|
}
|
|
}
|
|
|
|
fn locate_git_dir() -> Option<std::path::PathBuf> {
|
|
let out = Command::new("git")
|
|
.args(["rev-parse", "--git-dir"])
|
|
.output()
|
|
.ok()?;
|
|
if !out.status.success() {
|
|
return None;
|
|
}
|
|
let s = String::from_utf8(out.stdout).ok()?.trim().to_string();
|
|
if s.is_empty() {
|
|
return None;
|
|
}
|
|
Some(std::path::PathBuf::from(s))
|
|
}
|
|
|
|
fn short(commit: &str) -> &str {
|
|
if commit.len() >= 12 {
|
|
&commit[..12]
|
|
} else {
|
|
commit
|
|
}
|
|
}
|