use super::super::types::OutputEquivalence;
use crate::tripwire::hasher;
use indexmap::IndexMap;
use std::path::Path;
pub fn hash_outputs_with(
artifacts: &[String],
base_dir: &Path,
equivalence: &IndexMap<String, OutputEquivalence>,
) -> Result<Option<String>, String> {
if artifacts.is_empty() {
return Ok(None);
}
let mut components: Vec<String> = Vec::new();
for artifact in artifacts {
let rule = equivalence.get(artifact).cloned().unwrap_or_default();
if let Some(c) = artifact_component(artifact, &rule, base_dir)? {
components.push(c);
}
}
if components.is_empty() {
return Ok(None);
}
let refs: Vec<&str> = components.iter().map(|s| s.as_str()).collect();
Ok(Some(hasher::composite_hash(&refs)))
}
fn artifact_component(
artifact: &str,
rule: &OutputEquivalence,
base_dir: &Path,
) -> Result<Option<String>, String> {
let joined;
let path = {
let p = Path::new(artifact);
if p.is_absolute() {
p
} else {
joined = base_dir.join(p);
joined.as_path()
}
};
if !path.exists() {
return Ok(None);
}
if !rule.contributes_content() {
return Ok(Some(format!("{artifact}\0<{}>", rule.as_str())));
}
let hash = match rule {
OutputEquivalence::Command(script) => normalised_hash(script, path, base_dir)?,
_ if path.is_dir() => hasher::hash_directory(path)?,
_ => hasher::hash_file(path)?,
};
Ok(Some(format!("{artifact}\0{hash}")))
}
fn normalised_hash(script: &str, path: &Path, base_dir: &Path) -> Result<String, String> {
let out = std::process::Command::new("bash")
.arg("-c")
.arg(script)
.arg("bash")
.arg(path)
.env("FORJAR_ARTIFACT", path)
.current_dir(base_dir)
.output()
.map_err(|e| format!("output_equivalence command for {}: {e}", path.display()))?;
if !out.status.success() {
return Err(format!(
"output_equivalence command for {} failed ({}): {}",
path.display(),
out.status,
String::from_utf8_lossy(&out.stderr).trim()
));
}
Ok(hasher::hash_string(&String::from_utf8_lossy(&out.stdout)))
}