summaryrefslogtreecommitdiff
path: root/src/manifest.rs
diff options
context:
space:
mode:
authorHermes Agent <hermes@localhost>2026-08-12 01:34:51 +0000
committerHermes Agent <hermes@localhost>2026-08-12 01:34:51 +0000
commit9acf451473989fa868e21d2d6ff59cf3fb445562 (patch)
treec58417401e1ecb7304c21c2ebf6f2eaa032ad907 /src/manifest.rs
parent020fbf6a034b3c90de1bdd3ee5e3e5e15c783059 (diff)
Add semantic artifact manifest comparison
Diffstat (limited to 'src/manifest.rs')
-rw-r--r--src/manifest.rs127
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() }
+ }
+}