diff --git a/Cargo.lock b/Cargo.lock index a2073c4f..367bfb5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1057,6 +1057,7 @@ dependencies = [ "mimalloc", "path-slash", "pathdiff", + "ptree", "rayon", "rust-embed", "semver", @@ -1253,6 +1254,15 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "ptree" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "289cfd20ebec0e7ff2572e370dd7a1c9973ba666d3c38c5e747de0a4ada21f17" +dependencies = [ + "serde", +] + [[package]] name = "quote" version = "1.0.40" diff --git a/Cargo.toml b/Cargo.toml index ceedfba8..86b7c868 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ shell-words = "1.1.0" subst = "0.3.8" path-slash = "0.2.1" jobslot = "0.2.23" +ptree = { version = "0.5.2", default-features = false } [profile.release] lto = "fat" diff --git a/src/cli.rs b/src/cli.rs index b031c255..8395eaa8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -196,6 +196,20 @@ pub fn clap() -> clap::Command { .arg(define()) .add(SubcommandCandidates::new(task_completer)), ) + .subcommand( + Command::new("inspect") + .about("inspect current configuration") + .arg(build_dir()) + .subcommand( + Command::new("builders").arg( + Arg::new("tree") + .short('t') + .long("tree") + .help("output a tree of the configuration builders") + .action(ArgAction::SetTrue), + ), + ), + ) .subcommand( Command::new("clean") .about("clean current configuration") diff --git a/src/inspect.rs b/src/inspect.rs new file mode 100644 index 00000000..a8b67c57 --- /dev/null +++ b/src/inspect.rs @@ -0,0 +1,43 @@ +//! Inspect Laze project files, without any build output + +use std::io::Write; + +use anyhow::Result; +use camino::Utf8PathBuf; +use ptree::{write_tree, TreeBuilder}; + +use crate::{data::load, Context, ContextBag}; + +pub(crate) struct BuildInspector { + contexts: ContextBag, +} + +impl BuildInspector { + pub(crate) fn from_project(project_file: Utf8PathBuf, build_dir: Utf8PathBuf) -> Result { + let (contexts, _, _) = load(&project_file, &build_dir)?; + Ok(Self { contexts }) + } + pub(crate) fn inspect_builders(&self) -> Vec<&Context> { + self.contexts.builders_vec() + } + + fn add_tree_element(&self, context: &Context, tree: &mut TreeBuilder) { + self.contexts + .contexts + .iter() + .filter(|c| Some(context) == c.get_parent(&self.contexts)) + .for_each(|c| { + tree.begin_child(c.name.to_string()); + self.add_tree_element(c, tree); + tree.end_child(); + }) + } + + pub(crate) fn write_tree(&self, w: W) -> Result<()> { + let default = self.contexts.get_by_name(&"default".to_string()).unwrap(); + let mut tree_builder = TreeBuilder::new("default".to_string()); // The first node is always called default + self.add_tree_element(default, &mut tree_builder); + let tree = tree_builder.build(); + write_tree(&tree, w).map_err(|e| e.into()) + } +} diff --git a/src/main.rs b/src/main.rs index 3424a503..37cc74bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ mod data; mod download; mod generate; mod insights; +mod inspect; mod jobserver; mod model; mod nested_env; @@ -29,6 +30,7 @@ mod subst_ext; mod task_runner; mod utils; +use inspect::BuildInspector; use model::{Context, ContextBag, Dependency, Module, Rule, Task, TaskError}; use generate::{get_ninja_build_file, BuildInfo, GenerateMode, GeneratorBuilder, Selector}; @@ -147,85 +149,15 @@ fn try_main() -> Result { // handle project independent subcommands here match matches.subcommand() { - Some(("new", matches)) => { - new::from_matches(matches)?; - return Ok(0); - } - Some(("completion", matches)) => { - fn print_completions( - generator: G, - cmd: &mut clap::Command, - ) { - clap_complete::generate( - generator, - cmd, - cmd.get_name().to_string(), - &mut std::io::stdout(), - ); - } - if let Some(generator) = matches - .get_one::("generator") - .copied() - { - let mut cmd = cli::clap(); - eprintln!("Generating completion file for {}...", generator); - print_completions(generator, &mut cmd); - } - return Ok(0); - } - Some(("manpages", matches)) => { - fn create_manpage(cmd: clap::Command, outfile: &Utf8Path) -> Result<(), Error> { - let man = clap_mangen::Man::new(cmd); - let mut buffer: Vec = Default::default(); - man.render(&mut buffer)?; - - std::fs::write(outfile, buffer)?; - Ok(()) - } - let mut outpath: Utf8PathBuf = - matches.get_one::("outdir").unwrap().clone(); - let cmd = cli::clap(); - - outpath.push("laze.1"); - create_manpage(cmd.clone(), &outpath)?; - - for subcommand in cmd.get_subcommands() { - if subcommand.is_hide_set() { - continue; - } - let name = subcommand.get_name(); - outpath.pop(); - outpath.push(format!("laze-{name}.1")); - create_manpage(subcommand.clone(), &outpath)?; - } - - return Ok(0); - } - Some(("git-clone", matches)) => { - let repository = matches.get_one::("repository").unwrap(); - let target_path = matches.get_one::("target_path").cloned(); - let wanted_commit = matches.get_one::("commit"); - let sparse_paths = matches - .get_many::("sparse-add") - .map(|v| v.into_iter().cloned().collect::>()); - - GIT_CACHE - .get() - .unwrap() - .cloner() - .commit(wanted_commit.cloned()) - .extra_clone_args_from_matches(matches) - .repository_url(repository.clone()) - .sparse_paths(sparse_paths) - .target_path(target_path) - .update(matches.get_flag("update")) - .do_clone()?; - - return Ok(0); - } - _ => (), + Some(("new", matches)) => cmd_new(matches), + Some(("completion", matches)) => cmd_completion(matches), + Some(("manpages", matches)) => cmd_manpages(matches), + Some(("git-clone", matches)) => cmd_gitclone(matches), + _ => try_main_build(matches), } +} +fn try_main_build(matches: clap::ArgMatches) -> Result { if let Some(dir) = matches.get_one::("chdir") { env::set_current_dir(dir).context(format!("cannot change to directory \"{dir}\""))?; } @@ -253,266 +185,384 @@ fn try_main() -> Result { jobserver::maybe_init_fromenv(verbose); match matches.subcommand() { - Some(("build", build_matches)) => { - let build_dir = build_matches.get_one::("build-dir").unwrap(); + Some(("build", matches)) => cmd_build( + matches, + global, + verbose, + project_root, + project_file, + start_relpath, + ), + Some(("inspect", matches)) => cmd_inspect(matches, project_file), + Some(("clean", matches)) => cmd_clean(matches, global, verbose, start_relpath), + _ => Ok(0), + } +} + +fn cmd_new(matches: &clap::ArgMatches) -> Result { + new::from_matches(matches)?; + Ok(0) +} + +fn cmd_completion(matches: &clap::ArgMatches) -> Result { + fn print_completions(generator: G, cmd: &mut clap::Command) { + clap_complete::generate( + generator, + cmd, + cmd.get_name().to_string(), + &mut std::io::stdout(), + ); + } + if let Some(generator) = matches + .get_one::("generator") + .copied() + { + let mut cmd = cli::clap(); + eprintln!("Generating completion file for {}...", generator); + print_completions(generator, &mut cmd); + } + Ok(0) +} - // collect builder names from args - let builders = Selector::from(build_matches.get_many::("builders")); - // collect app names from args - let apps = Selector::from(build_matches.get_many::("apps")); +fn cmd_manpages(matches: &clap::ArgMatches) -> Result { + fn create_manpage(cmd: clap::Command, outfile: &Utf8Path) -> Result<(), Error> { + let man = clap_mangen::Man::new(cmd); + let mut buffer: Vec = Default::default(); + man.render(&mut buffer)?; - let jobs = build_matches.get_one::("jobs").copied(); + std::fs::write(outfile, buffer)?; + Ok(()) + } + let mut outpath: Utf8PathBuf = matches.get_one::("outdir").unwrap().clone(); + let cmd = cli::clap(); - // Unless we've inherited a jobserver, create one. - jobserver::maybe_set_limit( - jobs.unwrap_or_else(|| { - // default to number of logical cores. - // TODO: figure out in which case this might error - std::thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(1) - }), - verbose, - ); + outpath.push("laze.1"); + create_manpage(cmd.clone(), &outpath)?; - let keep_going = build_matches.get_one::("keep_going").copied(); + for subcommand in cmd.get_subcommands() { + if subcommand.is_hide_set() { + continue; + } + let name = subcommand.get_name(); + outpath.pop(); + outpath.push(format!("laze-{name}.1")); + create_manpage(subcommand.clone(), &outpath)?; + } - let partitioner = build_matches - .get_one::("partition") - .map(|v| v.build()); + return Ok(0); +} - let info_outfile = build_matches.get_one::("info-export"); +fn cmd_gitclone(matches: &clap::ArgMatches) -> Result { + let repository = matches.get_one::("repository").unwrap(); + let target_path = matches.get_one::("target_path").cloned(); + let wanted_commit = matches.get_one::("commit"); + let sparse_paths = matches + .get_many::("sparse-add") + .map(|v| v.into_iter().cloned().collect::>()); - println!("laze: building {apps} for {builders}"); + GIT_CACHE + .get() + .unwrap() + .cloner() + .commit(wanted_commit.cloned()) + .extra_clone_args_from_matches(matches) + .repository_url(repository.clone()) + .sparse_paths(sparse_paths) + .target_path(target_path) + .update(matches.get_flag("update")) + .do_clone()?; + + return Ok(0); +} - // collect CLI selected/disabled modules - let select = get_selects(build_matches); - let disable = get_disables(build_matches); +fn cmd_build( + matches: &clap::ArgMatches, + global: bool, + verbose: u8, + project_root: Utf8PathBuf, + project_file: Utf8PathBuf, + start_relpath: Utf8PathBuf, +) -> Result { + let build_dir = matches.get_one::("build-dir").unwrap(); + + // collect builder names from args + let builders = Selector::from(matches.get_many::("builders")); + // collect app names from args + let apps = Selector::from(matches.get_many::("apps")); + + let jobs = matches.get_one::("jobs").copied(); + + // Unless we've inherited a jobserver, create one. + jobserver::maybe_set_limit( + jobs.unwrap_or_else(|| { + // default to number of logical cores. + // TODO: figure out in which case this might error + std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1) + }), + verbose, + ); - // collect CLI env overrides - let cli_env = get_cli_vars(build_matches)?; + let keep_going = matches.get_one::("keep_going").copied(); - let mode = match global { - true => GenerateMode::Global, - false => GenerateMode::Local(start_relpath.clone()), - }; + let partitioner = matches + .get_one::("partition") + .map(|v| v.build()); - let generator = GeneratorBuilder::default() - .project_root(project_root.clone()) - .project_file(project_file) - .build_dir(build_dir.clone()) - .mode(mode.clone()) - .builders(builders.clone()) - .apps(apps.clone()) - .select(select) - .disable(disable) - .cli_env(cli_env) - .partitioner(partitioner.as_ref().map(|x| format!("{:?}", x))) - .collect_insights(info_outfile.is_some()) - .disable_cache(info_outfile.is_some()) - .build() - .unwrap(); - - // arguments parsed, launch generation of ninja file(s) - let builds = generator.execute(partitioner, verbose > 1)?; - - if let Some(info_outfile) = info_outfile { - use std::fs::File; - use std::io::BufWriter; - let info_outfile = start_relpath.join(info_outfile); - let insights = insights::Insights::from_builds(&builds.build_infos); - let buffer = - BufWriter::new(File::create(&info_outfile).with_context(|| { - format!("creating info export file \"{info_outfile}\"") - })?); - serde_json::to_writer_pretty(buffer, &insights) - .with_context(|| "exporting build info".to_string())?; - } + let info_outfile = matches.get_one::("info-export"); - let ninja_build_file = get_ninja_build_file(build_dir, &mode); + println!("laze: building {apps} for {builders}"); - if build_matches.get_flag("compile-commands") { - let mut compile_commands = project_root.clone(); - compile_commands.push("compile_commands.json"); - println!("laze: generating {compile_commands}"); - ninja::generate_compile_commands(&ninja_build_file, &compile_commands)?; - } + // collect CLI selected/disabled modules + let select = get_selects(matches); + let disable = get_disables(matches); - // collect (optional) task and it's arguments - let task = collect_tasks(build_matches); + // collect CLI env overrides + let cli_env = get_cli_vars(matches)?; - // generation of ninja build file complete. - // exit here if requested. - if task.is_none() && build_matches.get_flag("generate-only") { - return Ok(0); - } + let mode = match global { + true => GenerateMode::Global, + false => GenerateMode::Local(start_relpath.clone()), + }; - if let Some((task, args)) = task { - let builds: Vec<&BuildInfo> = builds - .build_infos - .iter() - .filter(|build_info| { - builders.selects(&build_info.builder) - && apps.selects(&build_info.binary) - && build_info.tasks.contains_key(task) - }) - .collect(); + let generator = GeneratorBuilder::default() + .project_root(project_root.clone()) + .project_file(project_file) + .build_dir(build_dir.clone()) + .mode(mode.clone()) + .builders(builders.clone()) + .apps(apps.clone()) + .select(select) + .disable(disable) + .cli_env(cli_env) + .partitioner(partitioner.as_ref().map(|x| format!("{:?}", x))) + .collect_insights(info_outfile.is_some()) + .disable_cache(info_outfile.is_some()) + .build() + .unwrap(); + + // arguments parsed, launch generation of ninja file(s) + let builds = generator.execute(partitioner, verbose > 1)?; + + if let Some(info_outfile) = info_outfile { + use std::fs::File; + use std::io::BufWriter; + let info_outfile = start_relpath.join(info_outfile); + let insights = insights::Insights::from_builds(&builds.build_infos); + let buffer = BufWriter::new( + File::create(&info_outfile) + .with_context(|| format!("creating info export file \"{info_outfile}\""))?, + ); + serde_json::to_writer_pretty(buffer, &insights) + .with_context(|| "exporting build info".to_string())?; + } - if !builds - .iter() - .any(|build_info| build_info.tasks.iter().any(|t| t.1.is_ok() && t.0 == task)) - { - let mut not_available = 0; - for b in builds { - for t in &b.tasks { - if t.1.is_err() && t.0 == task { - not_available += 1; - if verbose > 0 { - eprintln!( + let ninja_build_file = get_ninja_build_file(build_dir, &mode); + + if matches.get_flag("compile-commands") { + let mut compile_commands = project_root.clone(); + compile_commands.push("compile_commands.json"); + println!("laze: generating {compile_commands}"); + ninja::generate_compile_commands(&ninja_build_file, &compile_commands)?; + } + + // collect (optional) task and it's arguments + let task = collect_tasks(matches); + + // generation of ninja build file complete. + // exit here if requested. + if task.is_none() && matches.get_flag("generate-only") { + return Ok(0); + } + + if let Some((task, args)) = task { + let builds: Vec<&BuildInfo> = builds + .build_infos + .iter() + .filter(|build_info| { + builders.selects(&build_info.builder) + && apps.selects(&build_info.binary) + && build_info.tasks.contains_key(task) + }) + .collect(); + + if !builds + .iter() + .any(|build_info| build_info.tasks.iter().any(|t| t.1.is_ok() && t.0 == task)) + { + let mut not_available = 0; + for b in builds { + for t in &b.tasks { + if t.1.is_err() && t.0 == task { + not_available += 1; + if verbose > 0 { + eprintln!( "laze: warn: task \"{task}\" for binary \"{}\" on builder \"{}\": {}", b.binary, b.builder, t.1.as_ref().err().unwrap() ); - } - } } } - - if not_available > 0 && verbose == 0 { - println!("laze hint: {not_available} target(s) not available, try `--verbose` to list why"); - } - return Err(anyhow!("no matching target for task \"{}\" found.", task)); } + } - let multiple = build_matches.get_flag("multiple"); + if not_available > 0 && verbose == 0 { + println!("laze hint: {not_available} target(s) not available, try `--verbose` to list why"); + } + return Err(anyhow!("no matching target for task \"{}\" found.", task)); + } - if builds.len() > 1 && !multiple { - println!("laze: multiple task targets found:"); - for build_info in builds { - eprintln!("{} {}", build_info.builder, build_info.binary); - } + let multiple = matches.get_flag("multiple"); - // TODO: allow running tasks for multiple targets - return Err(anyhow!( - "please specify one of these builders, or -m/--multiple-tasks." - )); - } + if builds.len() > 1 && !multiple { + println!("laze: multiple task targets found:"); + for build_info in builds { + eprintln!("{} {}", build_info.builder, build_info.binary); + } - let task_name = task; - let mut targets = Vec::new(); - let mut ninja_targets = Vec::new(); + // TODO: allow running tasks for multiple targets + return Err(anyhow!( + "please specify one of these builders, or -m/--multiple-tasks." + )); + } - for build in builds { - let task = build.tasks.get(task).unwrap(); - if let Ok(task) = task { - if task.build_app() { - let build_target = build.out.clone(); - ninja_targets.push(build_target); - } - targets.push((build, task)); - } - } + let task_name = task; + let mut targets = Vec::new(); + let mut ninja_targets = Vec::new(); - if !ninja_targets.is_empty() && !build_matches.get_flag("generate-only") { - let ninja_build_file = get_ninja_build_file(build_dir, &mode); - if ninja_run( - ninja_build_file.as_path(), - verbose > 0, - Some(ninja_targets), - jobs, - None, // have to fail on build error b/c no way of knowing *which* target - // failed - )? != 0 - { - return Err(anyhow!("build error")); - }; + for build in builds { + let task = build.tasks.get(task).unwrap(); + if let Ok(task) = task { + if task.build_app() { + let build_target = build.out.clone(); + ninja_targets.push(build_target); } - - let (results, errors) = task_runner::run_tasks( - task_name, - targets.iter(), - args.as_ref(), - verbose, - keep_going.unwrap(), - project_root.as_std_path(), - )?; - - if errors > 0 { - if multiple { - // multiple tasks, more than zero errors. print them - println!("laze: the following tasks failed:"); - for result in results.iter().filter(|r| r.result.is_err()) { - println!( - "laze: task \"{task_name}\" on app \"{}\" for builder \"{}\"", - result.build.binary, result.build.builder - ); - } - } else { - // only one error. can't move out of first, cant clone, so print that here. - let (first, _rest) = results.split_first().unwrap(); - if let Err(e) = &first.result { - eprintln!("laze: error: {e:#}"); - } - } - return Ok(1); - } - } else { - // build ninja target arguments, if necessary - let targets: Option> = if let Selector::All = builders { - if let Selector::All = apps { - None - } else { - // TODO: filter by app - None - } - } else { - Some( - builds - .build_infos - .iter() - .filter_map(|build_info| { - (builders.selects(&build_info.builder) - && apps.selects(&build_info.binary)) - .then_some(build_info.out.clone()) - }) - .collect(), - ) - }; - - ninja_run( - ninja_build_file.as_path(), - verbose > 0, - targets, - jobs, - keep_going, - )?; + targets.push((build, task)); } } - Some(("clean", clean_matches)) => { - let unused = clean_matches.get_flag("unused"); - let build_dir = clean_matches.get_one::("build-dir").unwrap(); - let mode = match global { - true => GenerateMode::Global, - false => GenerateMode::Local(start_relpath), - }; + + if !ninja_targets.is_empty() && !matches.get_flag("generate-only") { let ninja_build_file = get_ninja_build_file(build_dir, &mode); - let tool = match unused { - true => "cleandead", - false => "clean", - }; - let clean_target: Option> = Some(vec!["-t".into(), tool.into()]); - ninja_run( + if ninja_run( ninja_build_file.as_path(), verbose > 0, - clean_target, - None, - None, - )?; + Some(ninja_targets), + jobs, + None, // have to fail on build error b/c no way of knowing *which* target + // failed + )? != 0 + { + return Err(anyhow!("build error")); + }; } - _ => {} + + let (results, errors) = task_runner::run_tasks( + task_name, + targets.iter(), + args.as_ref(), + verbose, + keep_going.unwrap(), + project_root.as_std_path(), + )?; + + if errors > 0 { + if multiple { + // multiple tasks, more than zero errors. print them + println!("laze: the following tasks failed:"); + for result in results.iter().filter(|r| r.result.is_err()) { + println!( + "laze: task \"{task_name}\" on app \"{}\" for builder \"{}\"", + result.build.binary, result.build.builder + ); + } + } else { + // only one error. can't move out of first, cant clone, so print that here. + let (first, _rest) = results.split_first().unwrap(); + if let Err(e) = &first.result { + eprintln!("laze: error: {e:#}"); + } + } + return Ok(1); + } + } else { + // build ninja target arguments, if necessary + let targets: Option> = if let Selector::All = builders { + if let Selector::All = apps { + None + } else { + // TODO: filter by app + None + } + } else { + Some( + builds + .build_infos + .iter() + .filter_map(|build_info| { + (builders.selects(&build_info.builder) && apps.selects(&build_info.binary)) + .then_some(build_info.out.clone()) + }) + .collect(), + ) + }; + + ninja_run( + ninja_build_file.as_path(), + verbose > 0, + targets, + jobs, + keep_going, + )?; + } + Ok(0) +} + +fn cmd_clean( + matches: &clap::ArgMatches, + global: bool, + verbose: u8, + start_relpath: Utf8PathBuf, +) -> Result { + let unused = matches.get_flag("unused"); + let build_dir = matches.get_one::("build-dir").unwrap(); + let mode = match global { + true => GenerateMode::Global, + false => GenerateMode::Local(start_relpath), + }; + let ninja_build_file = get_ninja_build_file(build_dir, &mode); + let tool = match unused { + true => "cleandead", + false => "clean", }; + let clean_target: Option> = Some(vec!["-t".into(), tool.into()]); + ninja_run( + ninja_build_file.as_path(), + verbose > 0, + clean_target, + None, + None, + )?; + Ok(0) +} +fn cmd_inspect(matches: &clap::ArgMatches, project_file: Utf8PathBuf) -> Result { + let build_dir = matches.get_one::("build-dir").unwrap(); + match matches.subcommand() { + Some(("builders", matches)) => { + let build_inspector = BuildInspector::from_project(project_file, build_dir.clone())?; + if matches.get_flag("tree") { + build_inspector.write_tree(&std::io::stdout())?; + } else { + let builders = build_inspector.inspect_builders(); + builders + .iter() + .for_each(|builder| println!("{}", builder.name)); + } + } + _ => (), + }; Ok(0) }