From 441ff4cd3290e90e160510e47436f9a39489bb6b Mon Sep 17 00:00:00 2001 From: Kyle Lacy Date: Sun, 29 Sep 2024 03:26:37 -0700 Subject: [PATCH] Add `--locked` flag for several subcommands (#133) * Add `--locked` flag for `build` and `check` subcommands * Update `Projects.load` to replace boolean arg with enum for clarity * Update `project` module to use an enum instead of a bool for project locking * Update `Projects` to take `ProjectLocking` arg when loading a project * Add `--locked` flag to `install` and `run` subcommands too --- crates/brioche-core/src/project.rs | 346 ++++++++++++---------- crates/brioche-core/src/script/bridge.rs | 19 +- crates/brioche-core/src/script/lsp.rs | 18 +- crates/brioche-core/tests/project_load.rs | 15 +- crates/brioche-test-support/src/lib.rs | 27 +- crates/brioche/src/build.rs | 28 +- crates/brioche/src/check.rs | 63 +++- crates/brioche/src/format.rs | 12 +- crates/brioche/src/install.rs | 50 +++- crates/brioche/src/main.rs | 30 +- crates/brioche/src/publish.rs | 12 +- crates/brioche/src/run.rs | 26 +- 12 files changed, 443 insertions(+), 203 deletions(-) diff --git a/crates/brioche-core/src/project.rs b/crates/brioche-core/src/project.rs index 0aa799b..a051f1c 100644 --- a/crates/brioche-core/src/project.rs +++ b/crates/brioche-core/src/project.rs @@ -16,6 +16,28 @@ use super::{vfs::FileId, Brioche}; pub mod analyze; +#[derive(Debug, Clone, Copy)] +pub enum ProjectValidation { + /// Fully validate the project, ensuring all modules and dependencies + /// resolve properly + Standard, + + /// Perform minimal validation while loading the project. This is suitable + /// for situations like the LSP, where syntax errors are common + Minimal, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ProjectLocking { + /// The project is unlocked, meaning the lockfile can be updated + Unlocked, + + /// The project is locked, meaning the lockfile must exist and be + /// up-to-date. This is useful for loading registry projects, or when + /// the `--locked` CLI flag is specified + Locked, +} + #[derive(Clone, Default)] pub struct Projects { inner: Arc>, @@ -26,7 +48,8 @@ impl Projects { &self, brioche: &Brioche, path: &Path, - fully_valid: bool, + validation: ProjectValidation, + locking: ProjectLocking, ) -> anyhow::Result { { let projects = self @@ -34,11 +57,14 @@ impl Projects { .read() .map_err(|_| anyhow::anyhow!("failed to acquire 'projects' lock"))?; if let Some(project_hash) = projects.paths_to_projects.get(path) { - if fully_valid { - let errors = &projects.project_load_errors[project_hash]; - if !errors.is_empty() { - anyhow::bail!("project load errors: {errors:?}"); + match validation { + ProjectValidation::Standard => { + let errors = &projects.project_load_errors[project_hash]; + if !errors.is_empty() { + anyhow::bail!("project load errors: {errors:?}"); + } } + ProjectValidation::Minimal => {} } return Ok(*project_hash); @@ -49,20 +75,24 @@ impl Projects { self.clone(), brioche.clone(), path.to_owned(), - fully_valid, + validation, + locking, 100, ) .await?; - if fully_valid { - let projects = self - .inner - .read() - .map_err(|_| anyhow::anyhow!("failed to acquire 'projects' lock"))?; - let errors = &projects.project_load_errors[&project_hash]; - if !errors.is_empty() { - anyhow::bail!("project load errors: {errors:?}"); + match validation { + ProjectValidation::Standard => { + let projects = self + .inner + .read() + .map_err(|_| anyhow::anyhow!("failed to acquire 'projects' lock"))?; + let errors = &projects.project_load_errors[&project_hash]; + if !errors.is_empty() { + anyhow::bail!("project load errors: {errors:?}"); + } } + ProjectValidation::Minimal => {} } Ok(project_hash) @@ -72,7 +102,8 @@ impl Projects { &self, brioche: &Brioche, path: &Path, - validate: bool, + validation: ProjectValidation, + locking: ProjectLocking, ) -> anyhow::Result { { let projects = self @@ -86,7 +117,7 @@ impl Projects { for ancestor in path.ancestors().skip(1) { if tokio::fs::try_exists(ancestor.join("project.bri")).await? { - return self.load(brioche, ancestor, validate).await; + return self.load(brioche, ancestor, validation, locking).await; } } @@ -106,7 +137,14 @@ impl Projects { .await .with_context(|| format!("failed to fetch '{project_name}' from registry"))?; - let loaded_project_hash = self.load(brioche, &local_path, true).await?; + let loaded_project_hash = self + .load( + brioche, + &local_path, + ProjectValidation::Standard, + ProjectLocking::Locked, + ) + .await?; anyhow::ensure!( loaded_project_hash == project_hash, @@ -493,7 +531,8 @@ async fn load_project( projects: Projects, brioche: Brioche, path: PathBuf, - fully_valid: bool, + validation: ProjectValidation, + locking: ProjectLocking, depth: usize, ) -> anyhow::Result { let rt = tokio::runtime::Handle::current(); @@ -503,7 +542,7 @@ async fn load_project( local_set.spawn_local(async move { let result = - load_project_inner(&projects, &brioche, &path, fully_valid, false, depth).await; + load_project_inner(&projects, &brioche, &path, validation, locking, depth).await; let _ = tx.send(result).inspect_err(|err| { tracing::warn!("failed to send project load result: {err:?}"); }); @@ -521,8 +560,8 @@ async fn load_project_inner( projects: &Projects, brioche: &Brioche, path: &Path, - fully_valid: bool, - lockfile_required: bool, + validation: ProjectValidation, + locking: ProjectLocking, depth: usize, ) -> anyhow::Result<(ProjectHash, Arc, Vec)> { tracing::debug!(path = %path.display(), "resolving project"); @@ -539,24 +578,22 @@ async fn load_project_inner( let lockfile: Option = match lockfile_contents { Ok(contents) => match serde_json::from_str(&contents) { Ok(lockfile) => Some(lockfile), - Err(error) => { - if lockfile_required { + Err(error) => match locking { + ProjectLocking::Locked => { return Err(error).context(format!( "failed to parse lockfile at {}", lockfile_path.display() )); - } else { - None } - } + ProjectLocking::Unlocked => None, + }, }, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - if lockfile_required { + Err(err) if err.kind() == std::io::ErrorKind::NotFound => match locking { + ProjectLocking::Locked => { anyhow::bail!("lockfile not found: {}", lockfile_path.display()); - } else { - None } - } + ProjectLocking::Unlocked => None, + }, Err(error) => { return Err(error).context(format!( "failed to read lockfile at {}", @@ -590,8 +627,8 @@ async fn load_project_inner( brioche, name, &dep_path, - fully_valid, - lockfile_required, + validation, + locking, dep_depth, &mut errors, ) @@ -609,8 +646,8 @@ async fn load_project_inner( workspace.as_ref(), name, version, - fully_valid, - lockfile_required, + validation, + locking, lockfile.as_ref(), dep_depth, &mut new_lockfile, @@ -652,8 +689,8 @@ async fn load_project_inner( workspace.as_ref(), dep_name, &Version::Any, - fully_valid, - lockfile_required, + validation, + locking, lockfile.as_ref(), dep_depth, &mut new_lockfile, @@ -681,20 +718,23 @@ async fn load_project_inner( let mut module_statics = BTreeMap::new(); for static_ in &module.statics { // Only resolve the static if we need a fully valid project - if fully_valid { - let recipe_hash = resolve_static( - brioche, - &path, - module, - static_, - lockfile_required, - lockfile.as_ref(), - &mut new_lockfile, - ) - .await?; - module_statics.insert(static_.clone(), Some(recipe_hash)); - } else { - module_statics.insert(static_.clone(), None); + match validation { + ProjectValidation::Standard => { + let recipe_hash = resolve_static( + brioche, + &path, + module, + static_, + locking, + lockfile.as_ref(), + &mut new_lockfile, + ) + .await?; + module_statics.insert(static_.clone(), Some(recipe_hash)); + } + ProjectValidation::Minimal => { + module_statics.insert(static_.clone(), None); + } } } @@ -726,33 +766,39 @@ async fn load_project_inner( // the new lockfile includes old statics that weren't updated. This // can mean that e.g. unnecessary downloads are kept, but this is // appropriate for situations like the LSP - if !fully_valid { - let Lockfile { - dependencies: _, - downloads: new_downloads, - git_refs: new_git_refs, - } = &mut new_lockfile; - - if let Some(lockfile) = &lockfile { - for (url, hash) in &lockfile.downloads { - new_downloads - .entry(url.clone()) - .or_insert_with(|| hash.clone()); - } + match validation { + ProjectValidation::Standard => {} + ProjectValidation::Minimal => { + let Lockfile { + dependencies: _, + downloads: new_downloads, + git_refs: new_git_refs, + } = &mut new_lockfile; + + if let Some(lockfile) = &lockfile { + for (url, hash) in &lockfile.downloads { + new_downloads + .entry(url.clone()) + .or_insert_with(|| hash.clone()); + } - for (url, options) in &lockfile.git_refs { - new_git_refs - .entry(url.clone()) - .or_insert_with(|| options.clone()); + for (url, options) in &lockfile.git_refs { + new_git_refs + .entry(url.clone()) + .or_insert_with(|| options.clone()); + } } } } if lockfile.as_ref() != Some(&new_lockfile) { - if lockfile_required { - anyhow::bail!("lockfile at {} is out of date", lockfile_path.display()); - } else { - projects.dirty_lockfiles.insert(lockfile_path, new_lockfile); + match locking { + ProjectLocking::Unlocked => { + projects.dirty_lockfiles.insert(lockfile_path, new_lockfile); + } + ProjectLocking::Locked => { + anyhow::bail!("lockfile at {} is out of date", lockfile_path.display()); + } } } @@ -779,20 +825,13 @@ async fn try_load_path_dependency_with_errors( brioche: &Brioche, name: &str, dep_path: &Path, - fully_valid: bool, - lockfile_required: bool, + validation: ProjectValidation, + locking: ProjectLocking, dep_depth: usize, errors: &mut Vec, ) -> Option { - let result = load_project_inner( - projects, - brioche, - dep_path, - fully_valid, - lockfile_required, - dep_depth, - ) - .await; + let result = + load_project_inner(projects, brioche, dep_path, validation, locking, dep_depth).await; match result { Ok((dep_hash, _, dep_errors)) => { @@ -824,22 +863,16 @@ async fn try_load_registry_dependency_with_errors( workspace: Option<&Workspace>, name: &str, version: &Version, - fully_valid: bool, - lockfile_required: bool, + validation: ProjectValidation, + locking: ProjectLocking, lockfile: Option<&Lockfile>, dep_depth: usize, new_lockfile: &mut Lockfile, errors: &mut Vec, ) -> Option { - let resolved_dep_result = resolve_dependency_to_local_path( - brioche, - workspace, - name, - version, - lockfile_required, - lockfile, - ) - .await; + let resolved_dep_result = + resolve_dependency_to_local_path(brioche, workspace, name, version, locking, lockfile) + .await; let resolved_dep = match resolved_dep_result { Ok(resolved_dep) => resolved_dep, Err(error) => { @@ -855,8 +888,8 @@ async fn try_load_registry_dependency_with_errors( projects, brioche, &resolved_dep.local_path, - fully_valid, - resolved_dep.lockfile_required, + validation, + resolved_dep.locking, dep_depth, ) .await; @@ -906,7 +939,7 @@ async fn resolve_dependency_to_local_path( workspace: Option<&Workspace>, dependency_name: &str, dependency_version: &Version, - lockfile_required: bool, + locking: ProjectLocking, lockfile: Option<&Lockfile>, ) -> anyhow::Result { if let Some(workspace) = workspace { @@ -922,7 +955,7 @@ async fn resolve_dependency_to_local_path( return Ok(ResolvedDependency { local_path: workspace_path, expected_hash: None, - lockfile_required, + locking, should_lock: None, }); } @@ -934,17 +967,18 @@ async fn resolve_dependency_to_local_path( lockfile.and_then(|lockfile| lockfile.dependencies.get(dependency_name)); let dep_hash = match lockfile_dep_hash { Some(dep_hash) => *dep_hash, - None => { - if lockfile_required { - anyhow::bail!("dependency '{}' not found in lockfile", dependency_name); - } else { + None => match locking { + ProjectLocking::Unlocked => { resolve_project_from_registry(brioche, dependency_name, dependency_version) .await .with_context(|| { format!("failed to resolve '{dependency_name}' from registry") })? } - } + ProjectLocking::Locked => { + anyhow::bail!("dependency '{}' not found in lockfile", dependency_name); + } + }, }; let local_path = fetch_project_from_registry(brioche, dep_hash) @@ -954,7 +988,7 @@ async fn resolve_dependency_to_local_path( Ok(ResolvedDependency { local_path, expected_hash: Some(dep_hash), - lockfile_required: true, + locking: ProjectLocking::Locked, should_lock: Some(dep_hash), }) } @@ -962,7 +996,7 @@ async fn resolve_dependency_to_local_path( struct ResolvedDependency { local_path: PathBuf, expected_hash: Option, - lockfile_required: bool, + locking: ProjectLocking, should_lock: Option, } @@ -1251,7 +1285,7 @@ async fn resolve_static( project_root: &Path, module: &analyze::ModuleAnalysis, static_: &analyze::StaticQuery, - lockfile_required: bool, + locking: ProjectLocking, lockfile: Option<&Lockfile>, new_lockfile: &mut Lockfile, ) -> anyhow::Result { @@ -1405,41 +1439,45 @@ async fn resolve_static( let download_hash: crate::Hash; let blob_hash: Option; - if let Some(hash) = current_download_hash { - // If we have the hash from the lockfile, use it to build - // the recipe. But, we don't have the blob hash yet - download_hash = hash.clone(); - blob_hash = None; - } else if lockfile_required { - // Error out if the download isn't in the lockfile but where - // updating the lockfile is disabled - anyhow::bail!("hash for download '{url}' not found in lockfile"); - } else { - // Download the URL as a blob - let new_blob_hash = crate::download::download(brioche, url, None).await?; - let blob_path = crate::blob::local_blob_path(brioche, new_blob_hash); - let mut blob = tokio::fs::File::open(&blob_path).await?; - - // Compute a hash to store in the lockfile - let mut hasher = crate::Hasher::new_sha256(); - let mut buffer = vec![0u8; 1024 * 1024]; - loop { - let length = blob - .read(&mut buffer) - .await - .context("failed to read blob")?; - if length == 0 { - break; + match (current_download_hash, locking) { + (Some(hash), _) => { + // If we have the hash from the lockfile, use it to build + // the recipe. But, we don't have the blob hash yet + download_hash = hash.clone(); + blob_hash = None; + } + (None, ProjectLocking::Unlocked) => { + // Download the URL as a blob + let new_blob_hash = crate::download::download(brioche, url, None).await?; + let blob_path = crate::blob::local_blob_path(brioche, new_blob_hash); + let mut blob = tokio::fs::File::open(&blob_path).await?; + + // Compute a hash to store in the lockfile + let mut hasher = crate::Hasher::new_sha256(); + let mut buffer = vec![0u8; 1024 * 1024]; + loop { + let length = blob + .read(&mut buffer) + .await + .context("failed to read blob")?; + if length == 0 { + break; + } + + hasher.update(&buffer[..length]); } - hasher.update(&buffer[..length]); + // Record both the hash for the recipe plus the output + // blob hash + download_hash = hasher.finish()?; + blob_hash = Some(new_blob_hash); } - - // Record both the hash for the recipe plus the output - // blob hash - download_hash = hasher.finish()?; - blob_hash = Some(new_blob_hash); - }; + (None, ProjectLocking::Locked) => { + // Error out if the download isn't in the lockfile but where + // updating the lockfile is disabled + anyhow::bail!("hash for download '{url}' not found in lockfile"); + } + } // Create the download recipe, which is equivalent to the URL // we downloaded or the one recorded in the lockfile @@ -1497,21 +1535,23 @@ async fn resolve_static( .and_then(|repo_refs| repo_refs.get(ref_)) }); - let commit = if let Some(commit) = current_commit { - commit.clone() - } else if lockfile_required { - // Error out if the git ref isn't in the lockfile but where - // updating the lockfile is disabled - anyhow::bail!( - "commit for git repo '{repository}' ref '{ref_}' not found in lockfile" - ); - } else { - // Fetch the current commit hash of the git ref from the repo - crate::download::fetch_git_commit_for_ref(repository, ref_) - .await - .with_context(|| { - format!("failed to fetch ref '{ref_}' from git repo '{repository}'") - })? + let commit = match (current_commit, locking) { + (Some(commit), _) => commit.clone(), + (None, ProjectLocking::Unlocked) => { + // Fetch the current commit hash of the git ref from the repo + crate::download::fetch_git_commit_for_ref(repository, ref_) + .await + .with_context(|| { + format!("failed to fetch ref '{ref_}' from git repo '{repository}'") + })? + } + (None, ProjectLocking::Locked) => { + // Error out if the git ref isn't in the lockfile but where + // updating the lockfile is disabled + anyhow::bail!( + "commit for git repo '{repository}' ref '{ref_}' not found in lockfile" + ); + } }; // Update the new lockfile with the commit diff --git a/crates/brioche-core/src/script/bridge.rs b/crates/brioche-core/src/script/bridge.rs index 268d270..b07742f 100644 --- a/crates/brioche-core/src/script/bridge.rs +++ b/crates/brioche-core/src/script/bridge.rs @@ -9,7 +9,7 @@ use crate::{ blob::BlobHash, project::{ analyze::{GitRefOptions, StaticInclude, StaticOutput, StaticOutputKind, StaticQuery}, - ProjectHash, Projects, + ProjectHash, ProjectLocking, ProjectValidation, Projects, }, recipe::{Artifact, DownloadRecipe, Recipe, WithMeta}, Brioche, @@ -41,8 +41,14 @@ impl RuntimeBridge { tokio::spawn(async move { match message { RuntimeBridgeMessage::LoadProjectFromModulePath { path, result_tx } => { - let result = - projects.load_from_module_path(&brioche, &path, false).await; + let result = projects + .load_from_module_path( + &brioche, + &path, + ProjectValidation::Minimal, + ProjectLocking::Unlocked, + ) + .await; let _ = result_tx.send(result); } RuntimeBridgeMessage::LoadSpecifierContents { @@ -124,7 +130,12 @@ impl RuntimeBridge { } let result = projects - .load_from_module_path(&brioche, &path, false) + .load_from_module_path( + &brioche, + &path, + ProjectValidation::Minimal, + ProjectLocking::Unlocked, + ) .await .map(|_| true); let _ = result_tx.send(result); diff --git a/crates/brioche-core/src/script/lsp.rs b/crates/brioche-core/src/script/lsp.rs index 5ceb4cd..8d441f0 100644 --- a/crates/brioche-core/src/script/lsp.rs +++ b/crates/brioche-core/src/script/lsp.rs @@ -7,7 +7,7 @@ use tower_lsp::lsp_types::request::GotoTypeDefinitionResponse; use tower_lsp::lsp_types::*; use tower_lsp::{Client, LanguageServer}; -use crate::project::Projects; +use crate::project::{ProjectLocking, ProjectValidation, Projects}; use crate::script::compiler_host::{brioche_compiler_host, BriocheCompilerHost}; use crate::script::format::format_code; use crate::{Brioche, BriocheBuilder}; @@ -501,10 +501,18 @@ async fn try_update_lockfile_for_module( .context("failed to build `Brioche` instance")?; let projects = Projects::default(); - tokio::time::timeout(load_timeout, projects.load(&brioche, &project_path, false)) - .await - .context("timed out trying to load project")? - .context("failed to load project")?; + tokio::time::timeout( + load_timeout, + projects.load( + &brioche, + &project_path, + ProjectValidation::Minimal, + ProjectLocking::Unlocked, + ), + ) + .await + .context("timed out trying to load project")? + .context("failed to load project")?; let updated = projects .commit_dirty_lockfile_for_project_path(&project_path) diff --git a/crates/brioche-core/tests/project_load.rs b/crates/brioche-core/tests/project_load.rs index 34438db..53504d5 100644 --- a/crates/brioche-core/tests/project_load.rs +++ b/crates/brioche-core/tests/project_load.rs @@ -1,4 +1,5 @@ use assert_matches::assert_matches; +use brioche_core::project::{ProjectLocking, ProjectValidation}; #[tokio::test] async fn test_project_load_simple() -> anyhow::Result<()> { @@ -792,11 +793,21 @@ async fn test_project_load_with_remote_workspace_registry_dep() -> anyhow::Resul let projects = brioche_core::project::Projects::default(); let bar_hash = projects - .load(&brioche, &bar_dir, true) + .load( + &brioche, + &bar_dir, + ProjectValidation::Standard, + ProjectLocking::Unlocked, + ) .await .expect("failed to load bar project"); let foo_hash = projects - .load(&brioche, &foo_dir, true) + .load( + &brioche, + &foo_dir, + ProjectValidation::Standard, + ProjectLocking::Unlocked, + ) .await .expect("failed to load foo project"); diff --git a/crates/brioche-test-support/src/lib.rs b/crates/brioche-test-support/src/lib.rs index 9d2b20b..1f6d85c 100644 --- a/crates/brioche-test-support/src/lib.rs +++ b/crates/brioche-test-support/src/lib.rs @@ -8,7 +8,7 @@ use std::{ use brioche_core::{ blob::{BlobHash, SaveBlobOptions}, - project::{self, ProjectHash, Projects}, + project::{self, ProjectHash, ProjectLocking, ProjectValidation, Projects}, recipe::{ CreateDirectory, Directory, File, ProcessRecipe, ProcessTemplate, ProcessTemplateComponent, Recipe, WithMeta, @@ -60,7 +60,14 @@ pub async fn load_project( path: &Path, ) -> anyhow::Result<(Projects, ProjectHash)> { let projects = Projects::default(); - let project_hash = projects.load(brioche, path, true).await?; + let project_hash = projects + .load( + brioche, + path, + ProjectValidation::Standard, + ProjectLocking::Unlocked, + ) + .await?; Ok((projects, project_hash)) } @@ -70,7 +77,14 @@ pub async fn load_project_no_validate( path: &Path, ) -> anyhow::Result<(Projects, ProjectHash)> { let projects = Projects::default(); - let project_hash = projects.load(brioche, path, false).await?; + let project_hash = projects + .load( + brioche, + path, + ProjectValidation::Minimal, + ProjectLocking::Unlocked, + ) + .await?; Ok((projects, project_hash)) } @@ -402,7 +416,12 @@ impl TestContext { let projects = Projects::default(); let project_hash = projects - .load(&self.brioche, &temp_project_path, true) + .load( + &self.brioche, + &temp_project_path, + ProjectValidation::Standard, + ProjectLocking::Unlocked, + ) .await .expect("failed to load temp project"); projects.commit_dirty_lockfiles().await.unwrap(); diff --git a/crates/brioche/src/build.rs b/crates/brioche/src/build.rs index d1ec830..bfb14e3 100644 --- a/crates/brioche/src/build.rs +++ b/crates/brioche/src/build.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, process::ExitCode}; use anyhow::Context as _; -use brioche_core::{fs_utils, reporter::ConsoleReporterKind}; +use brioche_core::{fs_utils, project::ProjectLocking, reporter::ConsoleReporterKind}; use clap::Parser; use human_repr::HumanDuration; use tracing::Instrument; @@ -24,6 +24,10 @@ pub struct BuildArgs { #[arg(long)] check: bool, + /// Validate that the lockfile is up-to-date + #[arg(long)] + locked: bool, + /// Replace the output path if it already exists #[arg(long)] replace: bool, @@ -53,12 +57,24 @@ pub async fn build(args: BuildArgs) -> anyhow::Result { .await?; let projects = brioche_core::project::Projects::default(); - let build_future = async { - let project_hash = super::load_project(&brioche, &projects, &args.project).await?; + let locking = if args.locked { + ProjectLocking::Locked + } else { + ProjectLocking::Unlocked + }; - let num_lockfiles_updated = projects.commit_dirty_lockfiles().await?; - if num_lockfiles_updated > 0 { - tracing::info!(num_lockfiles_updated, "updated lockfiles"); + let build_future = async { + let project_hash = super::load_project(&brioche, &projects, &args.project, locking).await?; + + // 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"); + } } if args.check { diff --git a/crates/brioche/src/check.rs b/crates/brioche/src/check.rs index 2e33666..6d62fa2 100644 --- a/crates/brioche/src/check.rs +++ b/crates/brioche/src/check.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use std::process::ExitCode; use brioche_core::project::ProjectHash; +use brioche_core::project::ProjectLocking; +use brioche_core::project::ProjectValidation; use brioche_core::project::Projects; use brioche_core::reporter::ConsoleReporterKind; use brioche_core::reporter::Reporter; @@ -13,6 +15,10 @@ use crate::consolidate_result; #[derive(Debug, Parser)] pub struct CheckArgs { + /// Validate that the lockfile is up-to-date + #[arg(long)] + locked: bool, + #[command(flatten)] project: super::MultipleProjectArgs, } @@ -26,6 +32,14 @@ pub async fn check(args: CheckArgs) -> anyhow::Result { .await?; let projects = brioche_core::project::Projects::default(); + let check_options = CheckOptions { + locked: args.locked, + }; + let locking = if args.locked { + ProjectLocking::Locked + } else { + ProjectLocking::Unlocked + }; let mut error_result = Option::None; // Handle the case where no projects and no registries are specified @@ -40,10 +54,25 @@ pub async fn check(args: CheckArgs) -> anyhow::Result { for project_path in projects_path { let project_name = format!("project '{name}'", name = project_path.display()); - match projects.load(&brioche, &project_path, true).await { + match projects + .load( + &brioche, + &project_path, + ProjectValidation::Standard, + locking, + ) + .await + { Ok(project_hash) => { - let result = - run_check(&reporter, &brioche, &projects, project_hash, &project_name).await; + let result = run_check( + &reporter, + &brioche, + &projects, + project_hash, + &project_name, + &check_options, + ) + .await; consolidate_result(&reporter, &project_name, result, &mut error_result); } Err(e) => { @@ -65,8 +94,15 @@ pub async fn check(args: CheckArgs) -> anyhow::Result { .await { Ok(project_hash) => { - let result = - run_check(&reporter, &brioche, &projects, project_hash, &project_name).await; + let result = run_check( + &reporter, + &brioche, + &projects, + project_hash, + &project_name, + &check_options, + ) + .await; consolidate_result(&reporter, &project_name, result, &mut error_result); } Err(e) => { @@ -86,17 +122,28 @@ pub async fn check(args: CheckArgs) -> anyhow::Result { Ok(exit_code) } +struct CheckOptions { + locked: bool, +} + async fn run_check( reporter: &Reporter, brioche: &Brioche, projects: &Projects, project_hash: ProjectHash, project_name: &String, + options: &CheckOptions, ) -> Result { let result = async { - let num_lockfiles_updated = projects.commit_dirty_lockfiles().await?; - if num_lockfiles_updated > 0 { - tracing::info!(num_lockfiles_updated, "updated lockfiles"); + // 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"); + } } brioche_core::script::check::check(brioche, projects, project_hash).await diff --git a/crates/brioche/src/format.rs b/crates/brioche/src/format.rs index 5430177..0444828 100644 --- a/crates/brioche/src/format.rs +++ b/crates/brioche/src/format.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, process::ExitCode}; use brioche_core::{ - project::{ProjectHash, Projects}, + project::{ProjectHash, ProjectLocking, ProjectValidation, Projects}, reporter::{ConsoleReporterKind, Reporter}, }; use clap::Parser; @@ -35,7 +35,15 @@ pub async fn format(args: FormatArgs) -> anyhow::Result { for project_path in args.project { let project_name = format!("project '{name}'", name = project_path.display()); - match projects.load(&brioche, &project_path, true).await { + match projects + .load( + &brioche, + &project_path, + ProjectValidation::Standard, + ProjectLocking::Unlocked, + ) + .await + { Ok(project_hash) => { let result = run_format( &reporter, diff --git a/crates/brioche/src/install.rs b/crates/brioche/src/install.rs index 2f36d4e..53e28f1 100644 --- a/crates/brioche/src/install.rs +++ b/crates/brioche/src/install.rs @@ -3,6 +3,8 @@ use std::process::ExitCode; use anyhow::Context as _; use brioche_core::project::ProjectHash; +use brioche_core::project::ProjectLocking; +use brioche_core::project::ProjectValidation; use brioche_core::project::Projects; use brioche_core::reporter::ConsoleReporterKind; use brioche_core::reporter::Reporter; @@ -25,6 +27,10 @@ pub struct InstallArgs { /// Check the project before building #[arg(long)] check: bool, + + /// Validate that the lockfile is up-to-date + #[arg(long)] + locked: bool, } pub async fn install(args: InstallArgs) -> anyhow::Result { @@ -36,6 +42,15 @@ pub async fn install(args: InstallArgs) -> anyhow::Result { .await?; 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 = Option::None; // Handle the case where no projects and no registries are specified @@ -50,7 +65,15 @@ pub async fn install(args: InstallArgs) -> anyhow::Result { for project_path in projects_path { let project_name = format!("project '{name}'", name = project_path.display()); - match projects.load(&brioche, &project_path, true).await { + match projects + .load( + &brioche, + &project_path, + ProjectValidation::Standard, + locking, + ) + .await + { Ok(project_hash) => { let result = run_install( &reporter, @@ -59,7 +82,7 @@ pub async fn install(args: InstallArgs) -> anyhow::Result { project_hash, &project_name, &args.export, - args.check, + &install_options, ) .await; @@ -94,7 +117,7 @@ pub async fn install(args: InstallArgs) -> anyhow::Result { project_hash, &project_name, &args.export, - args.check, + &install_options, ) .await; @@ -120,6 +143,11 @@ pub async fn install(args: InstallArgs) -> anyhow::Result { Ok(exit_code) } +struct InstallOptions { + check: bool, + locked: bool, +} + async fn run_install( reporter: &Reporter, brioche: &Brioche, @@ -127,17 +155,23 @@ async fn run_install( project_hash: ProjectHash, project_name: &String, export: &String, - check: bool, + options: &InstallOptions, ) -> Result { async { reporter.set_is_evaluating(true); - let num_lockfiles_updated = projects.commit_dirty_lockfiles().await?; - if num_lockfiles_updated > 0 { - tracing::info!(num_lockfiles_updated, "updated lockfiles"); + // 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 check { + if options.check { let checked = brioche_core::script::check::check(brioche, projects, project_hash).await?; diff --git a/crates/brioche/src/main.rs b/crates/brioche/src/main.rs index 5397349..8f64e16 100644 --- a/crates/brioche/src/main.rs +++ b/crates/brioche/src/main.rs @@ -1,6 +1,9 @@ use std::{collections::HashMap, path::PathBuf, process::ExitCode, sync::Arc}; -use brioche_core::reporter::ConsoleReporterKind; +use brioche_core::{ + project::{ProjectLocking, ProjectValidation}, + reporter::ConsoleReporterKind, +}; use clap::Parser; mod build; @@ -194,7 +197,14 @@ async fn export_project(args: ExportProjectArgs) -> anyhow::Result<()> { let brioche = brioche_core::BriocheBuilder::new(reporter).build().await?; let projects = brioche_core::project::Projects::default(); - let project_hash = projects.load(&brioche, &args.project, true).await?; + let project_hash = projects + .load( + &brioche, + &args.project, + ProjectValidation::Standard, + ProjectLocking::Unlocked, + ) + .await?; let project = projects.project(project_hash)?; let mut project_references = brioche_core::references::ProjectReferences::default(); brioche_core::references::project_references( @@ -245,9 +255,14 @@ 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, true).await?, + (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) @@ -256,7 +271,14 @@ async fn load_project( (None, None) => { // Default to the current directory if a project path // is not specified - projects.load(brioche, &PathBuf::from("."), true).await? + projects + .load( + brioche, + &PathBuf::from("."), + ProjectValidation::Standard, + ProjectLocking::Unlocked, + ) + .await? } (Some(_), Some(_)) => { anyhow::bail!("cannot specify both --project and --registry"); diff --git a/crates/brioche/src/publish.rs b/crates/brioche/src/publish.rs index 9d41e3c..b103326 100644 --- a/crates/brioche/src/publish.rs +++ b/crates/brioche/src/publish.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, process::ExitCode}; use brioche_core::{ - project::{ProjectHash, Projects}, + project::{ProjectHash, ProjectLocking, ProjectValidation, Projects}, reporter::{ConsoleReporterKind, Reporter}, Brioche, }; @@ -31,7 +31,15 @@ pub async fn publish(args: PublishArgs) -> anyhow::Result { for project_path in args.project { let project_name = format!("project '{name}'", name = project_path.display()); - match projects.load(&brioche, &project_path, true).await { + match projects + .load( + &brioche, + &project_path, + ProjectValidation::Standard, + ProjectLocking::Locked, + ) + .await + { Ok(project_hash) => { let result = run_publish(&reporter, &brioche, &projects, project_hash, &project_name).await; diff --git a/crates/brioche/src/run.rs b/crates/brioche/src/run.rs index 9ef30d1..6eb24fd 100644 --- a/crates/brioche/src/run.rs +++ b/crates/brioche/src/run.rs @@ -1,7 +1,7 @@ use std::process::ExitCode; use anyhow::Context as _; -use brioche_core::reporter::ConsoleReporterKind; +use brioche_core::{project::ProjectLocking, reporter::ConsoleReporterKind}; use clap::Parser; use human_repr::HumanDuration; use tracing::Instrument; @@ -27,6 +27,10 @@ pub struct RunArgs { #[arg(long)] check: bool, + /// Validate that the lockfile is up-to-date + #[arg(long)] + locked: bool, + /// Keep temporary build files. Useful for debugging build failures #[arg(long)] keep_temps: bool, @@ -50,12 +54,24 @@ pub async fn run(args: RunArgs) -> anyhow::Result { .await?; let projects = brioche_core::project::Projects::default(); + let locking = if args.locked { + ProjectLocking::Locked + } else { + ProjectLocking::Unlocked + }; + let build_future = async { - let project_hash = super::load_project(&brioche, &projects, &args.project).await?; + let project_hash = super::load_project(&brioche, &projects, &args.project, locking).await?; - let num_lockfiles_updated = projects.commit_dirty_lockfiles().await?; - if num_lockfiles_updated > 0 { - tracing::info!(num_lockfiles_updated, "updated lockfiles"); + // 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"); + } } if args.check {