#![forbid(unsafe_code)]
use std::collections::HashMap;
use std::io::IsTerminal;
use std::path::PathBuf;
use clap::Parser;
use mk_rs_core::graph::{build_graph_with_nrep, match_metarule};
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};
#[cfg(feature = "duckscript")]
use mk_rs_shell::DuckShell;
use mk_rs_shell::{CustomShell, ShShell};
#[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>,
targets: Vec<String>,
}
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 mut scope = builtin_scope();
import_env(&mut scope);
for arg in std::env::args().skip(1) {
if let Some(eq_pos) = arg.find('=') {
if !arg.starts_with('-') {
let var_name = &arg[..eq_pos];
let var_value = &arg[eq_pos + 1..];
scope.set_raw(var_name, var_value, Precedence::CommandLine);
}
}
}
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_with_scope(&tokens, &mut scope)?;
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,
all_targets: r.targets.clone(),
},
);
}
}
}
let target_names: Vec<String> = if cli.targets.is_empty() {
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
.iter()
.filter(|t| !t.contains('='))
.cloned()
.collect()
};
let nrep = scope
.get("NREP")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(1)
.max(1);
let mut graph = build_graph_with_nrep(&stmts, &target_names, nrep)?;
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 match_metarule(&node.name, pat).is_some() {
rules.insert(
node.name.clone(),
ResolvedRule {
recipe: r.recipe.clone().unwrap_or_default(),
attributes: r.attributes,
all_targets: vec![node.name.clone()], },
);
}
}
}
}
}
}
}
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(
"mk: duckscript support not compiled in (rebuild with --features duckscript)"
.to_string()
.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(())
}