diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 08f1b5ee2..f3ac89b99 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -4102,6 +4102,15 @@ pub struct ToolRunArgs { #[arg(long, short, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub constraints: Vec>, + /// Constrain build dependencies using the given requirements files when building source + /// distributions. + /// + /// Constraints files are `requirements.txt`-like files that only control the _version_ of a + /// requirement that's installed. However, including a package in a constraints file will _not_ + /// trigger the installation of that package. + #[arg(long, short, alias = "build-constraint", env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] + pub build_constraints: Vec>, + /// Override versions using the given requirements files. /// /// Overrides files are `requirements.txt`-like files that force a specific version of a @@ -4212,6 +4221,15 @@ pub struct ToolInstallArgs { #[arg(long, alias = "override", env = EnvVars::UV_OVERRIDE, value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub overrides: Vec>, + /// Constrain build dependencies using the given requirements files when building source + /// distributions. + /// + /// Constraints files are `requirements.txt`-like files that only control the _version_ of a + /// requirement that's installed. However, including a package in a constraints file will _not_ + /// trigger the installation of that package. + #[arg(long, short, alias = "build-constraint", env = EnvVars::UV_BUILD_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)] + pub build_constraints: Vec>, + #[command(flatten)] pub installer: ResolverInstallerArgs, diff --git a/crates/uv-tool/src/tool.rs b/crates/uv-tool/src/tool.rs index ed9b2cb40..a89aeb98c 100644 --- a/crates/uv-tool/src/tool.rs +++ b/crates/uv-tool/src/tool.rs @@ -19,6 +19,8 @@ pub struct Tool { constraints: Vec, /// The overrides requested by the user during installation. overrides: Vec, + /// The build constraints requested by the user during installation. + build_constraints: Vec, /// The Python requested by the user during installation. python: Option, /// A mapping of entry point names to their metadata. @@ -28,6 +30,7 @@ pub struct Tool { } #[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] struct ToolWire { #[serde(default)] requirements: Vec, @@ -35,6 +38,8 @@ struct ToolWire { constraints: Vec, #[serde(default)] overrides: Vec, + #[serde(default)] + build_constraint_dependencies: Vec, python: Option, entrypoints: Vec, #[serde(default)] @@ -61,6 +66,7 @@ impl From for ToolWire { .collect(), constraints: tool.constraints, overrides: tool.overrides, + build_constraint_dependencies: tool.build_constraints, python: tool.python, entrypoints: tool.entrypoints, options: tool.options, @@ -83,6 +89,7 @@ impl TryFrom for Tool { .collect(), constraints: tool.constraints, overrides: tool.overrides, + build_constraints: tool.build_constraint_dependencies, python: tool.python, entrypoints: tool.entrypoints, options: tool.options, @@ -156,6 +163,7 @@ impl Tool { requirements: Vec, constraints: Vec, overrides: Vec, + build_constraints: Vec, python: Option, entrypoints: impl Iterator, options: ToolOptions, @@ -166,6 +174,7 @@ impl Tool { requirements, constraints, overrides, + build_constraints, python, entrypoints, options, @@ -248,6 +257,28 @@ impl Tool { }); } + if !self.build_constraints.is_empty() { + table.insert("build-constraint-dependencies", { + let build_constraints = self + .build_constraints + .iter() + .map(|r#build_constraint| { + serde::Serialize::serialize( + &r#build_constraint, + toml_edit::ser::ValueSerializer::new(), + ) + }) + .collect::, _>>()?; + + let build_constraints = match build_constraints.as_slice() { + [] => Array::new(), + [r#build_constraint] => Array::from_iter([r#build_constraint]), + build_constraints => each_element_on_its_line_array(build_constraints.iter()), + }; + value(build_constraints) + }); + } + if let Some(ref python) = self.python { table.insert("python", value(python)); } @@ -292,6 +323,10 @@ impl Tool { &self.overrides } + pub fn build_constraints(&self) -> &[Requirement] { + &self.build_constraints + } + pub fn python(&self) -> &Option { &self.python } diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 89b3dd2e1..033666b2b 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -519,8 +519,8 @@ async fn build_package( let build_constraints = Constraints::from_requirements( build_constraints - .iter() - .map(|constraint| constraint.requirement.clone()), + .into_iter() + .map(|constraint| constraint.requirement), ); // Initialize the registry client. diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 7858087b8..e43c36894 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -213,8 +213,7 @@ pub(crate) async fn pip_compile( let build_constraints: Vec = operations::read_constraints(build_constraints, &client_builder) .await? - .iter() - .cloned() + .into_iter() .chain( build_constraints_from_workspace .into_iter() diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index d1790c697..da8ec6674 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -154,8 +154,7 @@ pub(crate) async fn pip_install( let build_constraints: Vec = operations::read_constraints(build_constraints, &client_builder) .await? - .iter() - .cloned() + .into_iter() .chain( build_constraints_from_workspace .iter() diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index 49b7cc236..73325ce85 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -2,7 +2,7 @@ use tracing::debug; use uv_cache::{Cache, CacheBucket}; use uv_cache_key::{cache_digest, hash_digest}; -use uv_configuration::{Concurrency, PreviewMode}; +use uv_configuration::{Concurrency, Constraints, PreviewMode}; use uv_distribution_types::{Name, Resolution}; use uv_python::{Interpreter, PythonEnvironment}; @@ -28,6 +28,7 @@ impl CachedEnvironment { /// Get or create an [`CachedEnvironment`] based on a given set of requirements. pub(crate) async fn from_spec( spec: EnvironmentSpecification<'_>, + build_constraints: Constraints, interpreter: &Interpreter, settings: &ResolverInstallerSettings, network_settings: &NetworkSettings, @@ -99,6 +100,7 @@ impl CachedEnvironment { venv, &resolution, Modifications::Exact, + build_constraints, settings.into(), network_settings, state, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index ee9266a12..dc9b93a54 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1824,6 +1824,7 @@ pub(crate) async fn sync_environment( venv: PythonEnvironment, resolution: &Resolution, modifications: Modifications, + build_constraints: Constraints, settings: InstallerSettingsRef<'_>, network_settings: &NetworkSettings, state: &PlatformState, @@ -1891,7 +1892,6 @@ pub(crate) async fn sync_environment( // TODO(charlie): These are all default values. We should consider whether we want to make them // optional on the downstream APIs. - let build_constraints = Constraints::default(); let build_hasher = HashStrategy::default(); let dry_run = DryRun::default(); let hasher = HashStrategy::default(); @@ -1982,6 +1982,7 @@ pub(crate) async fn update_environment( venv: PythonEnvironment, spec: RequirementsSpecification, modifications: Modifications, + build_constraints: Constraints, settings: &ResolverInstallerSettings, network_settings: &NetworkSettings, state: &SharedState, @@ -2113,7 +2114,6 @@ 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_constraints = Constraints::default(); let build_hasher = HashStrategy::default(); let extras = ExtrasSpecification::default(); let groups = BTreeMap::new(); diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 8ba505769..731fbd01f 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -17,9 +17,10 @@ use uv_cache::Cache; use uv_cli::ExternalCommand; use uv_client::BaseClientBuilder; use uv_configuration::{ - Concurrency, DependencyGroups, DryRun, EditableMode, ExtrasSpecification, InstallOptions, - PreviewMode, + Concurrency, Constraints, DependencyGroups, DryRun, EditableMode, ExtrasSpecification, + InstallOptions, PreviewMode, }; +use uv_distribution_types::Requirement; use uv_fs::which::is_executable; use uv_fs::{PythonExt, Simplified}; use uv_installer::{SatisfiesResult, SitePackages}; @@ -351,10 +352,28 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl .await? .into_environment()?; + let build_constraints = script + .metadata() + .tool + .as_ref() + .and_then(|tool| { + tool.uv + .as_ref() + .and_then(|uv| uv.build_constraint_dependencies.as_ref()) + }) + .map(|constraints| { + Constraints::from_requirements( + constraints + .iter() + .map(|constraint| Requirement::from(constraint.clone())), + ) + }); + match update_environment( environment, spec, modifications, + build_constraints.unwrap_or_default(), &settings, &network_settings, &sync_state, @@ -880,6 +899,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl lock.as_ref() .map(|(lock, install_path)| (lock, install_path.as_ref())), ), + // TODO(bluefact): Respect build constraints for `uv run --with` dependencies. + Constraints::default(), &base_interpreter, &settings, &network_settings, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 3bf88556c..c2089f86e 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -11,12 +11,12 @@ use uv_auth::UrlAuthPolicies; use uv_cache::Cache; use uv_client::{FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode, + Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode, ExtrasSpecification, HashCheckingMode, InstallOptions, PreviewMode, }; use uv_dispatch::BuildDispatch; use uv_distribution_types::{ - DirectorySourceDist, Dist, Index, Resolution, ResolvedDist, SourceDist, + DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist, }; use uv_fs::Simplified; use uv_installer::SitePackages; @@ -274,12 +274,33 @@ pub(crate) async fn sync( )); } + // Parse the requirements from the script. let spec = script_specification(Pep723ItemRef::Script(script), &settings.resolver)? .unwrap_or_default(); + + // Parse the build constraints from the script. + let build_constraints = script + .metadata + .tool + .as_ref() + .and_then(|tool| { + tool.uv + .as_ref() + .and_then(|uv| uv.build_constraint_dependencies.as_ref()) + }) + .map(|constraints| { + Constraints::from_requirements( + constraints + .iter() + .map(|constraint| Requirement::from(constraint.clone())), + ) + }); + match update_environment( Deref::deref(&environment).clone(), spec, modifications, + build_constraints.unwrap_or_default(), &settings, &network_settings, &PlatformState::default(), diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index d0f1cef44..ea504820b 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -168,6 +168,7 @@ pub(crate) fn install_executables( requirements: Vec, constraints: Vec, overrides: Vec, + build_constraints: Vec, printer: Printer, ) -> anyhow::Result { let site_packages = SitePackages::from_environment(environment)?; @@ -289,6 +290,7 @@ pub(crate) fn install_executables( requirements, constraints, overrides, + build_constraints, python, target_entry_points .into_iter() diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 8ad0f4234..c0b42d8f2 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -9,7 +9,7 @@ use tracing::{debug, trace}; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_client::BaseClientBuilder; -use uv_configuration::{Concurrency, DryRun, PreviewMode, Reinstall, Upgrade}; +use uv_configuration::{Concurrency, Constraints, DryRun, PreviewMode, Reinstall, Upgrade}; use uv_distribution_types::{ NameRequirementSpecification, Requirement, RequirementSource, UnresolvedRequirementSpecification, @@ -27,7 +27,7 @@ use uv_warnings::warn_user; use uv_workspace::WorkspaceCache; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; -use crate::commands::pip::operations::Modifications; +use crate::commands::pip::operations::{self, Modifications}; use crate::commands::project::{ resolve_environment, resolve_names, sync_environment, update_environment, EnvironmentSpecification, PlatformState, ProjectError, @@ -48,6 +48,7 @@ pub(crate) async fn install( with: &[RequirementsSource], constraints: &[RequirementsSource], overrides: &[RequirementsSource], + build_constraints: &[RequirementsSource], python: Option, install_mirrors: PythonInstallMirrors, force: bool, @@ -290,6 +291,14 @@ pub(crate) async fn install( ) .await?; + // Resolve the build constraints. + let build_constraints: Vec = + operations::read_constraints(build_constraints, &client_builder) + .await? + .into_iter() + .map(|constraint| constraint.requirement) + .collect(); + // Convert to tool options. let options = ToolOptions::from(options); @@ -362,6 +371,7 @@ pub(crate) async fn install( if requirements == tool_receipt.requirements() && constraints == tool_receipt.constraints() && overrides == tool_receipt.overrides() + && build_constraints == tool_receipt.build_constraints() { if *tool_receipt.options() != options { // ...but the options differ, we need to update the receipt. @@ -410,6 +420,7 @@ pub(crate) async fn install( environment, spec, Modifications::Exact, + Constraints::from_requirements(build_constraints.iter().cloned()), &settings, &network_settings, &state, @@ -540,6 +551,7 @@ pub(crate) async fn install( environment, &resolution.into(), Modifications::Exact, + Constraints::from_requirements(build_constraints.iter().cloned()), (&settings).into(), &network_settings, &state, @@ -576,6 +588,7 @@ pub(crate) async fn install( requirements, constraints, overrides, + build_constraints, printer, ) } diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 2a30961cd..7d4cbc174 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -17,6 +17,7 @@ use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_cli::ExternalCommand; use uv_client::BaseClientBuilder; +use uv_configuration::Constraints; use uv_configuration::{Concurrency, PreviewMode}; use uv_distribution_types::{ IndexUrl, Name, NameRequirementSpecification, Requirement, RequirementSource, @@ -43,6 +44,7 @@ use uv_workspace::WorkspaceCache; use crate::commands::pip::loggers::{ DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger, }; +use crate::commands::pip::operations; use crate::commands::project::{ resolve_names, EnvironmentSpecification, PlatformState, ProjectError, }; @@ -82,6 +84,7 @@ pub(crate) async fn run( with: &[RequirementsSource], constraints: &[RequirementsSource], overrides: &[RequirementsSource], + build_constraints: &[RequirementsSource], show_resolution: bool, python: Option, install_mirrors: PythonInstallMirrors, @@ -244,11 +247,12 @@ pub(crate) async fn run( }; // Get or create a compatible environment in which to execute the tool. - let result = get_or_create_environment( + let result = Box::pin(get_or_create_environment( &request, with, constraints, overrides, + build_constraints, show_resolution, python.as_deref(), install_mirrors, @@ -263,7 +267,7 @@ pub(crate) async fn run( &cache, printer, preview, - ) + )) .await; let (from, environment) = match result { @@ -602,6 +606,7 @@ async fn get_or_create_environment( with: &[RequirementsSource], constraints: &[RequirementsSource], overrides: &[RequirementsSource], + build_constraints: &[RequirementsSource], show_resolution: bool, python: Option<&str>, install_mirrors: PythonInstallMirrors, @@ -905,11 +910,20 @@ async fn get_or_create_environment( ..spec }); + // Read the `--build-constraints` requirements. + let build_constraints = Constraints::from_requirements( + operations::read_constraints(build_constraints, &client_builder) + .await? + .into_iter() + .map(|constraint| constraint.requirement), + ); + // TODO(zanieb): When implementing project-level tools, discover the project and check if it has the tool. // TODO(zanieb): Determine if we should layer on top of the project environment if it is present. let result = CachedEnvironment::from_spec( spec.clone(), + build_constraints.clone(), &interpreter, settings, network_settings, @@ -967,6 +981,7 @@ async fn get_or_create_environment( CachedEnvironment::from_spec( spec, + build_constraints, &interpreter, settings, network_settings, diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index ca355a5e7..215566dd9 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -7,7 +7,7 @@ use tracing::debug; use uv_cache::Cache; use uv_client::BaseClientBuilder; -use uv_configuration::{Concurrency, DryRun, PreviewMode}; +use uv_configuration::{Concurrency, Constraints, DryRun, PreviewMode}; use uv_distribution_types::Requirement; use uv_fs::CWD; use uv_normalize::PackageName; @@ -268,6 +268,9 @@ async fn upgrade_tool( ); let settings = ResolverInstallerSettings::from(options.clone()); + let build_constraints = + Constraints::from_requirements(existing_tool_receipt.build_constraints().iter().cloned()); + // Resolve the requirements. let spec = RequirementsSpecification::from_overrides( existing_tool_receipt.requirements().to_vec(), @@ -310,6 +313,7 @@ async fn upgrade_tool( environment, &resolution.into(), Modifications::Exact, + build_constraints, (&settings).into(), network_settings, &state, @@ -334,6 +338,7 @@ async fn upgrade_tool( environment, spec, Modifications::Exact, + build_constraints, &settings, network_settings, &state, @@ -379,6 +384,7 @@ async fn upgrade_tool( existing_tool_receipt.requirements().to_vec(), existing_tool_receipt.constraints().to_vec(), existing_tool_receipt.overrides().to_vec(), + existing_tool_receipt.build_constraints().to_vec(), printer, )?; } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 651ef10c8..203e6db56 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1104,12 +1104,19 @@ async fn run(mut cli: Cli) -> Result { .map(RequirementsSource::from_overrides_txt) .collect::>(); + let build_constraints = args + .build_constraints + .into_iter() + .map(RequirementsSource::from_constraints_txt) + .collect::>(); + Box::pin(commands::tool_run( args.command, args.from, &requirements, &constraints, &overrides, + &build_constraints, args.show_resolution || globals.verbose > 0, args.python, args.install_mirrors, @@ -1169,6 +1176,11 @@ async fn run(mut cli: Cli) -> Result { .into_iter() .map(RequirementsSource::from_overrides_txt) .collect::>(); + let build_constraints = args + .build_constraints + .into_iter() + .map(RequirementsSource::from_constraints_txt) + .collect::>(); Box::pin(commands::tool_install( args.package, @@ -1177,6 +1189,7 @@ async fn run(mut cli: Cli) -> Result { &requirements, &constraints, &overrides, + &build_constraints, args.python, args.install_mirrors, args.force, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 5d0e2e777..387d80aff 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -459,6 +459,7 @@ pub(crate) struct ToolRunSettings { pub(crate) with_editable: Vec, pub(crate) constraints: Vec, pub(crate) overrides: Vec, + pub(crate) build_constraints: Vec, pub(crate) isolated: bool, pub(crate) show_resolution: bool, pub(crate) python: Option, @@ -486,6 +487,7 @@ impl ToolRunSettings { with_requirements, constraints, overrides, + build_constraints, isolated, env_file, no_env_file, @@ -553,6 +555,10 @@ impl ToolRunSettings { .into_iter() .filter_map(Maybe::into_option) .collect(), + build_constraints: build_constraints + .into_iter() + .filter_map(Maybe::into_option) + .collect(), isolated, show_resolution, python: python.and_then(Maybe::into_option), @@ -577,6 +583,7 @@ pub(crate) struct ToolInstallSettings { pub(crate) with_editable: Vec, pub(crate) constraints: Vec, pub(crate) overrides: Vec, + pub(crate) build_constraints: Vec, pub(crate) python: Option, pub(crate) refresh: Refresh, pub(crate) options: ResolverInstallerOptions, @@ -599,6 +606,7 @@ impl ToolInstallSettings { with_requirements, constraints, overrides, + build_constraints, installer, force, build, @@ -644,6 +652,10 @@ impl ToolInstallSettings { .into_iter() .filter_map(Maybe::into_option) .collect(), + build_constraints: build_constraints + .into_iter() + .filter_map(Maybe::into_option) + .collect(), python: python.and_then(Maybe::into_option), force, editable, diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 2b8621832..0634ee59a 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -764,6 +764,79 @@ fn run_pep723_script_overrides() -> Result<()> { Ok(()) } +/// Run a PEP 723-compatible script with `tool.uv` build constraints. +#[test] +fn run_pep723_script_build_constraints() -> Result<()> { + let context = TestContext::new("3.8"); + + let test_script = context.temp_dir.child("main.py"); + + // Incompatible build constraints. + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.8" + # dependencies = [ + # "anyio>=3", + # "requests==1.2" + # ] + # + # [tool.uv] + # build-constraint-dependencies = ["setuptools==1"] + # /// + + import anyio + "# + })?; + + uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. + "###); + + // Compatible build constraints. + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.8" + # dependencies = [ + # "anyio>=3", + # "requests==1.2" + # ] + # + # [tool.uv] + # build-constraint-dependencies = ["setuptools>=40"] + # /// + + import anyio + "# + })?; + + uv_snapshot!(context.filters(), context.run().arg("main.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Prepared 6 packages in [TIME] + Installed 6 packages in [TIME] + + anyio==4.3.0 + + exceptiongroup==1.2.0 + + idna==3.6 + + requests==1.2.0 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "###); + + Ok(()) +} + /// Run a PEP 723-compatible script with a lockfile. #[test] fn run_pep723_script_lock() -> Result<()> { diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 3dc7454c2..f382e6d3c 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -2833,6 +2833,7 @@ fn resolve_tool() -> anyhow::Result<()> { with_editable: [], constraints: [], overrides: [], + build_constraints: [], python: None, refresh: None( Timestamp( diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 7de30d035..6a246c8bc 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -8399,6 +8399,106 @@ fn sync_locked_script() -> Result<()> { Ok(()) } +#[test] +fn sync_script_with_compatible_build_constraints() -> Result<()> { + let context = TestContext::new("3.8"); + + let test_script = context.temp_dir.child("script.py"); + + // Compatible build constraints. + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.8" + # dependencies = [ + # "anyio>=3", + # "requests==1.2" + # ] + # + # [tool.uv] + # build-constraint-dependencies = ["setuptools>=40"] + # /// + + import anyio + "# + })?; + + let filters = context + .filters() + .into_iter() + .chain(vec![( + r"environments-v2/script-\w+", + "environments-v2/script-[HASH]", + )]) + .collect::>(); + + uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Creating script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + Resolved 6 packages in [TIME] + Prepared 6 packages in [TIME] + Installed 6 packages in [TIME] + + anyio==4.3.0 + + exceptiongroup==1.2.0 + + idna==3.6 + + requests==1.2.0 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "###); + + Ok(()) +} + +#[test] +fn sync_script_with_incompatible_build_constraints() -> Result<()> { + let context = TestContext::new("3.8"); + + let test_script = context.temp_dir.child("script.py"); + let filters = context + .filters() + .into_iter() + .chain(vec![( + r"environments-v2/script-\w+", + "environments-v2/script-[HASH]", + )]) + .collect::>(); + + // Incompatible build constraints. + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.8" + # dependencies = [ + # "anyio>=3", + # "requests==1.2" + # ] + # + # [tool.uv] + # build-constraint-dependencies = ["setuptools==1"] + # /// + + import anyio + "# + })?; + + uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Creating script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. + "###); + + Ok(()) +} + #[test] fn unsupported_git_scheme() -> Result<()> { let context = TestContext::new_with_versions(&["3.12"]); diff --git a/crates/uv/tests/it/tool_install.rs b/crates/uv/tests/it/tool_install.rs index fbedc46d0..5733df92d 100644 --- a/crates/uv/tests/it/tool_install.rs +++ b/crates/uv/tests/it/tool_install.rs @@ -320,6 +320,117 @@ fn tool_install_with_editable() -> Result<()> { Ok(()) } +#[test] +fn tool_install_with_compatible_build_constraints() -> Result<()> { + let context = TestContext::new("3.8") + .with_exclude_newer("2024-05-04T00:00:00Z") + .with_filtered_counts() + .with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools>=40")?; + + uv_snapshot!(context.filters(), context.tool_install() + .arg("black") + .arg("--with") + .arg("requests==1.2") + .arg("--build-constraints") + .arg("build_constraints.txt") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) + .env(EnvVars::PATH, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + black==24.4.2 + + click==8.1.7 + + mypy-extensions==1.0.0 + + packaging==24.0 + + pathspec==0.12.1 + + platformdirs==4.2.1 + + requests==1.2.0 + + tomli==2.0.1 + + typing-extensions==4.11.0 + Installed 2 executables: black, blackd + "###); + + tool_dir + .child("black") + .child("uv-receipt.toml") + .assert(predicate::path::exists()); + + insta::with_settings!({ + filters => context.filters(), + }, { + // We should have a tool receipt + assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###" + [tool] + requirements = [ + { name = "black" }, + { name = "requests", specifier = "==1.2" }, + ] + build-constraint-dependencies = [{ name = "setuptools", specifier = ">=40" }] + entrypoints = [ + { name = "black", install-path = "[TEMP_DIR]/bin/black" }, + { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, + ] + + [tool.options] + exclude-newer = "2024-05-04T00:00:00Z" + "###); + }); + + Ok(()) +} + +#[test] +fn tool_install_with_incompatible_build_constraints() -> Result<()> { + let context = TestContext::new("3.8") + .with_exclude_newer("2024-05-04T00:00:00Z") + .with_filtered_counts() + .with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools==2")?; + + uv_snapshot!(context.filters(), context.tool_install() + .arg("black") + .arg("--with") + .arg("requests==1.2") + .arg("--build-constraints") + .arg("build_constraints.txt") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) + .env(EnvVars::PATH, bin_dir.as_os_str()), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40.8.0 and setuptools==2, we can conclude that your requirements are unsatisfiable. + "###); + + tool_dir + .child("black") + .child("uv-receipt.toml") + .assert(predicate::path::missing()); + + Ok(()) +} + #[test] fn tool_install_suggest_other_packages_with_executable() { // FastAPI 0.111 is only available from this date onwards. diff --git a/crates/uv/tests/it/tool_run.rs b/crates/uv/tests/it/tool_run.rs index 2206d2444..59d28f559 100644 --- a/crates/uv/tests/it/tool_run.rs +++ b/crates/uv/tests/it/tool_run.rs @@ -1,4 +1,5 @@ use crate::common::{uv_snapshot, TestContext}; +use anyhow::Result; use assert_cmd::prelude::*; use assert_fs::prelude::*; use indoc::indoc; @@ -2354,6 +2355,80 @@ fn tool_run_with_script_and_from_script() { "); } +#[test] +fn tool_run_with_compatible_build_constraints() -> Result<()> { + let context = TestContext::new("3.8") + .with_exclude_newer("2024-05-04T00:00:00Z") + .with_filtered_counts() + .with_filtered_exe_suffix(); + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools>=40")?; + + uv_snapshot!(context.filters(), context.tool_run() + .arg("--with") + .arg("requests==1.2") + .arg("--build-constraints") + .arg("build_constraints.txt") + .arg("pytest") + .arg("--version"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + pytest 8.2.0 + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + exceptiongroup==1.2.1 + + iniconfig==2.0.0 + + packaging==24.0 + + pluggy==1.5.0 + + pytest==8.2.0 + + requests==1.2.0 + + tomli==2.0.1 + "###); + + Ok(()) +} + +#[test] +fn tool_run_with_incompatible_build_constraints() -> Result<()> { + let context = TestContext::new("3.8") + .with_exclude_newer("2024-05-04T00:00:00Z") + .with_filtered_counts() + .with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools==2")?; + + uv_snapshot!(context.filters(), context.tool_run() + .arg("--with") + .arg("requests==1.2") + .arg("--build-constraints") + .arg("build_constraints.txt") + .arg("pytest") + .arg("--version") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) + .env(EnvVars::PATH, bin_dir.as_os_str()), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40.8.0 and setuptools==2, we can conclude that your requirements are unsatisfiable. + "###); + + Ok(()) +} + /// Test windows runnable types, namely console scripts and legacy setuptools scripts. /// Console Scripts /// Legacy Scripts . diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d1cea19f8..7c18deb89 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -3123,6 +3123,11 @@ uv tool run [OPTIONS] [COMMAND]

WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.

May also be set with the UV_INSECURE_HOST environment variable.

+
--build-constraints, --build-constraint, -b build-constraints

Constrain build dependencies using the given requirements files when building source distributions.

+ +

Constraints files are requirements.txt-like files that only control the version of a requirement that’s installed. However, including a package in a constraints file will not trigger the installation of that package.

+ +

May also be set with the UV_BUILD_CONSTRAINT environment variable.

--cache-dir cache-dir

Path to the cache directory.

Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.

@@ -3468,6 +3473,11 @@ uv tool install [OPTIONS]

WARNING: Hosts included in this list will not be verified against the system’s certificate store. Only use --allow-insecure-host in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.

May also be set with the UV_INSECURE_HOST environment variable.

+
--build-constraints, --build-constraint, -b build-constraints

Constrain build dependencies using the given requirements files when building source distributions.

+ +

Constraints files are requirements.txt-like files that only control the version of a requirement that’s installed. However, including a package in a constraints file will not trigger the installation of that package.

+ +

May also be set with the UV_BUILD_CONSTRAINT environment variable.

--cache-dir cache-dir

Path to the cache directory.

Defaults to $XDG_CACHE_HOME/uv or $HOME/.cache/uv on macOS and Linux, and %LOCALAPPDATA%\uv\cache on Windows.