From d8d9b025120a331e89874a2574c2b300242a7761 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 6 Aug 2024 17:11:48 -0400 Subject: [PATCH] Add `--no-build-isolation` to uv lock et al (#5829) ## Summary Closes https://github.com/astral-sh/uv/issues/5742. --- crates/uv-cli/src/lib.rs | 107 +++++++++++++++---------- crates/uv-cli/src/options.rs | 15 ++++ crates/uv-settings/src/settings.rs | 15 ++++ crates/uv/src/commands/project/lock.rs | 18 +++-- crates/uv/src/commands/project/mod.rs | 53 +++++++++--- crates/uv/src/commands/project/sync.rs | 12 ++- crates/uv/src/settings.rs | 44 ++++++---- crates/uv/tests/sync.rs | 80 ++++++++++++++++++ docs/reference/settings.md | 28 +++++++ uv.schema.json | 7 ++ 10 files changed, 299 insertions(+), 80 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index f7ca04442711..98311fe6e209 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -680,20 +680,6 @@ pub struct PipCompileArgs { #[arg(long, overrides_with("legacy_setup_py"), hide = true)] pub no_legacy_setup_py: bool, - /// Disable isolation when building source distributions. - /// - /// Assumes that build dependencies specified by PEP 518 are already installed. - #[arg( - long, - env = "UV_NO_BUILD_ISOLATION", - value_parser = clap::builder::BoolishValueParser::new(), - overrides_with("build_isolation") - )] - pub no_build_isolation: bool, - - #[arg(long, overrides_with("no_build_isolation"), hide = true)] - pub build_isolation: bool, - /// Don't build source distributions. /// /// When enabled, resolving will not run arbitrary Python code. The cached wheels of @@ -981,20 +967,6 @@ pub struct PipSyncArgs { #[arg(long, overrides_with("legacy_setup_py"), hide = true)] pub no_legacy_setup_py: bool, - /// Disable isolation when building source distributions. - /// - /// Assumes that build dependencies specified by PEP 518 are already installed. - #[arg( - long, - env = "UV_NO_BUILD_ISOLATION", - value_parser = clap::builder::BoolishValueParser::new(), - overrides_with("build_isolation") - )] - pub no_build_isolation: bool, - - #[arg(long, overrides_with("no_build_isolation"), hide = true)] - pub build_isolation: bool, - /// Don't build source distributions. /// /// When enabled, resolving will not run arbitrary Python code. The cached wheels of @@ -1286,20 +1258,6 @@ pub struct PipInstallArgs { #[arg(long, overrides_with("legacy_setup_py"), hide = true)] pub no_legacy_setup_py: bool, - /// Disable isolation when building source distributions. - /// - /// Assumes that build dependencies specified by PEP 518 are already installed. - #[arg( - long, - env = "UV_NO_BUILD_ISOLATION", - value_parser = clap::builder::BoolishValueParser::new(), - overrides_with("build_isolation") - )] - pub no_build_isolation: bool, - - #[arg(long, overrides_with("no_build_isolation"), hide = true)] - pub build_isolation: bool, - /// Don't build source distributions. /// /// When enabled, resolving will not run arbitrary Python code. The cached wheels of @@ -2103,9 +2061,10 @@ pub struct SyncArgs { #[arg(long, overrides_with("dev"))] pub no_dev: bool, - /// Does not clean the environment. + /// When syncing, make the minimum necessary changes to satisfy the requirements. /// - /// When omitted, any extraneous installations will be removed. + /// By default, `uv sync` will remove any extraneous packages from the environment, unless + /// `--no-build-isolation` is enabled. #[arg(long)] pub no_clean: bool, @@ -2874,6 +2833,26 @@ pub struct InstallerArgs { )] pub config_setting: Option>, + /// Disable isolation when building source distributions. + /// + /// Assumes that build dependencies specified by PEP 518 are already installed. + #[arg( + long, + overrides_with("build_isolation"), + help_heading = "Build options", + env = "UV_NO_BUILD_ISOLATION", + value_parser = clap::builder::BoolishValueParser::new(), + )] + pub no_build_isolation: bool, + + #[arg( + long, + overrides_with("no_build_isolation"), + hide = true, + help_heading = "Build options" + )] + pub build_isolation: bool, + /// Limit candidate packages to those that were uploaded prior to the given date. /// /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same @@ -3021,6 +3000,26 @@ pub struct ResolverArgs { )] pub config_setting: Option>, + /// Disable isolation when building source distributions. + /// + /// Assumes that build dependencies specified by PEP 518 are already installed. + #[arg( + long, + overrides_with("build_isolation"), + help_heading = "Build options", + env = "UV_NO_BUILD_ISOLATION", + value_parser = clap::builder::BoolishValueParser::new(), + )] + pub no_build_isolation: bool, + + #[arg( + long, + overrides_with("no_build_isolation"), + hide = true, + help_heading = "Build options" + )] + pub build_isolation: bool, + /// Limit candidate packages to those that were uploaded prior to the given date. /// /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same @@ -3166,6 +3165,26 @@ pub struct ResolverInstallerArgs { )] pub config_setting: Option>, + /// Disable isolation when building source distributions. + /// + /// Assumes that build dependencies specified by PEP 518 are already installed. + #[arg( + long, + overrides_with("build_isolation"), + help_heading = "Build options", + env = "UV_NO_BUILD_ISOLATION", + value_parser = clap::builder::BoolishValueParser::new(), + )] + pub no_build_isolation: bool, + + #[arg( + long, + overrides_with("no_build_isolation"), + hide = true, + help_heading = "Build options" + )] + pub build_isolation: bool, + /// Limit candidate packages to those that were uploaded prior to the given date. /// /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index 8fc3500d3f78..8ea9004acd78 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -42,6 +42,8 @@ impl From for PipOptions { prerelease, pre, config_setting, + no_build_isolation, + build_isolation, exclude_newer, link_mode, no_sources, @@ -60,6 +62,7 @@ impl From for PipOptions { }, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), + no_build_isolation: flag(no_build_isolation, build_isolation), exclude_newer, link_mode, no_sources: if no_sources { Some(true) } else { None }, @@ -78,6 +81,8 @@ impl From for PipOptions { index_strategy, keyring_provider, config_setting, + no_build_isolation, + build_isolation, exclude_newer, link_mode, compile_bytecode, @@ -92,6 +97,7 @@ impl From for PipOptions { keyring_provider, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), + no_build_isolation: flag(no_build_isolation, build_isolation), exclude_newer, link_mode, compile_bytecode: flag(compile_bytecode, no_compile_bytecode), @@ -117,6 +123,8 @@ impl From for PipOptions { prerelease, pre, config_setting, + no_build_isolation, + build_isolation, exclude_newer, link_mode, compile_bytecode, @@ -139,6 +147,7 @@ impl From for PipOptions { }, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), + no_build_isolation: flag(no_build_isolation, build_isolation), exclude_newer, link_mode, compile_bytecode: flag(compile_bytecode, no_compile_bytecode), @@ -185,6 +194,8 @@ pub fn resolver_options(resolver_args: ResolverArgs, build_args: BuildArgs) -> R prerelease, pre, config_setting, + no_build_isolation, + build_isolation, exclude_newer, link_mode, no_sources, @@ -225,6 +236,7 @@ pub fn resolver_options(resolver_args: ResolverArgs, build_args: BuildArgs) -> R }, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), + no_build_isolation: flag(no_build_isolation, build_isolation), exclude_newer, link_mode, no_build: flag(no_build, build), @@ -254,6 +266,8 @@ pub fn resolver_installer_options( prerelease, pre, config_setting, + no_build_isolation, + build_isolation, exclude_newer, link_mode, compile_bytecode, @@ -298,6 +312,7 @@ pub fn resolver_installer_options( }, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), + no_build_isolation: flag(no_build_isolation, build_isolation), exclude_newer, link_mode, compile_bytecode: flag(compile_bytecode, no_compile_bytecode), diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 291528b2983a..10552a2102fe 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -159,6 +159,8 @@ pub struct InstallerOptions { pub no_build_package: Option>, pub no_binary: Option, pub no_binary_package: Option>, + pub no_build_isolation: Option, + pub no_sources: Option, } /// Settings relevant to all resolver operations. @@ -184,6 +186,7 @@ pub struct ResolverOptions { pub no_build_package: Option>, pub no_binary: Option, pub no_binary_package: Option>, + pub no_build_isolation: Option, pub no_sources: Option, } @@ -317,6 +320,18 @@ pub struct ResolverInstallerOptions { "# )] pub config_settings: Option, + /// Disable isolation when building source distributions. + /// + /// Assumes that build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/) + /// are already installed. + #[option( + default = "false", + value_type = "bool", + example = r#" + no-build-isolation = true + "# + )] + pub no_build_isolation: Option, /// Limit candidate packages to those that were uploaded prior to the given date. /// /// Accepts both [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html) timestamps (e.g., diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 9733828bda67..a5d7fbc936aa 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -18,7 +18,7 @@ use uv_dispatch::BuildDispatch; use uv_fs::CWD; use uv_git::ResolvedRepositoryReference; use uv_normalize::{PackageName, DEV_DEPENDENCIES}; -use uv_python::{Interpreter, PythonFetch, PythonPreference, PythonRequest}; +use uv_python::{Interpreter, PythonEnvironment, PythonFetch, PythonPreference, PythonRequest}; use uv_requirements::upgrade::{read_lock_requirements, LockedRequirements}; use uv_resolver::{ FlatIndex, Lock, OptionsBuilder, PythonRequirement, RequiresPython, ResolverMarkers, @@ -219,6 +219,7 @@ async fn do_lock( resolution, prerelease, config_setting, + no_build_isolation, exclude_newer, link_mode, upgrade, @@ -277,6 +278,15 @@ async fn do_lock( .platform(interpreter.platform()) .build(); + // Determine whether to enable build isolation. + let environment; + let build_isolation = if no_build_isolation { + environment = PythonEnvironment::from_interpreter(interpreter.clone()); + BuildIsolation::Shared(&environment) + } else { + BuildIsolation::Isolated + }; + let options = OptionsBuilder::new() .resolution_mode(resolution) .prerelease_mode(prerelease) @@ -287,7 +297,7 @@ async fn do_lock( // TODO(charlie): These are all default values. We should consider whether we want to make them // optional on the downstream APIs. - let build_isolation = BuildIsolation::default(); + let build_constraints = []; let extras = ExtrasSpecification::default(); let setup_py = SetupPyStrategy::default(); @@ -404,8 +414,6 @@ async fn do_lock( // Prefill the index with the lockfile metadata. let index = lock.to_index(workspace.install_path(), upgrade)?; - // TODO: read locked build constraints - let build_constraints = []; // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, @@ -484,8 +492,6 @@ async fn do_lock( None => { debug!("Starting clean resolution"); - // TODO: read locked build constraints - let build_constraints = []; // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 28937a5aaff0..eeafd8905764 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -405,6 +405,7 @@ pub(crate) async fn resolve_names( resolution: _, prerelease: _, config_setting, + no_build_isolation, exclude_newer, link_mode, compile_bytecode: _, @@ -430,15 +431,22 @@ pub(crate) async fn resolve_names( .platform(interpreter.platform()) .build(); + // Determine whether to enable build isolation. + let environment; + let build_isolation = if *no_build_isolation { + environment = PythonEnvironment::from_interpreter(interpreter.clone()); + BuildIsolation::Shared(&environment) + } else { + BuildIsolation::Isolated + }; + // TODO(charlie): These are all default values. We should consider whether we want to make them // optional on the downstream APIs. - let build_isolation = BuildIsolation::default(); let hasher = HashStrategy::default(); let setup_py = SetupPyStrategy::default(); let flat_index = FlatIndex::default(); - - // TODO: read locked build constraints let build_constraints = []; + // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, @@ -496,6 +504,7 @@ pub(crate) async fn resolve_environment<'a>( resolution, prerelease, config_setting, + no_build_isolation, exclude_newer, link_mode, upgrade: _, @@ -534,6 +543,15 @@ pub(crate) async fn resolve_environment<'a>( .platform(interpreter.platform()) .build(); + // Determine whether to enable build isolation. + let environment; + let build_isolation = if no_build_isolation { + environment = PythonEnvironment::from_interpreter(interpreter.clone()); + BuildIsolation::Shared(&environment) + } else { + BuildIsolation::Isolated + }; + let options = OptionsBuilder::new() .resolution_mode(resolution) .prerelease_mode(prerelease) @@ -543,12 +561,12 @@ pub(crate) async fn resolve_environment<'a>( // TODO(charlie): These are all default values. We should consider whether we want to make them // optional on the downstream APIs. - let build_isolation = BuildIsolation::default(); let dev = Vec::default(); let extras = ExtrasSpecification::default(); let hasher = HashStrategy::default(); let preferences = Vec::default(); let setup_py = SetupPyStrategy::default(); + let build_constraints = []; // When resolving from an interpreter, we assume an empty environment, so reinstalls and // upgrades aren't relevant. @@ -562,8 +580,6 @@ pub(crate) async fn resolve_environment<'a>( FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; - // TODO: read locked build constraints - let build_constraints = []; // Create a build dispatch. let resolve_dispatch = BuildDispatch::new( &client, @@ -635,6 +651,7 @@ pub(crate) async fn sync_environment( index_strategy, keyring_provider, config_setting, + no_build_isolation, exclude_newer, link_mode, compile_bytecode, @@ -666,9 +683,16 @@ pub(crate) async fn sync_environment( .platform(interpreter.platform()) .build(); + // Determine whether to enable build isolation. + let build_isolation = if no_build_isolation { + BuildIsolation::Shared(&venv) + } else { + BuildIsolation::Isolated + }; + // TODO(charlie): These are all default values. We should consider whether we want to make them // optional on the downstream APIs. - let build_isolation = BuildIsolation::default(); + let build_constraints = []; let dry_run = false; let hasher = HashStrategy::default(); let setup_py = SetupPyStrategy::default(); @@ -680,8 +704,6 @@ pub(crate) async fn sync_environment( FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; - // TODO: read locked build constraints - let build_constraints = []; // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, @@ -757,6 +779,7 @@ pub(crate) async fn update_environment( resolution, prerelease, config_setting, + no_build_isolation, exclude_newer, link_mode, compile_bytecode, @@ -822,6 +845,13 @@ pub(crate) async fn update_environment( .platform(interpreter.platform()) .build(); + // Determine whether to enable build isolation. + let build_isolation = if *no_build_isolation { + BuildIsolation::Shared(&venv) + } else { + BuildIsolation::Isolated + }; + let options = OptionsBuilder::new() .resolution_mode(*resolution) .prerelease_mode(*prerelease) @@ -831,7 +861,7 @@ pub(crate) async fn update_environment( // TODO(charlie): These are all default values. We should consider whether we want to make them // optional on the downstream APIs. - let build_isolation = BuildIsolation::default(); + let build_constraints = []; let dev = Vec::default(); let dry_run = false; let extras = ExtrasSpecification::default(); @@ -846,9 +876,6 @@ pub(crate) async fn update_environment( FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; - // TODO: read locked build constraints - let build_constraints = []; - // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index fc981320904c..015d9dd1af6b 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -147,6 +147,7 @@ pub(super) async fn do_sync( index_strategy, keyring_provider, config_setting, + no_build_isolation, exclude_newer, link_mode, compile_bytecode, @@ -194,9 +195,16 @@ pub(super) async fn do_sync( .platform(venv.interpreter().platform()) .build(); + // Determine whether to enable build isolation. + let build_isolation = if no_build_isolation { + BuildIsolation::Shared(venv) + } else { + BuildIsolation::Isolated + }; + // TODO(charlie): These are all default values. We should consider whether we want to make them // optional on the downstream APIs. - let build_isolation = BuildIsolation::default(); + let build_constraints = []; let dry_run = false; let setup_py = SetupPyStrategy::default(); @@ -210,8 +218,6 @@ pub(super) async fn do_sync( FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; - // TODO: read locked build constraints - let build_constraints = []; // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index b2abc52d6f24..ce581300e3a8 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -569,7 +569,14 @@ impl SyncSettings { python, } = args; - let modifications = if no_clean { + let settings = ResolverInstallerSettings::combine( + resolver_installer_options(installer, build), + filesystem, + ); + + // By default, sync with exact semantics, unless the user set `--no-build-isolation`; + // otherwise, we'll end up removing build dependencies. + let modifications = if no_clean || settings.no_build_isolation { Modifications::Sufficient } else { Modifications::Exact @@ -587,10 +594,7 @@ impl SyncSettings { package, python, refresh: Refresh::from(refresh), - settings: ResolverInstallerSettings::combine( - resolver_installer_options(installer, build), - filesystem, - ), + settings, } } } @@ -859,8 +863,6 @@ impl PipCompileSettings { no_generate_hashes, legacy_setup_py, no_legacy_setup_py, - no_build_isolation, - build_isolation, no_build, build, no_binary, @@ -935,7 +937,6 @@ impl PipCompileSettings { no_build: flag(no_build, build), no_binary, only_binary, - no_build_isolation: flag(no_build_isolation, build_isolation), extra, all_extras: flag(all_extras, no_all_extras), no_deps: flag(no_deps, deps), @@ -1004,8 +1005,6 @@ impl PipSyncSettings { no_allow_empty_requirements, legacy_setup_py, no_legacy_setup_py, - no_build_isolation, - build_isolation, no_build, build, no_binary, @@ -1047,7 +1046,6 @@ impl PipSyncSettings { no_allow_empty_requirements, ), legacy_setup_py: flag(legacy_setup_py, no_legacy_setup_py), - no_build_isolation: flag(no_build_isolation, build_isolation), python_version, python_platform, strict: flag(strict, no_strict), @@ -1109,8 +1107,6 @@ impl PipInstallSettings { prefix, legacy_setup_py, no_legacy_setup_py, - no_build_isolation, - build_isolation, no_build, build, no_binary, @@ -1181,7 +1177,6 @@ impl PipInstallSettings { no_build: flag(no_build, build), no_binary, only_binary, - no_build_isolation: flag(no_build_isolation, build_isolation), strict: flag(strict, no_strict), extra, all_extras: flag(all_extras, no_all_extras), @@ -1507,6 +1502,7 @@ pub(crate) struct InstallerSettingsRef<'a> { pub(crate) index_strategy: IndexStrategy, pub(crate) keyring_provider: KeyringProviderType, pub(crate) config_setting: &'a ConfigSettings, + pub(crate) no_build_isolation: bool, pub(crate) exclude_newer: Option, pub(crate) link_mode: LinkMode, pub(crate) compile_bytecode: bool, @@ -1528,6 +1524,7 @@ pub(crate) struct ResolverSettings { pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, pub(crate) config_setting: ConfigSettings, + pub(crate) no_build_isolation: bool, pub(crate) exclude_newer: Option, pub(crate) link_mode: LinkMode, pub(crate) upgrade: Upgrade, @@ -1543,6 +1540,7 @@ pub(crate) struct ResolverSettingsRef<'a> { pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, pub(crate) config_setting: &'a ConfigSettings, + pub(crate) no_build_isolation: bool, pub(crate) exclude_newer: Option, pub(crate) link_mode: LinkMode, pub(crate) upgrade: &'a Upgrade, @@ -1563,6 +1561,7 @@ impl ResolverSettings { resolution, prerelease, config_settings, + no_build_isolation, exclude_newer, link_mode, compile_bytecode: _, @@ -1603,6 +1602,10 @@ impl ResolverSettings { .config_settings .combine(config_settings) .unwrap_or_default(), + no_build_isolation: args + .no_build_isolation + .combine(no_build_isolation) + .unwrap_or_default(), exclude_newer: args.exclude_newer.combine(exclude_newer), link_mode: args.link_mode.combine(link_mode).unwrap_or_default(), upgrade: Upgrade::from_args( @@ -1642,6 +1645,7 @@ impl ResolverSettings { resolution: self.resolution, prerelease: self.prerelease, config_setting: &self.config_setting, + no_build_isolation: self.no_build_isolation, exclude_newer: self.exclude_newer, link_mode: self.link_mode, upgrade: &self.upgrade, @@ -1665,6 +1669,7 @@ pub(crate) struct ResolverInstallerSettings { pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, pub(crate) config_setting: ConfigSettings, + pub(crate) no_build_isolation: bool, pub(crate) exclude_newer: Option, pub(crate) link_mode: LinkMode, pub(crate) compile_bytecode: bool, @@ -1682,6 +1687,7 @@ pub(crate) struct ResolverInstallerSettingsRef<'a> { pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, pub(crate) config_setting: &'a ConfigSettings, + pub(crate) no_build_isolation: bool, pub(crate) exclude_newer: Option, pub(crate) link_mode: LinkMode, pub(crate) compile_bytecode: bool, @@ -1707,6 +1713,7 @@ impl ResolverInstallerSettings { resolution, prerelease, config_settings, + no_build_isolation, exclude_newer, link_mode, compile_bytecode, @@ -1747,6 +1754,10 @@ impl ResolverInstallerSettings { .config_settings .combine(config_settings) .unwrap_or_default(), + no_build_isolation: args + .no_build_isolation + .combine(no_build_isolation) + .unwrap_or_default(), exclude_newer: args.exclude_newer.combine(exclude_newer), link_mode: args.link_mode.combine(link_mode).unwrap_or_default(), sources: SourceStrategy::from_args( @@ -1796,6 +1807,7 @@ impl ResolverInstallerSettings { resolution: self.resolution, prerelease: self.prerelease, config_setting: &self.config_setting, + no_build_isolation: self.no_build_isolation, exclude_newer: self.exclude_newer, link_mode: self.link_mode, compile_bytecode: self.compile_bytecode, @@ -1933,6 +1945,7 @@ impl PipSettings { resolution: top_level_resolution, prerelease: top_level_prerelease, config_settings: top_level_config_settings, + no_build_isolation: top_level_no_build_isolation, exclude_newer: top_level_exclude_newer, link_mode: top_level_link_mode, compile_bytecode: top_level_compile_bytecode, @@ -1960,6 +1973,7 @@ impl PipSettings { let resolution = resolution.combine(top_level_resolution); let prerelease = prerelease.combine(top_level_prerelease); let config_settings = config_settings.combine(top_level_config_settings); + let no_build_isolation = no_build_isolation.combine(top_level_no_build_isolation); let exclude_newer = exclude_newer.combine(top_level_exclude_newer); let link_mode = link_mode.combine(top_level_link_mode); let compile_bytecode = compile_bytecode.combine(top_level_compile_bytecode); @@ -2151,6 +2165,7 @@ impl<'a> From> for ResolverSettingsRef<'a> { resolution: settings.resolution, prerelease: settings.prerelease, config_setting: settings.config_setting, + no_build_isolation: settings.no_build_isolation, exclude_newer: settings.exclude_newer, link_mode: settings.link_mode, upgrade: settings.upgrade, @@ -2167,6 +2182,7 @@ impl<'a> From> for InstallerSettingsRef<'a> { index_strategy: settings.index_strategy, keyring_provider: settings.keyring_provider, config_setting: settings.config_setting, + no_build_isolation: settings.no_build_isolation, exclude_newer: settings.exclude_newer, link_mode: settings.link_mode, compile_bytecode: settings.compile_bytecode, diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index 45c8478a90d8..cf42ca151f02 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -436,3 +436,83 @@ fn virtual_workspace_dev_dependencies() -> Result<()> { Ok(()) } + +#[test] +fn sync_build_isolation() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz"] + "#, + )?; + + // Running `uv sync` should fail. + let filters = std::iter::once((r"exit code: 1", "exit status: 1")) + .chain(context.filters()) + .collect::>(); + uv_snapshot!(filters, context.sync().arg("--no-build-isolation"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning + error: Failed to download and build: `iniconfig @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz` + Caused by: Failed to build: `iniconfig @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.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 'hatchling' + --- + "###); + + // Install `setuptools` (for the root project) plus `hatchling`, `hatch-vcs`, and `wheel` (for `iniconfig`). + uv_snapshot!(context.filters(), context.pip_install().arg("wheel").arg("setuptools").arg("hatchling").arg("hatch-vcs"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Prepared 10 packages in [TIME] + Installed 10 packages in [TIME] + + hatch-vcs==0.4.0 + + hatchling==1.22.4 + + packaging==24.0 + + pathspec==0.12.1 + + pluggy==1.4.0 + + setuptools==69.2.0 + + setuptools-scm==8.0.4 + + trove-classifiers==2024.3.3 + + typing-extensions==4.10.0 + + wheel==0.43.0 + "###); + + // Running `uv sync` should succeed. + uv_snapshot!(context.filters(), context.sync().arg("--no-build-isolation"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv sync` is experimental and may change without warning + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 (from https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz) + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + assert!(context.temp_dir.child("uv.lock").exists()); + + Ok(()) +} diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 8beff7b75db8..4b4cba8938ae 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -448,6 +448,34 @@ distributions will exit with an error. --- +#### [`no-build-isolation`](#no-build-isolation) {: #no-build-isolation } + +Disable isolation when building source distributions. + +Assumes that build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/) +are already installed. + +**Default value**: `false` + +**Type**: `bool` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + no-build-isolation = true + ``` +=== "uv.toml" + + ```toml + + no-build-isolation = true + ``` + +--- + #### [`no-build-package`](#no-build-package) {: #no-build-package } Don't build source distributions for a specific package. diff --git a/uv.schema.json b/uv.schema.json index ab837d5d9392..99e4f59215a7 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -161,6 +161,13 @@ "null" ] }, + "no-build-isolation": { + "description": "Disable isolation when building source distributions.\n\nAssumes that build dependencies specified by [PEP 518](https://peps.python.org/pep-0518/) are already installed.", + "type": [ + "boolean", + "null" + ] + }, "no-build-package": { "description": "Don't build source distributions for a specific package.", "type": [