use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode;
use mmz::error::Error;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const USAGE: &str = concat!(
"mmz ",
env!("CARGO_PKG_VERSION"),
" — memoized command runner
Usage:
mmz <command> [args...] run a command, skipping it when its inputs are unchanged
mmz --init write a starter .mmz/config.yaml in the current directory
mmz --status [--tag t]... show each rule's freshness as a table
mmz --status=json [--tag t]... the same as JSON, with each rule's inputs and hashes
mmz --status=json-schema print the JSON Schema for --status=json
mmz --is-fresh [--tag t]... [-- cmd]
exit 0 if cmd's rule (or every/tagged rule) is fresh; runs nothing
mmz --prune delete cache records whose rule no longer exists
mmz --import <dir> copy cache records in from another checkout of this repo
mmz --schema print the config JSON Schema
mmz --schema=fragment print the JSON Schema for an imported fragment
mmz --dump-config print the merged manifest and where each entry came from
mmz --dump-config=json the same as JSON
mmz --version print version and how many languages it parses
mmz --help print this help
mmz -- <command> [args] run a command whose name begins with a dash
Config lives in .mmz/config.yaml (nearest one, searching upward). mmz errors when no
manifest is found, the manifest is invalid, no rule matches, or a matched rule
has no inputs; relax the last two per project with the `strict` list.
`--tag`/`-t <tag>` (repeatable) narrows --is-fresh/--status to rules carrying
every listed tag (AND, not OR); untagged rules never match. Combining --tag
with a targeted command is a usage error — a command already resolves to one rule.
A --is-fresh whose selection holds no rule is refused (exit 7) rather than passing
on the strength of having checked nothing; --status reports it and exits 0.
Environment:
MMZ_NOW pin \"now\" to a Unix epoch in seconds, so a record's ran_at and
the AGE column read the same on every run. A malformed value is
refused, never ignored. Unset, mmz reads the system clock.
Exit codes:
0 fresh, skipped, or succeeded 6 a probe failed, could not run, or
1 --is-fresh: not fresh printed nothing (nothing recorded)
2 usage error 7 --is-fresh: nothing to gate (the
3 strict refusal (no rule / inputs) selection holds no rule)
4 manifest missing or invalid 8 a file mmz needed could not be
5 declared output missing after a read or written (nothing recorded)
successful run (nothing recorded) 70 internal error
127 command could not be spawned
otherwise the wrapped command's own exit code"
);
fn main() -> ExitCode {
env_logger::init();
let args: Vec<String> = std::env::args().skip(1).collect();
run_cli(&args)
}
fn run_cli(args: &[String]) -> ExitCode {
let Some((first, rest)) = args.split_first() else {
eprintln!("{USAGE}");
return ExitCode::from(2);
};
if first == "--" {
return wrap(rest);
}
if let Some(code) = action(first, rest) {
return code;
}
wrap(args)
}
fn action(first: &str, rest: &[String]) -> Option<ExitCode> {
match first {
"--version" | "-V" => Some(meta(
rest,
&format!("mmz {VERSION} ({})\n", mmz::ast::language_summary()),
)),
"--help" | "-h" => Some(meta(rest, &format!("{USAGE}\n"))),
schema if schema == "--schema" || schema.starts_with("--schema=") => {
Some(run_schema(schema, rest))
}
dump if dump == "--dump-config" || dump.starts_with("--dump-config=") => {
Some(run_dump_config(dump, rest))
}
"--init" => Some(run_init(rest)),
"--prune" => Some(run_prune(rest)),
"--import" => Some(run_import(rest)),
"--is-fresh" => Some(run_is_fresh(rest)),
status if status == "--status" || status.starts_with("--status=") => {
Some(run_status(status, rest))
}
other if other.starts_with('-') => Some(unknown_option(other)),
_ => None,
}
}
fn meta(rest: &[String], text: &str) -> ExitCode {
if rest.is_empty() {
emit(text)
} else {
usage("this option takes no arguments")
}
}
fn run_init(rest: &[String]) -> ExitCode {
if !rest.is_empty() {
return usage("`--init` takes no arguments");
}
let cwd = match current_dir() {
Ok(dir) => dir,
Err(code) => return code,
};
match mmz::init::init(&cwd) {
Ok(path) => emit(&format!("wrote {}\n", path.display())),
Err(err) => report_error(&err),
}
}
fn run_prune(rest: &[String]) -> ExitCode {
if !rest.is_empty() {
return usage("`--prune` takes no arguments");
}
let cwd = match current_dir() {
Ok(dir) => dir,
Err(code) => return code,
};
match mmz::prune::prune(&cwd) {
Ok(text) => emit(&text),
Err(err) => report_error(&err),
}
}
fn run_import(rest: &[String]) -> ExitCode {
let [dir] = rest else {
return usage("`--import` takes exactly one directory");
};
let cwd = match current_dir() {
Ok(dir) => dir,
Err(code) => return code,
};
match mmz::import_records::import(&cwd, std::path::Path::new(dir)) {
Ok(text) => emit(&text),
Err(err) => report_error(&err),
}
}
fn run_schema(arg: &str, rest: &[String]) -> ExitCode {
if !rest.is_empty() {
return usage("`--schema` takes no arguments");
}
let format = arg.strip_prefix("--schema").unwrap_or("");
match format {
"" => emit(mmz::schema::SCHEMA),
"=fragment" => emit(mmz::schema::FRAGMENT_SCHEMA),
other => usage(&format!(
"unknown `--schema` format `{}`; use fragment, or omit the suffix for the config schema",
other.trim_start_matches('=')
)),
}
}
fn run_dump_config(arg: &str, rest: &[String]) -> ExitCode {
if !rest.is_empty() {
return usage("`--dump-config` takes no arguments");
}
let cwd = match current_dir() {
Ok(dir) => dir,
Err(code) => return code,
};
let format = arg.strip_prefix("--dump-config").unwrap_or("");
let rendered = match format {
"" => mmz::dump::dump(&cwd),
"=json" => mmz::dump::dump_json(&cwd),
other => {
return usage(&format!(
"unknown `--dump-config` format `{}`; use json, or omit the suffix for the human form",
other.trim_start_matches('=')
));
}
};
match rendered {
Ok(text) => emit(&text),
Err(err) => report_error(&err),
}
}
fn run_status(arg: &str, rest: &[String]) -> ExitCode {
let (tags, rest) = match parse_tags(rest) {
Ok(parsed) => parsed,
Err(code) => return code,
};
if !rest.is_empty() {
return usage("`--status` takes no arguments");
}
let format = arg.strip_prefix("--status").unwrap_or("");
match format {
"=json-schema" => return emit(mmz::status::SCHEMA),
"" | "=json" => {}
other => {
return usage(&format!(
"unknown `--status` format `{}`; use json or json-schema",
other.trim_start_matches('=')
));
}
}
let cwd = match current_dir() {
Ok(dir) => dir,
Err(code) => return code,
};
let rendered = if format == "=json" {
mmz::status::report_json(&cwd, &tags)
} else {
mmz::status::report(&cwd, &tags)
};
match rendered {
Ok(text) => emit(&text),
Err(err) => report_error(&err),
}
}
fn run_is_fresh(rest: &[String]) -> ExitCode {
let cwd = match current_dir() {
Ok(dir) => dir,
Err(code) => return code,
};
let (tags, rest) = match parse_tags(rest) {
Ok(parsed) => parsed,
Err(code) => return code,
};
let argv = strip_separator(rest);
let target = if argv.is_empty() { None } else { Some(argv) };
match mmz::freshness::evaluate(&cwd, target, &tags) {
Ok(verdicts) => report_freshness(&verdicts),
Err(err) => report_error(&err),
}
}
fn parse_tags(rest: &[String]) -> Result<(Vec<String>, &[String]), ExitCode> {
let mut tags = Vec::new();
let mut cursor = rest;
while let Some((flag, tail)) = cursor.split_first() {
if flag != "-t" && flag != "--tag" {
break;
}
let Some((value, after)) = tail.split_first() else {
return Err(usage(&format!("`{flag}` requires a value")));
};
tags.push(value.clone());
cursor = after;
}
Ok((tags, cursor))
}
fn strip_separator(rest: &[String]) -> &[String] {
match rest.split_first() {
Some((first, tail)) if first == "--" => tail,
_ => rest,
}
}
fn report_freshness(verdicts: &[mmz::freshness::Verdict]) -> ExitCode {
let mut fresh = true;
let mut example: Option<&str> = None;
for verdict in verdicts {
if verdict.is_fresh() {
continue;
}
fresh = false;
let reason = verdict.reason().unwrap_or_else(|| "not fresh".to_owned());
eprintln!("mmz: `{}` is {} ({reason})", verdict.rule, verdict.state());
if example.is_none() && verdict.is_remediable() {
example = Some(&verdict.rule);
}
}
if fresh {
return ExitCode::SUCCESS;
}
if let Some(rule) = example {
eprintln!(
"mmz: re-run each listed command under mmz (e.g. `mmz {rule}`) to record a pass; a standalone run is not tracked"
);
}
ExitCode::from(1)
}
fn emit(text: &str) -> ExitCode {
let mut out = std::io::stdout().lock();
match out.write_all(text.as_bytes()).and_then(|()| out.flush()) {
Ok(()) => ExitCode::SUCCESS,
Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => ExitCode::SUCCESS,
Err(err) => {
eprintln!("mmz: cannot write to stdout: {err}");
ExitCode::from(70)
}
}
}
fn wrap(argv: &[String]) -> ExitCode {
let cwd = match current_dir() {
Ok(dir) => dir,
Err(code) => return code,
};
match mmz::run(argv, &cwd) {
Ok(code) => ExitCode::from(code),
Err(err) => report_error(&err),
}
}
fn current_dir() -> Result<PathBuf, ExitCode> {
std::env::current_dir().map_err(|err| {
eprintln!("mmz: cannot determine working directory: {err}");
ExitCode::from(70)
})
}
fn unknown_option(opt: &str) -> ExitCode {
eprintln!(
"mmz: unknown option `{opt}`; run `mmz --help`, or `mmz -- {opt} …` to wrap a command starting with a dash"
);
ExitCode::from(2)
}
fn usage(message: &str) -> ExitCode {
eprintln!("mmz: {message}");
ExitCode::from(2)
}
fn report_error(err: &Error) -> ExitCode {
eprintln!("mmz: {err}");
ExitCode::from(exit_for(err))
}
fn exit_for(err: &Error) -> u8 {
match err {
Error::EmptyCommand
| Error::ManifestExists { .. }
| Error::TagWithCommand
| Error::InvalidNow { .. }
| Error::ImportNotCheckout { .. }
| Error::ImportSelf { .. } => 2,
Error::NoMatch { .. } | Error::NoInputs { .. } => 3,
Error::NoManifest { .. }
| Error::ManifestParse { .. }
| Error::ManifestUnreadable { .. }
| Error::UnknownScope { .. }
| Error::UnknownInput { .. }
| Error::NameCollision { .. }
| Error::EmptyCommandName(_)
| Error::DuplicateCommand(_)
| Error::DuplicateTag { .. }
| Error::InvalidOutput { .. }
| Error::MacroSyntax { .. }
| Error::CollidingIdentity { .. }
| Error::Pattern { .. }
| Error::ImportMissing { .. }
| Error::ImportNotReadable { .. }
| Error::ImportCycle { .. }
| Error::DuplicateScope { .. }
| Error::DuplicateProbe { .. }
| Error::DuplicateCommandAcrossFiles { .. }
| Error::FragmentPolicyKey { .. }
| Error::NullPolicyKey { .. }
| Error::ProbeSource { .. }
| Error::EmptyProbeShell => 4,
Error::MissingOutput { .. } => 5,
Error::Probe { .. } => 6,
Error::NoRules { .. } | Error::NoTaggedRules { .. } | Error::NoExpansions { .. } => 7,
Error::InputVanished { .. }
| Error::InputUnreadable { .. }
| Error::InitWrite { .. }
| Error::CacheIo { .. } => 8,
Error::Spawn { .. } => 127,
Error::Serialize(_) | Error::Internal(_) => 70,
}
}