use super::io_tracking::{hash_inputs, hash_outputs_in};
use crate::core::types::Resource;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IoDigest {
pub input_hash: Option<String>,
pub output_hash: Option<String>,
pub outputs_missing: bool,
}
impl IoDigest {
pub fn is_empty(&self) -> bool {
self.input_hash.is_none() && self.output_hash.is_none() && !self.outputs_missing
}
}
pub fn probe_base_dir(resource: &Resource) -> PathBuf {
match resource.working_dir.as_deref() {
Some(d) if !d.is_empty() => PathBuf::from(d),
_ => PathBuf::from("."),
}
}
pub fn probe_resource(resource: &Resource) -> Option<IoDigest> {
if resource.task_inputs.is_empty() && resource.output_artifacts.is_empty() {
return None;
}
let base = probe_base_dir(resource);
let input_hash = if resource.task_inputs.is_empty() {
None
} else {
hash_inputs(&resource.task_inputs, &base).ok().flatten()
};
let mut outputs_missing = false;
for artifact in &resource.output_artifacts {
if !resolve_under(&base, artifact).exists() {
outputs_missing = true;
break;
}
}
let file_artifacts: Vec<String> = resource
.output_artifacts
.iter()
.filter(|a| !resolve_under(&base, a).is_dir())
.cloned()
.collect();
let output_hash = if file_artifacts.is_empty() || outputs_missing {
None
} else {
hash_outputs_in(&file_artifacts, &base).ok().flatten()
};
Some(IoDigest {
input_hash,
output_hash,
outputs_missing,
})
}
pub fn resolve_under(base: &Path, path: &str) -> PathBuf {
let p = Path::new(path);
if p.is_absolute() {
p.to_path_buf()
} else {
base.join(p)
}
}
pub fn probe_all<F>(
resources: &indexmap::IndexMap<String, Resource>,
is_local: F,
) -> HashMap<String, IoDigest>
where
F: Fn(&str) -> bool,
{
let mut out = HashMap::new();
for (id, resource) in resources {
if !resource.machine.iter().any(&is_local) {
continue;
}
if let Some(d) = probe_resource(resource) {
if !d.is_empty() {
out.insert(id.clone(), d);
}
}
}
out
}
pub fn staleness_reason(
probe: &IoDigest,
stored_input_hash: Option<&str>,
stored_output_hash: Option<&str>,
) -> Option<String> {
if probe.outputs_missing {
return Some("output artifact missing".to_string());
}
if let Some(current) = probe.input_hash.as_deref() {
match stored_input_hash {
None => return Some("no recorded input hash".to_string()),
Some(stored) if stored != current => {
return Some("inputs changed".to_string());
}
_ => {}
}
}
if let Some(current) = probe.output_hash.as_deref() {
if let Some(stored) = stored_output_hash {
if stored != current {
return Some("output artifact modified".to_string());
}
}
}
None
}
pub fn record_io_hashes(
resource: &Resource,
details: &mut std::collections::HashMap<String, serde_yaml_ng::Value>,
) {
if resource.task_inputs.is_empty() && resource.output_artifacts.is_empty() {
return;
}
let base = probe_base_dir(resource);
if !resource.task_inputs.is_empty() {
if let Ok(Some(hash)) = hash_inputs(&resource.task_inputs, &base) {
details.insert("input_hash".to_string(), serde_yaml_ng::Value::String(hash));
}
}
if !resource.output_artifacts.is_empty() {
if let Ok(Some(hash)) = hash_outputs_in(&resource.output_artifacts, &base) {
details.insert(
"output_hash".to_string(),
serde_yaml_ng::Value::String(hash),
);
}
}
}
pub fn probe_config(config: &crate::core::types::ForjarConfig) -> HashMap<String, IoDigest> {
let resolved = crate::core::resolver::resolve_all(
&config.resources,
&config.params,
&config.machines,
&config.secrets,
);
probe_all(&resolved, |m| {
config
.machines
.get(m)
.is_some_and(crate::transport::machine_is_local)
})
}