Support build constraints in `uv tool` and PEP723 scripts. (#12842)

## Summary

Closes https://github.com/astral-sh/uv/issues/12496.

## Test Plan

`cargo test`

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
Ahmed Ilyas 2025-04-14 15:26:57 +02:00 committed by GitHub
parent 66df255a9c
commit e4047e5888
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 544 additions and 18 deletions

View File

@ -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<Maybe<PathBuf>>,
/// 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<Maybe<PathBuf>>,
/// 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<Maybe<PathBuf>>,
/// 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<Maybe<PathBuf>>,
#[command(flatten)]
pub installer: ResolverInstallerArgs,

View File

@ -19,6 +19,8 @@ pub struct Tool {
constraints: Vec<Requirement>,
/// The overrides requested by the user during installation.
overrides: Vec<Requirement>,
/// The build constraints requested by the user during installation.
build_constraints: Vec<Requirement>,
/// The Python requested by the user during installation.
python: Option<String>,
/// 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<RequirementWire>,
@ -35,6 +38,8 @@ struct ToolWire {
constraints: Vec<Requirement>,
#[serde(default)]
overrides: Vec<Requirement>,
#[serde(default)]
build_constraint_dependencies: Vec<Requirement>,
python: Option<String>,
entrypoints: Vec<ToolEntrypoint>,
#[serde(default)]
@ -61,6 +66,7 @@ impl From<Tool> 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<ToolWire> 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<Requirement>,
constraints: Vec<Requirement>,
overrides: Vec<Requirement>,
build_constraints: Vec<Requirement>,
python: Option<String>,
entrypoints: impl Iterator<Item = ToolEntrypoint>,
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::<Result<Vec<_>, _>>()?;
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<String> {
&self.python
}

View File

@ -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.

View File

@ -213,8 +213,7 @@ pub(crate) async fn pip_compile(
let build_constraints: Vec<NameRequirementSpecification> =
operations::read_constraints(build_constraints, &client_builder)
.await?
.iter()
.cloned()
.into_iter()
.chain(
build_constraints_from_workspace
.into_iter()

View File

@ -154,8 +154,7 @@ pub(crate) async fn pip_install(
let build_constraints: Vec<NameRequirementSpecification> =
operations::read_constraints(build_constraints, &client_builder)
.await?
.iter()
.cloned()
.into_iter()
.chain(
build_constraints_from_workspace
.iter()

View File

@ -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,

View File

@ -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();

View File

@ -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,

View File

@ -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(),

View File

@ -168,6 +168,7 @@ pub(crate) fn install_executables(
requirements: Vec<Requirement>,
constraints: Vec<Requirement>,
overrides: Vec<Requirement>,
build_constraints: Vec<Requirement>,
printer: Printer,
) -> anyhow::Result<ExitStatus> {
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()

View File

@ -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<String>,
install_mirrors: PythonInstallMirrors,
force: bool,
@ -290,6 +291,14 @@ pub(crate) async fn install(
)
.await?;
// Resolve the build constraints.
let build_constraints: Vec<Requirement> =
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,
)
}

View File

@ -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<String>,
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,

View File

@ -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,
)?;
}

View File

@ -1104,12 +1104,19 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();
let build_constraints = args
.build_constraints
.into_iter()
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
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<ExitStatus> {
.into_iter()
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();
let build_constraints = args
.build_constraints
.into_iter()
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
Box::pin(commands::tool_install(
args.package,
@ -1177,6 +1189,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
&requirements,
&constraints,
&overrides,
&build_constraints,
args.python,
args.install_mirrors,
args.force,

View File

@ -459,6 +459,7 @@ pub(crate) struct ToolRunSettings {
pub(crate) with_editable: Vec<String>,
pub(crate) constraints: Vec<PathBuf>,
pub(crate) overrides: Vec<PathBuf>,
pub(crate) build_constraints: Vec<PathBuf>,
pub(crate) isolated: bool,
pub(crate) show_resolution: bool,
pub(crate) python: Option<String>,
@ -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<String>,
pub(crate) constraints: Vec<PathBuf>,
pub(crate) overrides: Vec<PathBuf>,
pub(crate) build_constraints: Vec<PathBuf>,
pub(crate) python: Option<String>,
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,

View File

@ -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<()> {

View File

@ -2833,6 +2833,7 @@ fn resolve_tool() -> anyhow::Result<()> {
with_editable: [],
constraints: [],
overrides: [],
build_constraints: [],
python: None,
refresh: None(
Timestamp(

View File

@ -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::<Vec<_>>();
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::<Vec<_>>();
// 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"]);

View File

@ -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.

View File

@ -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 <https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#console-scripts>
/// Legacy Scripts <https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#scripts>.

View File

@ -3123,6 +3123,11 @@ uv tool run [OPTIONS] [COMMAND]
<p>WARNING: Hosts included in this list will not be verified against the system&#8217;s certificate store. Only use <code>--allow-insecure-host</code> in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.</p>
<p>May also be set with the <code>UV_INSECURE_HOST</code> environment variable.</p>
</dd><dt id="uv-tool-run--build-constraints"><a href="#uv-tool-run--build-constraints"><code>--build-constraints</code></a>, <code>--build-constraint</code>, <code>-b</code> <i>build-constraints</i></dt><dd><p>Constrain build dependencies using the given requirements files when building source distributions.</p>
<p>Constraints files are <code>requirements.txt</code>-like files that only control the <em>version</em> of a requirement that&#8217;s installed. However, including a package in a constraints file will <em>not</em> trigger the installation of that package.</p>
<p>May also be set with the <code>UV_BUILD_CONSTRAINT</code> environment variable.</p>
</dd><dt id="uv-tool-run--cache-dir"><a href="#uv-tool-run--cache-dir"><code>--cache-dir</code></a> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
<p>Defaults to <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on macOS and Linux, and <code>%LOCALAPPDATA%\uv\cache</code> on Windows.</p>
@ -3468,6 +3473,11 @@ uv tool install [OPTIONS] <PACKAGE>
<p>WARNING: Hosts included in this list will not be verified against the system&#8217;s certificate store. Only use <code>--allow-insecure-host</code> in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.</p>
<p>May also be set with the <code>UV_INSECURE_HOST</code> environment variable.</p>
</dd><dt id="uv-tool-install--build-constraints"><a href="#uv-tool-install--build-constraints"><code>--build-constraints</code></a>, <code>--build-constraint</code>, <code>-b</code> <i>build-constraints</i></dt><dd><p>Constrain build dependencies using the given requirements files when building source distributions.</p>
<p>Constraints files are <code>requirements.txt</code>-like files that only control the <em>version</em> of a requirement that&#8217;s installed. However, including a package in a constraints file will <em>not</em> trigger the installation of that package.</p>
<p>May also be set with the <code>UV_BUILD_CONSTRAINT</code> environment variable.</p>
</dd><dt id="uv-tool-install--cache-dir"><a href="#uv-tool-install--cache-dir"><code>--cache-dir</code></a> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
<p>Defaults to <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on macOS and Linux, and <code>%LOCALAPPDATA%\uv\cache</code> on Windows.</p>