Only drop build directories on program exit (#14304)

## Summary

This PR ensures that we avoid cleaning up build directories until the
end of a resolve-and-install cycle. It's not bulletproof (since we could
still run into issues with `uv lock` followed by `uv sync` whereby a
build directory gets cleaned up that's still referenced in the `build`
artifacts), but it at least gets PyTorch building without error with `uv
pip install .`, which is a case that's been reported several times.

Closes https://github.com/astral-sh/uv/issues/14269.
This commit is contained in:
Charlie Marsh 2025-07-01 12:50:19 -04:00 committed by GitHub
parent c777491bf4
commit c078683217
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 65 additions and 10 deletions

2
Cargo.lock generated
View file

@ -5937,7 +5937,9 @@ name = "uv-types"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"boxcar",
"rustc-hash", "rustc-hash",
"tempfile",
"thiserror 2.0.12", "thiserror 2.0.12",
"uv-cache", "uv-cache",
"uv-configuration", "uv-configuration",

View file

@ -862,6 +862,10 @@ impl SourceBuild {
} }
impl SourceBuildTrait for SourceBuild { impl SourceBuildTrait for SourceBuild {
fn into_build_dir(self) -> TempDir {
self.temp_dir
}
async fn metadata(&mut self) -> Result<Option<PathBuf>, AnyErrorBuild> { async fn metadata(&mut self) -> Result<Option<PathBuf>, AnyErrorBuild> {
Ok(self.get_metadata_without_build().await?) Ok(self.get_metadata_without_build().await?)
} }

View file

@ -2,15 +2,15 @@
//! [installer][`uv_installer`] and [build][`uv_build`] through [`BuildDispatch`] //! [installer][`uv_installer`] and [build][`uv_build`] through [`BuildDispatch`]
//! implementing [`BuildContext`]. //! implementing [`BuildContext`].
use std::ffi::{OsStr, OsString};
use std::path::Path;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use futures::FutureExt; use futures::FutureExt;
use itertools::Itertools; use itertools::Itertools;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::ffi::{OsStr, OsString};
use std::path::Path;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, instrument, trace}; use tracing::{debug, instrument, trace};
use uv_build_backend::check_direct_build; use uv_build_backend::check_direct_build;
use uv_build_frontend::{SourceBuild, SourceBuildContext}; use uv_build_frontend::{SourceBuild, SourceBuildContext};
use uv_cache::Cache; use uv_cache::Cache;
@ -35,8 +35,8 @@ use uv_resolver::{
PythonRequirement, Resolver, ResolverEnvironment, PythonRequirement, Resolver, ResolverEnvironment,
}; };
use uv_types::{ use uv_types::{
AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, EmptyInstalledPackages, HashStrategy, AnyErrorBuild, BuildArena, BuildContext, BuildIsolation, BuildStack, EmptyInstalledPackages,
InFlight, HashStrategy, InFlight,
}; };
use uv_workspace::WorkspaceCache; use uv_workspace::WorkspaceCache;
@ -179,6 +179,10 @@ impl BuildContext for BuildDispatch<'_> {
&self.shared_state.git &self.shared_state.git
} }
fn build_arena(&self) -> &BuildArena {
&self.shared_state.build_arena
}
fn capabilities(&self) -> &IndexCapabilities { fn capabilities(&self) -> &IndexCapabilities {
&self.shared_state.capabilities &self.shared_state.capabilities
} }
@ -521,6 +525,8 @@ pub struct SharedState {
index: InMemoryIndex, index: InMemoryIndex,
/// The downloaded distributions. /// The downloaded distributions.
in_flight: InFlight, in_flight: InFlight,
/// Build directories for any PEP 517 builds executed during resolution or installation.
build_arena: BuildArena,
} }
impl SharedState { impl SharedState {
@ -533,6 +539,7 @@ impl SharedState {
Self { Self {
git: self.git.clone(), git: self.git.clone(),
capabilities: self.capabilities.clone(), capabilities: self.capabilities.clone(),
build_arena: self.build_arena.clone(),
..Default::default() ..Default::default()
} }
} }
@ -556,4 +563,9 @@ impl SharedState {
pub fn capabilities(&self) -> &IndexCapabilities { pub fn capabilities(&self) -> &IndexCapabilities {
&self.capabilities &self.capabilities
} }
/// Return the [`BuildArena`] used by the [`SharedState`].
pub fn build_arena(&self) -> &BuildArena {
&self.build_arena
}
} }

View file

