use std::path::{Path, PathBuf};
use running_process::broker::builders::CacheManifestBuilder;
use running_process::broker::protocol::CacheRootKind;
use running_process::broker::protocol_v2;
use crate::core::NormalizedPath;
pub const ZCCACHE_SERVICE_NAME: &str = "zccache";
const SHARED_BROKER_INSTANCE: &str = "shared";
#[must_use]
pub fn build_manifest_builder(cache_dir: &NormalizedPath) -> CacheManifestBuilder {
let index_dir = cache_dir.join("depgraph");
let log_dir = cache_dir.join("logs");
CacheManifestBuilder::new(ZCCACHE_SERVICE_NAME, crate::core::VERSION)
.broker_instance(SHARED_BROKER_INSTANCE)
.root(
CacheRootKind::CacheData,
path_string(&crate::core::config::artifacts_dir_from_cache_dir(cache_dir)),
)
.root(CacheRootKind::CacheIndex, path_string(&index_dir))
.root(CacheRootKind::CacheLogs, path_string(&log_dir))
.root(CacheRootKind::CacheLocks, path_string(cache_dir))
.root(
CacheRootKind::CacheTmp,
path_string(&crate::core::config::tmp_dir_from_cache_dir(cache_dir)),
)
}
pub fn publish_manifest(cache_dir: &NormalizedPath) -> Option<PathBuf> {
if super::running_process_disabled() {
return None;
}
let v1_path = match build_manifest_builder(cache_dir).publish() {
Ok(path) => Some(path),
Err(err) => {
tracing::warn!(error = %err, "failed to publish running-process cache manifest");
None
}
};
if let Err(err) = build_manifest_builder_v2(cache_dir).publish() {
tracing::warn!(
error = %err,
"v2 manifest dual-write failed (non-fatal during rollout)"
);
}
v1_path
}
pub fn publish_manifest_in(
registry_dir: &Path,
cache_dir: &NormalizedPath,
) -> Result<PathBuf, running_process::broker::manifest::ManifestError> {
let v1_path = build_manifest_builder(cache_dir).publish_in(registry_dir)?;
let _v2_path = build_manifest_builder_v2(cache_dir).publish_in(registry_dir)?;
Ok(v1_path)
}
#[must_use]
pub fn build_manifest_builder_v2(cache_dir: &NormalizedPath) -> protocol_v2::CacheManifestBuilder {
use running_process::broker::protocol_v2::CacheRootKind as V2;
let index_dir = cache_dir.join("depgraph");
let log_dir = cache_dir.join("logs");
protocol_v2::CacheManifestBuilder::new(ZCCACHE_SERVICE_NAME, crate::core::VERSION)
.broker_instance(SHARED_BROKER_INSTANCE)
.root(
V2::CacheData,
path_string(&crate::core::config::artifacts_dir_from_cache_dir(
cache_dir,
)),
)
.root(V2::CacheIndex, path_string(&index_dir))
.root(V2::CacheLogs, path_string(&log_dir))
.root(V2::CacheLocks, path_string(cache_dir))
.root(
V2::CacheTmp,
path_string(&crate::core::config::tmp_dir_from_cache_dir(cache_dir)),
)
}
pub fn publish_service_definition(daemon_binary: &Path) -> Option<PathBuf> {
use running_process::broker::builders::ServiceDefinitionBuilder;
use running_process::broker::server::service_definition_dir;
if super::running_process_disabled() {
return None;
}
let binary = match std::fs::canonicalize(daemon_binary) {
Ok(p) => p,
Err(err) => {
tracing::warn!(
daemon_binary = %daemon_binary.display(),
error = %err,
"failed to canonicalize zccache-daemon binary for service-definition install"
);
return None;
}
};
let Some(binary_dir) = binary.parent() else {
tracing::warn!(
binary = %binary.display(),
"zccache-daemon binary has no parent directory; skipping service-definition install"
);
return None;
};
match ServiceDefinitionBuilder::shared_broker(
ZCCACHE_SERVICE_NAME,
binary.display().to_string(),
)
.per_version_binary_dir(binary_dir.display().to_string())
.min_version(crate::core::VERSION)
.allow_version(crate::core::VERSION)
.label("vendor", "zackees")
.label("package", "zccache")
.label("consumer", "zccache")
.label("running-process-tracker", "zackees/running-process#435")
.install_in(&service_definition_dir())
{
Ok(path) => {
tracing::debug!(servicedef = %path.display(), "installed running-process service definition");
Some(path)
}
Err(err) => {
tracing::warn!(error = %err, "failed to install running-process service definition");
None
}
}
}
fn path_string(path: &NormalizedPath) -> String {
path.as_path().display().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use running_process::broker::manifest::read_manifest;
use running_process::broker::protocol::CacheRoot;
fn roots_by_kind(roots: &[CacheRoot]) -> Vec<(i32, String)> {
roots.iter().map(|r| (r.kind, r.path.clone())).collect()
}
#[test]
fn manifest_records_all_five_cache_roots() {
let cache_dir = NormalizedPath::from("/tmp/zccache-manifest-test");
let manifest = build_manifest_builder(&cache_dir)
.build()
.expect("seal manifest");
assert_eq!(manifest.service_name, "zccache");
assert_eq!(manifest.service_version, crate::core::VERSION);
assert_eq!(manifest.broker_instance, "shared");
let kinds: Vec<i32> = manifest.roots.iter().map(|r| r.kind).collect();
assert!(
kinds.contains(&(CacheRootKind::CacheData as i32)),
"artifact"
);
assert!(kinds.contains(&(CacheRootKind::CacheIndex as i32)), "index");
assert!(kinds.contains(&(CacheRootKind::CacheLogs as i32)), "log");
assert!(kinds.contains(&(CacheRootKind::CacheLocks as i32)), "lock");
assert!(kinds.contains(&(CacheRootKind::CacheTmp as i32)), "temp");
assert_eq!(manifest.roots.len(), 5);
}
#[test]
fn publish_round_trips_through_central_registry() {
let registry = tempfile::tempdir().expect("tempdir");
let cache_dir = NormalizedPath::from("/tmp/zccache-manifest-roundtrip");
let written = publish_manifest_in(registry.path(), &cache_dir).expect("publish manifest");
assert!(written.exists(), "manifest file should exist on disk");
let loaded = read_manifest(&written).expect("read + verify sealed manifest");
assert_eq!(loaded.service_name, "zccache");
let original = roots_by_kind(
&build_manifest_builder(&cache_dir)
.build()
.expect("seal manifest")
.roots,
);
assert_eq!(roots_by_kind(&loaded.roots), original);
}
#[test]
fn publish_also_writes_v2_manifest() {
use prost::Message;
let registry = tempfile::tempdir().expect("tempdir");
let cache_dir = NormalizedPath::from("/tmp/zccache-manifest-v2-roundtrip");
publish_manifest_in(registry.path(), &cache_dir).expect("publish manifest");
let v2_path = registry
.path()
.join(format!("zccache-{}.v2.pb", crate::core::VERSION));
assert!(
v2_path.exists(),
"v2 manifest must exist at {}",
v2_path.display()
);
let bytes = std::fs::read(&v2_path).expect("read v2 file");
let decoded =
protocol_v2::CacheManifest::decode(bytes.as_slice()).expect("v2 CacheManifest decodes");
assert_eq!(decoded.service_name, "zccache");
assert_eq!(decoded.service_version, crate::core::VERSION);
assert_eq!(decoded.broker_envelope_version, "v2");
assert_eq!(decoded.broker_instance, "shared");
assert_eq!(decoded.roots.len(), 5, "all 5 cache roots present in v2");
use running_process::broker::protocol_v2::CacheRootKind as V2;
let v2_kinds: Vec<i32> = decoded.roots.iter().map(|r| r.kind).collect();
assert!(v2_kinds.contains(&(V2::CacheData as i32)));
assert!(v2_kinds.contains(&(V2::CacheIndex as i32)));
assert!(v2_kinds.contains(&(V2::CacheLogs as i32)));
assert!(v2_kinds.contains(&(V2::CacheLocks as i32)));
assert!(v2_kinds.contains(&(V2::CacheTmp as i32)));
}
#[test]
fn v1_and_v2_manifest_roots_agree_on_wire_values() {
let cache_dir = NormalizedPath::from("/tmp/zccache-manifest-alignment");
let v1 = build_manifest_builder(&cache_dir).build().expect("seal v1");
let v2 = build_manifest_builder_v2(&cache_dir).build();
assert_eq!(v1.roots.len(), v2.roots.len(), "same root count");
for (a, b) in v1.roots.iter().zip(v2.roots.iter()) {
assert_eq!(a.kind, b.kind, "kind mismatch for path={}", a.path);
assert_eq!(a.path, b.path, "path mismatch for kind={}", a.kind);
}
}
}