use std::io::IsTerminal;
use std::collections::HashMap;
use std::path::PathBuf;
use clap::Parser;
use mk_rs_core::graph::build_graph;
use mk_rs_core::lex::{tokenize, ShellMode};
use mk_rs_core::parse::Stmt;
use mk_rs_core::sched::{execute, ResolvedRule, SchedOptions};
use mk_rs_core::var::{builtin_scope, import_env, Precedence};
use mk_rs_shell::{CustomShell, ShShell};
#[cfg(feature = "duckscript")]
use mk_rs_shell::DuckShell;
#[derive(Parser)]
#[command(
name = "mk",
version = concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HASH"), ")"),
about
)]
struct Cli {
#[arg(short = 'f', default_value = "mkfile")]
file: PathBuf,
#[arg(short = 'n')]
no_exec: bool,
#[arg(short = 'e')]
explain: bool,
#[arg(short = 't')]
touch: bool,
#[arg(short = 'a')]
all: bool,
#[arg(short = 'k')]
keep_going: bool,
#[arg(short = 'i')]
force_intermediates: bool,
#[arg(short = 'q')]
silent: bool,
#[arg(long, default_value = "auto")]
color: String,
#[arg(short = 'd')]
debug: Option<String>,
#[arg(short = 'w')]
whatif: Option<String>,
#[arg(short = 'C')]
directory: Option<PathBuf>,
#[arg(long = "graph")]
graph: bool,
#[arg(long = "graph-of")]
graph_of: Option<String>,
targets: Vec<String>,
}
fn match_simple(target: &str, pattern: &str) -> Option<String> {
if let Some(pos) = pattern.find('%') {
let prefix = &pattern[..pos];
let suffix = &pattern[pos + 1..];
if target.starts_with(prefix) && target.ends_with(suffix) {
let stem = &target[prefix.len()..target.len() - suffix.len()];
return Some(stem.to_string());
}
}
None
}
fn main() {
if let Err(e) = run() {
eprintln!("mk: {}", e);
std::process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let _ = &cli.force_intermediates;
let _ = &cli.whatif;
if let Some(ref debug) = cli.debug {
for ch in debug.chars() {
match ch {
'p' => eprintln!("mk: debug: parsing enabled"),
'g' => eprintln!("mk: debug: graph building enabled"),
'e' => eprintln!("mk: debug: execution enabled"),
_ => eprintln!("mk: warning: unknown debug flag '{}'", ch),
}
}
}
if let Some(ref dir) = cli.directory {
std::env::set_current_dir(dir)?;
}
let input = std::fs::read_to_string(&cli.file).or_else(|_| {
std::fs::read_to_string("mkfile")
}).map_err(|_| {
format!(
"no mkfile: could not read '{}' or 'mkfile'",
cli.file.display()
)
})?;
let tokens = tokenize(&input, ShellMode::Sh)?;
let stmts = mk_rs_core::parse::parse(&tokens)?;
let mut scope = builtin_scope();
import_env(&mut scope);
for stmt in &stmts {
if let Stmt::Assign(a) = stmt {
scope.set(&a.name, &a.value, Precedence::Mkfile);
}
}
let mut rules: HashMap<String, ResolvedRule> = HashMap::new();
for stmt in &stmts {
if let Stmt::Rule(r) = stmt {
for t in &r.targets {
rules.insert(
t.clone(),
ResolvedRule {
recipe: r.recipe.clone().unwrap_or_default(),
attributes: r.attributes,
},
);
}
}
}
let target_names: Vec<String> = if cli.targets.is_empty() {
if cli.graph || cli.graph_of.is_some() {
stmts.iter()
.filter_map(|s| if let Stmt::Rule(r) = s { Some(r) } else { None })
.flat_map(|r| r.targets.iter().cloned())
.filter(|t| !t.contains('%') && !t.contains('&'))
.collect()
} else {
let first_rule = stmts.iter().find_map(|s| {
if let Stmt::Rule(r) = s {
Some(r)
} else {
None
}
});
match first_rule {
Some(r) => vec![r.targets[0].clone()],
None => {
eprintln!("mk: no targets specified and no rules in mkfile");
std::process::exit(1);
}
}
}
} else {
cli.targets.clone()
};
let mut graph = build_graph(&stmts, &target_names)?;
if cli.graph || cli.graph_of.is_some() {
use mk_rs_core::graph::GraphScope;
let scope = if cli.graph_of.is_some() {
GraphScope::Subgraph
} else {
GraphScope::All
};
let dot = graph.to_dot(scope, cli.graph_of.as_deref());
if !dot.is_empty() {
println!("{}", dot);
}
return Ok(());
}
for node in &graph.nodes {
if !rules.contains_key(&node.name) {
for stmt in &stmts {
if let Stmt::Rule(r) = stmt {
if r.is_metarule && !r.is_regex {
for pat in &r.targets {
if !pat.contains('%') && !pat.contains('&') { continue; }
if let Some(_) = match_simple(&node.name, pat) {
rules.insert(node.name.clone(), ResolvedRule {
recipe: r.recipe.clone().unwrap_or_default(),
attributes: r.attributes,
});
}
}
}
}
}
}
}
let mkshell = scope.get("MKSHELL").unwrap_or("/bin/sh").to_string();
let shell: Box<dyn mk_rs_core::shell::Shell> = {
if mkshell.contains("duckscript") || mkshell.ends_with(".ds") {
#[cfg(not(feature = "duckscript"))]
{
return Err(format!(
"mk: duckscript support not compiled in (rebuild with --features duckscript)"
).into());
}
#[cfg(feature = "duckscript")]
{
Box::new(DuckShell)
}
} else if mkshell == "/bin/sh" || mkshell == "sh" {
Box::new(ShShell)
} else {
Box::new(CustomShell::new(&mkshell))
}
};
let mkflags = std::env::args()
.skip(1)
.filter(|a| a.starts_with('-') || a.contains('='))
.collect::<Vec<_>>()
.join(" ");
let mkargs = std::env::args()
.skip(1)
.filter(|a| !a.starts_with('-') && !a.contains('='))
.collect::<Vec<_>>()
.join(" ");
let use_color = match cli.color.as_str() {
"always" => true,
"never" => false,
_ => std::io::stderr().is_terminal(),
};
let opts = SchedOptions {
keep_going: cli.keep_going,
no_exec: cli.no_exec,
explain: cli.explain,
touch: cli.touch,
silent: cli.silent,
all: cli.all,
nproc: 1, force_intermediates: cli.force_intermediates,
mkshell: shell.name().to_string(),
mkflags,
mkargs,
color: use_color,
};
let env = scope.export();
let working_dir = std::env::current_dir()?;
let outcome = execute(&mut graph, &rules, shell.as_ref(), &working_dir, &env, &opts)?;
if !outcome.failed.is_empty() {
for (target, msg) in &outcome.failed {
eprintln!("mk: {}: {}", target, msg);
}
std::process::exit(1);
}
Ok(())
}