//! 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); struct Inner { file: PathBuf, /// The last value read, which [`HyperhiveFlake::current`] falls back to /// when the file cannot be read. last: Mutex, } 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::load_from(Path::new(FILE)) } fn load_from(file: &Path) -> Result { 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 { 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"); } }