# install — guide
> **Class:** `guide` · audience: a techie installing a product on a target. The *what* is in
> [`install-design.md`](install-design.md); the *why* in [`install-reasoning.md`](install-reasoning.md).

## f6 — one spec, any init, three lifecycle verbs (the whole flow)
```rust
use skidbladnir::{archive::TarZstd, layout::Layout, product::ComponentSpec,
service::Backend, install};
let eng = TarZstd::new();
let layout = Layout::dated("/var/lib/nordisk", "/var/log/nordisk", "/opt/nordisk");
let spec = ComponentSpec::holger();
// INSTALL: provision dirs, unfold the product, mint identity, render + write unit files
let outcome = install::install_product(&spec, &layout, &bundle_path, &eng,
Backend::SystemdSystem, "/")?; // install.rs:88
// nothing privileged has run yet — inspect the plan as data:
assert_eq!(outcome.verbs, vec![vec!["systemctl","enable","holger.service"],
vec!["systemctl","start","holger.service"]]);
install::apply(&outcome)?; // the only step that runs the verbs (systemd → D-Bus)
```
Swap `Backend::SystemdSystem` for `Backend::OpenRc` and the *same* `ComponentSpec` installs on
Alpine — different files (`/etc/init.d/holger` + `/etc/conf.d/holger`), different verbs
(`rc-update add …`), same call.
## Update (preserve data + log)
```rust
let next = Layout::dated("/var/lib/nordisk", "/var/log/nordisk", "/opt/nordisk"); // new version
let outcome = install::update_product(&spec, &next, &new_bundle, &eng, Backend::SystemdSystem, "/")?;
// data/ and log/ are preserved; the install id is loaded (not re-minted); verbs = [stop, start]
install::apply(&outcome)?;
```
## Uninstall (optionally purge)
```rust
let outcome = install::uninstall_product(&spec, &layout, Backend::SystemdSystem, "/", /* purge */ false)?;
// unit files removed; data/log kept (purge=false) or removed (purge=true); verbs = [stop, disable]
install::apply(&outcome)?;
```
## Build a spec by hand
```rust
use skidbladnir::{product::{ComponentSpec, HealthCheck}, systemd::RestartPolicy};
let spec = ComponentSpec::new("holger", "linux-x86_64/holger", "_holger")
.with_arg("serve").with_arg("--config").with_arg("/var/lib/nordisk/holger/config.toml")
.with_port(7070)
.after("nornir-server")
.with_health(HealthCheck::Tcp(7070));
```
`bundle_binary` is the **full relative path inside the bundle** — the lifecycle unfolds the whole
product tree (`extract_all`), so keep the real path (`linux-x86_64/holger`), not a flattened name.
## Test a render with zero privilege
Because verbs are data, you can assert an install without installing anything:
```rust
let neutral = spec.to_service_spec(&layout, Backend::OpenRc); // product.rs:174
let rendered = skidbladnir::service::renderer_for(Backend::OpenRc).unwrap().render(&neutral);
assert!(rendered.enable == vec!["rc-update","add","holger","default"]);
assert!(rendered.file_ending("/etc/init.d/holger").unwrap().contains("supervise-daemon"));
```
## Identity
Each install mints (or, on update, loads) an identity under the data dir:
```rust
let id = skidbladnir::identity::ensure(&data_dir, "holger", scope, "_holger")?; // identity.rs:135
println!("{}", id.wire()); // nid1:<machine_fp>:<install_id>
```
## Gotchas
- **Full coverage — all eleven backends render** (`renderer_for` never returns `None` today; the
`Option` return is kept for a future backend). systemd-system, systemd-user, OpenRC, SysV, runit,
dinit, s6-rc, Kubernetes, OCI, Quadlet and Windows SCM — swap `Backend::OpenRc` above for
`Backend::Runit` and the SAME `ComponentSpec` renders a `/etc/sv/holger/` runsv service directory,
`Backend::Dinit` for a `/etc/dinit.d/holger` textual service description, `Backend::S6Rc` for a
`/etc/s6-rc/source/holger/` compiled-source definition dir, `Backend::Oci` for a `podman run -d
--name holger …` container invocation (+ a 0600 `--env-file`) — a container runtime is the supervisor,
so the spec is the run argv, not a unit file — `Backend::Quadlet` for a systemd
`/etc/containers/systemd/holger.container` unit that `podman-systemd` turns into a `holger.service`
on `daemon-reload` (that reload IS the enable-at-boot step; no separate `systemctl enable`), or
`Backend::WindowsService` for a WiX `C:\ProgramData\holger\holger.wxs` service fragment (via the MSI
`WindowsServiceSpec` seam) driven by `sc.exe create/start/query/stop` — SCM is machine-scoped, env
lives in the `.wxs`, and `restart_sec` maps to the service's crash-recovery restart delay.
- Pass `root = "/"` in production; pass a tempdir in tests to capture the exact files written.
- `to_service_spec` surfaces data/log as `DATA_DIR`/`LOG_DIR` env (there is no native systemd
`StateDirectory=`); the dirs are provisioned explicitly by `prepare_product`.
- **Secrets go in `with_secret_env`, never `with_env`.** A `SystemdServiceSpec` non-secret env
(`with_env`) is rendered inline as `Environment=KEY=VALUE` in the *world-readable* unit
(`/etc/systemd/system/<svc>.service`, 0644). A token/password/join-key belongs in
`with_secret_env(key, value)`: it is written ONLY into the mode-0600 env file and never inline.
A secret makes `with_env_file(path)` mandatory — `spec.validate()` (and `install_systemd`) reject
a secret-without-env_file spec. The env file and the ed25519 `node_key` are both persisted via
`systemd::write_private_file`, created 0600 from the start (no world-readable window).