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.
This commit is contained in:
Charlie Marsh 2024-03-07 06:04:02 -08:00 committed by GitHub
parent d249574a47
commit 5ae5980c88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 244 additions and 64 deletions

View File

@ -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) - [`--trusted-host`](https://github.com/astral-sh/uv/issues/1339)
- [`--user`](https://github.com/astral-sh/uv/issues/2077) - [`--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 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 already been reported, and if not, consider opening a new issue. Feel free to upvote any existing

View File

@ -31,7 +31,9 @@ use distribution_types::Resolution;
use pep508_rs::Requirement; use pep508_rs::Requirement;
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_interpreter::{Interpreter, PythonEnvironment}; 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` /// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory`
static MISSING_HEADER_RE: Lazy<Regex> = Lazy::new(|| { static MISSING_HEADER_RE: Lazy<Regex> = Lazy::new(|| {
@ -357,6 +359,7 @@ impl SourceBuild {
package_id: String, package_id: String,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
config_settings: ConfigSettings, config_settings: ConfigSettings,
build_isolation: BuildIsolation<'_>,
build_kind: BuildKind, build_kind: BuildKind,
mut environment_variables: FxHashMap<OsString, OsString>, mut environment_variables: FxHashMap<OsString, OsString>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
@ -402,15 +405,21 @@ impl SourceBuild {
let pep517_backend = Self::get_pep517_backend(setup_py, &source_tree, &default_backend) let pep517_backend = Self::get_pep517_backend(setup_py, &source_tree, &default_backend)
.map_err(|err| *err)?; .map_err(|err| *err)?;
let venv = uv_virtualenv::create_venv( // 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"), &temp_dir.path().join(".venv"),
interpreter.clone(), interpreter.clone(),
uv_virtualenv::Prompt::None, uv_virtualenv::Prompt::None,
false, false,
Vec::new(), Vec::new(),
)?; )?,
BuildIsolation::Shared(venv) => venv.clone(),
};
// Setup the build environment. // 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( let resolved_requirements = Self::get_resolved_requirements(
build_context, build_context,
source_build_context, source_build_context,
@ -422,7 +431,10 @@ impl SourceBuild {
build_context build_context
.install(&resolved_requirements, &venv) .install(&resolved_requirements, &venv)
.await .await
.map_err(|err| Error::RequirementsInstall("build-system.requires (install)", err))?; .map_err(|err| {
Error::RequirementsInstall("build-system.requires (install)", err)
})?;
}
// Figure out what the modified path should be // Figure out what the modified path should be
// Remove the PATH variable from the environment variables if it's there // Remove the PATH variable from the environment variables if it's there
@ -454,6 +466,9 @@ impl SourceBuild {
OsString::from(venv.scripts()) OsString::from(venv.scripts())
}; };
// 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 { if let Some(pep517_backend) = &pep517_backend {
create_pep517_build_environment( create_pep517_build_environment(
&source_tree, &source_tree,
@ -468,6 +483,7 @@ impl SourceBuild {
) )
.await?; .await?;
} }
}
Ok(Self { Ok(Self {
temp_dir, temp_dir,

View File

@ -15,7 +15,9 @@ use uv_dispatch::BuildDispatch;
use uv_installer::NoBinary; use uv_installer::NoBinary;
use uv_interpreter::PythonEnvironment; use uv_interpreter::PythonEnvironment;
use uv_resolver::InMemoryIndex; 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)] #[derive(Parser)]
pub(crate) struct BuildArgs { pub(crate) struct BuildArgs {
@ -74,6 +76,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
&in_flight, &in_flight,
setup_py, setup_py,
&config_settings, &config_settings,
BuildIsolation::Isolated,
&NoBuild::None, &NoBuild::None,
&NoBinary::None, &NoBinary::None,
); );
@ -87,6 +90,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
args.sdist.display().to_string(), args.sdist.display().to_string(),
setup_py, setup_py,
config_settings.clone(), config_settings.clone(),
BuildIsolation::Isolated,
build_kind, build_kind,
FxHashMap::default(), FxHashMap::default(),
) )

View File

@ -24,7 +24,7 @@ use uv_installer::{Downloader, NoBinary};
use uv_interpreter::PythonEnvironment; use uv_interpreter::PythonEnvironment;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_resolver::{DistFinder, InMemoryIndex}; use uv_resolver::{DistFinder, InMemoryIndex};
use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use uv_traits::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
#[derive(Parser)] #[derive(Parser)]
pub(crate) struct InstallManyArgs { pub(crate) struct InstallManyArgs {
@ -81,6 +81,7 @@ pub(crate) async fn install_many(args: InstallManyArgs) -> Result<()> {
&in_flight, &in_flight,
setup_py, setup_py,
&config_settings, &config_settings,
BuildIsolation::Isolated,
&no_build, &no_build,
&NoBinary::None, &NoBinary::None,
); );

View File

@ -18,7 +18,7 @@ use uv_dispatch::BuildDispatch;
use uv_installer::NoBinary; use uv_installer::NoBinary;
use uv_interpreter::PythonEnvironment; use uv_interpreter::PythonEnvironment;
use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; 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)] #[derive(ValueEnum, Default, Clone)]
pub(crate) enum ResolveCliFormat { pub(crate) enum ResolveCliFormat {
@ -85,6 +85,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> {
&in_flight, &in_flight,
SetupPyStrategy::default(), SetupPyStrategy::default(),
&config_settings, &config_settings,
BuildIsolation::Isolated,
&no_build, &no_build,
&NoBinary::None, &NoBinary::None,
); );

View File

@ -21,7 +21,7 @@ use uv_installer::NoBinary;
use uv_interpreter::PythonEnvironment; use uv_interpreter::PythonEnvironment;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_resolver::InMemoryIndex; use uv_resolver::InMemoryIndex;
use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use uv_traits::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
#[derive(Parser)] #[derive(Parser)]
pub(crate) struct ResolveManyArgs { pub(crate) struct ResolveManyArgs {
@ -109,6 +109,7 @@ pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> {
&in_flight, &in_flight,
setup_py, setup_py,
&config_settings, &config_settings,
BuildIsolation::Isolated,
&no_build, &no_build,
&NoBinary::None, &NoBinary::None,
); );

View File

@ -20,7 +20,9 @@ use uv_client::{FlatIndex, RegistryClient};
use uv_installer::{Downloader, Installer, NoBinary, Plan, Planner, Reinstall, SitePackages}; use uv_installer::{Downloader, Installer, NoBinary, Plan, Planner, Reinstall, SitePackages};
use uv_interpreter::{Interpreter, PythonEnvironment}; use uv_interpreter::{Interpreter, PythonEnvironment};
use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; 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`] /// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`]
/// documentation. /// documentation.
@ -33,6 +35,7 @@ pub struct BuildDispatch<'a> {
index: &'a InMemoryIndex, index: &'a InMemoryIndex,
in_flight: &'a InFlight, in_flight: &'a InFlight,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
build_isolation: BuildIsolation<'a>,
no_build: &'a NoBuild, no_build: &'a NoBuild,
no_binary: &'a NoBinary, no_binary: &'a NoBinary,
config_settings: &'a ConfigSettings, config_settings: &'a ConfigSettings,
@ -53,6 +56,7 @@ impl<'a> BuildDispatch<'a> {
in_flight: &'a InFlight, in_flight: &'a InFlight,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
config_settings: &'a ConfigSettings, config_settings: &'a ConfigSettings,
build_isolation: BuildIsolation<'a>,
no_build: &'a NoBuild, no_build: &'a NoBuild,
no_binary: &'a NoBinary, no_binary: &'a NoBinary,
) -> Self { ) -> Self {
@ -66,6 +70,7 @@ impl<'a> BuildDispatch<'a> {
in_flight, in_flight,
setup_py, setup_py,
config_settings, config_settings,
build_isolation,
no_build, no_build,
no_binary, no_binary,
source_build_context: SourceBuildContext::default(), source_build_context: SourceBuildContext::default(),
@ -107,6 +112,10 @@ impl<'a> BuildContext for BuildDispatch<'a> {
self.interpreter self.interpreter
} }
fn build_isolation(&self) -> BuildIsolation {
self.build_isolation
}
fn no_build(&self) -> &NoBuild { fn no_build(&self) -> &NoBuild {
self.no_build self.no_build
} }
@ -180,7 +189,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
local, local,
remote, remote,
reinstalls, reinstalls,
extraneous, extraneous: _,
} = Planner::with_requirements(&resolution.requirements()).build( } = Planner::with_requirements(&resolution.requirements()).build(
site_packages, site_packages,
&Reinstall::None, &Reinstall::None,
@ -191,6 +200,12 @@ impl<'a> BuildContext for BuildDispatch<'a> {
tags, 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. // Resolve any registry-based requirements.
let remote = remote let remote = remote
.iter() .iter()
@ -207,7 +222,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
vec![] vec![]
} else { } else {
// TODO(konstin): Check that there is no endless recursion. // 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!( debug!(
"Downloading and building requirement{} for build: {}", "Downloading and building requirement{} for build: {}",
if remote.len() == 1 { "" } else { "s" }, if remote.len() == 1 { "" } else { "s" },
@ -221,8 +236,8 @@ impl<'a> BuildContext for BuildDispatch<'a> {
}; };
// Remove any unnecessary packages. // Remove any unnecessary packages.
if !extraneous.is_empty() || !reinstalls.is_empty() { if !reinstalls.is_empty() {
for dist_info in extraneous.iter().chain(reinstalls.iter()) { for dist_info in &reinstalls {
let summary = uv_installer::uninstall(dist_info) let summary = uv_installer::uninstall(dist_info)
.await .await
.context("Failed to uninstall build dependencies")?; .context("Failed to uninstall build dependencies")?;
@ -295,6 +310,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
package_id.to_string(), package_id.to_string(),
self.setup_py, self.setup_py,
self.config_settings.clone(), self.config_settings.clone(),
self.build_isolation,
build_kind, build_kind,
self.build_extra_env_vars.clone(), self.build_extra_env_vars.clone(),
) )

View File

@ -949,6 +949,8 @@ impl<'a, T: BuildContext> SourceDistCachedBuilder<'a, T> {
editable_wheel_dir: &Path, editable_wheel_dir: &Path,
) -> Result<(Dist, String, WheelFilename, Metadata21), Error> { ) -> Result<(Dist, String, WheelFilename, Metadata21), Error> {
debug!("Building (editable) {editable}"); debug!("Building (editable) {editable}");
// Build the wheel.
let disk_filename = self let disk_filename = self
.build_context .build_context
.setup_build( .setup_build(

View File

@ -65,8 +65,11 @@ impl PythonEnvironment {
} }
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and root directory. /// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and root directory.
pub fn from_interpreter(interpreter: Interpreter, root: PathBuf) -> Self { pub fn from_interpreter(interpreter: Interpreter) -> Self {
Self { root, interpreter } Self {
root: interpreter.prefix().to_path_buf(),
interpreter,
}
} }
/// Returns the location of the Python interpreter. /// Returns the location of the Python interpreter.
@ -100,7 +103,7 @@ impl PythonEnvironment {
self.interpreter.scripts() 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<LockedFile, std::io::Error> { pub fn lock(&self) -> Result<LockedFile, std::io::Error> {
if self.interpreter.is_virtualenv() { if self.interpreter.is_virtualenv() {
// If the environment a virtualenv, use a virtualenv-specific lock file. // If the environment a virtualenv, use a virtualenv-specific lock file.

View File

@ -21,7 +21,9 @@ use uv_resolver::{
DisplayResolutionGraph, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, DisplayResolutionGraph, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
ResolutionGraph, ResolutionMode, Resolver, 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. // Exclude any packages uploaded after this date.
static EXCLUDE_NEWER: Lazy<DateTime<Utc>> = Lazy::new(|| { static EXCLUDE_NEWER: Lazy<DateTime<Utc>> = Lazy::new(|| {
@ -57,6 +59,10 @@ impl BuildContext for DummyContext {
&self.interpreter &self.interpreter
} }
fn build_isolation(&self) -> BuildIsolation {
BuildIsolation::Isolated
}
fn no_build(&self) -> &NoBuild { fn no_build(&self) -> &NoBuild {
&NoBuild::None &NoBuild::None
} }

View File

@ -54,16 +54,19 @@ use uv_normalize::PackageName;
/// `uv-build` to depend on `uv-resolver` which having actual crate dependencies between /// `uv-build` to depend on `uv-resolver` which having actual crate dependencies between
/// them. /// them.
// TODO(konstin): Proper error types
pub trait BuildContext: Sync { pub trait BuildContext: Sync {
type SourceDistBuilder: SourceBuildTrait + Send + Sync; type SourceDistBuilder: SourceBuildTrait + Send + Sync;
/// Return a reference to the cache.
fn cache(&self) -> &Cache; fn cache(&self) -> &Cache;
/// All (potentially nested) source distribution builds use the same base python and can reuse /// All (potentially nested) source distribution builds use the same base python and can reuse
/// it's metadata (e.g. wheel compatibility tags). /// it's metadata (e.g. wheel compatibility tags).
fn interpreter(&self) -> &Interpreter; 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 /// 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 /// will fail in this case. This method exists to avoid fetching source distributions if we know
/// we can't build them /// we can't build them
@ -137,6 +140,20 @@ pub struct InFlight {
pub downloads: OnceMap<DistributionId, Result<CachedDist, String>>, pub downloads: OnceMap<DistributionId, Result<CachedDist, String>>,
} }
/// 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`. /// The strategy to use when building source distributions that lack a `pyproject.toml`.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum SetupPyStrategy { pub enum SetupPyStrategy {

View File

@ -64,6 +64,5 @@ pub fn create_venv(
// Create the corresponding `PythonEnvironment`. // Create the corresponding `PythonEnvironment`.
let interpreter = interpreter.with_virtualenv(virtualenv); let interpreter = interpreter.with_virtualenv(virtualenv);
let root = interpreter.prefix().to_path_buf(); Ok(PythonEnvironment::from_interpreter(interpreter))
Ok(PythonEnvironment::from_interpreter(interpreter, root))
} }

View File

@ -24,13 +24,13 @@ use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder}
use uv_dispatch::BuildDispatch; use uv_dispatch::BuildDispatch;
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_installer::{Downloader, NoBinary}; use uv_installer::{Downloader, NoBinary};
use uv_interpreter::{Interpreter, PythonVersion}; use uv_interpreter::{Interpreter, PythonEnvironment, PythonVersion};
use uv_normalize::{ExtraName, PackageName}; use uv_normalize::{ExtraName, PackageName};
use uv_resolver::{ use uv_resolver::{
AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest, AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest,
OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver, 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 uv_warnings::warn_user;
use crate::commands::reporters::{DownloadReporter, ResolverReporter}; use crate::commands::reporters::{DownloadReporter, ResolverReporter};
@ -62,6 +62,7 @@ pub(crate) async fn pip_compile(
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
config_settings: ConfigSettings, config_settings: ConfigSettings,
connectivity: Connectivity, connectivity: Connectivity,
no_build_isolation: bool,
no_build: &NoBuild, no_build: &NoBuild,
python_version: Option<PythonVersion>, python_version: Option<PythonVersion>,
exclude_newer: Option<DateTime<Utc>>, exclude_newer: Option<DateTime<Utc>>,
@ -137,6 +138,7 @@ pub(crate) async fn pip_compile(
interpreter.python_version(), interpreter.python_version(),
interpreter.sys_executable().simplified_display().cyan() interpreter.sys_executable().simplified_display().cyan()
); );
if let Some(python_version) = python_version.as_ref() { if let Some(python_version) = python_version.as_ref() {
// If the requested version does not match the version we're using warn the user // 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 // _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. // Track in-flight downloads, builds, etc., across resolutions.
let in_flight = InFlight::default(); 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( let build_dispatch = BuildDispatch::new(
&client, &client,
&cache, &cache,
@ -213,6 +224,7 @@ pub(crate) async fn pip_compile(
&in_flight, &in_flight,
setup_py, setup_py,
&config_settings, &config_settings,
build_isolation,
no_build, no_build,
&NoBinary::None, &NoBinary::None,
) )

View File

@ -32,7 +32,7 @@ use uv_resolver::{
DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
ResolutionGraph, ResolutionMode, Resolver, 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::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus}; use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
@ -59,6 +59,7 @@ pub(crate) async fn pip_install(
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
connectivity: Connectivity, connectivity: Connectivity,
config_settings: &ConfigSettings, config_settings: &ConfigSettings,
no_build_isolation: bool,
no_build: &NoBuild, no_build: &NoBuild,
no_binary: &NoBinary, no_binary: &NoBinary,
strict: bool, strict: bool,
@ -190,6 +191,13 @@ pub(crate) async fn pip_install(
FlatIndex::from_entries(entries, tags) 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. // Create a shared in-memory index.
let index = InMemoryIndex::default(); let index = InMemoryIndex::default();
@ -206,6 +214,7 @@ pub(crate) async fn pip_install(
&in_flight, &in_flight,
setup_py, setup_py,
config_settings, config_settings,
build_isolation,
no_build, no_build,
no_binary, no_binary,
) )
@ -289,6 +298,7 @@ pub(crate) async fn pip_install(
&in_flight, &in_flight,
setup_py, setup_py,
config_settings, config_settings,
build_isolation,
no_build, no_build,
no_binary, no_binary,
) )

View File

@ -20,7 +20,7 @@ use uv_installer::{
}; };
use uv_interpreter::{Interpreter, PythonEnvironment}; use uv_interpreter::{Interpreter, PythonEnvironment};
use uv_resolver::InMemoryIndex; 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::reporters::{DownloadReporter, FinderReporter, InstallReporter};
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus}; use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
@ -38,6 +38,7 @@ pub(crate) async fn pip_sync(
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
connectivity: Connectivity, connectivity: Connectivity,
config_settings: &ConfigSettings, config_settings: &ConfigSettings,
no_build_isolation: bool,
no_build: &NoBuild, no_build: &NoBuild,
no_binary: &NoBinary, no_binary: &NoBinary,
strict: bool, strict: bool,
@ -135,6 +136,13 @@ pub(crate) async fn pip_sync(
// Track in-flight downloads, builds, etc., across resolutions. // Track in-flight downloads, builds, etc., across resolutions.
let in_flight = InFlight::default(); 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. // Prep the build context.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
@ -146,6 +154,7 @@ pub(crate) async fn pip_sync(
&in_flight, &in_flight,
setup_py, setup_py,
config_settings, config_settings,
build_isolation,
no_build, no_build,
no_binary, no_binary,
); );

View File

@ -21,7 +21,7 @@ use uv_fs::Simplified;
use uv_installer::NoBinary; use uv_installer::NoBinary;
use uv_interpreter::{find_default_python, find_requested_python, Error}; use uv_interpreter::{find_default_python, find_requested_python, Error};
use uv_resolver::{InMemoryIndex, OptionsBuilder}; 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::commands::ExitStatus;
use crate::printer::Printer; use crate::printer::Printer;
@ -172,6 +172,7 @@ async fn venv_impl(
&in_flight, &in_flight,
SetupPyStrategy::default(), SetupPyStrategy::default(),
&config_settings, &config_settings,
BuildIsolation::Isolated,
&NoBuild::All, &NoBuild::All,
&NoBinary::None, &NoBinary::None,
) )

View File

@ -30,9 +30,6 @@ pub(crate) struct PipCompileCompatArgs {
#[clap(long, hide = true)] #[clap(long, hide = true)]
build_isolation: bool, build_isolation: bool,
#[clap(long, hide = true)]
no_build_isolation: bool,
#[clap(long, hide = true)] #[clap(long, hide = true)]
resolver: Option<Resolver>, resolver: Option<Resolver>,
@ -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 { if let Some(resolver) = self.resolver {
match resolver { match resolver {
Resolver::Backtracking => { Resolver::Backtracking => {

View File

@ -370,6 +370,12 @@ struct PipCompileArgs {
#[clap(long)] #[clap(long)]
legacy_setup_py: bool, 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. /// Don't build source distributions.
/// ///
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built /// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
@ -550,6 +556,12 @@ struct PipSyncArgs {
#[clap(long)] #[clap(long)]
legacy_setup_py: bool, 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. /// Don't build source distributions.
/// ///
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built /// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
@ -790,6 +802,12 @@ struct PipInstallArgs {
#[clap(long)] #[clap(long)]
legacy_setup_py: bool, 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. /// Don't build source distributions.
/// ///
/// When enabled, resolving will not run arbitrary code. The cached wheels of already-built /// When enabled, resolving will not run arbitrary code. The cached wheels of already-built
@ -1358,6 +1376,7 @@ async fn run() -> Result<ExitStatus> {
} else { } else {
Connectivity::Online Connectivity::Online
}, },
args.no_build_isolation,
&no_build, &no_build,
args.python_version, args.python_version,
args.exclude_newer, args.exclude_newer,
@ -1411,6 +1430,7 @@ async fn run() -> Result<ExitStatus> {
Connectivity::Online Connectivity::Online
}, },
&config_settings, &config_settings,
args.no_build_isolation,
&no_build, &no_build,
&no_binary, &no_binary,
args.strict, args.strict,
@ -1504,6 +1524,7 @@ async fn run() -> Result<ExitStatus> {
Connectivity::Online Connectivity::Online
}, },
&config_settings, &config_settings,
args.no_build_isolation,
&no_build, &no_build,
&no_binary, &no_binary,
args.strict, args.strict,

View File

@ -2348,3 +2348,74 @@ requires-python = "<=3.8"
Ok(()) 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::<Vec<_>>();
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 "<string>", line 8, in <module>
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(())
}