Installer Engine Documentation
Installer Engine is a YAML-driven cross-platform installer framework built in Rust. It combines a native backend (requirements checks, install plan execution, rollback) with a WebView2/WebKitGTK UI layer and the `hagane CLI for packaging and compilation. Windows uses WebView2; Linux uses wry + WebKitGTK.
Why
| Problem with NSIS | This engine |
|---|---|
| PowerShell/CMD for system checks -> slow, laggy | Native WinAPI calls only — microseconds |
| Fixed UI with limited branding | WebView2 HTML/CSS — fully brandable |
| Script language with no type safety | YAML with JSON Schema validation + IDE autocomplete |
| No parallel processing | Requirement checks run in parallel via Rayon |
| Bloated runtime | Pure Rust binary, no .NET/runtime dependency |
Quick Start
1. Write your manifest
# sdk/example/installer.yaml
app:
name: "MyApp"
version: "1.0.0"
publisher: "Your Company"
logo: "assets/logo.png"
default_install_dir: "{{PROGRAMFILES64}}/YourCompany/MyApp"
theme:
accent_color: "#0078D4"
pages:
- type: welcome
- type: license
- type: requirements
- type: install_dir
- type: install
- type: finish
requirements:
- type: os
platform: windows
min_build: 18362
label: "Windows 10 1903+"
- type: ram
min_mb: 2048
label: "2 GB RAM"
- type: disk
min_mb: 200
label: "200 MB free space"
install:
setup:
create_dirs:
- "{{INSTDIR}}"
components:
core:
archive: "payload.zst"
target: "{{INSTDIR}}"
system:
windows:
shortcuts:
- name: "MyApp"
target: "{{INSTDIR}}/MyApp.exe"
location: desktop
finalize:
windows:
write_uninstaller: "{{INSTDIR}}/uninstall.exe"
2. Place your payload
sdk/example/
├── installer.yaml
├── assets/
│ ├── logo.png
│ ├── banner.png
│ └── icon.ico
└── payload/ <- folder named after archive (without .zst)
├── MyApp.exe
└── ...
3. Build
Installed Hagane (after installing Hagane on the machine):
# Auto-discovery: run from the directory containing your installer.yaml
hagane run --release
# Or with an explicit manifest path
hagane run installer.yaml --release
.yml file exists in your working directory, hagane run --release (no manifest argument) selects it automatically. If multiple YAML files are present, Hagane lists them all and asks you to specify one explicitly.
Build from source (from this repository):
cargo build --release -p builder --bin hagane
.\target\release\hagane.exe run .\path\to\installer.yaml --release
For iterative work during development:
cargo run -p builder --bin hagane -- run .\path\to\installer.yaml --release
4. Run
# GUI mode
myapp-setup.exe
# Silent install (no UI)
myapp-setup.exe /S
Theme Customization
The installer supports full theme customization via optional manifest fields.
Basic Theme Token
theme:
accent_color: "#0078D4" # Buttons, links, accents
background_color: "#FFFFFF" # Main background
text_color: "#1A1A1A" # Primary text
font_family: "'Segoe UI', sans-serif"
Advanced Theme Tokens
theme:
# Color variants for depth and interactivity
accent_dark_color: "#005A9E" # Button hover/pressed states
accent_light_color: "#EBF3FB" # Focus rings, light backgrounds
# Surfaces and text
surface_color: "#F5F5F5" # Cards, alt backgrounds
text_muted_color: "#6B6B6B" # Secondary text, labels
border_color: "#E0E0E0" # Borders, dividers
border_radius: 6 # Corner roundness (px)
# Semantic colors
success_color: "#107C10" # Success text
success_bg_color: "#F7F9F8" # Success background
error_color: "#C42B1C" # Error text
error_bg_color: "#FFF7F6" # Error background
# Progress bar — gradient from start to end
progress_color: "#0078D4" # Gradient start color
progress_light_color: "#EBF3FB" # Gradient end color
# Window geometry
window_width: 780 # Pixels
window_height: 540 # Pixels
All Theme Fields Are Optional
Every field in the `theme block is completely optional. Omit any field and the installer will use a sensible built-in default.
| Field | Default | Purpose |
|---|---|---|
| `accent_color | `#0078D4 | Primary button color, links, active states |
| `accent_dark_color | `#005A9E | Button hover/pressed states, emphasis |
| `accent_light_color | `#EBF3FB | Focus rings, light backgrounds, hover underlay |
| `background_color | `#FFFFFF | Main window background |
| `surface_color | `#F5F5F5 | Cards, alternate backgrounds, section dividers |
| `text_color | `#1A1A1A | Primary text, headings, body copy |
| `text_muted_color | `#6B6B6B | Secondary text, labels, hints, disabled text |
| `border_color | `#E0E0E0 | Borders, dividers, input outlines |
| `border_radius | `6 | Corner roundness in pixels (applies to buttons, cards, inputs) |
| `success_color | `#107C10 | Success message text, checkmarks |
| `success_bg_color | `#F7F9F8 | Success message background |
| `error_color | `#C42B1C | Error message text, warnings |
| `error_bg_color | `#FFF7F6 | Error message background |
| `progress_color | `#0078D4 | Progress bar gradient start color |
| `progress_light_color | `#EBF3FB | Progress bar gradient end color |
| `font_family | `'Segoe UI', system-ui, sans-serif | Typography, applies to all text |
| `window_width | `780 | Setup window width in pixels |
| `window_height | `540 | Setup window height in pixels |
**Minimal theme** (just brand color):
theme:
accent_color: "#FF6B35"
**Moderate theme** (brand + light/dark mode):
theme:
accent_color: "#2563EB"
background_color: "#FFFFFF"
text_color: "#1A1A1A"
**Complete theme** (full control):
theme:
accent_color: "#4F8FF7"
accent_dark_color: "#2E6FDB"
accent_light_color: "#D9E7FF"
background_color: "#0F172A"
surface_color: "#111C33"
text_color: "#E5EEF9"
text_muted_color: "#94A3B8"
border_color: "#24344D"
success_color: "#22C55E"
success_bg_color: "#102A1A"
error_color: "#F87171"
error_bg_color: "#2A1414"
progress_color: "#4F8FF7"
progress_light_color: "#A5C8FF"
font_family: "'Inter', sans-serif"
border_radius: 8
window_width: 800
window_height: 600
Example Presets
**Minimal Modern** (clean, light, blue accent):
theme:
accent_color: "#2563EB"
accent_dark_color: "#1D4ED8"
accent_light_color: "#DBEAFE"
background_color: "#FFFFFF"
surface_color: "#F8FAFC"
text_color: "#0F172A"
text_muted_color: "#475569"
border_color: "#E2E8F0"
success_color: "#15803D"
success_bg_color: "#F0FDF4"
error_color: "#B91C1C"
error_bg_color: "#FEF2F2"
progress_color: "#2563EB"
progress_light_color: "#DBEAFE"
**Dark Corporate** (dark background, light text, blue accent):
theme:
accent_color: "#4F8FF7"
accent_dark_color: "#2E6FDB"
accent_light_color: "#D9E7FF"
background_color: "#0F172A"
surface_color: "#111C33"
text_color: "#E5EEF9"
text_muted_color: "#94A3B8"
border_color: "#24344D"
success_color: "#22C55E"
success_bg_color: "#102A1A"
error_color: "#F87171"
error_bg_color: "#2A1414"
progress_color: "#4F8FF7"
progress_light_color: "#A5C8FF"
For Open-Source Users
If you are using this engine to ship your own app installer:
- You must create your own `installer.yaml (app metadata, pages, requirements, and
installplan). - You do not ship `installer.yaml to end users — it is embedded into the generated setup EXE at build time.
- `installer.schema.json is optional at runtime, but strongly recommended during authoring for IDE validation/autocomplete.
Minimal author workflow
- Copy `sdk/example/installer.yaml and edit it for your app.
- Create payload folders next to your manifest (for each `extract archive name).
- Build your setup EXE:
# Auto-discovery (from the directory containing installer.yaml)
hagane run --release
# Or explicit:
hagane run ./path/to/installer.yaml --release
- Distribute only the output setup EXE (for example `myapp-setup.exe).
Enable YAML Schema in VS Code
At the top of your manifest, add:
# yaml-language-server: $schema=../../sdk/schema/installer.schema.json
This gives field completion, type checks, and early validation errors while authoring.
Project Structure
installer-engine/
├── engine/ # Core library crate
│ └── src/
│ ├── parser/ # YAML schema + validation (serde)
│ ├── requirements/ # Native WinAPI system checks (parallel)
│ ├── install/ # Step runner, file ops, registry, shortcuts
│ ├── state.rs # Installer state machine
│ └── ipc.rs # Rust ↔ WebView2 JSON message protocol
├── runner/ # Binary — Win32 window + WebView2 host
├── builder/ # hagane CLI — compresses & packages installer
├── ui/
│ ├── pages/ # HTML pages (welcome, license, requirements…)
│ └── assets/ # style.css, bridge.js
└── sdk/
├── example/ # Example installer.yaml + assets
└── schema/ # installer.schema.json for IDE support
Requirements Checks
**Platform note:** The checks below use Windows-native APIs. Linux requirements checks use equivalent POSIX calls. Both platforms evaluate checks in parallel via Rayon.
| Check | Windows API used |
|---|---|
| Windows version | `RtlGetVersion() |
| RAM | `GlobalMemoryStatusEx() |
| Disk space | `GetDiskFreeSpaceEx() |
| .NET Framework | Registry read — no subprocess |
| VC++ Redistributable | Registry scan — no subprocess |
All checks run **in parallel** via Rayon the moment the requirements page loads.
Available Pages
| type | Description |
|---|---|
| `welcome | Splash with logo, app name, description |
| `license | Scrollable license text with accept checkbox |
| `requirements | Live parallel check results |
| `install_dir | Path picker with disk space indicator |
| `components | Optional feature selection with sizes |
| `user_info | Name, organization, serial key fields |
| `summary | Review before install |
| `install | Progress bar, real-time log, rollback on error |
| `finish | Launch app / desktop shortcut toggles |
| `error | Error detail with rollback confirmation |
Install DSL Blocks
The installer now uses a required top-level `install block (legacy top-level steps is rejected).
| block | Purpose |
|---|---|
| `install.setup.create_dirs | Creates required directories before extraction |
| `install.components.<id> | Maps each component to an archive and destination target |
| `install.system.register_app | Writes app registration metadata |
| `install.system.register_uninstall | Writes Add/Remove Programs metadata |
| `install.system.shortcuts | Creates desktop/start-menu/startup shortcuts |
| `install.system.path | (Windows flat form) Adds a directory to PATH. Single entry only. Scope: `user (HKCU) or system (HKLM). |
| `install.system.linux.path | **(Linux)** Adds a directory to PATH. Accepts a **single entry or a list** — use a list to define user and system scope entries simultaneously, each gated on its own component. |
| `install.hooks.post_install | Runs post-install hooks. `shell can be powershell (Windows), bash (Linux), or program (all platforms). |
| `install.finalize.write_uninstaller | Writes the generated uninstaller executable |
Declared Variables (Define Once, Reuse Anywhere)
Use a top-level `variables block to avoid repeating the same paths and keys.
variables:
COMPANY: "Acme"
PRODUCT: "MyApp"
COMPANY_PRODUCT: "{{COMPANY}}/{{PRODUCT}}"
APP_REG_KEY: "SOFTWARE/{{COMPANY_PRODUCT}}"
platform:
windows:
INSTALL_ROOT: "{{PROGRAMFILES64}}/{{COMPANY}}/{{PRODUCT}}"
linux:
INSTALL_ROOT: "/opt/{{COMPANY}}/{{PRODUCT}}"
app:
default_install_dir: "{{INSTALL_ROOT}}"
install:
setup:
create_dirs:
- "{{INSTDIR}}"
components:
core:
archive: "payload.zst"
target: "{{INSTDIR}}"
system:
windows:
register_app:
hive: HKLM
key: "{{APP_REG_KEY}}"
install_location: "{{INSTDIR}}"
version: "2.1.0"
finalize:
windows:
write_uninstaller: "{{INSTDIR}}/uninstall.exe"
linux:
write_uninstaller: "{{INSTDIR}}/uninstall"
Rules:
- Variable keys should use `A-Z,
0-9, and_(optionally prefixed with$). - Preferred syntax is `{{KEY}} (for example
{{INSTDIR}}). - Built-in variables cannot be overridden: `{{INSTDIR}},
{{PROGRAMFILES}},{{PROGRAMFILES64}},{{APPDATA}},{{LOCALAPPDATA}},{{TEMP}},{{WINDIR}}. - Declared variables can reference other declared variables.
Variables in Paths
| Variable | Resolves to |
|---|---|
| `{{INSTDIR}} | Chosen installation directory |
| `{{PROGRAMFILES}} | `C:\Program Files (x86) |
| `{{PROGRAMFILES64}} | `C:\Program Files |
| `{{APPDATA}} | `C:\Users\<user>\AppData\Roaming |
| `{{LOCALAPPDATA}} | `C:\Users\<user>\AppData\Local |
| `{{TEMP}} | Temp directory |
| `{{WINDIR}} | `C:\Windows |
Use `{{KEY}} syntax in new manifests for consistent schema validation.
Logging and Error Codes
The installer supports two logging modes:
- `auto: lifecycle logging is generated automatically for each executed step (start, slow-step warn, success in file logs, and classified failures).
- `manual_only: normal lifecycle logging is suppressed during step execution.
In both modes, classified failure lines and rollback errors are still emitted when a step fails.
Logging Configuration
Add a top-level `logging block to control mode and file output:
logging:
mode: auto
path: "{{INSTDIR}}/logs"
file_name: "installation.log"
timestamp: true
include_raw_os_error: false
slow_step_warn_sec: 10
- Set both `path and
file_namewhen you want file logging enabled. - `slow_step_warn_sec controls when long-running steps produce a warning.
- When file logging is enabled, completion messages stay in the file log but are not echoed into the UI log box.
Install Logging Notes
In the current `install DSL, logging is controlled primarily by top-level logging.mode:
- `auto emits lifecycle logs for compiled install operations.
- `manual_only suppresses normal lifecycle logs and keeps failure logging deterministic.
For a full behavior matrix and end-to-end examples, see [LOGGING.md](LOGGING.md).
Post-Install Hook Actions
Use `install.hooks.post_install to run commands after all install steps complete.
**Windows — PowerShell:**
install:
hooks:
post_install:
- run:
command: |
Write-Host "Hello from installer"
shell: powershell
wait: true
fail_on_nonzero: true
timeout_sec: 30
**Linux — Bash:**
install:
hooks:
post_install:
- run:
platform: linux
command: |
chmod +x "{{INSTDIR}}/bin/myapp"
ln -sf "{{INSTDIR}}/bin/myapp" /usr/local/bin/myapp
shell: bash
wait: true
fail_on_nonzero: false
timeout_sec: 10
`platform** restricts the hook to a single OS (windows or linux). Omit it to run on all platforms.
Bash hook stdout is logged at `INFO level and stderr at WARN level — both appear in the installer log stream so failures are always visible.
Supported parameters:
| Parameter | Type | Notes |
|---|---|---|
| `command | string | Script content (`powershell/bash) or command line (program). |
| `shell | string | `powershell (Windows only), bash (Linux only), or program (all platforms). |
| `platform | string | Optional. Restrict hook to `windows or linux. Omit to run everywhere. |
| `wait | boolean | Wait for completion before continuing. Default: `true. |
| `fail_on_nonzero | boolean | Fail the installation on non-zero exit. Default: `true. |
| `timeout_sec | number | Kill and classify as `HG-PS-004 if exceeded. |
Stable Error Codes
The installer classifies step failures into stable v1 error codes:
- `HG-YAML-001 - manifest validation failure
- `HG-VAR-001 - unresolved installer variable
- `HG-EXTRACT-001 - archive missing from payload
- `HG-EXTRACT-002 - extraction I/O failure
- `HG-COPY-001 - copy source missing or invalid
- `HG-REG-001 - invalid registry configuration
- `HG-REG-002 - registry access denied / elevation required
- `HG-ENV-001 - environment variable operation failure
- `HG-RUN-001 - executable not found
- `HG-RUN-002 - process non-zero exit or execution failure
- `HG-PS-001 - PowerShell syntax/parse error
- `HG-PS-002 - PowerShell/command not found
- `HG-PS-003 - PowerShell non-zero exit
- `HG-PS-004 - PowerShell timeout
- `HG-PS-005 - PowerShell access denied or execution policy blocked
See [ERROR_CODES.md](ERROR_CODES.md) for the full field-by-field format and fix guidance.
Conditional Install Execution
Component selection is now controlled through `install.components and per-entry component fields under system blocks.
Supported `component fields include install.system.shortcuts[*].component and install.system.path[*].component (Linux list form) or install.system.path.component (single-entry form).
components:
- id: docs
name: "Documentation"
required: false
selected: true
install:
components:
docs:
archive: "docs.zst"
target: "{{INSTDIR}}/docs"
system:
windows:
shortcuts:
- name: "Documentation"
target: "{{INSTDIR}}/docs/manual.txt"
location: start_menu
component: docs
Linux PATH Integration
On Linux, `install.system.linux.path controls how the installed binary directory is added to $PATH. Two scopes are supported:
| Scope | What it writes | Takes effect |
|---|---|---|
| `user | Appends to `~/.bashrc and ~/.profile for the installing user |
New user terminal sessions |
| `system | Writes `/etc/profile.d/hagane-path.sh **and** appends to /etc/bash.bashrc |
All users, all new terminal sessions (login and non-login) |
WSL2 note: WSL2 terminal sessions are non-login interactive shells by default. `/etc/profile.d/ is only sourced for login shells. Using scope: system writes to **both** /etc/profile.d/ and /etc/bash.bashrc so the PATH is active in all shell types without requiring a login.
Single scope (simple case)
install:
system:
linux:
path:
add: "{{INSTDIR}}/bin"
scope: user # writes ~/.bashrc and ~/.profile
Both scopes simultaneously (list form)
When you want users to choose between user-only and system-wide PATH, define two entries and gate each on its own component:
components:
- id: user_path
name: "Add to user PATH"
description: "Appends to ~/.bashrc and ~/.profile (this user only)."
selected: true
- id: system_path
name: "Add to system PATH"
description: "Writes /etc/bash.bashrc and /etc/profile.d/ for all users. Requires admin."
selected: false
install:
system:
linux:
path:
- add: "{{INSTDIR}}/bin"
scope: user
component: user_path
- add: "{{INSTDIR}}/bin"
scope: system
component: system_path
Only the selected component's entry is executed at install time. An existing single-entry `path: block (without a list) continues to work unchanged for backwards compatibility.
High-Level Registry Abstractions
Use high-level actions to avoid repetitive registry write blocks.
`register_uninstall
For Add/Remove Programs metadata, use `install.system.register_uninstall:
install:
system:
register_uninstall:
hive: HKLM
key: "{{UNINSTALL_KEY}}"
name: "MyApp 2.1.0"
version: "2.1.0"
publisher: "Acme Corporation"
install_location: "{{INSTDIR}}"
uninstall: "{{INSTDIR}}/uninstall.exe"
estimated_size_kb: 180224
no_modify: true
no_repair: true
This expands internally into writes for:
- `DisplayName
- `DisplayVersion
- `Publisher
- `InstallLocation
- `UninstallString
- `EstimatedSize (if provided)
- `NoModify
- `NoRepair
Preferred fields in the DSL are `install_location and uninstall.
`register_app
For app settings, use `install.system.register_app:
install:
system:
register_app:
hive: HKLM
key: "{{APP_REG_KEY}}"
install_location: "{{INSTDIR}}"
version: "2.1.0"
This writes:
- `InstallDir =
inst_loc - `Version =
version
Administrator Elevation
Set `app.require_admin to control whether the installer requests elevation:
app:
require_admin: true
Use `true for operations that need system access:
| Platform | When required |
|---|---|
| Windows | `HKLM registry writes, system PATH, protected install locations like C:\Program Files |
| Linux | Writing to `/usr/local/, /etc/profile.d/, /etc/bash.bashrc, or creating symlinks in /usr/local/bin |
Use `false for user-level installs that should not prompt for elevation.
Linux elevation behavior: When `require_admin: true and the installer is not already running as root, it re-launches itself with sudo automatically and exits the original process. The elevated re-launch inherits all command-line arguments. The original calling user's home directory is tracked via $SUDO_USER so PATH entries are written to the correct user's shell config files.
Theme Customization
All colors, fonts, and sizing are CSS variables injected at runtime from `theme: in your YAML. No recompilation needed to rebrand the installer. For named theme presets and the folder layout used by this repository, see [THEMING_PRESETS.md](THEMING_PRESETS.md).
IDE Autocomplete
Add this comment to the top of your `installer.yaml for VS Code YAML extension:
# yaml-language-server: $schema=../../sdk/schema/installer.schema.json
Building from Source
# Requirements: Rust stable, Windows SDK, WebView2 SDK
cargo build --release # builds all crates
cargo build --release -p runner # just the installer runner
cargo build --release -p builder # just hagane
Quick Commands
cargo build -p builder --bin hagane --release
Copy-Item .\target\release\hagane.exe .\hagane\payload\bin\hagane.exe -Force
cargo run -p builder --bin hagane -- run hagane/installer.yaml --release
Useful Commands
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\AcmeMyApp" /s
reg query "HKLM\SOFTWARE\Acme\MyApp" /s
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Hagane" /s
reg query "HKLM\SOFTWARE\InstallerEngine\Hagane" /s
Notes
- The runner binary compiles on both **Windows** (WebView2/Win32) and **Linux** (wry + WebKitGTK/GTK3).
- The Windows runner uses `windows-rs and
webview2-com. WebView2 Runtime must be installed on the target machine. - The Linux runner uses `wry 0.43 +
tao 0.30with WebKitGTK. Build dependencies:libwebkit2gtk-4.1-dev,libgtk-3-dev. - The engine library and builder compile cross-platform.
Prerequisites & Installation
Everything you need installed before building installers with Hagane.
Hagane targets both **Windows** (WebView2/Win32) and **Linux** (wry + WebKitGTK/GTK3). Requirements differ by platform.
Windows
System Requirements
| Requirement | Minimum |
|---|---|
| OS | Windows 10 (x64) build 18362+ or Windows 11 |
| Rust | Stable channel via `rustup |
| WebView2 Runtime | Pre-installed on Windows 11; installer available for Windows 10 |
| MSVC Build Tools | Visual Studio 2019+ or Build Tools for Visual Studio 2022 |
Install Rust
winget install Rustlang.Rustup
Restart your terminal, then verify:
rustc --version
cargo --version
Install WebView2 Runtime
WebView2 ships with Windows 11 by default. For Windows 10:
winget install Microsoft.EdgeWebView2Runtime
Or download the installer directly from the [WebView2 download page](https://developer.microsoft.com/en-us/microsoft-edge/webview2/).
Install MSVC Build Tools
The Rust toolchain for Windows requires MSVC linker and headers. Install via:
winget install Microsoft.VisualStudio.2022.BuildTools --override "--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --quiet"
Or install the full **Visual Studio 2022** IDE which includes these tools.
Install Hagane (Windows)
Run the Hagane setup executable. It places the CLI at:
C:\Program Files\Hagane\bin\hagane.exe
The installer automatically adds this directory to your `PATH. Verify after installation:
hagane --version
PATH Setup (Manual, Windows)
If Hagane is not found on your `PATH after installation, add it manually:
[Environment]::SetEnvironmentVariable(
"PATH",
$env:PATH + ";C:\Program Files\Hagane\bin",
"User"
)
Re-open your terminal after running this.
Linux (Ubuntu / Debian / WSL2)
System Requirements
| Requirement | Notes |
|---|---|
| OS | Ubuntu 22.04+ / Debian 12+ (or WSL2 with WSLg) |
| Rust | Stable channel via `rustup |
| WebKitGTK 4.1 | For the installer GUI (wry + GTK3) |
| GTK3 dev headers | Required to compile the runner |
| GCC / Clang | Usually pre-installed; required by `cc build crate |
Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
rustc --version
cargo --version
Install WebKitGTK and GTK3 Build Dependencies
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libssl-dev \
pkg-config \
build-essential
WSL2 note: The GUI requires a display server. WSL2 with WSLg provides `$DISPLAY automatically on Windows 11. Verify with echo $DISPLAY — it should return a non-empty value.
Install Hagane (Linux)
Run the Hagane Linux installer binary (requires root — the installer re-launches with `sudo automatically):
./hagane-linux-x86_64
The installer places the CLI at `/usr/local/hagane/bin/hagane and creates a symlink at /usr/local/bin/hagane. Verify in a new terminal:
hagane --version
PATH Setup (Manual, Linux)
If `hagane is not found after installation, add the install bin directory to your PATH manually:
echo 'export PATH="/usr/local/hagane/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
hagane --version
Build Hagane From Source
If you are developing the engine itself, build the CLI directly from the workspace root:
Windows
cargo build --release -p builder --bin hagane
.\target\release\hagane.exe --version
Linux
cargo build --release -p builder --bin hagane
./target/release/hagane --version
Use the built binary in place of `hagane for all commands when running from source.
Quick Start
Build your first installer in under 5 minutes.
This guide shows a **Windows** installer. For Linux-specific steps (WebKitGTK setup, PATH scope, bash hooks, and uninstall) see [hagane.md](hagane.md) and [documentation.md](documentation.md).
What You Will Build
A standalone Windows setup EXE (`myapp-setup.exe) that:
- Shows a welcome screen with your app branding
- Checks system requirements natively (no PowerShell, no subprocess)
- Lets the user pick an install directory
- Extracts your app files to the chosen location
- Creates desktop shortcuts and registry entries
- Writes a working uninstaller
1. Create Your Manifest
Create a folder for your installer project and add `installer.yaml:
# installer.yaml
# yaml-language-server: $schema=../../sdk/schema/installer.schema.json
app:
name: "MyApp"
version: "1.0.0"
publisher: "Acme Corp"
logo: "assets/logo.png"
default_install_dir: "{{PROGRAMFILES64}}/Acme/MyApp"
theme:
accent_color: "#0078D4"
pages:
- type: welcome
- type: license
- type: requirements
- type: install_dir
- type: install
- type: finish
requirements:
- type: os
platform: windows
min_build: 18362
label: "Windows 10 1903+"
- type: ram
min_mb: 2048
label: "2 GB RAM"
- type: disk
min_mb: 200
label: "200 MB free space"
install:
setup:
create_dirs:
- "{{INSTDIR}}"
components:
core:
archive: "payload.zst"
target: "{{INSTDIR}}"
system:
windows:
register_app:
hive: HKCU
key: "Software/Acme/MyApp"
version: "1.0.0"
install_location: "{{INSTDIR}}"
shortcuts:
- name: "MyApp"
target: "{{INSTDIR}}/MyApp.exe"
location: desktop
finalize:
windows:
write_uninstaller: "{{INSTDIR}}/uninstall.exe"
2. Add Your Payload
Create a `payload/ folder next to installer.yaml and put your application files inside. Hagane compresses it automatically into payload.zst during the build step.
my-installer/
├── installer.yaml
├── assets/
│ └── logo.png
└── payload/ ← compressed to payload.zst at build time
└── MyApp.exe
3. Build
Run from the directory containing your `installer.yaml:
hagane run --release
.yml file is in your working directory, Hagane selects it automatically. No path argument needed.
If multiple YAML files are detected in the directory, Hagane shows a warning and exits cleanly — listing every candidate so you can pick the right one:

Hagane streams the full build pipeline — banner, manifest validation, payload compression, and `cargo build output:

