use std::collections::BTreeMap;
use std::path::Path;
use std::process::{Command, ExitStatus};
use crate::clock::Clock;
use crate::error::{Error, Result};
use crate::manifest::{Command as Rule, Manifest, StrictCase};
use crate::{cache, hashing, notice, outputs, parametric, probe, resolve, ttl};
struct Digested {
digest: String,
probes: BTreeMap<String, String>,
}
struct Invocation<'a> {
argv: &'a [String],
cwd: &'a Path,
clock: Clock,
}
pub fn run(argv: &[String], cwd: &Path) -> Result<u8> {
let call = Invocation {
argv,
cwd,
clock: Clock::resolve()?,
};
let located = Manifest::locate(cwd)?;
let manifest = &located.manifest;
let base = located.root.as_path();
let matches = parametric::resolve_matches(manifest, base, argv)?;
parametric::detect_collision(&matches)?;
match matches.first() {
Some(hit) => memoized(manifest, hit, base, &call),
None => no_match(manifest, &call),
}
}
fn no_match(manifest: &Manifest, call: &Invocation) -> Result<u8> {
if manifest.strict.enforces(StrictCase::NoMatch) {
return Err(Error::NoMatch {
command: call.argv.join(" "),
});
}
log::debug!("mmz: no rule matches; running unmemoized");
exec(call)
}
fn memoized(
manifest: &Manifest,
hit: ¶metric::Match,
base: &Path,
call: &Invocation,
) -> Result<u8> {
let identity = hit.exp.identity.as_str();
let rule = hit.rule;
let Some(resolved) = digest_inputs(manifest, rule, hit.exp.file.as_deref(), base)? else {
if manifest.strict.enforces(StrictCase::NoInputs) {
return Err(Error::NoInputs {
rule: identity.to_owned(),
});
}
log::warn!("mmz: `{identity}` matched no input files; running unmemoized");
return exec(call);
};
let digest = resolved.digest;
let cache_dir = base.join(&manifest.cache_dir);
if let Some(cached) = cache::read(&cache_dir, identity) {
let voided = outputs::first_missing(base, &rule.outputs);
let expired = ttl::outlived(rule.ttl.as_ref(), cached.ok, cached.ran_at, call.clock);
if cached.ok && cached.digest == digest && voided.is_none() && expired.is_none() {
log::info!("mmz: skip `{identity}` (inputs unchanged)");
announce_hit(manifest, rule, &cached);
return Ok(0);
}
if let Some(path) = voided {
log::info!("mmz: `{identity}` declared output `{path}` is missing; the record is void");
} else if let Some(ttl) = expired {
log::info!("mmz: `{identity}` last passed longer ago than its ttl `{ttl}`; re-running");
}
}
let code = exec(call)?;
confirm_outputs(rule, identity, base, code)?;
cache::write(
&cache_dir,
identity,
call.clock,
&cache::Outcome {
digest: &digest,
ok: code == 0,
outputs: &rule.outputs,
probes: resolved.probes,
},
);
Ok(code)
}
fn confirm_outputs(rule: &Rule, identity: &str, base: &Path, code: u8) -> Result<()> {
if code != 0 {
return Ok(());
}
match outputs::first_missing(base, &rule.outputs) {
Some(path) => Err(Error::MissingOutput {
rule: identity.to_owned(),
path,
}),
None => Ok(()),
}
}
fn announce_hit(manifest: &Manifest, rule: &Rule, cached: &cache::Cached) {
let Some(template) = rule.on_hit.as_deref().or(manifest.on_hit.as_deref()) else {
return;
};
if template.is_empty() {
return;
}
eprintln!("{}", notice::expand(template, &cached.fields));
}
fn digest_inputs(
manifest: &Manifest,
rule: &Rule,
extra: Option<&str>,
base: &Path,
) -> Result<Option<Digested>> {
let probes = probe::Resolver::new(manifest, base).for_rule(rule)?;
let groups = manifest.glob_groups(rule)?;
let mut files = resolve::expand_groups(&groups, base)?;
if let Some(file) = extra {
files.push(file.to_owned());
files.sort();
files.dedup();
}
if files.is_empty() && probes.is_empty() && !ttl::time_only(rule) {
return Ok(None);
}
Ok(Some(Digested {
digest: hashing::digest_with(base, &files, &probes)?,
probes,
}))
}
fn exec(call: &Invocation) -> Result<u8> {
let Some((program, rest)) = call.argv.split_first() else {
return Err(Error::EmptyCommand);
};
let status = Command::new(program)
.args(rest)
.current_dir(call.cwd)
.status()
.map_err(|source| Error::Spawn {
program: program.clone(),
source,
})?;
Ok(exit_code(status))
}
fn exit_code(status: ExitStatus) -> u8 {
let code = status.code().unwrap_or(1);
u8::try_from(code).unwrap_or(1)
}
#[cfg(test)]
#[path = "engine_tests.rs"]
mod tests;