use std::collections::HashSet; use std::fs; use std::path::{Path, PathBuf}; use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ArtifactManifest { pub packages: Vec, pub files: Vec, pub initrd: Option, pub services: Vec, pub archive_members: Vec, } impl ArtifactManifest { pub fn new( mut packages: Vec, mut files: Vec, initrd: Option, mut services: Vec, mut archive_members: Vec, ) -> Result { packages.sort_by(|left, right| left.name.cmp(&right.name)); files.sort_by(|left, right| left.path.cmp(&right.path)); services.sort_by(|left, right| left.name.cmp(&right.name)); archive_members.sort_by(|left, right| left.path.cmp(&right.path)); ensure_unique("package", packages.iter().map(|record| record.name.as_str()))?; ensure_unique("file", files.iter().map(|record| record.path.as_str()))?; ensure_unique("service", services.iter().map(|record| record.name.as_str()))?; ensure_unique("archive member", archive_members.iter().map(|record| record.path.as_str()))?; Ok(Self { packages, files, initrd, services, archive_members }) } pub fn load(path: impl AsRef) -> Result { let path = path.as_ref(); let text = fs::read_to_string(path).with_context(|| format!("read artifact manifest {}", path.display()))?; let manifest: Self = toml::from_str(&text).with_context(|| format!("parse artifact manifest {}", path.display()))?; Self::new(manifest.packages, manifest.files, manifest.initrd, manifest.services, manifest.archive_members) } pub fn write_beside(&self, artifact: impl AsRef) -> Result { let artifact = artifact.as_ref(); let directory = artifact.parent().unwrap_or_else(|| Path::new(".")); let path = directory.join("artifact.manifest.toml"); let text = toml::to_string_pretty(self).context("serialize artifact manifest")?; fs::write(&path, text).with_context(|| format!("write artifact manifest {}", path.display()))?; Ok(path) } } fn ensure_unique<'a>(kind: &str, keys: impl IntoIterator) -> Result<()> { let mut seen = HashSet::new(); for key in keys { if key.is_empty() || !seen.insert(key) { bail!("duplicate {kind} record: {key}"); } } Ok(()) } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PackageRecord { pub name: String, pub version: String, } impl PackageRecord { pub fn new(name: impl Into, version: impl Into) -> Self { Self { name: name.into(), version: version.into() } } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct FileRecord { pub path: String, pub kind: String, pub digest: Option, pub target: Option, } impl FileRecord { pub fn file(path: impl Into, digest: impl Into) -> Self { Self { path: path.into(), kind: "file".into(), digest: Some(digest.into()), target: None } } pub fn symlink(path: impl Into, target: impl Into) -> Self { Self { path: path.into(), kind: "symlink".into(), digest: None, target: Some(target.into()) } } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct InitrdRecord { pub path: String, pub sha256: String, } impl InitrdRecord { pub fn new(path: impl Into, sha256: impl Into) -> Self { Self { path: path.into(), sha256: sha256.into() } } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServiceRecord { pub name: String, pub enabled: bool, } impl ServiceRecord { pub fn new(name: impl Into, enabled: bool) -> Self { Self { name: name.into(), enabled } } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ArchiveMemberRecord { pub path: String, pub kind: String, } impl ArchiveMemberRecord { pub fn new(path: impl Into, kind: impl Into) -> Self { Self { path: path.into(), kind: kind.into() } } }