The output EXE is written to `bin/ next to your manifest:
my-installer/
└── bin/
└── MyApp-setup.exe ← ready to distribute
4. Run Your Installer
# GUI mode
.\bin\MyApp-setup.exe
# Silent / automated install
.\bin\MyApp-setup.exe /S
CLI Help
Run `hagane at any time to see the banner and available commands:

What's Next
- Apply a **theme preset** — see [Theming & Presets](#theming)
- Collect extra info from users with a **custom page** — see [Custom Pages](#custom_pages)
- Configure **install logging** for diagnostics — see [Logging](#logging)
- Understand all **error codes** — see [Error Codes](#error_codes)
Theme Presets
This guide explains the preset-based theme system used by the installer UI.
The goal is simple:
- keep the installer logic and page behavior stable,
- let each preset change the visual style,
- make it easy for teammates to add new looks without editing core UI code,
- keep the default appearance available when no preset is selected.
The repository currently supports a preset called `caramel_latte, which gives the installer a warm latte/tan/beige look while keeping the same core UI and interactions.
How The Theme Flow Works
Theme presets are resolved in layers.
- The manifest declares `theme.preset.
- The engine stores that preset in installer state.
- The runner converts the preset into CSS payloads.
- The shell injects the preset CSS into the page iframe.
- The page HTML and JavaScript stay the same.
- CSS changes the appearance.
That means the installer can look completely different without rewriting the page logic.
What A Preset Changes
A preset can change things like:
- background color and panel tint
- button shape and fill style
- borders, spacing, and shadows
- font family
- banner treatment
- page-level accents for specific screens
- subtle motion and surface styling
A preset should not change core installer behavior. That means navigation, validation, IPC, and step execution stay shared.
What Stays The Same
These parts remain core and reusable:
- installer manifest parsing
- step runner and hook execution
- page navigation logic
- custom page widget behavior
- IPC message shapes
- validation rules
You should think of preset theming as a presentation layer, not a logic layer.
File Layout
The theme files live under `ui/themes.
Recommended structure:
ui/
themes/
default/
global.css
caramel_latte/
global.css
theme.json
css/
global.css
pages/
welcome.css
license.css
requirements.css
install_dir.css
components.css
summary.css
progress.css
finish.css
custom.css
user_info.css
error.css
html/
welcome.html
license.html
requirements.html
install_dir.html
components.html
summary.html
progress.html
finish.html
custom.html
user_info.html
error.html
What Each File Means
- `global.css applies the preset across the whole installer UI.
- `css/pages/<page>.css adjusts the look of one page only.
- `html/<page>.html is the page template with
:root { ... }CSS variable defaults baked in. - `theme.json is optional metadata for humans and tooling.
If a preset has no page-specific CSS, the installer still works. It just uses the global styling.
Current Manifest Contract
The manifest can use the following theme pattern:
theme:
preset: "caramel_latte"
accent_color: "#A0522D"
accent_dark_color: "#7A3B1E"
accent_light_color: "#F5E6D3"
background_color: "#FDF6EE"
surface_color: "#F5EAD8"
text_color: "#3B2314"
text_muted_color: "#8B6347"
border_color: "#D4B896"
success_color: "#5A7A3A"
success_bg_color: "#EFF5E8"
error_color: "#8B2500"
error_bg_color: "#FCF0EB"
progress_color: "#A0522D"
progress_light_color: "#E8C9A8"
font_family: "'Georgia', 'Palatino Linotype', serif"
border_radius: 12
Rules:
- `preset selects the visual theme pack.
- The other fields still work as overrides.
- If `preset is missing, the installer falls back to the default look.
- If a preset exists but a field is omitted, the UI still uses the built-in default token values.
How The Preset Is Applied
The runtime path is important.
- `installer.yaml is parsed into the manifest model.
- Theme data is stored in installer state.
- The runner sends theme data to the shell.
- The shell injects CSS into the page iframe before the page renders.
- Page JavaScript keeps handling validation, buttons, and events.
This is why the preset approach is safe: it changes the visual layer while preserving the core flow.
Why CSS First, Not HTML Replacement
The recommended theme mechanism is CSS-first.
That gives you:
- less duplication,
- less risk of breaking page logic,
- easier maintenance for teammates,
- a stable DOM contract across all presets,
- simpler diffs when adjusting a theme.
You can still use small JavaScript helpers if a preset needs micro-adjustments, but the default strategy should be CSS.
Avoid replacing the core page HTML for a preset unless you truly need a special one-off experience. That makes the system harder to maintain and easier to break.
How To Use A Theme Preset In `installer.yaml
Use the preset name in the `theme block.
Example:
theme:
preset: "caramel_latte"
Optional overrides can be added below it:
theme:
preset: "caramel_latte"
accent_color: "#B9764D"
border_radius: 10
That means:
- the preset provides the overall look,
- the override fields fine-tune specific tokens.
Example Walkthrough: Caramel Latte
The file [sdk/example/caramel_latte.yaml](../sdk/example/caramel_latte.yaml) shows the preset in action.
What it does:
- Selects `caramel_latte as the preset.
- Uses warm tan and beige colors.
- Uses a softer font and rounder surfaces.
- Keeps the same installer flow and custom pages.
- Still allows the existing color token system to override any individual value.
This is the pattern to copy when you create the next theme.
Step-by-Step: Add A New Theme
Follow these steps when creating a new preset.
1. Choose a clear name
Use a lowercase, underscore-separated preset name.
Examples:
- `caramel_latte
- `midnight_ember
- `forest_mist
Avoid spaces and punctuation.
2. Create the theme folder
Add a new folder under `ui/themes/<preset_name>.
Example:
ui/themes/midnight_ember/
global.css
pages/
welcome.css
summary.css
Start small. You do not need every page file on day one.
3. Define the global style layer
Put the broad look and feel in `global.css.
Use it for:
- background gradients,
- buttons,
- card surfaces,
- shadows,
- borders,
- typography,
- banner styling.
This file should make the preset recognizable at a glance.
4. Add page-specific overrides only when needed
Use `pages/<page>.css for page-specific polish.
Examples:
- a more dramatic welcome screen,
- a different summary card layout,
- a progress bar style,
- custom field borders for custom pages.
Do not repeat the same rule in every page file. Keep shared styling in `global.css.
5. Register the preset in the runner
The runner must know how to load the preset CSS files.
In practice, that means adding the preset to the theme bundle loader in [runner/src/main.rs](../runner/src/main.rs).
If the preset is not registered there, the installer can still parse the manifest, but the preset styling will not be shipped into the UI.
6. Use the preset in a manifest
Point the manifest at the new preset:
theme:
preset: "midnight_ember"
Then test the installer end to end.
7. Verify fallback behavior
Check that the installer still works if:
- `preset is omitted,
- a page-specific CSS file is missing,
- only token overrides are provided.
The default UI should remain functional in all three cases.
Design Rules For Future Themes
Use these rules so themes stay maintainable.
- Keep the page HTML unchanged unless there is a strong reason to change it.
- Prefer CSS over JS.
- Keep each theme visually distinct.
- Keep text readable and contrast high.
- Avoid loading remote assets.
- Keep assets local and packaged with the installer.
- Do not hard-code business logic in theme files.
- Treat themes as presentation packs, not application code.
Recommended Team Workflow
When a teammate adds a new theme:
- Duplicate an existing preset folder.
- Rename it.
- Change only the CSS variables and surface rules first.
- Test the welcome page and summary page.
- Add page-specific CSS only after the global look is stable.
- Update the example manifest if the theme should be demoed.
- Run a full build and launch the installer.
This avoids a common mistake: designing a theme from page files before the global visual system is stable.
When To Use Tokens Versus Presets
Use tokens when you only want brand tuning.
Examples:
- change the primary accent color,
- change fonts,
- slightly adjust radius or borders.
Use presets when you want a full visual identity.
Examples:
- Caramel Latte,
- a dark industrial theme,
- a soft pastel theme,
- a high-contrast enterprise theme.
A good rule of thumb is this:
- tokens customize,
- presets transform.
Current State Of The Repository
Right now the repository supports:
- `theme.preset in the manifest,
- runtime preset delivery from the runner,
- CSS injection through the shell,
- a shipped `caramel_latte example manifest,
- a clean folder structure for new presets.
That is enough to add more theme packs without changing the core installer flow.
Practical Summary
If you want a new theme, do this:
- Add a preset folder under `ui/themes.
- Put shared visual rules in `global.css.
- Put page-specific overrides in `pages/*.css.
- Register the preset in the runner.
- Set `theme.preset in
installer.yaml. - Build and test.
That is the safe, scalable way to theme this installer.
Custom Pages
Custom pages let an installer ask the user for extra information that does not fit the built-in welcome, license, install directory, component, summary, or finish screens.
In this codebase, a custom page is still a first-class installer page. It participates in normal navigation, validation, state snapshots, and variable substitution. The main difference is that you define the page data in the manifest instead of hard-coding the UI in Rust.
What A Custom Page Does
A custom page can:
- Collect text, multiline text, checkbox, choice, and folder path values.
- Bind those values to installer variables such as `CERT_DIR or
IMPORT_CERTS. - Block `Next until required fields are valid.
- Pass the collected values into later install steps and hooks.
- Optionally render advanced raw HTML when you need more control than the built-in widgets provide.
When To Use It
Use a custom page when you need one or more of these:
- A folder selection that is specific to your product.
- A setup question that changes later install behavior.
- A small decision form before the install starts.
- A page that needs several fields bound to variables.
Do not use a custom page when a built-in page already solves the problem. For example, the install directory page should still be used for the main target folder.
How The Runtime Uses It
The flow is simple:
- The manifest declares a page with `type: custom.
- The engine parses the page and validates its widget definitions.
- The runner loads the generic `custom.html template for that page.
- The template renders widgets or raw custom HTML.
- User input is sent back to Rust through IPC.
- Rust stores the values in installer state.
- When the install starts, the runner merges those custom values into the variable map.
- Later install steps and hooks can reference those values with `{{VARIABLE_NAME}}.
That means custom page values behave like normal installer variables once the install begins.
Manifest Shape
A custom page is defined under `pages.
A minimal page looks like this:
pages:
- type: custom
id: cert_folder
title: "Certificate Folder"
subtitle: "Choose where certificate files should be read from during setup."
widgets:
- type: folder_picker
id: cert_dir
label: "Certificates folder"
bind_to: CERT_DIR
default: "{{INSTDIR}}/certs"
browse_title: "Select certificate folder"
help_text: "This path is exposed to install steps as {{CERT_DIR}}."
required: true
must_exist: false
The important fields are:
- `type: custom identifies the page as a custom page.
- `id gives the page a stable identifier.
- `title is the page heading.
- `subtitle is optional explanatory text.
- `widgets defines the controls that the user sees.
You can also use `custom_html for advanced rendering, but the widget-based flow is the recommended path.
Widget Reference
The supported widget types are deliberately small and predictable.
`label
A read-only text block. Use it for instructions, warnings, or separators.
Example:
- type: label
id: cert_hint
text: "This folder will be used for certificate import during setup."
`text_input
A single-line text field. Bind it to a variable if you want the value available later.
Useful fields:
- `label
- `bind_to
- `default
- `placeholder
- `required
- `min_length
- `max_length
Example:
- type: text_input
id: company_name
label: "Company name"
bind_to: COMPANY_NAME
required: true
`multiline_input
A larger text box for notes, commands, or free-form content.
Example:
- type: multiline_input
id: install_notes
label: "Notes"
bind_to: INSTALL_NOTES
placeholder: "Optional notes for the installer"
`checkbox
A boolean toggle.
Example:
- type: checkbox
id: import_certs
label: "Import certificates during setup"
bind_to: IMPORT_CERTS
default: true
`radio_group
A set of mutually exclusive choices where the user must pick one option.
Useful fields:
- `label
- `bind_to
- `required
- `options
Example:
- type: radio_group
id: mode
label: "Install mode"
bind_to: INSTALL_MODE
required: true
options:
- label: "Standard"
value: standard
- label: "Advanced"
value: advanced
`dropdown
A compact choice selector.
Example:
- type: dropdown
id: region
label: "Region"
bind_to: REGION
options:
- label: "US"
value: us
- label: "EU"
value: eu
`folder_picker
A field that opens the native folder browser. This is the best option when the user needs to point the installer at an existing directory.
Useful fields:
- `label
- `bind_to
- `default
- `browse_title
- `help_text
- `required
- `must_exist
Example:
- type: folder_picker
id: cert_dir
label: "Certificates folder"
bind_to: CERT_DIR
default: "{{INSTDIR}}/certs"
browse_title: "Select certificate folder"
required: true
Binding Values To Later Steps
The `bind_to field is what turns a widget value into an installer variable.
If a widget is bound to `CERT_DIR, later steps can reference it with {{CERT_DIR}}. Hooks are defined under install.hooks.post_install:
install:
hooks:
post_install:
- run:
platform: windows
shell: powershell
wait: true
fail_on_nonzero: true
timeout_sec: 30
command: |
Write-Host "Certificate folder: {{CERT_DIR}}"
That is the key idea behind custom pages: the UI collects data once, then the rest of the installer uses it like any other variable.
The validator checks several things before the installer is built:
- Custom pages must have a non-empty `id.
- Page ids must not be duplicated.
- A custom page must define either `custom_html or at least one widget.
- Interactive widgets must have a non-empty `bind_to value.
- Choice widgets must contain options.
- Widget ids must be unique within the page.
This is intentional. Validation fails early so the manifest is not allowed to ship with broken page wiring.
Example Walkthrough
The example manifest at [sdk/example/installer.yaml](../sdk/example/installer.yaml) shows a concrete page that asks for a certificate folder and a boolean import flag.
The flow is:
- The page appears after component selection.
- The user chooses a folder in the native folder picker.
- The user optionally toggles whether certificates should be imported.
- The installer stores those values as `CERT_DIR and
IMPORT_CERTS. - The post-install hook prints the chosen values.
That is the simplest real-world pattern to start with.
Raw HTML Mode
If `custom_html is present, the generic template renders that HTML instead of generating widgets.
Use this mode only when you need something the widget set does not support yet. It is powerful, but it is also easier to make mistakes with because the installer cannot infer validation from arbitrary HTML.
When using raw HTML, you are responsible for sending values back to the engine through the page script.
Practical Recommendations
- Prefer widgets over `custom_html for most pages.
- Keep the page focused on a small number of decisions.
- Bind every user-entered value that later install logic depends on.
- Use clear page ids and variable names so the manifest stays readable.
- Test the page in a full build, not only by reading the YAML.
If you follow those rules, custom pages stay predictable and easy to maintain.
Hagane Logging Guide
This guide explains exactly how installer logging works with the `install DSL, what auto mode does, what manual_only mode does, and how to choose between them.
What logging controls
Hagane logs can go to two destinations:
- Installer UI log stream
- Optional log file on disk
Top-level logging config:
logging:
mode: auto
path: "{{INSTDIR}}/logs"
file_name: "installation.log"
timestamp: true
include_raw_os_error: false
slow_step_warn_sec: 10
Field meanings:
- mode: `auto or
manual_only. - path: folder for log file output.
- file_name: log file name.
- timestamp: when true, each file log line includes local timestamp.
- include_raw_os_error: reserved compatibility field for diagnostics policy.
- slow_step_warn_sec: threshold in seconds for slow-step warning lines. Must be greater than 0.
Exact mode behavior
Auto mode
In `mode: auto, Hagane emits lifecycle logs for every compiled install operation:
- Start of step: info
- Slow step (elapsed >= slow_step_warn_sec): warn
- Step failure: error with classified HG-* code
- Rollback failures: error
When file logging is configured, completion lines remain in the log file but are not echoed into the UI log box. This avoids showing the same lifecycle text twice in the visible installer UI.
Skip behavior in auto mode:
- If a compiled operation has `component and that component is not selected, the operation is skipped.
- Hagane emits an info skip line showing the reason.
Manual-only mode
In `mode: manual_only, normal execution logs are suppressed:
- Hagane does not auto-emit lifecycle start/success/skip messages.
- Slow-step warnings are not emitted for normal operations.
Important: failures are still always logged.
- Classified step failures (HG-* lines) are emitted in both modes.
- Rollback errors are emitted in both modes.
This keeps troubleshooting reliable even when normal logs are fully manual.
Install DSL logging scope
The top-level `install DSL does not expose per-operation inline log blocks. Logging is controlled by logging.mode and emitted from the compiled execution plan.
Rules:
- File logging requires both `logging.path and
logging.file_name. - `auto mode gives lifecycle visibility with minimal YAML.
- `manual_only is best when you only want failure-classification output during install execution.
Recommended usage patterns
Pattern A: Fully automatic lifecycle logging
Use this when you want full traceability with minimal YAML noise.
logging:
mode: auto
path: "{{TEMP}}/MyAppLogs"
file_name: "installation.log"
slow_step_warn_sec: 10
install:
setup:
create_dirs:
- "{{INSTDIR}}"
components:
core:
archive: "payload.zst"
target: "{{INSTDIR}}"
finalize:
windows:
write_uninstaller: "{{INSTDIR}}/uninstall.exe"
linux:
write_uninstaller: "{{INSTDIR}}/uninstall"
What you get:
- Automatic start/success lines for each step
- Automatic slow-step warnings
- Automatic classified failures
Pattern B: Failure-focused logs
Use this when you only want deterministic classified failures during install execution.
logging:
mode: manual_only
path: "{{TEMP}}/MyAppLogs"
file_name: "installation.log"
install:
setup:
create_dirs:
- "{{INSTDIR}}"
components:
core:
archive: "payload.zst"
target: "{{INSTDIR}}"
finalize:
windows:
write_uninstaller: "{{INSTDIR}}/uninstall.exe"
linux:
write_uninstaller: "{{INSTDIR}}/uninstall"
What you get:
- No automatic lifecycle chatter for successful execution
- Still gets classified error lines on failure
Example output
Auto mode success path (illustrative):
[INFO] Starting step 1/3: Creating directory C:\Program Files\Acme\MyApp
[INFO] Completed step 1/3: Creating directory C:\Program Files\Acme\MyApp
[INFO] Starting step 2/3: Extracting payload.zst
[WARN] Extracting payload.zst is taking longer than expected (12s)
[INFO] Completed step 2/3: Extracting payload.zst
Failure output (both modes):
[ERROR] HG-EXTRACT-001 step=2 action=extract field=archive value=payload.zst reason="archive 'payload.zst' is missing from embedded payload" fix="Run hagane build again and ensure the archive source folder exists near installer.yaml."
[ERROR] Rollback error: ...
Validation rules to remember
- File logging always requires top-level `logging.path and
logging.file_name. - `logging.slow_step_warn_sec must be greater than 0 when present.
- For hooks, `run.command and
run.shellare required.
Quick decision guide
Use `auto when:
- You want complete step lifecycle visibility by default.
- You want fewer per-step log entries in YAML.
- You want easier support diagnostics.
Use `manual_only when:
- You want minimal normal logging output.
- You rely on HG error codes and rollback logs for diagnostics.
- You are validating error handling behavior.
Hagane Installer Error Codes and Logging Guide
This document defines stable v1 error codes and logging behavior in `installer.yaml.
Goals
- Show actionable install failures to users.
- Keep logging behavior deterministic across auto and manual modes.
- Provide consistent error codes for support and troubleshooting.
Logging Modes and Hooks
Hagane logging is configured globally and applies to the compiled operations from `install. Post-install commands are defined in install.hooks.post_install.
Logging Configuration Block
Add a top-level `logging block:
logging:
mode: auto # "auto" or "manual_only" (default: auto)
path: "{{INSTDIR}}/logs" # Directory to store log files
file_name: "installation.log" # Log file name
timestamp: true # Prefix each log line with ISO timestamp (default: true)
include_raw_os_error: false # Include raw OS error details in auto-logged errors (default: false)
slow_step_warn_sec: 10 # Warn threshold in seconds for long-running steps
Logging Configuration Parameters
| Parameter | Type | Default | Required? | Notes |
|---|---|---|---|---|
| `mode | string | `auto | No | `auto logs lifecycle messages (start/warn/success) and classified failures. manual_only suppresses normal lifecycle logging during execution. |
| `path | string | — | Recommended | Installation directory must be resolvable. Supports `{{INSTDIR}}, {{PROGRAMFILES}}, {{APPDATA}}, {{LOCALAPPDATA}}. |
| `file_name | string | — | Recommended | Name of the log file (e.g., `installation.log, setup.log). |
| `timestamp | boolean | `true | No | If `true, each log line is prefixed with ISO 8601 timestamp (e.g., 2026-04-12T14:32:01.234Z). |
| `include_raw_os_error | boolean | `false | No | If `true, automatic error classification includes raw Windows OS error details (may expose implementation details). |
| `slow_step_warn_sec | integer | `10 | No | Threshold in seconds for slow-step warning lines. Must be greater than 0. |
Install DSL Logging Behavior
Log level is automatic in `mode: auto:
- Start-of-operation log: `info
- Long-running operation notice: `warn
- Failures: `error
Note: When file logging is desired, set both `logging.path and logging.file_name.
Conditional Execution with Components
Component-based operations are skipped if that component is not selected by the user during installation.
Supported DSL Locations with `component
- `install.system.shortcuts[*].component
- `install.system.path.component
Example: Component-Based Installation
components:
- id: core
name: "Core Application"
required: true
selected: true
- id: docs
name: "Documentation"
required: false
selected: true
- id: dev_tools
name: "Developer Tools"
required: false
selected: false
install:
components:
core:
archive: "core.zst"
target: "{{INSTDIR}}"
docs:
archive: "docs.zst"
target: "{{INSTDIR}}/docs"
dev_tools:
archive: "devtools.zst"
target: "{{INSTDIR}}/devtools"
system:
windows:
shortcuts:
- name: "Developer Shell"
target: "{{INSTDIR}}/devtools/shell.exe"
location: start_menu
component: dev_tools
path:
add: "{{INSTDIR}}/devtools/bin"
scope: user
component: dev_tools
Post-Install Command Hooks
`install.hooks.post_install executes commands with automatic error classification.
PowerShell Hook (Windows)
install:
hooks:
post_install:
- run:
platform: windows
command: |
Write-Host "Hello from installer"
[Environment]::SetEnvironmentVariable("MY_VAR", "value", "User")
shell: powershell
wait: true
fail_on_nonzero: true
timeout_sec: 30
Bash Hook (Linux)
install:
hooks:
post_install:
- run:
platform: linux
command: |
chmod +x "{{INSTDIR}}/bin/myapp"
ln -sf "{{INSTDIR}}/bin/myapp" /usr/local/bin/myapp
shell: bash
wait: true
fail_on_nonzero: false
timeout_sec: 10
Program Hook Execution
Execute a normal process command:
install:
hooks:
post_install:
- run:
command: "{{INSTDIR}}/bin/post_install.exe --mode setup"
shell: program
wait: true
fail_on_nonzero: true
timeout_sec: 60
`run Parameters
| Parameter | Type | Default | Required? | Notes |
|---|---|---|---|---|
| `command | string | — | Yes | Script content (`powershell/bash) or command line (program). |
| `shell | string | — | Yes | `powershell (Windows only), bash (Linux only), or program (all platforms). |
| `platform | string | (all) | No | Restrict hook to `windows or linux. Omit to run on all platforms. |
| `wait | boolean | `true | No | If `true, the installer waits for the script to complete before continuing. If false, script runs in background. |
| `fail_on_nonzero | boolean | `true | No | If `true, a non-zero exit code from the script fails the entire installation. If false, non-zero exits are ignored. |
| `timeout_sec | number | (none) | No | Maximum execution time in seconds. If the script exceeds this time and `wait=true, it’s terminated and classified as HG-PS-004 (timeout). |
Hook Execution Notes
- PowerShell error handling: Scripts are automatically wrapped with `$ErrorActionPreference='Stop' to ensure deterministic error codes.
- Bash output: Bash hook stdout is logged at `INFO level and stderr at
WARNlevel. Both appear in the installer log stream so failures are always visible even whenfail_on_nonzero: false. - **Elevation**: If the installer runs with admin elevation, hooks execute with the same privileges.
- **Error classification**: HG-PS-001 through HG-PS-005 codes apply to both PowerShell and bash hook failures.
- Working Directory: Hooks inherit the installer's working directory (`{{INSTDIR}}).
Error Line Format
When an installation step fails and logging is enabled (default `mode: auto), the installer automatically generates an error line in the following format:
[ERROR] <CODE> step=<N> action=<ACTION> field=<FIELD> value=<VALUE> reason="..." fix="..."
Error Line Components
| Component | Description | Example |
|---|---|---|
| `CODE | 9-character stable v1 error code (HG-XXXX-NNN) | `HG-EXTRACT-001 |
| `step | Compiled operation index (1-indexed) | `step=4 |
| `action | The operation action type | `action=extract |
| `field | The problematic field in the operation configuration | `field=archive |
| `value | The value of that field (may be truncated) | `value=docs.zst |
| `reason | Root cause of the failure (auto-classified) | `reason="archive 'docs.zst' is missing from embedded payload" |
| `fix | Recommended action to resolve the error | `fix="Run hagane build again and ensure the archive source folder exists near installer.yaml." |
Example Error Log Line
[ERROR] HG-EXTRACT-001 step=4 action=extract field=archive value=docs.zst reason="archive 'docs.zst' is missing from embedded payload" fix="Run hagane build again and ensure the archive source folder exists near installer.yaml."
Stable v1 Error Codes (Complete Reference)
The screenshots below show what CLI errors look like in practice. A missing or unresolvable manifest path:

An invalid or malformed `installer.yaml that fails schema validation:

Manifest and Variable Resolution
`HG-YAML-001 — Manifest Schema Validation Failure
**When it occurs:** During manifest parsing or validation, the installer detects invalid YAML structure, missing required fields, or invalid field values.
**Common causes:**
- Missing `app.name,
app.version, orapp.publisher - Missing `pages with at least one
installtype page - Invalid registry hive name (not one of: HKLM, HKCU, HKCR, HKU, HKCC)
- Invalid environment scope (`scope must be
userorsystem) - Invalid environment operation (`operation must be
set,append, orprepend) - Missing or malformed `install block
- Hook with missing `run.command or
run.shell
Typical fix: Review the error reason and correct the manifest YAML. Re-run `hagane build to re-validate.
**Field mapping in error line:**
field=key field (e.g., app.name, logging.path, [registry|env|step] configuration)
value=malformed value
`HG-VAR-001 — Unresolved Installer Variable
**When it occurs:** During step execution, a path-like field contains a variable reference that cannot be resolved.
**Supported variables:**
- `{{INSTDIR}} (or legacy
$INSTDIR) — installation directory - `{{PROGRAMFILES}} (or legacy
$PROGRAMFILES) — typically C:\Program Files - `{{PROGRAMFILES64}} (or legacy
$PROGRAMFILES64) — 64-bit variant - `{{APPDATA}} (or legacy
$APPDATA) — per-user Application Data folder - `{{LOCALAPPDATA}} (or legacy
$LOCALAPPDATA) — per-user Local\Application Data folder - `{{TEMP}} (or legacy
$TEMP) — user temp directory - `{{WINDIR}} (or legacy
$WINDIR) — Windows system directory - Any custom variable declared in top-level `variables: (for example
{{APP_REG_KEY}})
**Common causes:**
- Misspelled variable name (e.g., `{{INSTDIR_ROOT}} instead of
{{INSTDIR}}) - Typo inside braces (e.g., `${INSDIR})
- Custom variables (not supported)
**Typical fix:** Correct the variable name and re-run the installer.
Authoring note: Custom variables must be declared in `variables: and use keys with A-Z, 0-9, and _ (optionally prefixed with $). Use {{KEY}} as the preferred template syntax.
Extract and Copy Operations
`HG-EXTRACT-001 — Archive Missing from Payload
When it occurs: An `extract action references an archive name that was not embedded during build.
**Cause:** The archive source folder specified in the build command does not exist, or its contents were not found.
**Typical fix:**
- Verify the archive source directory exists in your build context.
- Re-run `hagane build to re-scan and embed archives.
- Verify the `archive field in the manifest matches the embedded name.
**Field mapping:**
field=archive
value=<archive name from manifest>
`HG-EXTRACT-002 — Extraction I/O Failure or Destination Not Writable
**When it occurs:** The extraction process fails due to I/O errors, permission issues, or an invalid destination path.
**Common causes:**
- Destination directory is on a read-only drive
- Destination path contains invalid characters or is malformed
- File locking (e.g., antivirus or file explorer blocking writes)
- Insufficient disk space
- Running without elevation when destination is protected (e.g., C:\Program Files under some Windows configurations)
**Typical fix:** Check destination path permissions, close file locks, ensure adequate free disk space, or run the installer with elevated privileges.
**Field mapping:**
field=destination
value=<destination path from manifest>
`HG-COPY-001 — Copy Source File Missing or Invalid
When it occurs: A `copy_file action references a source file that does not exist or cannot be read.
**Common causes:**
- Source file was not extracted or copied in a previous step
- Source path typo or incorrect variable resolution
- Source file permission-denied (rare)
- Source file was deleted between build and installation
Typical fix: Verify the source file path in the manifest and confirm previous `extract or copy_file steps produce the expected file.
**Field mapping:**
field=source
value=<source path from manifest>
Registry Operations
You can either use low-level `registry actions or high-level actions:
- `register_uninstall for Add/Remove Programs metadata
- `register_app for app settings (
InstallDir,Version)
`HG-REG-001 — Invalid Registry Configuration
When it occurs: A `registry step contains structurally invalid configuration (invalid hive, key format, or type).
**Common causes:**
- Invalid `hive name (must be: HKLM, HKCU, HKCR, HKU, or HKCC)
- `value_type mismatch (e.g.,
BINARYtype with string value) - Invalid key path syntax
- Missing required fields for the operation
- Missing required `register_uninstall fields (name/version/publisher/install location/uninstall string)
- Missing required `register_app fields (
inst_loc/install_location,version)
**Typical fix:** Validate the registry hive, key, value type, and value format. Test with regedit if unsure.
**Field mapping:**
field=key
value=<HIVE\Key path>
`HG-REG-002 — Registry Access Denied (Permission Required)
When it occurs: A `registry action attempts to write to a protected hive or key that requires elevated privileges.
**Common registry locations requiring admin:**
- `HKLM\Software\* — HKEY_LOCAL_MACHINE (system-wide)
- `HKCR\* — HKEY_CLASSES_ROOT (system-wide)
**Typical fix:**
- For system-wide settings: Ensure installer runs with administrator elevation. Set `app.require_admin: true in manifest.
- For user-only settings: Use `HKCU instead of
HKLM. - At build time: Verify `app.require_admin is set correctly in
installer.yaml.
**Field mapping:**
field=key
value=<HIVE\Key path>
Environment Variables
`HG-ENV-001 — Environment Variable Operation Failed
When it occurs: An `env_var step fails due to invalid scope, operation, or permission issues.
**Valid Scope Values:**
- `user — per-user environment
- Windows: writes to `HKEY_CURRENT_USER
- Linux: appends an `export line to
~/.bashrcand~/.profilefor the installing user (tracked via$SUDO_USERwhen running withsudo) - `system — system-wide environment — **requires admin elevation**
- Windows: writes to `HKEY_LOCAL_MACHINE
- Linux: writes `/etc/profile.d/hagane-<name>.sh (login shells) **and** appends to
/etc/bash.bashrc(non-login interactive shells such as WSL2 default terminals)
**Valid Operation Values:**
- `set — replace or create variable
- `append — add to the end of existing value (with
;separator on Windows,:on Linux via shell export) - `prepend — add to the beginning of existing value
**Common causes:**
- Invalid scope or operation spelling
- Attempting system-wide operation (`scope: system) without elevation
- On Windows: registry write failure due to ACLs
- On Linux: insufficient permissions to write `/etc/bash.bashrc or
/etc/profile.d/
**Typical fix:**
- Validate `scope and
operationvalues in manifest. - For system scope: Run installer as Administrator (Windows) or with `sudo (Linux). Set
app.require_admin: true. - **For user scope:** No elevation required; ensure the shell config files exist in the user's home directory.
**Field mapping:**
field=operation
value=scope=<scope>, operation=<operation>
Program Execution
`HG-RUN-001 — Executable Not Found
When it occurs: A `run_program action references an executable that cannot be located in PATH or at the specified path.
**Common causes:**
- Executable not extracted or copied in previous steps
- Path typo or incorrect variable resolution
- File extension missing (e.g., `.exe)
- Executable is for a different architecture or OS
**Typical fix:**
- Verify the executable was extracted or copied by a previous step.
- Check the path in the manifest; ensure variables resolve correctly.
- Test the executable exists by extracting manually and checking with file explorer.
**Field mapping:**
field=executable
value=<executable path/filename from manifest>
`HG-RUN-002 — Process Non-Zero Exit Code
When it occurs: A `run_program step executes but returns a non-zero exit code, indicating failure.
**Common causes:**
- Program encountered an error (e.g., setup hook failed)
- Program requires arguments or configuration not provided
- Program dependency is missing (e.g., .NET Framework, library)
- Permission issue during program execution
**Typical fix:**
- Review the program's documentation and verify arguments are correct.
- Ensure all dependencies are present in the system or bundled.
- If the program's failure should not stop installation, set `fail_on_nonzero: false in
run_programdefinition. - Run the program manually from the install directory to see detailed error output.
**Field mapping:**
field=executable
value=<executable path from manifest>
PowerShell Execution
`HG-PS-001 — PowerShell Parse/Syntax Error
**When it occurs:** PowerShell script contains syntax errors and cannot be parsed.
**Common causes:**
- Unclosed quotes or braces
- Misspelled keywords (e.g., `if () with invalid condition)
- Invalid pipeline syntax
- Escape character issues in YAML (remember PowerShell needs correct escaping)
**Typical fix:**
- Test the script manually in PowerShell: `powershell -NoProfile -File script.ps1
- Fix syntax errors.
- Use inline `script: in manifest for simple code and external
.ps1files for complex scripts.
**Field mapping:**
field=script
value=<script name or first line>
`HG-PS-002 — PowerShell/Command Not Found
**When it occurs:** The PowerShell executable cannot be invoked, or a referenced cmdlet/command is not available.
**Common causes:**
- PowerShell not in PATH (rare on Windows)
- Cmdlet does not exist or is misspelled
- Cmdlet from a module that is not imported
- Module path issues in restricted environments
**Typical fix:**
- Verify PowerShell exists on the target system (should be standard on Windows 7+).
- Verify cmdlet names and module availability.
- Wrap cmdlet usage in full namespace if needed (e.g., `Microsoft.PowerShell.Utility\Get-Member).
- Pre-import modules at the start of the script if needed.
**Field mapping:**
field=script
value=<script name or first line>
`HG-PS-003 — PowerShell Non-Zero Exit Code
**When it occurs:** PowerShell script executes but exits with a non-zero code (script logic error or explicit exit call).
**Common causes:**
- Script explicitly calls `exit 1 or similar
- Terminating error occurs and `$ErrorActionPreference is not set to
Stop(handled automatically) - Script returns a falsy value that PowerShell interprets as failure
**Typical fix:**
- Review script logic near the end; check for explicit `exit calls.
- Ensure error handling is correct (`try/
catchblocks). - Test script manually and note the exact PowerShell error.
**Field mapping:**
field=script
value=<script name or first line>
`HG-PS-004 — PowerShell Timeout
When it occurs: A PowerShell script runs longer than the specified `timeout_sec and is terminated.
**Cause:** Script took too long to execute, likely due to waiting for resources, network operations, or infinite loops.
**Typical fix:**
- Increase `timeout_sec in the manifest (if the delay is expected).
- Optimize the script to reduce execution time.
- Break long-running operations into smaller async tasks.
**Field mapping:**
field=timeout_sec
value=<timeout value in seconds>
`HG-PS-005 — PowerShell Access Denied / Execution Policy Blocked
**When it occurs:** PowerShell script is blocked by execution policy, security manager, or access control.
**Common causes:**
- Script Execution Policy is `Restricted (default on non-admin shells)
- Script is not digitally signed but policy requires it
- User lacks permissions to run scripts
- Antivirus or security software blocked execution
**Typical fix:**
- **For user scripts:** Run installer with elevated privileges (admin), which relaxes policy temporarily.
- Persistent: Edit ExecutionPolicy (requires admin): `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
- **For secure environments:** Request script signing or provide security exemption.
- In manifest: Set `app.require_admin: true to ensure elevated context during script execution.
**Field mapping:**
field=script
value=<script name or first line>
Recommended Authoring Pattern
Manifest Structure with Logging
Here's a complete example showing best practices for using logging and error detection:
app:
name: "My Application"
version: "1.0.0"
publisher: "Acme Inc"
require_admin: false # Set to true if registry (HKLM) or system env vars are needed
logging:
mode: auto # "auto" (default) or "manual_only"
path: "{{INSTDIR}}/logs"
file_name: "installation.log"
timestamp: true
include_raw_os_error: false
pages:
- type: welcome
title: "Welcome to My Application"
- type: requirements
- type: install_dir
- type: install
- type: finish
install:
setup:
create_dirs:
- "{{INSTDIR}}"
- "{{INSTDIR}}/logs"
components:
core:
archive: "app_binaries.zst"
target: "{{INSTDIR}}/bin"
system:
windows:
register_app:
hive: HKCU
key: "Software/Acme/MyApp"
version: "1.0.0"
install_location: "{{INSTDIR}}"
path:
add: "{{INSTDIR}}/bin"
scope: user
hooks:
post_install:
- run:
platform: windows
command: |
Write-Host "Configuring application at {{INSTDIR}}"
shell: powershell
wait: true
fail_on_nonzero: true
timeout_sec: 60
- run:
platform: linux
command: |
chmod +x "{{INSTDIR}}/bin/myapp"
ln -sf "{{INSTDIR}}/bin/myapp" /usr/local/bin/myapp
shell: bash
wait: true
fail_on_nonzero: true
timeout_sec: 30
finalize:
windows:
write_uninstaller: "{{INSTDIR}}/Uninstall.exe"
linux:
write_uninstaller: "{{INSTDIR}}/uninstall"
Error Handling and Troubleshooting Pattern
When errors occur, use the logged error line to diagnose:
- Extract the error code from the log: `[ERROR] HG-XXXXX-NNN ...
- **Find the code section** in this document
- Read the `field and
value**: These describe what went wrong - Review the `fix**: Recommended resolution steps
**Example log output:**
[ERROR] HG-ENV-001 step=8 action=env_var field=operation value=scope=system, operation=set reason="registry open failed" fix="Use scope user/system and operation set/append/prepend. For system scope, run installer elevated."
**Interpretation:**
- Step 8 tried to set a system environment variable
- The installer was not run with admin privileges
- Fix: Set `app.require_admin: true in manifest or run installer as Administrator
Troubleshooting Checklist
If users report an error code:
- Extract the error code from the log line: `HG-XXXX-NNN
- **Find the code** in the "Stable v1 Error Codes" section above
- **Read the description** to understand what failed
- Review the `field,
value,reason, andfix** from the error line - **Apply the recommended fix:**
- Correct `installer.yaml configuration
- Fix manifest logic (e.g., use correct registry hives, variables, operators)
- Update payload layout or ensure proper extraction order
- For elevation issues: Set `app.require_admin: true
- **Re-build and re-test:**
- Run `hagane run installer.yaml --release to build the installer
- Test the installer in silence mode: `installer-setup.exe /S
- Check `%TEMP%\HaganeInstall\logs\installation.log for detailed error output
- **Iterate** until the error no longer appears
Logging Configuration Best Practices
- **Include a top-level logging block** when you need file output.
- **Use mode: auto** (default) for automatic error classification — provides rich context
- **Use mode: manual_only** only if you want to suppress normal lifecycle logs
- **Set timestamp: true** for debugging sequences of steps
- Store logs in user-writable location like `{{TEMP}} for non-admin testing,
{{INSTDIR}}for normal installs - **Review logs after test failures** to extract error codes and diagnose issues
Compatibility Notes
- **Stable v1 Codes**: These 15 codes are stable and will not change in v1. New failure types will receive new code families.
- Field/Value changes: The `field,
value,reason, andfixsections may be enhanced but will maintain semantic compatibility. - **Variable Support**: The variable list ({{INSTDIR}}, {{PROGRAMFILES}}, etc.) is fixed v1. New variables will only be added in v2+.
- **Scope/Operation Enums**: Environment scope and operation values are fixed v1 (user, system; set, append, prepend).
Testing the Installer Engine
Complete Workflow: Build & Test
Prerequisites
✅ All crates compile successfully:
cargo build --release
1️⃣ Build the `hagane CLI Tool
cargo build --release -p builder
Output: `target/release/hagane.exe
2️⃣ Prepare Payload (Example Installer)
The builder compresses directories referenced in your `install.components block. For the example, you need:
Create minimal test payload:
cd c:\Users\monip\code\Installer-Engine\sdk\example
# Create payload directories
mkdir -Force payload docs samples
# Add dummy files (required for archives to exist)
echo "MyApp version 2.1.0" > payload\version.txt
echo "Sample documentation" > docs\README.txt
echo "Sample project files" > samples\example.txt
Directory structure should look like:
sdk/example/
├── installer.yaml
├── assets/
│ ├── logo.png
│ ├── banner.png
│ └── icon.ico
├── payload/ ← compressed to payload.zst
│ └── version.txt
├── docs/ ← compressed to docs.zst
│ └── README.txt
└── samples/ ← compressed to samples.zst
└── example.txt
3️⃣ Build the Installer Executable
cd sdk\example
# Auto-discovery: if installer.yaml is the only YAML in the directory
..\..\target\release\hagane.exe run --release
# Or with an explicit manifest path
..\..\target\release\hagane.exe run installer.yaml --release
.yml file is present in the current directory. If multiple YAML files are found, Hagane prints a warning listing them all and exits — specify the manifest explicitly in that case.
What it does:
- ✅ Loads and validates `installer.yaml
- ✅ Loads assets (logo, banner, icon)
- ✅ Compresses payload directories (payload/, docs/, samples/)
- ✅ Generates runtime embedded artifacts
- ✅ Runs `cargo build --release to compile the final
.exe
Output:
target/release/MyApp-setup.exe
4️⃣ Test the Installer
**GUI Mode (Default)**
# Run the installer
..\..\target\release\MyApp-setup.exe
This opens the WebView2-based GUI with pages:
- Welcome screen
- License agreement
- System requirements check
- Installation directory picker
- Component selection
- Installation summary
- Progress bar
- Finish screen
**Silent Mode (No UI)**
# Install without UI
..\..\target\release\MyApp-setup.exe /S
Uses default settings from the manifest's `silent: section.
5️⃣ Verify Installation
Default install location:
C:\Program Files\Acme\MyApp\
What should be installed:
- `version.txt (from payload)
- `docs/ (from docs archive)
- `samples/ (from samples archive)
- `uninstall.exe (auto-generated)
Check registry:
# Verify Add/Remove Programs entry
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\AcmeMyApp"
# Verify app configuration
reg query "HKLM\SOFTWARE\Acme\MyApp"
Troubleshooting
Error: "Missing archive 'payload'"
Cause: No `payload/ directory exists **Fix:**
mkdir payload
echo "test" > payload\test.txt
Error: "Missing asset 'assets/banner.png'"
**Cause:** Referenced in manifest but file doesn't exist **Fix:** Either:
- Create the file: `copy assets\logo.png assets\banner.png
- Remove from manifest: `banner: null or delete the line
No UI / WebView2 error
**Cause:** WebView2 Runtime not installed **Fix:**
- Install [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/download/)
- Or use `--build-runtime-check flag (if implemented)
Installation path issues
Edit `app.default_install_dir in installer.yaml:
app:
default_install_dir: "{{PROGRAMFILES64}}/MyCompany/MyApp"
Logging and error code validation
Use the following checks to verify the implemented logging and error code behavior:
- Add a `logging block with
pathandfile_nameto your test manifest. - Set `logging.mode to
autoand run once to verify lifecycle logs are emitted automatically. - Switch to `logging.mode: manual_only and verify normal lifecycle messages are suppressed.
- Confirm the installer writes a log file in the configured location.
- Trigger a known failure, such as a missing archive, to confirm the installer emits an `HG-* code.
- Confirm `install.hooks.post_install failures classify correctly for syntax errors, non-zero exit, timeout, and access denied cases.
Example test output should include lines like:
[ERROR] HG-EXTRACT-001 step=4 action=extract field=archive value=payload.zst reason="..." fix="..."
Quick Start Template
Minimal installer with no archives:
1. Create `installer.yaml:
app:
name: "HelloWorld"
version: "1.0.0"
publisher: "MyCompany"
default_install_dir: "{{PROGRAMFILES64}}/MyCompany/HelloWorld"
require_admin: false
pages:
- type: welcome
- type: summary
- type: install
- type: finish
install:
setup:
create_dirs:
- "{{INSTDIR}}"
components:
core:
archive: "payload.zst"
target: "{{INSTDIR}}"
system:
windows:
register_app:
hive: HKCU
key: "Software/MyCompany/HelloWorld"
version: "1.0.0"
install_location: "{{INSTDIR}}"
finalize:
windows:
write_uninstaller: "{{INSTDIR}}/uninstall.exe"
linux:
write_uninstaller: "{{INSTDIR}}/uninstall"
2. Build:
hagane.exe run installer.yaml --release
3. Test:
target/release/HelloWorld-setup.exe
Logging-focused Quick Start
If you want to test the logging pipeline directly, add logging and a post-install hook:
logging:
mode: auto
path: "{{TEMP}}/MyAppLogs"
file_name: "installation.log"
timestamp: true
slow_step_warn_sec: 5
install:
hooks:
post_install:
- run:
platform: windows
command: "Write-Host 'Testing PowerShell action'"
shell: powershell
wait: true
fail_on_nonzero: true
Build Notes
Use release mode for shipping builds:
hagane.exe run installer.yaml --release
For rapid local iteration, run without `--release from source during development:
cargo run -p builder --bin hagane -- run installer.yaml
Testing Requirements Check
All system requirements are checked **in parallel** (no PowerShell):
- OS Version → WinAPI `RtlGetVersion()
- RAM → WinAPI `GlobalMemoryStatusEx()
- Disk Space → WinAPI `GetDiskFreeSpaceEx()
- **Windows Update KB** → Registry query
- .NET Framework → Registry `HKLM\SOFTWARE\Microsoft\NET Framework Setup
- **VC++ Redistributable** → Registry scan
Verify these work on your system by:
- Opening the installer
- Going to Requirements page
- Checking results display instantly (parallel evaluation)
Advanced Testing
Capture build logs:
hagane.exe run installer.yaml --release 2>&1 | Tee-Object build.log
Check embedded.rs:
# View generated manifest
Get-Content runner/src/generated/embedded.rs | Select-Object -First 50
Monitor installation:
# Watch the installer write files
Get-Process explorer | ForEach-Object { watcher }
# Or use Process Monitor: https://docs.microsoft.com/en-us/sysinternals/downloads/procmon
Next Steps
- ✅ Run example: `MyApp-setup.exe
- ✅ Customize `installer.yaml with your app
- ✅ Add your files to `payload/,
docs/, etc. - ✅ Rebuild and test
- ✅ Ship the `.exe
Linux Testing (WSL2 / Ubuntu)
Prerequisites
Install WebKitGTK build dependencies in WSL2:
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev
Build Linux Installer Binary
From the workspace root on Linux/WSL2:
cargo build --release -p runner
Output: `target/release/<app-name>-linux-x86_64
Run the Linux Installer
The GUI uses wry + WebKitGTK. WSLg provides `$DISPLAY automatically.
./target/release/myapp-linux-x86_64
If `app.require_admin: true, the installer re-launches itself with sudo automatically.
Verify PATH Integration
After installation completes, open a **new terminal** and check:
# If user scope was selected:
grep "INSTDIR" ~/.bashrc
# If system scope was selected:
cat /etc/profile.d/hagane-path.sh
grep "INSTDIR" /etc/bash.bashrc
# Verify the binary is in PATH:
which myapp
myapp --version
WSL2 note: `/etc/profile.d/ only applies to login shells. System scope also writes to /etc/bash.bashrc so the PATH works in all WSL2 terminal sessions without requiring a login shell.
Verify Symlink (if created by post-install hook)
ls -la /usr/local/bin/myapp
# Should show: /usr/local/bin/myapp -> /usr/local/myapp/bin/myapp
Test Linux Uninstall
sudo /usr/local/myapp/uninstall.sh
The uninstall script:
- Removes the install directory (`rm -rf)
- Removes the `/usr/local/bin/<name> symlink if it points into the install directory
- Cleans `~/.bashrc and
~/.profileentries (using$SUDO_USERto target the correct user's home) - Cleans `/etc/bash.bashrc entries
- Removes `/etc/profile.d/hagane-path.sh
Verify after uninstall:
test -d /usr/local/myapp && echo "FAIL: dir still exists" || echo "OK: removed"
which myapp 2>/dev/null && echo "FAIL: still in PATH" || echo "OK: not in PATH"
Test Post-Install Bash Hook Logging
Add a bash hook to your test manifest:
install:
hooks:
post_install:
- run:
platform: linux
command: |
echo "Hook stdout line"
echo "Hook stderr line" >&2
shell: bash
wait: true
fail_on_nonzero: true
After running the installer, confirm the log shows:
- `[INFO] [bash] Hook stdout line
- `[WARN] [bash stderr] Hook stderr line
Hagane Shipping Guide
This document covers building, packaging, installing, and validating the Hagane CLI that is shipped to users. Both Windows and Linux targets are covered.
What Is Shipped
Windows install layout
- `C:\Program Files\Hagane\bin\hagane.exe
- `C:\Program Files\Hagane\runtime\... (embedded workspace used at build time)
Linux install layout
- `/usr/local/hagane/bin/hagane
- `/usr/local/bin/hagane → symlink created by post-install hook for immediate PATH access
- `/etc/profile.d/hagane-path.sh and
/etc/bash.bashrcentry (if system PATH component selected) - `/usr/local/hagane/uninstall.sh — generated uninstall script
The installed `hagane binary compiles user installers from any directory by using the bundled runtime workspace.
Runtime Source Of Truth
- The authoritative code lives in root workspace crates: `engine/,
runner/, andui/. - `hagane/payload/runtime is generated at build time when packaging
hagane/installer.yaml. - This avoids maintaining duplicate runtime source trees in git while still shipping a self-contained installed Hagane.
Build Hagane CLI
Windows
cargo build -p builder --bin hagane --release
Output: `target/release/hagane.exe
Linux
cargo build -p builder --bin hagane --release
Output: `target/release/hagane
Workspace Version Management
The version is defined once in the root `Cargo.toml under [workspace.package] and inherited by all crates (engine, runner, builder). To bump the version, edit only the root Cargo.toml:
[workspace.package]
version = "0.1.5"
edition = "2021"
All crates pick it up automatically via `version.workspace = true.
Stage Hagane Into Its Own Payload
Windows
Copy-Item .\target\release\hagane.exe .\hagane\payload\bin\hagane.exe -Force
Linux
cp ./target/release/hagane ./hagane/payload/bin/hagane
chmod +x ./hagane/payload/bin/hagane
Build Hagane Installer
Windows
With auto-discovery (if `hagane/installer.yaml is the only YAML in the current directory):
hagane run --release
Or with an explicit path:
hagane run .\hagane\installer.yaml --release
Expected output: `hagane\bin\hagane-setup.exe
Linux
# From the repo root — builds the Linux tarball/installer binary
./target/release/hagane run hagane/installer.yaml --release
Expected output: `hagane/bin/hagane-linux-x86_64
.yml file exists in the current directory, Hagane selects it automatically. If multiple are found, Hagane prints a warning listing all of them and asks you to specify explicitly. If none are found, Hagane tries hagane/installer.yaml as a fallback.
Install And Verify
Windows
Start-Process .\hagane\bin\hagane-setup.exe -Wait
Test-Path "C:\Program Files\Hagane\bin\hagane.exe"
& "C:\Program Files\Hagane\bin\hagane.exe" --version
Linux
The installer requires root to write to `/usr/local/. When require_admin: true, it re-launches itself with sudo automatically.
./hagane/bin/hagane-linux-x86_64
Verify after the GUI installer completes:
# Symlink created by post-install hook:
ls -la /usr/local/bin/hagane
# Binary accessible in PATH (new terminal):
hagane --version
# System PATH entry (if system PATH component was selected):
cat /etc/profile.d/hagane-path.sh
grep hagane /etc/bash.bashrc
Open a new terminal after installation. The PATH update takes effect in new terminal sessions. No re-login required \u2014 `/etc/bash.bashrc is sourced for all non-login interactive shells (including the default WSL2 terminal).
Test User Flow
Windows — Installed Hagane
# Auto-discovery (single YAML in directory)
Set-Location C:\your-installer.yaml-folder-path
hagane run --release
# Explicit manifest
hagane run installer.yaml --release
# From source build (developer workflow)
.\target\release\hagane.exe run .\path\to\installer.yaml --release
Linux — Installed Hagane
# Auto-discovery
cd /path/to/your-project
hagane run --release
# Explicit manifest
hagane run installer.yaml --release
# From source build (developer workflow)
./target/release/hagane run ./path/to/installer.yaml --release
Expected output: `<manifest-dir>/bin/<app-name>-linux-x86_64
Linux Uninstall
The Linux installer generates `/usr/local/hagane/uninstall.sh. Run it as root:
sudo /usr/local/hagane/uninstall.sh
The script performs these steps in order:
- `rm -rf /usr/local/hagane — removes the entire install directory
- Removes `/usr/local/bin/hagane symlink if it points into the install directory
- Removes PATH entries from `~/.bashrc and
~/.profile— uses$SUDO_USERto target the actual user's home, not root's - Removes the `# hagane: PATH entry from
/etc/bash.bashrc - Removes `/etc/profile.d/hagane-path.sh
Verify after uninstall:
test -d /usr/local/hagane && echo "FAIL" || echo "OK: install dir removed"
ls /usr/local/bin/hagane 2>/dev/null && echo "FAIL" || echo "OK: symlink removed"
grep -c hagane /etc/bash.bashrc 2>/dev/null || echo "OK: bash.bashrc clean"
Troubleshooting
`bin folder is empty after install
Cause: `hagane\payload\bin\hagane.exe was not staged before building hagane-setup.exe.
Fix:
- Rebuild Hagane CLI.
- Copy it into `hagane\payload\bin.
- Rebuild and reinstall `hagane-setup.exe.
`Could not find workspace root
Cause: old Hagane binary or missing runtime structure.
Fix:
- Rebuild and reinstall latest Hagane.
- Ensure installed structure includes `bin\hagane.exe and
runtime\Cargo.toml.
No custom EXE icon in Explorer
Cause: stale build or shell icon cache.
Fix:
- Rebuild using latest Hagane.
- Confirm logs show `Using EXE icon: during pack.
- Re-open Explorer (or sign out/in) if cache still shows old icon.
Installer error codes are not visible
Cause: file logging is not configured, the destination is not writable, or the error happened before the installer reached the step runner.
Fix:
- Add a top-level `logging block to
installer.yaml. - Use `mode: auto for lifecycle logs, or
mode: manual_onlyto suppress normal lifecycle logging. - For file logs, ensure `logging.path and
logging.file_nameare set. - Use a writable path during testing, such as `{{TEMP}}.
- Check [LOGGING.md](LOGGING.md) for behavior details and [ERROR_CODES.md](ERROR_CODES.md) for code-level troubleshooting.
Variable syntax note:
- Preferred manifest variable syntax is `{{KEY}} (for example
{{INSTDIR}}/logs). - Use `{{KEY}} syntax consistently in new manifests.
PowerShell step fails with access denied
Cause: the script needs elevation, or execution policy blocks the command.
Fix:
- Set `app.require_admin: true when the script writes to protected locations.
- Confirm the PowerShell command is valid and available in PATH.
- Use `timeout_sec only if the script is expected to finish quickly.
Logging file not created
Cause: `logging.path or logging.file_name is missing, or the destination folder cannot be created.
Fix:
- Add `logging.path and
logging.file_nameto the manifest. - Use a writable location during development.
- Confirm the destination path is writable by the installer process.
Release Checklist
Windows
- Build `hagane.exe in release mode.
- Stage binary into `hagane/payload/bin/hagane.exe.
- Build `hagane-setup.exe.
- Install on clean machine or VM.
- Verify PATH integration (user/system choices).
- Verify ability to build external installer projects.
- Verify EXE icon and UI branding.
- Verify installation logs and error codes are emitted correctly during a failing test manifest.
Linux
- Build `hagane in release mode (
cargo build -p builder --bin hagane --release). - Stage binary into `hagane/payload/bin/hagane and
chmod +x. - Build `hagane-linux-x86_64.
- Install on clean machine or WSL2 VM (`sudo).
- Verify `/usr/local/bin/hagane symlink is created by the post-install hook.
- Open a new terminal and confirm `hagane --version works without modifying PATH manually.
- Verify user PATH scope writes to the correct user's `~/.bashrc (check
$SUDO_USER). - Verify system PATH scope writes to both `/etc/profile.d/hagane-path.sh and
/etc/bash.bashrc. - Verify `uninstall.sh removes all installed files, symlinks, and PATH entries cleanly.