159 lines
5.2 KiB
Rust
159 lines
5.2 KiB
Rust
//! The hyperhive source builds are made on, read from the file
|
|
//! `swarm-builder.nix` writes it to.
|
|
//!
|
|
//! It is a file, not part of the unit: the store path changes with almost
|
|
//! any commit, and a changed unit restarts the daemon and ends the build in
|
|
//! progress. [`HyperhiveFlake::current`] reads the file again for each build
|
|
//! and evaluation.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
|
|
/// Where `swarm-builder.nix` writes the file, in the container's `/etc`.
|
|
pub const FILE: &str = "/etc/hyperhive/swarm-builder.json";
|
|
|
|
/// The hyperhive flake's store path, as the file last named it.
|
|
#[derive(Clone)]
|
|
pub struct HyperhiveFlake(Arc<Inner>);
|
|
|
|
struct Inner {
|
|
file: PathBuf,
|
|
/// The last value read, which [`HyperhiveFlake::current`] falls back to
|
|
/// when the file cannot be read.
|
|
last: Mutex<String>,
|
|
}
|
|
|
|
impl HyperhiveFlake {
|
|
/// [`FILE`] as it is now.
|
|
///
|
|
/// # Errors
|
|
/// When the file is missing, unreadable, not JSON, or names no path: the
|
|
/// daemon cannot build without a source.
|
|
pub fn load() -> Result<Self> {
|
|
Self::load_from(Path::new(FILE))
|
|
}
|
|
|
|
fn load_from(file: &Path) -> Result<Self> {
|
|
let initial = read(file)?;
|
|
Ok(Self::new(file.to_owned(), initial))
|
|
}
|
|
|
|
/// `initial` until `file` can be read.
|
|
pub fn new(file: PathBuf, initial: String) -> Self {
|
|
Self(Arc::new(Inner {
|
|
file,
|
|
last: Mutex::new(initial),
|
|
}))
|
|
}
|
|
|
|
/// `path`, with a file that does not exist: what a test hands code that
|
|
/// reads the flake.
|
|
#[cfg(test)]
|
|
pub fn fixed(path: &str) -> Self {
|
|
Self::new(
|
|
PathBuf::from("/nonexistent/swarm-builder.json"),
|
|
path.to_owned(),
|
|
)
|
|
}
|
|
|
|
/// The path the file names now. A file that cannot be read keeps the last
|
|
/// path read, and is logged.
|
|
///
|
|
/// # Panics
|
|
/// When another thread panicked while it held the stored value.
|
|
#[must_use]
|
|
pub fn current(&self) -> String {
|
|
let mut last = self.0.last.lock().expect("unpoisoned");
|
|
match read(&self.0.file) {
|
|
Ok(path) => *last = path,
|
|
Err(e) => {
|
|
tracing::warn!(error = %format!("{e:#}"), "hyperhive flake file not read; using the last path");
|
|
}
|
|
}
|
|
last.clone()
|
|
}
|
|
}
|
|
|
|
fn read(file: &Path) -> Result<String> {
|
|
let raw = std::fs::read_to_string(file).with_context(|| format!("read {}", file.display()))?;
|
|
let contents: serde_json::Value =
|
|
serde_json::from_str(&raw).with_context(|| format!("parse {}", file.display()))?;
|
|
match contents["hyperhive_flake"].as_str() {
|
|
Some(path) if !path.is_empty() => Ok(path.to_owned()),
|
|
_ => bail!("{}: hyperhive_flake is absent or empty", file.display()),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// A fresh directory for one test, removed on drop.
|
|
struct Dir(PathBuf);
|
|
|
|
impl Dir {
|
|
fn new(name: &str) -> Self {
|
|
let dir =
|
|
std::env::temp_dir().join(format!("hyperhive-flake-{name}-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
Self(dir)
|
|
}
|
|
|
|
fn file(&self) -> PathBuf {
|
|
self.0.join("swarm-builder.json")
|
|
}
|
|
|
|
fn write(&self, content: &str) {
|
|
std::fs::write(self.file(), content).unwrap();
|
|
}
|
|
}
|
|
|
|
impl Drop for Dir {
|
|
fn drop(&mut self) {
|
|
let _ = std::fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_path_is_read_again_each_time() {
|
|
let dir = Dir::new("reread");
|
|
dir.write(r#"{"hyperhive_flake":"/nix/store/a-source"}"#);
|
|
let flake = HyperhiveFlake::load_from(&dir.file()).unwrap();
|
|
assert_eq!(flake.current(), "/nix/store/a-source");
|
|
dir.write(r#"{"hyperhive_flake":"/nix/store/b-source"}"#);
|
|
assert_eq!(flake.current(), "/nix/store/b-source");
|
|
}
|
|
|
|
#[test]
|
|
fn a_file_that_cannot_be_used_keeps_the_last_path() {
|
|
let dir = Dir::new("keeps");
|
|
dir.write(r#"{"hyperhive_flake":"/nix/store/a-source"}"#);
|
|
let flake = HyperhiveFlake::load_from(&dir.file()).unwrap();
|
|
// Control: `the_path_is_read_again_each_time` shows a good file
|
|
// replacing the path.
|
|
for (name, content) in [
|
|
("empty value", Some(r#"{"hyperhive_flake":""}"#)),
|
|
("no key", Some("{}")),
|
|
("not JSON", Some("not json")),
|
|
("missing file", None),
|
|
] {
|
|
match content {
|
|
Some(content) => dir.write(content),
|
|
None => std::fs::remove_file(dir.file()).unwrap(),
|
|
}
|
|
assert_eq!(flake.current(), "/nix/store/a-source", "{name}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn start_fails_without_a_usable_file() {
|
|
let dir = Dir::new("start");
|
|
assert!(HyperhiveFlake::load_from(&dir.file()).is_err(), "missing");
|
|
dir.write(r#"{"hyperhive_flake":""}"#);
|
|
assert!(HyperhiveFlake::load_from(&dir.file()).is_err(), "empty");
|
|
dir.write(r#"{"hyperhive_flake":"/nix/store/a-source"}"#);
|
|
assert!(HyperhiveFlake::load_from(&dir.file()).is_ok(), "control");
|
|
}
|
|
}
|