This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/crates/maestro-installer/src/systemd.rs
T
naps62-yolo 2da41cc720
ci / rust (push) Successful in 1m35s
feat(maestro): add maestro-install wizard (cargo install + systemd)
New `maestro-installer` crate exposing the `maestro-install` binary. Bootstrap
flow: `cargo install --git ... maestro-installer && maestro-install` runs
`cargo install` for `maestro-daemon` and `maestro-cli`, optionally drives
`maestrod configure`, and on systemd hosts writes a user unit at
`~/.config/systemd/user/maestrod.service` plus reload/enable/start. Pure
builders (cargo install args, systemctl args, unit body) live in the lib and
are unit-tested; main.rs is the prompt glue.

Refs NAP-225.
2026-05-25 14:30:03 +00:00

103 lines
3.3 KiB
Rust

//! systemd detection + user-unit rendering.
//!
//! The wizard only ever writes a *user* unit (`~/.config/systemd/user/`), since
//! `maestrod` runs per-user and uses tmux sockets in the user's runtime dir.
use std::path::{Path, PathBuf};
/// Standard probe path. Present on hosts booted under systemd.
pub const SYSTEMD_PROBE_PATH: &str = "/run/systemd/system";
/// Filename written under `~/.config/systemd/user/`.
pub const UNIT_FILE_NAME: &str = "maestrod.service";
/// `true` when the given path exists. Production callers should pass
/// [`SYSTEMD_PROBE_PATH`]; tests inject a temp path so they're hermetic.
pub fn is_systemd_host(probe: &Path) -> bool {
probe.exists()
}
/// `~/.config/systemd/user/maestrod.service`. `None` if `$HOME` / config dir
/// is unresolvable.
pub fn user_unit_path() -> Option<PathBuf> {
let base = dirs::config_dir()?;
Some(base.join("systemd").join("user").join(UNIT_FILE_NAME))
}
/// Render the unit file body. `exec_start` is the absolute path to `maestrod`
/// — we default to `~/.cargo/bin/maestrod` because that's where `cargo install`
/// lands binaries; systemd doesn't inherit the user's PATH, so we can't rely
/// on a bare `maestrod`.
pub fn render_user_unit(exec_start: &Path) -> String {
format!(
"[Unit]\n\
Description=Maestro per-host daemon (maestrod)\n\
Documentation=https://git.naps.pt/naps62/maestro\n\
After=network-online.target\n\
\n\
[Service]\n\
Type=simple\n\
ExecStart={exec}\n\
Restart=on-failure\n\
RestartSec=5\n\
\n\
[Install]\n\
WantedBy=default.target\n",
exec = exec_start.display()
)
}
/// Default `ExecStart` target: `~/.cargo/bin/maestrod`. `None` if `$HOME` is
/// unresolvable.
pub fn default_maestrod_path() -> Option<PathBuf> {
Some(
dirs::home_dir()?
.join(".cargo")
.join("bin")
.join("maestrod"),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_systemd_host_true_when_probe_exists() {
let dir = tempfile::tempdir().unwrap();
let probe = dir.path().join("system");
std::fs::create_dir(&probe).unwrap();
assert!(is_systemd_host(&probe));
}
#[test]
fn is_systemd_host_false_when_probe_missing() {
let dir = tempfile::tempdir().unwrap();
let probe = dir.path().join("not-here");
assert!(!is_systemd_host(&probe));
}
#[test]
fn unit_renders_with_absolute_exec_start() {
let body = render_user_unit(Path::new("/home/alice/.cargo/bin/maestrod"));
assert!(body.contains("ExecStart=/home/alice/.cargo/bin/maestrod"));
assert!(body.contains("[Unit]"));
assert!(body.contains("[Service]"));
assert!(body.contains("[Install]"));
assert!(body.contains("WantedBy=default.target"));
assert!(body.contains("Restart=on-failure"));
}
#[test]
fn unit_ends_with_newline() {
let body = render_user_unit(Path::new("/x"));
assert!(body.ends_with('\n'), "unit must end with newline");
}
#[test]
fn user_unit_path_points_under_config_dir() {
let p = user_unit_path().expect("config dir resolvable in test env");
assert!(p.ends_with("systemd/user/maestrod.service"), "got {p:?}");
}
}