server/lib/src/declaration.rs
//! **Where a box's server declaration comes from** (lane B1, 2026-10-21).
//!
//! An estate box's `holger.ron` rides the RELEASE: the medium carries it with the app,
//! signed with it, and korp-start lays it under the release directory on every start
//! (`korp_installer::release::RUN_DIR`, `/run/korp/release`). A keep medium or a live
//! update therefore changes it — which a first-boot file on `/data` never was: R10's
//! installed main panicked on a `/data/holger.ron` that was never laid.
//!
//! `/data` stays the SITE's: a file at the path `start --config` names is an override, laid
//! over the release one whole. With neither, the start is refused by name — on an estate box
//! that is a pair status and a retry (lane D10), never an ended PID 1.
use std::path::{Path, PathBuf};
/// The release directory korp-start fills (`korp_installer::release::RUN_DIR`; held equal by
/// `xtask`'s test, which links korp-installer).
pub const RELEASE_DIR: &str = "/run/korp/release";
/// Moves [`RELEASE_DIR`] (a test, a container shape).
pub const RELEASE_DIR_ENV: &str = "HOLGER_RELEASE_DIR";
/// Which declaration a start reads.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Source {
/// The site's file at the configured path — it overrides the release one.
Site,
/// The release's, laid by korp-start with the app.
Release,
}
/// The release directory in force ([`RELEASE_DIR_ENV`], else [`RELEASE_DIR`]).
pub fn release_dir() -> PathBuf {
std::env::var_os(RELEASE_DIR_ENV)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(RELEASE_DIR))
}
/// **Resolve** the declaration for `configured` (`start --config <path>`): the site's file
/// when it is there, else the release's file of the same name under `release`. Neither is a
/// refusal that names both places.
pub fn resolve(configured: &Path, release: &Path) -> anyhow::Result<(PathBuf, Source)> {
if configured.is_file() {
return Ok((configured.to_path_buf(), Source::Site));
}
let name = configured.file_name().ok_or_else(|| {
anyhow::anyhow!(
"[holger-declaration] {} names no file",
configured.display()
)
})?;
let rel = release.join(name);
if rel.is_file() {
return Ok((rel, Source::Release));
}
anyhow::bail!(
"[holger-declaration-missing] no server declaration: no site override at {} and no release declaration at {} — \
the medium carries it with the app (holger_ron lands on the release); this box stays up and says so",
configured.display(),
rel.display()
)
}
/// [`resolve`] against [`release_dir`], then [`crate::read_ron_config`]. Answers the config
/// and a line that says which file it is.
pub fn read(configured: &Path) -> anyhow::Result<(crate::Holger, String)> {
let (path, source) = resolve(configured, &release_dir())?;
let holger = crate::read_ron_config(&path)?;
let said = match source {
Source::Site => format!(
"declaration: the site override {} (it overrides the release declaration)",
path.display()
),
Source::Release => format!(
"declaration: the release declaration {} (no site override at {})",
path.display(),
configured.display()
),
};
Ok((holger, said))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_release_dir_is_the_recipes() {
assert_eq!(RELEASE_DIR, holger_recipe::estate_ron::RELEASE_ROOT);
}
#[test]
fn the_site_file_overrides_the_release_and_neither_is_refused_by_name() {
let t = tempfile::tempdir().unwrap();
let data = t.path().join("data");
let release = t.path().join("run/korp/release");
std::fs::create_dir_all(&data).unwrap();
std::fs::create_dir_all(&release).unwrap();
let configured = data.join("holger.ron");
let err = resolve(&configured, &release).unwrap_err().to_string();
assert!(
err.contains("[holger-declaration-missing]")
&& err.contains(&configured.display().to_string())
&& err.contains("run/korp/release/holger.ron"),
"{err}"
);
std::fs::write(release.join("holger.ron"), "()").unwrap();
assert_eq!(
resolve(&configured, &release).unwrap(),
(release.join("holger.ron"), Source::Release)
);
std::fs::write(&configured, "()").unwrap();
assert_eq!(
resolve(&configured, &release).unwrap(),
(configured.clone(), Source::Site)
);
}
}