From 5ae5980c885e350a34ca019a84ba14a2a228d262 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 7 Mar 2024 06:04:02 -0800 Subject: [PATCH] Add support for `--no-build-isolation` (#2258) ## Summary This PR adds support for pip's `--no-build-isolation`. When enabled, build requirements won't be installed during PEP 517-style builds, but the source environment _will_ be used when executing the build steps themselves. Closes https://github.com/astral-sh/uv/issues/1715. --- PIP_COMPATIBILITY.md | 1 - crates/uv-build/src/lib.rs | 82 +++++++++++-------- crates/uv-dev/src/build.rs | 6 +- crates/uv-dev/src/install_many.rs | 3 +- crates/uv-dev/src/resolve_cli.rs | 3 +- crates/uv-dev/src/resolve_many.rs | 3 +- crates/uv-dispatch/src/lib.rs | 26 ++++-- crates/uv-distribution/src/source/mod.rs | 2 + .../uv-interpreter/src/python_environment.rs | 9 +- crates/uv-resolver/tests/resolver.rs | 8 +- crates/uv-traits/src/lib.rs | 19 ++++- crates/uv-virtualenv/src/lib.rs | 3 +- crates/uv/src/commands/pip_compile.rs | 16 +++- crates/uv/src/commands/pip_install.rs | 12 ++- crates/uv/src/commands/pip_sync.rs | 11 ++- crates/uv/src/commands/venv.rs | 3 +- crates/uv/src/compat/mod.rs | 9 -- crates/uv/src/main.rs | 21 +++++ crates/uv/tests/pip_install.rs | 71 ++++++++++++++++ 19 files changed, 244 insertions(+), 64 deletions(-) diff --git a/PIP_COMPATIBILITY.md b/PIP_COMPATIBILITY.md index 5f8b889c1..a4b105406 100644 --- a/PIP_COMPATIBILITY.md +++ b/PIP_COMPATIBILITY.md @@ -307,7 +307,6 @@ the implementation, and tend to be tracked in individual issues. For example: - [`--trusted-host`](https://github.com/astral-sh/uv/issues/1339) - [`--user`](https://github.com/astral-sh/uv/issues/2077) -- [`--no-build-isolation`](https://github.com/astral-sh/uv/issues/1715) If you encounter a missing option or subcommand, please search the issue tracker to see if it has already been reported, and if not, consider opening a new issue. Feel free to upvote any existing diff --git a/crates/uv-build/src/lib.rs b/crates/uv-build/src/lib.rs index d429de233..e4812b4de 100644 --- a/crates/uv-build/src/lib.rs +++ b/crates/uv-build/src/lib.rs @@ -31,7 +31,9 @@ use distribution_types::Resolution; use pep508_rs::Requirement; use uv_fs::Simplified; use uv_interpreter::{Interpreter, PythonEnvironment}; -use uv_traits::{BuildContext, BuildKind, ConfigSettings, SetupPyStrategy, SourceBuildTrait}; +use uv_traits::{ + BuildContext, BuildIsolation, BuildKind, ConfigSettings, SetupPyStrategy, SourceBuildTrait, +}; /// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory` static MISSING_HEADER_RE: Lazy = Lazy::new(|| { @@ -357,6 +359,7 @@ impl SourceBuild { package_id: String, setup_py: SetupPyStrategy, config_settings: ConfigSettings, + build_isolation: BuildIsolation<'_>, build_kind: BuildKind, mut environment_variables: FxHashMap, ) -> Result { @@ -402,27 +405,36 @@ impl SourceBuild { let pep517_backend = Self::get_pep517_backend(setup_py, &source_tree, &default_backend) .map_err(|err| *err)?; - let venv = uv_virtualenv::create_venv( - &temp_dir.path().join(".venv"), - interpreter.clone(), - uv_virtualenv::Prompt::None, - false, - Vec::new(), - )?; + // Create a virtual environment, or install into the shared environment if requested. + let venv = match build_isolation { + BuildIsolation::Isolated => uv_virtualenv::create_venv( + &temp_dir.path().join(".venv"), + interpreter.clone(), + uv_virtualenv::Prompt::None, + false, + Vec::new(), + )?, + BuildIsolation::Shared(venv) => venv.clone(), + }; - // Setup the build environment. - let resolved_requirements = Self::get_resolved_requirements( - build_context, - source_build_context, - &default_backend, - pep517_backend.as_ref(), - ) - .await?; + // Setup the build environment. If build isolation is disabled, we assume the build + // environment is already setup. + if build_isolation.is_isolated() { + let resolved_requirements = Self::get_resolved_requirements( + build_context, + source_build_context, + &default_backend, + pep517_backend.as_ref(), + ) + .await?; - build_context - .install(&resolved_requirements, &venv) - .await - .map_err(|err| Error::RequirementsInstall("build-system.requires (install)", err))?; + build_context + .install(&resolved_requirements, &venv) + .await + .map_err(|err| { + Error::RequirementsInstall("build-system.requires (install)", err) + })?; + } // Figure out what the modified path should be // Remove the PATH variable from the environment variables if it's there @@ -454,19 +466,23 @@ impl SourceBuild { OsString::from(venv.scripts()) }; - if let Some(pep517_backend) = &pep517_backend { - create_pep517_build_environment( - &source_tree, - &venv, - pep517_backend, - build_context, - &package_id, - build_kind, - &config_settings, - &environment_variables, - &modified_path, - ) - .await?; + // Create the PEP 517 build environment. If build isolation is disabled, we assume the build + // environment is already setup. + if build_isolation.is_isolated() { + if let Some(pep517_backend) = &pep517_backend { + create_pep517_build_environment( + &source_tree, + &venv, + pep517_backend, + build_context, + &package_id, + build_kind, + &config_settings, + &environment_variables, + &modified_path, + ) + .await?; + } } Ok(Self { diff --git a/crates/uv-dev/src/build.rs b/crates/uv-dev/src/build.rs index 85b6ae2d0..3acc32161 100644 --- a/crates/uv-dev/src/build.rs +++ b/crates/uv-dev/src/build.rs @@ -15,7 +15,9 @@ use uv_dispatch::BuildDispatch; use uv_installer::NoBinary; use uv_interpreter::PythonEnvironment; use uv_resolver::InMemoryIndex; -use uv_traits::{BuildContext, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{ + BuildContext, BuildIsolation, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy, +}; #[derive(Parser)] pub(crate) struct BuildArgs { @@ -74,6 +76,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result { &in_flight, setup_py, &config_settings, + BuildIsolation::Isolated, &NoBuild::None, &NoBinary::None, ); @@ -87,6 +90,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result { args.sdist.display().to_string(), setup_py, config_settings.clone(), + BuildIsolation::Isolated, build_kind, FxHashMap::default(), ) diff --git a/crates/uv-dev/src/install_many.rs b/crates/uv-dev/src/install_many.rs index 43028b892..adcd3a381 100644 --- a/crates/uv-dev/src/install_many.rs +++ b/crates/uv-dev/src/install_many.rs @@ -24,7 +24,7 @@ use uv_installer::{Downloader, NoBinary}; use uv_interpreter::PythonEnvironment; use uv_normalize::PackageName; use uv_resolver::{DistFinder, InMemoryIndex}; -use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; #[derive(Parser)] pub(crate) struct InstallManyArgs { @@ -81,6 +81,7 @@ pub(crate) async fn install_many(args: InstallManyArgs) -> Result<()> { &in_flight, setup_py, &config_settings, + BuildIsolation::Isolated, &no_build, &NoBinary::None, ); diff --git a/crates/uv-dev/src/resolve_cli.rs b/crates/uv-dev/src/resolve_cli.rs index e39fe10c5..b00cf89bc 100644 --- a/crates/uv-dev/src/resolve_cli.rs +++ b/crates/uv-dev/src/resolve_cli.rs @@ -18,7 +18,7 @@ use uv_dispatch::BuildDispatch; use uv_installer::NoBinary; use uv_interpreter::PythonEnvironment; use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; -use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; #[derive(ValueEnum, Default, Clone)] pub(crate) enum ResolveCliFormat { @@ -85,6 +85,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { &in_flight, SetupPyStrategy::default(), &config_settings, + BuildIsolation::Isolated, &no_build, &NoBinary::None, ); diff --git a/crates/uv-dev/src/resolve_many.rs b/crates/uv-dev/src/resolve_many.rs index ebcb86cf4..5e0d43470 100644 --- a/crates/uv-dev/src/resolve_many.rs +++ b/crates/uv-dev/src/resolve_many.rs @@ -21,7 +21,7 @@ use uv_installer::NoBinary; use uv_interpreter::PythonEnvironment; use uv_normalize::PackageName; use uv_resolver::InMemoryIndex; -use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; #[derive(Parser)] pub(crate) struct ResolveManyArgs { @@ -109,6 +109,7 @@ pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> { &in_flight, setup_py, &config_settings, + BuildIsolation::Isolated, &no_build, &NoBinary::None, ); diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index cb5326d42..e43d2253b 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -20,7 +20,9 @@ use uv_client::{FlatIndex, RegistryClient}; use uv_installer::{Downloader, Installer, NoBinary, Plan, Planner, Reinstall, SitePackages}; use uv_interpreter::{Interpreter, PythonEnvironment}; use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; -use uv_traits::{BuildContext, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{ + BuildContext, BuildIsolation, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy, +}; /// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`] /// documentation. @@ -33,6 +35,7 @@ pub struct BuildDispatch<'a> { index: &'a InMemoryIndex, in_flight: &'a InFlight, setup_py: SetupPyStrategy, + build_isolation: BuildIsolation<'a>, no_build: &'a NoBuild, no_binary: &'a NoBinary, config_settings: &'a ConfigSettings, @@ -53,6 +56,7 @@ impl<'a> BuildDispatch<'a> { in_flight: &'a InFlight, setup_py: SetupPyStrategy, config_settings: &'a ConfigSettings, + build_isolation: BuildIsolation<'a>, no_build: &'a NoBuild, no_binary: &'a NoBinary, ) -> Self { @@ -66,6 +70,7 @@ impl<'a> BuildDispatch<'a> { in_flight, setup_py, config_settings, + build_isolation, no_build, no_binary, source_build_context: SourceBuildContext::default(), @@ -107,6 +112,10 @@ impl<'a> BuildContext for BuildDispatch<'a> { self.interpreter } + fn build_isolation(&self) -> BuildIsolation { + self.build_isolation + } + fn no_build(&self) -> &NoBuild { self.no_build } @@ -180,7 +189,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { local, remote, reinstalls, - extraneous, + extraneous: _, } = Planner::with_requirements(&resolution.requirements()).build( site_packages, &Reinstall::None, @@ -191,6 +200,12 @@ impl<'a> BuildContext for BuildDispatch<'a> { tags, )?; + // Nothing to do. + if remote.is_empty() && local.is_empty() && reinstalls.is_empty() { + debug!("No build requirements to install for build"); + return Ok(()); + } + // Resolve any registry-based requirements. let remote = remote .iter() @@ -207,7 +222,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { vec![] } else { // TODO(konstin): Check that there is no endless recursion. - let downloader = Downloader::new(self.cache(), tags, self.client, self); + let downloader = Downloader::new(self.cache, tags, self.client, self); debug!( "Downloading and building requirement{} for build: {}", if remote.len() == 1 { "" } else { "s" }, @@ -221,8 +236,8 @@ impl<'a> BuildContext for BuildDispatch<'a> { }; // Remove any unnecessary packages. - if !extraneous.is_empty() || !reinstalls.is_empty() { - for dist_info in extraneous.iter().chain(reinstalls.iter()) { + if !reinstalls.is_empty() { + for dist_info in &reinstalls { let summary = uv_installer::uninstall(dist_info) .await .context("Failed to uninstall build dependencies")?; @@ -295,6 +310,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { package_id.to_string(), self.setup_py, self.config_settings.clone(), + self.build_isolation, build_kind, self.build_extra_env_vars.clone(), ) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 078283e4e..5f8051ce1 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -949,6 +949,8 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> { editable_wheel_dir: &Path, ) -> Result<(Dist, String, WheelFilename, Metadata21), Error> { debug!("Building (editable) {editable}"); + + // Build the wheel. let disk_filename = self .build_context .setup_build( diff --git a/crates/uv-interpreter/src/python_environment.rs b/crates/uv-interpreter/src/python_environment.rs index 3987b0fa7..44335a3ba 100644 --- a/crates/uv-interpreter/src/python_environment.rs +++ b/crates/uv-interpreter/src/python_environment.rs @@ -65,8 +65,11 @@ impl PythonEnvironment { } /// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and root directory. - pub fn from_interpreter(interpreter: Interpreter, root: PathBuf) -> Self { - Self { root, interpreter } + pub fn from_interpreter(interpreter: Interpreter) -> Self { + Self { + root: interpreter.prefix().to_path_buf(), + interpreter, + } } /// Returns the location of the Python interpreter. @@ -100,7 +103,7 @@ impl PythonEnvironment { self.interpreter.scripts() } - /// Lock the virtual environment to prevent concurrent writes. + /// Grab a file lock for the virtual environment to prevent concurrent writes across processes. pub fn lock(&self) -> Result { if self.interpreter.is_virtualenv() { // If the environment a virtualenv, use a virtualenv-specific lock file. diff --git a/crates/uv-resolver/tests/resolver.rs b/crates/uv-resolver/tests/resolver.rs index 06e6a0569..ff76fe17e 100644 --- a/crates/uv-resolver/tests/resolver.rs +++ b/crates/uv-resolver/tests/resolver.rs @@ -21,7 +21,9 @@ use uv_resolver::{ DisplayResolutionGraph, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, ResolutionGraph, ResolutionMode, Resolver, }; -use uv_traits::{BuildContext, BuildKind, NoBinary, NoBuild, SetupPyStrategy, SourceBuildTrait}; +use uv_traits::{ + BuildContext, BuildIsolation, BuildKind, NoBinary, NoBuild, SetupPyStrategy, SourceBuildTrait, +}; // Exclude any packages uploaded after this date. static EXCLUDE_NEWER: Lazy> = Lazy::new(|| { @@ -57,6 +59,10 @@ impl BuildContext for DummyContext { &self.interpreter } + fn build_isolation(&self) -> BuildIsolation { + BuildIsolation::Isolated + } + fn no_build(&self) -> &NoBuild { &NoBuild::None } diff --git a/crates/uv-traits/src/lib.rs b/crates/uv-traits/src/lib.rs index fe80e337d..335195c93 100644 --- a/crates/uv-traits/src/lib.rs +++ b/crates/uv-traits/src/lib.rs @@ -54,16 +54,19 @@ use uv_normalize::PackageName; /// `uv-build` to depend on `uv-resolver` which having actual crate dependencies between /// them. -// TODO(konstin): Proper error types pub trait BuildContext: Sync { type SourceDistBuilder: SourceBuildTrait + Send + Sync; + /// Return a reference to the cache. fn cache(&self) -> &Cache; /// All (potentially nested) source distribution builds use the same base python and can reuse /// it's metadata (e.g. wheel compatibility tags). fn interpreter(&self) -> &Interpreter; + /// Whether to enforce build isolation when building source distributions. + fn build_isolation(&self) -> BuildIsolation; + /// Whether source distribution building is disabled. This [`BuildContext::setup_build`] calls /// will fail in this case. This method exists to avoid fetching source distributions if we know /// we can't build them @@ -137,6 +140,20 @@ pub struct InFlight { pub downloads: OnceMap>, } +/// Whether to enforce build isolation when building source distributions. +#[derive(Debug, Copy, Clone)] +pub enum BuildIsolation<'a> { + Isolated, + Shared(&'a PythonEnvironment), +} + +impl<'a> BuildIsolation<'a> { + /// Returns `true` if build isolation is enforced. + pub fn is_isolated(&self) -> bool { + matches!(self, Self::Isolated) + } +} + /// The strategy to use when building source distributions that lack a `pyproject.toml`. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum SetupPyStrategy { diff --git a/crates/uv-virtualenv/src/lib.rs b/crates/uv-virtualenv/src/lib.rs index bc65c6dda..800e1847b 100644 --- a/crates/uv-virtualenv/src/lib.rs +++ b/crates/uv-virtualenv/src/lib.rs @@ -64,6 +64,5 @@ pub fn create_venv( // Create the corresponding `PythonEnvironment`. let interpreter = interpreter.with_virtualenv(virtualenv); - let root = interpreter.prefix().to_path_buf(); - Ok(PythonEnvironment::from_interpreter(interpreter, root)) + Ok(PythonEnvironment::from_interpreter(interpreter)) } diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 9f7389c41..4d64fc942 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -24,13 +24,13 @@ use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder} use uv_dispatch::BuildDispatch; use uv_fs::Simplified; use uv_installer::{Downloader, NoBinary}; -use uv_interpreter::{Interpreter, PythonVersion}; +use uv_interpreter::{Interpreter, PythonEnvironment, PythonVersion}; use uv_normalize::{ExtraName, PackageName}; use uv_resolver::{ AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest, OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver, }; -use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, ResolverReporter}; @@ -62,6 +62,7 @@ pub(crate) async fn pip_compile( setup_py: SetupPyStrategy, config_settings: ConfigSettings, connectivity: Connectivity, + no_build_isolation: bool, no_build: &NoBuild, python_version: Option, exclude_newer: Option>, @@ -137,6 +138,7 @@ pub(crate) async fn pip_compile( interpreter.python_version(), interpreter.sys_executable().simplified_display().cyan() ); + if let Some(python_version) = python_version.as_ref() { // If the requested version does not match the version we're using warn the user // _unless_ they have not specified a patch version and that is the only difference @@ -203,6 +205,15 @@ pub(crate) async fn pip_compile( // Track in-flight downloads, builds, etc., across resolutions. let in_flight = InFlight::default(); + // Determine whether to enable build isolation. + let venv; + let build_isolation = if no_build_isolation { + venv = PythonEnvironment::from_interpreter(interpreter.clone()); + BuildIsolation::Shared(&venv) + } else { + BuildIsolation::Isolated + }; + let build_dispatch = BuildDispatch::new( &client, &cache, @@ -213,6 +224,7 @@ pub(crate) async fn pip_compile( &in_flight, setup_py, &config_settings, + build_isolation, no_build, &NoBinary::None, ) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 8cee0bd01..cd6bd75fd 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -32,7 +32,7 @@ use uv_resolver::{ DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, ResolutionGraph, ResolutionMode, Resolver, }; -use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter}; use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus}; @@ -59,6 +59,7 @@ pub(crate) async fn pip_install( setup_py: SetupPyStrategy, connectivity: Connectivity, config_settings: &ConfigSettings, + no_build_isolation: bool, no_build: &NoBuild, no_binary: &NoBinary, strict: bool, @@ -190,6 +191,13 @@ pub(crate) async fn pip_install( FlatIndex::from_entries(entries, tags) }; + // Determine whether to enable build isolation. + let build_isolation = if no_build_isolation { + BuildIsolation::Shared(&venv) + } else { + BuildIsolation::Isolated + }; + // Create a shared in-memory index. let index = InMemoryIndex::default(); @@ -206,6 +214,7 @@ pub(crate) async fn pip_install( &in_flight, setup_py, config_settings, + build_isolation, no_build, no_binary, ) @@ -289,6 +298,7 @@ pub(crate) async fn pip_install( &in_flight, setup_py, config_settings, + build_isolation, no_build, no_binary, ) diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index f18728396..3daacbf02 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -20,7 +20,7 @@ use uv_installer::{ }; use uv_interpreter::{Interpreter, PythonEnvironment}; use uv_resolver::InMemoryIndex; -use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use crate::commands::reporters::{DownloadReporter, FinderReporter, InstallReporter}; use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus}; @@ -38,6 +38,7 @@ pub(crate) async fn pip_sync( setup_py: SetupPyStrategy, connectivity: Connectivity, config_settings: &ConfigSettings, + no_build_isolation: bool, no_build: &NoBuild, no_binary: &NoBinary, strict: bool, @@ -135,6 +136,13 @@ pub(crate) async fn pip_sync( // Track in-flight downloads, builds, etc., across resolutions. let in_flight = InFlight::default(); + // Determine whether to enable build isolation. + let build_isolation = if no_build_isolation { + BuildIsolation::Shared(&venv) + } else { + BuildIsolation::Isolated + }; + // Prep the build context. let build_dispatch = BuildDispatch::new( &client, @@ -146,6 +154,7 @@ pub(crate) async fn pip_sync( &in_flight, setup_py, config_settings, + build_isolation, no_build, no_binary, ); diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 7ff396c13..5e2a67df0 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -21,7 +21,7 @@ use uv_fs::Simplified; use uv_installer::NoBinary; use uv_interpreter::{find_default_python, find_requested_python, Error}; use uv_resolver::{InMemoryIndex, OptionsBuilder}; -use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use crate::commands::ExitStatus; use crate::printer::Printer; @@ -172,6 +172,7 @@ async fn venv_impl( &in_flight, SetupPyStrategy::default(), &config_settings, + BuildIsolation::Isolated, &NoBuild::All, &NoBinary::None, ) diff --git a/crates/uv/src/compat/mod.rs b/crates/uv/src/compat/mod.rs index d95b3b9a1..bcea487a7 100644 --- a/crates/uv/src/compat/mod.rs +++ b/crates/uv/src/compat/mod.rs @@ -30,9 +30,6 @@ pub(crate) struct PipCompileCompatArgs { #[clap(long, hide = true)] build_isolation: bool, - #[clap(long, hide = true)] - no_build_isolation: bool, - #[clap(long, hide = true)] resolver: Option, @@ -117,12 +114,6 @@ impl CompatArgs for PipCompileCompatArgs { ); } - if self.no_build_isolation { - return Err(anyhow!( - "pip-compile's `--no-build-isolation` is unsupported (uv always uses build isolation)." - )); - } - if let Some(resolver) = self.resolver { match resolver { Resolver::Backtracking => { diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 904bc0f3c..4b4790452 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -370,6 +370,12 @@ struct PipCompileArgs { #[clap(long)] legacy_setup_py: bool, + /// Disable isolation when building source distributions. + /// + /// Assumes that build dependencies specified by PEP 518 are already installed. + #[clap(long)] + no_build_isolation: bool, + /// Don't build source distributions. /// /// When enabled, resolving will not run arbitrary code. The cached wheels of already-built @@ -550,6 +556,12 @@ struct PipSyncArgs { #[clap(long)] legacy_setup_py: bool, + /// Disable isolation when building source distributions. + /// + /// Assumes that build dependencies specified by PEP 518 are already installed. + #[clap(long)] + no_build_isolation: bool, + /// Don't build source distributions. /// /// When enabled, resolving will not run arbitrary code. The cached wheels of already-built @@ -790,6 +802,12 @@ struct PipInstallArgs { #[clap(long)] legacy_setup_py: bool, + /// Disable isolation when building source distributions. + /// + /// Assumes that build dependencies specified by PEP 518 are already installed. + #[clap(long)] + no_build_isolation: bool, + /// Don't build source distributions. /// /// When enabled, resolving will not run arbitrary code. The cached wheels of already-built @@ -1358,6 +1376,7 @@ async fn run() -> Result { } else { Connectivity::Online }, + args.no_build_isolation, &no_build, args.python_version, args.exclude_newer, @@ -1411,6 +1430,7 @@ async fn run() -> Result { Connectivity::Online }, &config_settings, + args.no_build_isolation, &no_build, &no_binary, args.strict, @@ -1504,6 +1524,7 @@ async fn run() -> Result { Connectivity::Online }, &config_settings, + args.no_build_isolation, &no_build, &no_binary, args.strict, diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 527e7cda1..cc6b45852 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -2348,3 +2348,74 @@ requires-python = "<=3.8" Ok(()) } + +/// Install with `--no-build-isolation`, to disable isolation during PEP 517 builds. +#[test] +fn no_build_isolation() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz")?; + + // We expect the build to fail, because `setuptools` is not installed. + let filters = std::iter::once((r"exit code: 1", "exit status: 1")) + .chain(INSTA_FILTERS.to_vec()) + .collect::>(); + uv_snapshot!(filters, command(&context) + .arg("-r") + .arg("requirements.in") + .arg("--no-build-isolation"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download and build: anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz + Caused by: Failed to build: anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz + Caused by: Build backend failed to determine metadata through `prepare_metadata_for_build_wheel` with exit status: 1 + --- stdout: + + --- stderr: + Traceback (most recent call last): + File "", line 8, in + ModuleNotFoundError: No module named 'setuptools' + --- + "### + ); + + // Install `setuptools` and `wheel`. + uv_snapshot!(command(&context) + .arg("setuptools") + .arg("wheel"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Downloaded 2 packages in [TIME] + Installed 2 packages in [TIME] + + setuptools==68.2.2 + + wheel==0.41.3 + "###); + + // We expect the build to succeed, since `setuptools` is now installed. + uv_snapshot!(command(&context) + .arg("-r") + .arg("requirements.in") + .arg("--no-build-isolation"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Downloaded 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==0.0.0 (from https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz) + + idna==3.4 + + sniffio==1.3.0 + "### + ); + + Ok(()) +}