use std::collections::BTreeMap;
use std::path::Path;
use std::sync::{Arc, Mutex, OnceLock};
use surtgis_core::provenance::{self, InputRecord, Provenance, hash_file};
struct Context {
argv: Vec<String>,
cwd: Option<String>,
inputs: Mutex<BTreeMap<String, InputRecord>>,
}
static CONTEXT: OnceLock<Arc<Context>> = OnceLock::new();
pub fn install(argv: Vec<String>) {
let ctx = CONTEXT
.get_or_init(|| {
Arc::new(Context {
argv,
cwd: std::env::current_dir()
.ok()
.map(|p| p.display().to_string()),
inputs: Mutex::new(BTreeMap::new()),
})
})
.clone();
let observer = ctx.clone();
provenance::set_input_observer(Some(Arc::new(move |source: &str| {
observer.note_input(source)
})));
let provider = ctx;
provenance::set_output_provider(Some(Arc::new(move || Some(provider.record()))));
}
pub fn disabled_by_env() -> bool {
matches!(
std::env::var("SURTGIS_NO_PROVENANCE").as_deref(),
Ok("1") | Ok("true") | Ok("yes")
)
}
impl Context {
fn note_input(&self, source: &str) {
let mut inputs = self.inputs.lock().unwrap_or_else(|e| e.into_inner());
if inputs.contains_key(source) {
return;
}
let path = Path::new(source);
let record = if source.contains("://") || !path.is_file() {
InputRecord {
source: source.to_string(),
blake3: None,
bytes: None,
role: None,
}
} else {
match hash_file(path) {
Ok((digest, bytes)) => InputRecord {
source: source.to_string(),
blake3: Some(digest),
bytes: Some(bytes),
role: None,
},
Err(_) => InputRecord {
source: source.to_string(),
blake3: None,
bytes: None,
role: None,
},
}
};
inputs.insert(source.to_string(), record);
}
fn record(&self) -> Provenance {
let mut p = Provenance::new()
.with_operation(operation_name(&self.argv))
.with_command(self.argv.iter().cloned())
.with_threads(rayon::current_num_threads());
if let Some(cwd) = &self.cwd {
p = p.with_cwd(cwd.clone());
}
let inputs = self.inputs.lock().unwrap_or_else(|e| e.into_inner());
for rec in inputs.values() {
p.push_input(rec.clone());
}
p
}
}
fn operation_name(argv: &[String]) -> String {
argv.iter()
.skip(1)
.filter(|a| !a.starts_with('-'))
.take(2)
.cloned()
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn operation_is_the_two_leading_subcommands() {
let argv: Vec<String> = [
"surtgis",
"--compress",
"terrain",
"slope",
"a.tif",
"b.tif",
]
.map(String::from)
.to_vec();
assert_eq!(operation_name(&argv), "terrain slope");
assert_eq!(operation_name(&["surtgis".to_string()]), "");
}
}