blob: e84ec264cce4fc83b1a9738df76645b52a59efa7 [file] [log] [blame]
use crate::{
load,
progress::{DumbConsoleProgress, FancyConsoleProgress, Progress},
terminal, trace, work,
};
use anyhow::anyhow;
use std::path::Path;
fn build(
options: work::Options,
build_filename: String,
targets: Vec<String>,
verbose: bool,
) -> anyhow::Result<Option<usize>> {
let (mut dumb_console, mut fancy_console);
let progress: &mut dyn Progress = if terminal::use_fancy() {
fancy_console = FancyConsoleProgress::new(verbose);
&mut fancy_console
} else {
dumb_console = DumbConsoleProgress::new(verbose);
&mut dumb_console
};
let mut state = trace::scope("load::read", || load::read(&build_filename))?;
let mut work = work::Work::new(
state.graph,
state.hashes,
state.db,
&options,
progress,
state.pools,
);
let mut tasks_finished = 0;
// Attempt to rebuild build.ninja.
let build_file_target = work.lookup(&build_filename);
if let Some(target) = build_file_target {
work.want_file(target)?;
match trace::scope("work.run", || work.run())? {
None => return Ok(None),
Some(0) => {
// build.ninja already up to date.
// TODO: this logic is not right in the case where a build has
// a step that doesn't touch build.ninja. We should instead
// verify the specific FileId was updated.
}
Some(n) => {
// Regenerated build.ninja; start over.
tasks_finished = n;
state = trace::scope("load::read", || load::read(&build_filename))?;
work = work::Work::new(
state.graph,
state.hashes,
state.db,
&options,
progress,
state.pools,
);
}
}
}
if !targets.is_empty() {
for name in &targets {
let target = work
.lookup(name)
.ok_or_else(|| anyhow::anyhow!("unknown path requested: {:?}", name))?;
if Some(target) == build_file_target {
// Already built above.
continue;
}
work.want_file(target)?;
}
} else if !state.default.is_empty() {
for target in state.default {
work.want_file(target)?;
}
} else {
work.want_every_file(build_file_target)?;
}
let tasks = trace::scope("work.run", || work.run())?;
// Include any tasks from initial build in final count of steps.
Ok(tasks.map(|n| n + tasks_finished))
}
fn default_parallelism() -> anyhow::Result<usize> {
// Ninja uses available processors + a constant, but I don't think the
// difference matters too much.
let par = std::thread::available_parallelism()?;
Ok(usize::from(par))
}
#[derive(argh::FromArgs)] // this struct generates the flags and --help output
/// n2, a ninja compatible build system
struct Args {
/// chdir before running
#[argh(option, short = 'C')]
chdir: Option<String>,
/// input build file [default=build.ninja]
#[argh(option, short = 'f', default = "(\"build.ninja\".into())")]
build_file: String,
/// debugging tools
#[argh(option, short = 'd')]
debug: Option<String>,
/// subcommands
#[argh(option, short = 't')]
tool: Option<String>,
/// parallelism [default uses system thread count]
#[argh(option, short = 'j')] // tododefault_parallelism()")]
parallelism: Option<usize>,
/// keep going until at least N failures (0 means infinity) [default=1]
#[argh(option, short = 'k', default = "1")]
keep_going: usize,
/// print version (required by cmake)
#[argh(switch, hidden_help)]
version: bool,
/// compdb flag (required by meson)
#[allow(dead_code)]
#[argh(switch, short = 'x', hidden_help)]
expand_rspfile: bool,
/// print executed command lines
#[argh(switch, short = 'v')]
verbose: bool,
/// targets to build
#[argh(positional)]
targets: Vec<String>,
}
fn run_impl() -> anyhow::Result<i32> {
let mut fake_ninja_compat = Path::new(&std::env::args().next().unwrap())
.file_name()
.unwrap()
== std::ffi::OsStr::new(&format!("ninja{}", std::env::consts::EXE_SUFFIX));
let args: Args = argh::from_env();
let mut options = work::Options {
parallelism: match args.parallelism {
Some(p) => p,
None => default_parallelism()?,
},
failures_left: Some(args.keep_going).filter(|&n| n > 0),
explain: false,
adopt: false,
};
if let Some(dir) = args.chdir {
let dir = Path::new(&dir);
std::env::set_current_dir(dir).map_err(|err| anyhow!("chdir {:?}: {}", dir, err))?;
}
if let Some(debug) = args.debug {
match debug.as_str() {
"ninja_compat" => fake_ninja_compat = true,
"explain" => options.explain = true,
"list" => {
println!("debug tools:");
println!(" explain print why each target is considered out of date");
println!(" trace generate json performance trace");
return Ok(1);
}
"trace" => trace::open("trace.json")?,
_ => anyhow::bail!("unknown -d {:?}, use -d list to list", debug),
}
}
if args.version {
if fake_ninja_compat {
// CMake requires a particular Ninja version.
println!("1.10.2");
return Ok(0);
} else {
println!("{}", env!("CARGO_PKG_VERSION"));
}
return Ok(0);
}
if let Some(tool) = args.tool {
match tool.as_str() {
"list" => {
println!("subcommands:");
println!(" (none yet, but see README if you're looking here trying to get CMake to work)");
return Ok(1);
}
"compdb" if fake_ninja_compat => {
// meson wants to invoke this tool.
return Ok(0); // do nothing; TODO
}
"recompact" if fake_ninja_compat => {
// CMake unconditionally invokes this tool, yuck.
return Ok(0); // do nothing
}
"restat" if fake_ninja_compat => {
// CMake invokes this after generating build files; mark build
// targets as up to date by running the build with "adopt" flag
// on.
options.adopt = true;
}
_ => {
anyhow::bail!("unknown -t {:?}, use -t list to list", tool);
}
}
}
match build(options, args.build_file, args.targets, args.verbose)? {
None => {
// Don't print any summary, the failing task is enough info.
return Ok(1);
}
Some(0) => {
// Special case: don't print numbers when no work done.
println!("n2: no work to do");
}
Some(n) => {
println!(
"n2: ran {} task{}, now up to date",
n,
if n == 1 { "" } else { "s" }
);
}
}
Ok(0)
}
pub fn run() -> anyhow::Result<i32> {
let res = run_impl();
trace::close();
res
}