@ -2276,6 +2276,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
fs::create_dir_all(&cache_shard) fs::create_dir_all(&cache_shard)
.await .await
.map_err(Error::CacheWrite)?; .map_err(Error::CacheWrite)?;
// Try a direct build if that isn't disabled and the uv build backend is used. // Try a direct build if that isn't disabled and the uv build backend is used.
let disk_filename = if let Some(name) = self let disk_filename = if let Some(name) = self
.build_context .build_context
@ -2296,7 +2297,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
// In the uv build backend, the normalized filename and the disk filename are the same. // In the uv build backend, the normalized filename and the disk filename are the same.
name.to_string() name.to_string()
} else { } else {
self.build_context let builder = self
.build_context
.setup_build( .setup_build(
source_root, source_root,
subdirectory, subdirectory,
@ -2313,10 +2315,17 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
self.build_stack.cloned().unwrap_or_default(), self.build_stack.cloned().unwrap_or_default(),
) )
.await .await
.map_err(|err| Error::Build(err.into()))? .map_err(|err| Error::Build(err.into()))?;
.wheel(temp_dir.path())
.await // Build the wheel.
.map_err(Error::Build)? let wheel = builder.wheel(temp_dir.path()).await.map_err(Error::Build)?;
// Store a reference to the build context.
self.build_context
.build_arena()
.push(builder.into_build_dir());
wheel
}; };
// Read the metadata from the wheel. // Read the metadata from the wheel.
@ -2398,6 +2407,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> {
return Ok(None); return Ok(None);
}; };
// Store a reference to the build context.
self.build_context
.build_arena()
.push(builder.into_build_dir());
// Read the metadata from disk. // Read the metadata from disk.
debug!("Prepared metadata for: {source}"); debug!("Prepared metadata for: {source}");
let content = fs::read(dist_info.join("METADATA")) let content = fs::read(dist_info.join("METADATA"))

View file

@ -31,7 +31,9 @@ uv-redacted = { workspace = true }
uv-workspace = { workspace = true } uv-workspace = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
boxcar = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
[features] [features]

View file

@ -1,3 +1,5 @@
use std::sync::Arc;
use tempfile::TempDir;
use uv_pep508::PackageName; use uv_pep508::PackageName;
use uv_python::PythonEnvironment; use uv_python::PythonEnvironment;
@ -37,3 +39,14 @@ impl BuildIsolation<'_> {
} }
} }
} }
/// An arena of temporary directories used for builds.
#[derive(Default, Debug, Clone)]
pub struct BuildArena(Arc<boxcar::Vec<TempDir>>);
impl BuildArena {
/// Push a new temporary directory into the arena.
pub fn push(&self, temp_dir: TempDir) {
self.0.push(temp_dir);
}
}

View file

@ -5,7 +5,9 @@ use std::path::{Path, PathBuf};
use anyhow::Result; use anyhow::Result;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use tempfile::TempDir;
use crate::BuildArena;
use uv_cache::Cache; use uv_cache::Cache;
use uv_configuration::{BuildKind, BuildOptions, BuildOutput, ConfigSettings, SourceStrategy}; use uv_configuration::{BuildKind, BuildOptions, BuildOutput, ConfigSettings, SourceStrategy};
use uv_distribution_filename::DistFilename; use uv_distribution_filename::DistFilename;
@ -67,6 +69,9 @@ pub trait BuildContext {
/// Return a reference to the Git resolver. /// Return a reference to the Git resolver.
fn git(&self) -> &GitResolver; fn git(&self) -> &GitResolver;
/// Return a reference to the build arena.
fn build_arena(&self) -> &BuildArena;
/// Return a reference to the discovered registry capabilities. /// Return a reference to the discovered registry capabilities.
fn capabilities(&self) -> &IndexCapabilities; fn capabilities(&self) -> &IndexCapabilities;
@ -148,6 +153,9 @@ pub trait BuildContext {
/// You can either call only `wheel()` to build the wheel directly, call only `metadata()` to get /// You can either call only `wheel()` to build the wheel directly, call only `metadata()` to get
/// the metadata without performing the actual or first call `metadata()` and then `wheel()`. /// the metadata without performing the actual or first call `metadata()` and then `wheel()`.
pub trait SourceBuildTrait { pub trait SourceBuildTrait {
/// Return the temporary build directory.
fn into_build_dir(self) -> TempDir;
/// A wrapper for `uv_build::SourceBuild::get_metadata_without_build`. /// A wrapper for `uv_build::SourceBuild::get_metadata_without_build`.
/// ///
/// For PEP 517 builds, this calls `prepare_metadata_for_build_wheel` /// For PEP 517 builds, this calls `prepare_metadata_for_build_wheel`