diff options
| author | Hermes Agent <hermes@localhost> | 2026-08-12 01:34:51 +0000 |
|---|---|---|
| committer | Hermes Agent <hermes@localhost> | 2026-08-12 01:34:51 +0000 |
| commit | 9acf451473989fa868e21d2d6ff59cf3fb445562 (patch) | |
| tree | c58417401e1ecb7304c21c2ebf6f2eaa032ad907 /src/manifest.rs | |
| parent | 020fbf6a034b3c90de1bdd3ee5e3e5e15c783059 (diff) | |
Add semantic artifact manifest comparison
Diffstat (limited to 'src/manifest.rs')
| -rw-r--r-- | src/manifest.rs | 127 |
1 files changed, 127 insertions, 0 deletions
diff --git a/src/manifest.rs b/src/manifest.rs new file mode 100644 index 0000000..1a5829b --- /dev/null +++ b/src/manifest.rs @@ -0,0 +1,127 @@ +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<PackageRecord>, + pub files: Vec<FileRecord>, + pub initrd: Option<InitrdRecord>, + pub services: Vec<ServiceRecord>, + pub archive_members: Vec<ArchiveMemberRecord>, +} + +impl ArtifactManifest { + pub fn new( + mut packages: Vec<PackageRecord>, + mut files: Vec<FileRecord>, + initrd: Option<InitrdRecord>, + mut services: Vec<ServiceRecord>, + mut archive_members: Vec<ArchiveMemberRecord>, + ) -> Result<Self> { + 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<Path>) -> Result<Self> { + 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<Path>) -> Result<PathBuf> { + 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<Item = &'a str>) -> 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<String>, version: impl Into<String>) -> 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<String>, + pub target: Option<String>, +} + +impl FileRecord { + pub fn file(path: impl Into<String>, digest: impl Into<String>) -> Self { + Self { path: path.into(), kind: "file".into(), digest: Some(digest.into()), target: None } + } + + pub fn symlink(path: impl Into<String>, target: impl Into<String>) -> 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<String>, sha256: impl Into<String>) -> 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<String>, 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<String>, kind: impl Into<String>) -> Self { + Self { path: path.into(), kind: kind.into() } + } +} |