diff --git a/crates/brioche/src/build.rs b/crates/brioche/src/build.rs index e83481e..e813951 100644 --- a/crates/brioche/src/build.rs +++ b/crates/brioche/src/build.rs @@ -3,15 +3,16 @@ use std::path::PathBuf; use std::process::ExitCode; use anyhow::Context as _; -use brioche_core::project::{ProjectHash, ProjectLocking, ProjectValidation, Projects}; +use brioche_core::Brioche; +use brioche_core::fs_utils; +use brioche_core::project::{ProjectHash, ProjectLocking, Projects}; use brioche_core::reporter::Reporter; use brioche_core::utils::DisplayDuration; -use brioche_core::{Brioche, fs_utils}; use clap::Parser; use tracing::Instrument as _; use crate::utils::{ - ProjectRef, ProjectRefs, ProjectRefsParser, ProjectSource, consolidate_result, + ProjectRef, ProjectRefs, ProjectRefsParser, consolidate_result, load_project_source, numbered_output_paths, resolve_project_refs, }; @@ -269,26 +270,6 @@ pub async fn build( Ok(exit_code) } -async fn load_project_source( - brioche: &Brioche, - projects: &Projects, - source: &ProjectSource, - locking: ProjectLocking, -) -> anyhow::Result { - match source { - ProjectSource::Local(path) => { - projects - .load(brioche, path, ProjectValidation::Standard, locking) - .await - } - ProjectSource::Registry(name) => { - projects - .load_from_registry(brioche, name, &brioche_core::project::Version::Any) - .await - } - } -} - struct BuildTargetOptions<'a> { project_hash: ProjectHash, export: &'a str, diff --git a/crates/brioche/src/install.rs b/crates/brioche/src/install.rs index 4983d50..dcebe51 100644 --- a/crates/brioche/src/install.rs +++ b/crates/brioche/src/install.rs @@ -1,28 +1,37 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::process::ExitCode; use anyhow::Context as _; use brioche_core::Brioche; -use brioche_core::project::ProjectHash; -use brioche_core::project::ProjectLocking; -use brioche_core::project::ProjectValidation; -use brioche_core::project::Projects; +use brioche_core::project::{ProjectHash, ProjectLocking, Projects}; use brioche_core::reporter::Reporter; use brioche_core::utils::DisplayDuration; use clap::Parser; use tracing::Instrument as _; -use crate::utils::consolidate_result; +use crate::utils::{ + ProjectRef, ProjectRefs, ProjectRefsParser, consolidate_result, load_project_source, + resolve_project_refs, +}; #[derive(Debug, Parser)] pub struct InstallArgs { - #[command(flatten)] - project: super::MultipleProjectArgs, + /// Projects to install (e.g., `./pkg`, `curl`, `./pkg^test`, `^test`, `curl^test,default`). + #[arg(value_parser = ProjectRefsParser, conflicts_with_all = ["project", "registry", "export"])] + targets: Vec, - /// Which TypeScript export to build. - #[arg(short, long, default_value = "default")] - export: String, + /// Deprecated: use positional arguments instead. + #[arg(short, long, hide = true, conflicts_with = "registry")] + project: Option, + + /// Deprecated: use positional arguments instead. + #[arg(short, long, hide = true)] + registry: Option, + + /// Deprecated: use positional arguments instead. + #[arg(short, long, hide = true)] + export: Option, /// Check the project before building. #[arg(long)] @@ -50,94 +59,120 @@ pub async fn install( .await?; crate::start_shutdown_handler(brioche.clone()); + let project_refs = resolve_project_refs(args.targets, args.project, args.registry, args.export); + let projects = brioche_core::project::Projects::default(); - let install_options = InstallOptions { - check: args.check, - locked: args.locked, - }; let locking = if args.locked { ProjectLocking::Locked } else { ProjectLocking::Unlocked }; - let mut error_result = None; - // Handle the case where no projects and no registries are specified - let projects_path = - if args.project.project.is_empty() && args.project.registry_project.is_empty() { - vec![PathBuf::from(".")] - } else { - args.project.project - }; + let mut error_result = None; - // Loop over the projects - for project_path in projects_path { - let project_name = format!("project '{name}'", name = project_path.display()); + // Load projects and pair each ref with its resolved hash + let mut load_cache: HashMap<_, _> = HashMap::new(); + let mut projects_resolved = Vec::with_capacity(project_refs.len()); - match projects - .load( - &brioche, - &project_path, - ProjectValidation::Standard, - locking, - ) - .await - { - Ok(project_hash) => { - let result = run_install( - &reporter, - &brioche, - js_platform, - &projects, - project_hash, - &project_name, - &args.export, - &install_options, - ) - .await; - - consolidate_result(&reporter, Some(&project_name), result, &mut error_result); + for project_ref in &project_refs { + if let Some(&hash) = load_cache.get(&project_ref.source) { + projects_resolved.push((hash, project_ref)); + continue; + } + let result = load_project_source(&brioche, &projects, &project_ref.source, locking).await; + match result { + Ok(hash) => { + load_cache.insert(&project_ref.source, hash); + projects_resolved.push((hash, project_ref)); } - Err(e) => { - consolidate_result(&reporter, Some(&project_name), Err(e), &mut error_result); + Err(err) => { + let name = project_ref.source.to_string(); + consolidate_result(&reporter, Some(&name), Err(err), &mut error_result); } } } + drop(load_cache); + + if error_result.is_some() { + guard.shutdown_console().await; + brioche.wait_for_tasks().await; + return Ok(ExitCode::FAILURE); + } + + // If the `--locked` flag is used, validate that all lockfiles are + // up-to-date. Otherwise, write any out-of-date lockfiles + if args.locked { + projects.validate_no_dirty_lockfiles()?; + } else { + let num_lockfiles_updated = projects.commit_dirty_lockfiles().await?; + if num_lockfiles_updated > 0 { + tracing::info!(num_lockfiles_updated, "updated lockfiles"); + } + } - // Loop over the registry projects - for registry_project in args.project.registry_project { - let project_name = format!("registry project '{registry_project}'"); + // Check (if --check): batch all loaded project hashes + if args.check { + let project_hashes = projects_resolved + .iter() + .map(|(hash, _)| *hash) + .collect::>(); - match projects - .load_from_registry( + if !project_hashes.is_empty() { + let checked = brioche_core::script::check::check( &brioche, - ®istry_project, - &brioche_core::project::Version::Any, + js_platform, + &projects, + &project_hashes, ) - .await - { - Ok(project_hash) => { - let result = run_install( - &reporter, - &brioche, - js_platform, - &projects, - project_hash, - &project_name, - &args.export, - &install_options, - ) - .await; + .await?; - consolidate_result(&reporter, Some(&project_name), result, &mut error_result); - } - Err(e) => { - consolidate_result(&reporter, Some(&project_name), Err(e), &mut error_result); + let result = checked.ensure_ok(brioche_core::script::check::DiagnosticLevel::Error); + + match result { + Ok(()) => reporter.emit(superconsole::Lines::from_multiline_string( + "No errors found", + superconsole::style::ContentStyle { + foreground_color: Some(superconsole::style::Color::Green), + ..superconsole::style::ContentStyle::default() + }, + )), + Err(diagnostics) => { + guard.shutdown_console().await; + + let mut output = Vec::new(); + diagnostics.write(&brioche.vfs, &mut output)?; + + reporter.emit(superconsole::Lines::from_multiline_string( + &String::from_utf8(output)?, + superconsole::style::ContentStyle::default(), + )); + + brioche.wait_for_tasks().await; + return Ok(ExitCode::FAILURE); + } } } } + // Install loop + for &(project_hash, ProjectRef { source, export }) in &projects_resolved { + let project_name = source.to_string(); + + let result = run_install( + &reporter, + &brioche, + js_platform, + &projects, + project_hash, + &project_name, + export, + ) + .await; + + consolidate_result(&reporter, Some(&project_name), result, &mut error_result); + } + guard.shutdown_console().await; brioche.wait_for_tasks().await; @@ -146,12 +181,6 @@ pub async fn install( Ok(exit_code) } -struct InstallOptions { - check: bool, - locked: bool, -} - -#[expect(clippy::too_many_arguments)] async fn run_install( reporter: &Reporter, brioche: &Brioche, @@ -160,48 +189,8 @@ async fn run_install( project_hash: ProjectHash, project_name: &str, export: &str, - options: &InstallOptions, ) -> Result { async { - // If the `--locked` flag is used, validate that all lockfiles are - // up-to-date. Otherwise, write any out-of-date lockfiles - if options.locked { - projects.validate_no_dirty_lockfiles()?; - } else { - let num_lockfiles_updated = projects.commit_dirty_lockfiles().await?; - if num_lockfiles_updated > 0 { - tracing::info!(num_lockfiles_updated, "updated lockfiles"); - } - } - - if options.check { - let project_hashes = HashSet::from_iter([project_hash]); - - let checked = - brioche_core::script::check::check(brioche, js_platform, projects, &project_hashes) - .await?; - - let result = checked.ensure_ok(brioche_core::script::check::DiagnosticLevel::Error); - - match result { - Ok(()) => reporter.emit(superconsole::Lines::from_multiline_string( - &format!("No errors found in {project_name}"), - superconsole::style::ContentStyle::default(), - )), - Err(diagnostics) => { - let mut output = Vec::new(); - diagnostics.write(&brioche.vfs, &mut output)?; - - reporter.emit(superconsole::Lines::from_multiline_string( - &String::from_utf8(output)?, - superconsole::style::ContentStyle::default(), - )); - - return Ok(false); - } - } - } - let recipe = brioche_core::script::evaluate::evaluate( brioche, js_platform, diff --git a/crates/brioche/src/live_update.rs b/crates/brioche/src/live_update.rs index 42d99ec..f0f2cfa 100644 --- a/crates/brioche/src/live_update.rs +++ b/crates/brioche/src/live_update.rs @@ -1,15 +1,26 @@ use std::collections::HashSet; +use std::path::PathBuf; use anyhow::Context as _; -use brioche_core::{project::ProjectLocking, utils::DisplayDuration}; +use brioche_core::project::ProjectLocking; +use brioche_core::utils::DisplayDuration; use bstr::ByteSlice as _; use clap::Parser; use tracing::Instrument as _; +use crate::utils::{ + ProjectRefs, ProjectRefsParser, ProjectSource, load_project_source, resolve_project_refs, +}; + #[derive(Debug, Parser)] pub struct LiveUpdateArgs { - #[command(flatten)] - project: super::ProjectArgs, + /// Local project directory to update (e.g., `./pkg`, `.`). + #[arg(value_parser = ProjectRefsParser, conflicts_with = "project")] + target: Option, + + /// Deprecated: use positional arguments instead. + #[arg(short, long, hide = true)] + project: Option, /// Check the project before building. #[arg(long)] @@ -33,8 +44,18 @@ pub async fn live_update( js_platform: brioche_core::script::JsPlatform, args: LiveUpdateArgs, ) -> anyhow::Result<()> { + let project_refs = + resolve_project_refs(args.target.into_iter().collect(), args.project, None, None); + + anyhow::ensure!( + project_refs.len() == 1, + "should accept a single project, but got {} targets", + project_refs.len() + ); + let project_ref = &project_refs[0]; + anyhow::ensure!( - args.project.registry.is_none(), + matches!(project_ref.source, ProjectSource::Local(_)), "cannot edit a registry project" ); @@ -56,7 +77,8 @@ pub async fn live_update( ProjectLocking::Unlocked }; - let project_hash = super::load_project(&brioche, &projects, &args.project, locking).await?; + let project_hash = + load_project_source(&brioche, &projects, &project_ref.source, locking).await?; // If the `--locked` flag is used, validate that all lockfiles are // up-to-date. Otherwise, write any out-of-date lockfiles @@ -206,7 +228,13 @@ pub async fn live_update( if did_update { // Reload the project from scratch let projects = brioche_core::project::Projects::default(); - super::load_project(&brioche, &projects, &args.project, ProjectLocking::Unlocked).await?; + load_project_source( + &brioche, + &projects, + &project_ref.source, + ProjectLocking::Unlocked, + ) + .await?; // Update lockfiles projects.commit_dirty_lockfiles().await?; diff --git a/crates/brioche/src/main.rs b/crates/brioche/src/main.rs index 8a3c354..f74712e 100644 --- a/crates/brioche/src/main.rs +++ b/crates/brioche/src/main.rs @@ -273,66 +273,6 @@ async fn export_project(args: ExportProjectArgs) -> anyhow::Result<()> { Ok(()) } -#[derive(Debug, clap::Args)] -struct ProjectArgs { - /// The path of the project directory to build [default: .] - #[clap(short, long)] - project: Option, - - /// The name of a registry project to build. - #[clap(short, long)] - registry: Option, -} - -#[derive(Debug, clap::Args)] -#[group(required = false, multiple = false)] -struct MultipleProjectArgs { - /// The path of the project directory to build [default: .] - #[clap(short, long)] - project: Vec, - - /// The name of a registry project to build. - #[clap(id = "registry", short, long)] - registry_project: Vec, -} - -async fn load_project( - brioche: &brioche_core::Brioche, - projects: &brioche_core::project::Projects, - args: &ProjectArgs, - locking: ProjectLocking, -) -> anyhow::Result { - let project_hash = match (&args.project, &args.registry) { - (Some(project), None) => { - projects - .load(brioche, project, ProjectValidation::Standard, locking) - .await? - } - (None, Some(registry)) => { - projects - .load_from_registry(brioche, registry, &brioche_core::project::Version::Any) - .await? - } - (None, None) => { - // Default to the current directory if a project path - // is not specified - projects - .load( - brioche, - &PathBuf::from("."), - ProjectValidation::Standard, - ProjectLocking::Unlocked, - ) - .await? - } - (Some(_), Some(_)) => { - anyhow::bail!("cannot specify both --project and --registry"); - } - }; - - Ok(project_hash) -} - #[derive(Debug, Default, Clone, Copy, clap::ValueEnum)] enum DisplayMode { /// Display with console output if stdout is a tty, otherwise use diff --git a/crates/brioche/src/run.rs b/crates/brioche/src/run.rs index e427356..28073f8 100644 --- a/crates/brioche/src/run.rs +++ b/crates/brioche/src/run.rs @@ -1,20 +1,34 @@ -use std::{collections::HashSet, process::ExitCode}; +use std::collections::HashSet; +use std::path::PathBuf; +use std::process::ExitCode; use anyhow::Context as _; -use brioche_core::{project::ProjectLocking, utils::DisplayDuration}; +use brioche_core::project::ProjectLocking; +use brioche_core::utils::DisplayDuration; use clap::Parser; #[cfg(unix)] use std::os::unix::process::CommandExt as _; use tracing::Instrument as _; +use crate::utils::{ProjectRefs, ProjectRefsParser, load_project_source, resolve_project_refs}; + #[derive(Debug, Parser)] pub struct RunArgs { - #[command(flatten)] - project: super::ProjectArgs, + /// Project to run (e.g., `./pkg`, `curl`, `./pkg^test`, `^test`). + #[arg(value_parser = ProjectRefsParser, conflicts_with_all = ["project", "registry", "export"])] + target: Option, + + /// Deprecated: use positional arguments instead. + #[arg(short, long, hide = true, conflicts_with = "registry")] + project: Option, - /// Which TypeScript export to build. - #[arg(short, long, default_value = "default")] - export: String, + /// Deprecated: use positional arguments instead. + #[arg(short, long, hide = true)] + registry: Option, + + /// Deprecated: use positional arguments instead. + #[arg(short, long, hide = true)] + export: Option, /// The path within the build artifact to execute. #[arg(short, long, default_value = "brioche-run")] @@ -50,6 +64,20 @@ pub async fn run( js_platform: brioche_core::script::JsPlatform, args: RunArgs, ) -> anyhow::Result { + let project_refs = resolve_project_refs( + args.target.into_iter().collect(), + args.project, + args.registry, + args.export, + ); + + anyhow::ensure!( + project_refs.len() == 1, + "should accept a single project and export, but got {} targets", + project_refs.len() + ); + let project_ref = &project_refs[0]; + let (reporter, mut guard) = if args.quiet { brioche_core::reporter::start_null_reporter() } else { @@ -73,7 +101,8 @@ pub async fn run( }; let build_future = async { - let project_hash = super::load_project(&brioche, &projects, &args.project, locking).await?; + let project_hash = + load_project_source(&brioche, &projects, &project_ref.source, locking).await?; // If the `--locked` flag is used, validate that all lockfiles are // up-to-date. Otherwise, write any out-of-date lockfiles @@ -121,7 +150,7 @@ pub async fn run( js_platform, &projects, project_hash, - &args.export, + &project_ref.export, ) .await?; @@ -130,7 +159,7 @@ pub async fn run( recipe, &brioche_core::bake::BakeScope::Project { project_hash, - export: args.export.clone(), + export: project_ref.export.clone(), }, ) .instrument(tracing::info_span!("bake")) diff --git a/crates/brioche/src/utils.rs b/crates/brioche/src/utils.rs index 6fb4b02..0ceaeba 100644 --- a/crates/brioche/src/utils.rs +++ b/crates/brioche/src/utils.rs @@ -3,6 +3,8 @@ use std::ffi::OsStr; use std::fmt; use std::path::{Path, PathBuf}; +use brioche_core::project::{ProjectHash, ProjectLocking, ProjectValidation, Projects}; + const DEFAULT_EXPORT: &str = "default"; /// Resolves input paths by merging positional args with the deprecated `--project` flag, @@ -174,7 +176,7 @@ pub fn resolve_project_refs( project: Option, registry: Option, export: Option, -) -> HashSet { +) -> Vec { let project_refs = if positional.is_empty() { let export = export.unwrap_or_else(|| DEFAULT_EXPORT.to_string()); let exports = vec![export]; @@ -191,6 +193,7 @@ pub fn resolve_project_refs( positional }; + let mut seen = HashSet::new(); project_refs .into_iter() .flat_map(|target| { @@ -199,9 +202,31 @@ pub fn resolve_project_refs( export, }) }) + .filter(|r| seen.insert((r.source.clone(), r.export.clone()))) .collect() } +/// Loads a project from either a local path or the registry. +pub async fn load_project_source( + brioche: &brioche_core::Brioche, + projects: &Projects, + source: &ProjectSource, + locking: ProjectLocking, +) -> anyhow::Result { + match source { + ProjectSource::Local(path) => { + projects + .load(brioche, path, ProjectValidation::Standard, locking) + .await + } + ProjectSource::Registry(name) => { + projects + .load_from_registry(brioche, name, &brioche_core::project::Version::Any) + .await + } + } +} + /// Generate numbered output paths from a base path. /// /// - `count == 0`: `[]` @@ -320,8 +345,7 @@ mod tests { #[test] fn test_resolve_project_refs_default() { - let project_refs = resolve_project_refs(vec![], None, None, None); - let result = project_refs.iter().collect::>(); + let result = resolve_project_refs(vec![], None, None, None); assert_eq!(result.len(), 1); assert_eq!(result[0].source, ProjectSource::Local(PathBuf::from(".")));