From 3fc0143fbf7baf42b210f4176cc632135806a3c0 Mon Sep 17 00:00:00 2001 From: Ross Sullivan Date: Tue, 14 Oct 2025 23:25:58 +0900 Subject: [PATCH] feat(locking): Added build unit level locking --- .../build_runner/compilation_files.rs | 5 + src/cargo/core/compiler/layout.rs | 14 ++- src/cargo/core/compiler/locking.rs | 110 ++++++++++++++++++ src/cargo/core/compiler/mod.rs | 17 +++ 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 src/cargo/core/compiler/locking.rs diff --git a/src/cargo/core/compiler/build_runner/compilation_files.rs b/src/cargo/core/compiler/build_runner/compilation_files.rs index 34d4660c5be..ac6f698fa84 100644 --- a/src/cargo/core/compiler/build_runner/compilation_files.rs +++ b/src/cargo/core/compiler/build_runner/compilation_files.rs @@ -277,6 +277,11 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> { self.layout(unit.kind).build_dir().fingerprint(&dir) } + pub fn build_unit_lock(&self, unit: &Unit) -> PathBuf { + let dir = self.pkg_dir(unit); + self.layout(unit.kind).build_dir().build_unit_lock(&dir) + } + /// Directory where incremental output for the given unit should go. pub fn incremental_dir(&self, unit: &Unit) -> &Path { self.layout(unit.kind).build_dir().incremental() diff --git a/src/cargo/core/compiler/layout.rs b/src/cargo/core/compiler/layout.rs index d0207db8aea..f1a4cde63d5 100644 --- a/src/cargo/core/compiler/layout.rs +++ b/src/cargo/core/compiler/layout.rs @@ -152,10 +152,13 @@ impl Layout { // For now we don't do any more finer-grained locking on the artifact // directory, so just lock the entire thing for the duration of this // compile. - let artifact_dir_lock = - dest.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "build directory")?; + let artifact_dir_lock = if !is_new_layout { + Some(dest.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "build directory")?) + } else { + None + }; - let build_dir_lock = if root != build_root { + let build_dir_lock = if root != build_root && !is_new_layout { Some(build_dest.open_rw_exclusive_create( ".cargo-lock", ws.gctx(), @@ -222,7 +225,7 @@ pub struct ArtifactDirLayout { timings: PathBuf, /// The lockfile for a build (`.cargo-lock`). Will be unlocked when this /// struct is `drop`ped. - _lock: FileLock, + _lock: Option, } impl ArtifactDirLayout { @@ -345,6 +348,9 @@ impl BuildDirLayout { self.build().join(pkg_dir) } } + pub fn build_unit_lock(&self, pkg_dir: &str) -> PathBuf { + self.build_unit(pkg_dir).join("primary.lck") + } /// Fetch the artifact path. pub fn artifact(&self) -> &Path { &self.artifact diff --git a/src/cargo/core/compiler/locking.rs b/src/cargo/core/compiler/locking.rs new file mode 100644 index 00000000000..d07b37bcf8f --- /dev/null +++ b/src/cargo/core/compiler/locking.rs @@ -0,0 +1,110 @@ +//! This module handles the locking logic during compilation. +//! +//! The locking scheme is based on build unit level locking. +//! Generally a build unit will follow the following flow: +//! 1. Acquire an exclusive lock for the current build unit. +//! 2. Acquire shared locks on all dependency build units. +//! 3. Begin building with rustc +//! 5. Once complete release all locks. +//! +//! [`CompilationLock`] is the primary interface for locking. + +use std::{ + fs::{File, OpenOptions}, + path::{Path, PathBuf}, +}; + +use itertools::Itertools; +use tracing::instrument; + +use crate::{ + CargoResult, + core::compiler::{BuildRunner, Unit}, +}; + +/// A lock for compiling a build unit. +/// +/// Internally this lock is made up of many [`UnitLock`]s for the unit and it's dependencies. +pub struct CompilationLock { + /// The path to the lock file of the unit to compile + unit: UnitLock, + /// The paths to lock files of the unit's dependencies + dependency_units: Vec, +} + +impl CompilationLock { + pub fn new(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> Self { + let unit_lock = build_runner.files().build_unit_lock(unit).into(); + + let dependency_units = build_runner + .unit_deps(unit) + .into_iter() + .map(|unit| build_runner.files().build_unit_lock(&unit.unit).into()) + .collect_vec(); + + Self { + unit: unit_lock, + dependency_units, + } + } + + #[instrument(skip(self))] + pub fn lock(&mut self) -> CargoResult<()> { + self.unit.lock_exclusive()?; + + for d in self.dependency_units.iter_mut() { + d.lock_shared()?; + } + + Ok(()) + } +} + +/// A lock for a single build unit. +struct UnitLock { + lock: PathBuf, + gaurd: Option, +} + +struct UnitLockGuard { + _handle: File, +} + +impl UnitLock { + pub fn lock_exclusive(&mut self) -> CargoResult<()> { + assert!(self.gaurd.is_none()); + + let lock = file_lock(&self.lock)?; + lock.lock()?; + + self.gaurd = Some(UnitLockGuard { _handle: lock }); + Ok(()) + } + + pub fn lock_shared(&mut self) -> CargoResult<()> { + assert!(self.gaurd.is_none()); + + let lock = file_lock(&self.lock)?; + lock.lock_shared()?; + + self.gaurd = Some(UnitLockGuard { _handle: lock }); + Ok(()) + } +} + +impl From for UnitLock { + fn from(value: PathBuf) -> Self { + Self { + lock: value, + gaurd: None, + } + } +} + +fn file_lock>(f: T) -> CargoResult { + Ok(OpenOptions::new() + .create(true) + .write(true) + .append(true) + .open(f)?) +} diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index adeae259af7..de4779a4161 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -45,6 +45,7 @@ pub mod future_incompat; pub(crate) mod job_queue; pub(crate) mod layout; mod links; +mod locking; mod lto; mod output_depinfo; mod output_sbom; @@ -95,6 +96,7 @@ use self::output_depinfo::output_depinfo; use self::output_sbom::build_sbom; use self::unit_graph::UnitDep; use crate::core::compiler::future_incompat::FutureIncompatReport; +use crate::core::compiler::locking::CompilationLock; use crate::core::compiler::timings::SectionTiming; pub use crate::core::compiler::unit::{Unit, UnitInterner}; use crate::core::manifest::TargetSourcePath; @@ -351,7 +353,22 @@ fn rustc( output_options.show_diagnostics = false; } let env_config = Arc::clone(build_runner.bcx.gctx.env_config()?); + + let mut lock = if build_runner.bcx.gctx.cli_unstable().build_dir_new_layout { + Some(CompilationLock::new(build_runner, unit)) + } else { + None + }; + return Ok(Work::new(move |state| { + if let Some(lock) = &mut lock { + lock.lock().expect("failed to take lock"); + + // TODO: We need to revalidate the fingerprint here as another Cargo instance could + // have already compiled the crate before we recv'd the lock. + // For large crates re-compiling here would be quiet costly. + } + // Artifacts are in a different location than typical units, // hence we must assure the crate- and target-dependent // directory is present.