mirror of https://github.com/astral-sh/uv
Accept `--build-constraints` in `uv build` (#7085)
## Summary Closes #7082. Closes #7065.
This commit is contained in:
parent
b36b7badff
commit
80f51cee06
|
|
@ -1979,6 +1979,15 @@ pub struct BuildArgs {
|
|||
#[arg(long)]
|
||||
pub wheel: bool,
|
||||
|
||||
/// Constrain build dependencies using the given requirements files when building
|
||||
/// distributions.
|
||||
///
|
||||
/// Constraints files are `requirements.txt`-like files that only control the _version_ of a
|
||||
/// build dependency that's installed. However, including a package in a constraints file will
|
||||
/// _not_ trigger the inclusion of that package on its own.
|
||||
#[arg(long, short, env = "UV_BUILD_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)]
|
||||
pub build_constraint: Vec<Maybe<PathBuf>>,
|
||||
|
||||
/// The Python interpreter to use for the build environment.
|
||||
///
|
||||
/// By default, builds are executed in isolated virtual environments. The
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use crate::printer::Printer;
|
|||
use crate::settings::{ResolverSettings, ResolverSettingsRef};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::commands::pip::operations;
|
||||
use anyhow::Result;
|
||||
use distribution_filename::SourceDistExtension;
|
||||
use owo_colors::OwoColorize;
|
||||
|
|
@ -12,7 +13,7 @@ use std::path::{Path, PathBuf};
|
|||
use uv_auth::store_credentials_from_url;
|
||||
use uv_cache::Cache;
|
||||
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||
use uv_configuration::{BuildKind, BuildOutput, Concurrency, Constraints};
|
||||
use uv_configuration::{BuildKind, BuildOutput, Concurrency, Constraints, HashCheckingMode};
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_fs::{Simplified, CWD};
|
||||
use uv_normalize::PackageName;
|
||||
|
|
@ -20,6 +21,7 @@ use uv_python::{
|
|||
EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation,
|
||||
PythonPreference, PythonRequest, PythonVersionFile, VersionRequest,
|
||||
};
|
||||
use uv_requirements::RequirementsSource;
|
||||
use uv_resolver::{FlatIndex, RequiresPython};
|
||||
use uv_types::{BuildContext, BuildIsolation, HashStrategy};
|
||||
use uv_workspace::{DiscoveryOptions, Workspace};
|
||||
|
|
@ -32,6 +34,7 @@ pub(crate) async fn build(
|
|||
output_dir: Option<PathBuf>,
|
||||
sdist: bool,
|
||||
wheel: bool,
|
||||
build_constraints: Vec<RequirementsSource>,
|
||||
python: Option<String>,
|
||||
settings: ResolverSettings,
|
||||
no_config: bool,
|
||||
|
|
@ -49,6 +52,7 @@ pub(crate) async fn build(
|
|||
output_dir.as_deref(),
|
||||
sdist,
|
||||
wheel,
|
||||
&build_constraints,
|
||||
python.as_deref(),
|
||||
settings.as_ref(),
|
||||
no_config,
|
||||
|
|
@ -88,6 +92,7 @@ async fn build_impl(
|
|||
output_dir: Option<&Path>,
|
||||
sdist: bool,
|
||||
wheel: bool,
|
||||
build_constraints: &[RequirementsSource],
|
||||
python_request: Option<&str>,
|
||||
settings: ResolverSettingsRef<'_>,
|
||||
no_config: bool,
|
||||
|
|
@ -225,6 +230,27 @@ async fn build_impl(
|
|||
store_credentials_from_url(url);
|
||||
}
|
||||
|
||||
// Read build constraints.
|
||||
let build_constraints =
|
||||
operations::read_constraints(build_constraints, &client_builder).await?;
|
||||
|
||||
// Collect the set of required hashes.
|
||||
// Enforce (but never require) the build constraints, if `--require-hashes` or `--verify-hashes`
|
||||
// is provided. _Requiring_ hashes would be too strict, and would break with pip.
|
||||
let build_hasher = HashStrategy::from_requirements(
|
||||
std::iter::empty(),
|
||||
build_constraints
|
||||
.iter()
|
||||
.map(|entry| (&entry.requirement, entry.hashes.as_slice())),
|
||||
Some(&interpreter.resolver_markers()),
|
||||
HashCheckingMode::Verify,
|
||||
)?;
|
||||
let build_constraints = Constraints::from_requirements(
|
||||
build_constraints
|
||||
.iter()
|
||||
.map(|constraint| constraint.requirement.clone()),
|
||||
);
|
||||
|
||||
// Initialize the registry client.
|
||||
let client = RegistryClientBuilder::new(cache.clone())
|
||||
.native_tls(native_tls)
|
||||
|
|
@ -249,17 +275,11 @@ async fn build_impl(
|
|||
BuildIsolation::SharedPackage(&environment, no_build_isolation_package)
|
||||
};
|
||||
|
||||
// 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 hasher = HashStrategy::None;
|
||||
|
||||
// Resolve the flat indexes from `--find-links`.
|
||||
let flat_index = {
|
||||
let client = FlatIndexClient::new(&client, cache);
|
||||
let entries = client.fetch(index_locations.flat_index()).await?;
|
||||
FlatIndex::from_entries(entries, None, &hasher, build_options)
|
||||
FlatIndex::from_entries(entries, None, &build_hasher, build_options)
|
||||
};
|
||||
|
||||
// Initialize any shared state.
|
||||
|
|
|
|||
|
|
@ -321,12 +321,12 @@ pub(crate) async fn pip_compile(
|
|||
};
|
||||
|
||||
// Don't enforce hashes in `pip compile`.
|
||||
let build_hashes = HashStrategy::None;
|
||||
let build_constraints = Constraints::from_requirements(
|
||||
build_constraints
|
||||
.iter()
|
||||
.map(|constraint| constraint.requirement.clone()),
|
||||
);
|
||||
let build_hashes = HashStrategy::None;
|
||||
|
||||
let build_dispatch = BuildDispatch::new(
|
||||
&client,
|
||||
|
|
|
|||
|
|
@ -670,12 +670,20 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
|||
.combine(Refresh::from(args.settings.upgrade.clone())),
|
||||
);
|
||||
|
||||
// Resolve the build constraints.
|
||||
let build_constraints = args
|
||||
.build_constraint
|
||||
.into_iter()
|
||||
.map(RequirementsSource::from_constraints_txt)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
commands::build(
|
||||
args.src,
|
||||
args.package,
|
||||
args.out_dir,
|
||||
args.sdist,
|
||||
args.wheel,
|
||||
build_constraints,
|
||||
args.python,
|
||||
args.settings,
|
||||
cli.no_config,
|
||||
|
|
|
|||
|
|
@ -1629,6 +1629,7 @@ pub(crate) struct BuildSettings {
|
|||
pub(crate) out_dir: Option<PathBuf>,
|
||||
pub(crate) sdist: bool,
|
||||
pub(crate) wheel: bool,
|
||||
pub(crate) build_constraint: Vec<PathBuf>,
|
||||
pub(crate) python: Option<String>,
|
||||
pub(crate) refresh: Refresh,
|
||||
pub(crate) settings: ResolverSettings,
|
||||
|
|
@ -1643,6 +1644,7 @@ impl BuildSettings {
|
|||
package,
|
||||
sdist,
|
||||
wheel,
|
||||
build_constraint,
|
||||
python,
|
||||
build,
|
||||
refresh,
|
||||
|
|
@ -1655,6 +1657,10 @@ impl BuildSettings {
|
|||
out_dir,
|
||||
sdist,
|
||||
wheel,
|
||||
build_constraint: build_constraint
|
||||
.into_iter()
|
||||
.filter_map(Maybe::into_option)
|
||||
.collect(),
|
||||
python,
|
||||
refresh: Refresh::from(refresh),
|
||||
settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem),
|
||||
|
|
|
|||
|
|
@ -1146,3 +1146,225 @@ fn workspace() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_constraints() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let filters = context
|
||||
.filters()
|
||||
.into_iter()
|
||||
.chain([
|
||||
(r"exit code: 1", "exit status: 1"),
|
||||
(r"bdist\.[^/\\\s]+-[^/\\\s]+", "bdist.linux-x86_64"),
|
||||
(r"\\\.", ""),
|
||||
])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let project = context.temp_dir.child("project");
|
||||
|
||||
let constraints = project.child("constraints.txt");
|
||||
constraints.write_str("setuptools==0.1.0")?;
|
||||
|
||||
let pyproject_toml = project.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["anyio==3.7.0"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
project.child("src").child("__init__.py").touch()?;
|
||||
project.child("README").touch()?;
|
||||
|
||||
uv_snapshot!(&filters, context.build().arg("--build-constraint").arg("constraints.txt").current_dir(&project), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Building source distribution...
|
||||
error: Failed to install requirements from `build-system.requires` (resolve)
|
||||
Caused by: No solution found when resolving: setuptools>=42
|
||||
Caused by: Because you require setuptools>=42 and setuptools==0.1.0, we can conclude that your requirements are unsatisfiable.
|
||||
"###);
|
||||
|
||||
project
|
||||
.child("dist")
|
||||
.child("project-0.1.0.tar.gz")
|
||||
.assert(predicate::path::missing());
|
||||
project
|
||||
.child("dist")
|
||||
.child("project-0.1.0-py3-none-any.whl")
|
||||
.assert(predicate::path::missing());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sha() -> Result<()> {
|
||||
let context = TestContext::new("3.8");
|
||||
let filters = context
|
||||
.filters()
|
||||
.into_iter()
|
||||
.chain([
|
||||
(r"exit code: 1", "exit status: 1"),
|
||||
(r"bdist\.[^/\\\s]+-[^/\\\s]+", "bdist.linux-x86_64"),
|
||||
(r"\\\.", ""),
|
||||
])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let project = context.temp_dir.child("project");
|
||||
|
||||
let pyproject_toml = project.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = ["anyio==3.7.0"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
project.child("src").child("__init__.py").touch()?;
|
||||
project.child("README").touch()?;
|
||||
|
||||
// Reject an incorrect hash.
|
||||
let constraints = project.child("constraints.txt");
|
||||
constraints.write_str("setuptools==68.2.2 --hash=sha256:a248cb506794bececcddeddb1678bc722f9cfcacf02f98f7c0af6b9ed893caf2")?;
|
||||
|
||||
uv_snapshot!(&filters, context.build().arg("--build-constraint").arg("constraints.txt").current_dir(&project), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Building source distribution...
|
||||
error: Failed to install requirements from `build-system.requires` (install)
|
||||
Caused by: Failed to prepare distributions
|
||||
Caused by: Failed to fetch wheel: setuptools==68.2.2
|
||||
Caused by: Hash mismatch for `setuptools==68.2.2`
|
||||
|
||||
Expected:
|
||||
sha256:a248cb506794bececcddeddb1678bc722f9cfcacf02f98f7c0af6b9ed893caf2
|
||||
|
||||
Computed:
|
||||
sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a
|
||||
"###);
|
||||
|
||||
project
|
||||
.child("dist")
|
||||
.child("project-0.1.0.tar.gz")
|
||||
.assert(predicate::path::missing());
|
||||
project
|
||||
.child("dist")
|
||||
.child("project-0.1.0-py3-none-any.whl")
|
||||
.assert(predicate::path::missing());
|
||||
|
||||
// Accept a correct hash.
|
||||
let constraints = project.child("constraints.txt");
|
||||
constraints.write_str("setuptools==68.2.2 --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a")?;
|
||||
|
||||
uv_snapshot!(&filters, context.build().arg("--build-constraint").arg("constraints.txt").current_dir(&project), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Building source distribution...
|
||||
running egg_info
|
||||
creating src/project.egg-info
|
||||
writing src/project.egg-info/PKG-INFO
|
||||
writing dependency_links to src/project.egg-info/dependency_links.txt
|
||||
writing requirements to src/project.egg-info/requires.txt
|
||||
writing top-level names to src/project.egg-info/top_level.txt
|
||||
writing manifest file 'src/project.egg-info/SOURCES.txt'
|
||||
reading manifest file 'src/project.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/project.egg-info/SOURCES.txt'
|
||||
running sdist
|
||||
running egg_info
|
||||
writing src/project.egg-info/PKG-INFO
|
||||
writing dependency_links to src/project.egg-info/dependency_links.txt
|
||||
writing requirements to src/project.egg-info/requires.txt
|
||||
writing top-level names to src/project.egg-info/top_level.txt
|
||||
reading manifest file 'src/project.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/project.egg-info/SOURCES.txt'
|
||||
running check
|
||||
creating project-0.1.0
|
||||
creating project-0.1.0/src
|
||||
creating project-0.1.0/src/project.egg-info
|
||||
copying files to project-0.1.0...
|
||||
copying README -> project-0.1.0
|
||||
copying pyproject.toml -> project-0.1.0
|
||||
copying src/__init__.py -> project-0.1.0/src
|
||||
copying src/project.egg-info/PKG-INFO -> project-0.1.0/src/project.egg-info
|
||||
copying src/project.egg-info/SOURCES.txt -> project-0.1.0/src/project.egg-info
|
||||
copying src/project.egg-info/dependency_links.txt -> project-0.1.0/src/project.egg-info
|
||||
copying src/project.egg-info/requires.txt -> project-0.1.0/src/project.egg-info
|
||||
copying src/project.egg-info/top_level.txt -> project-0.1.0/src/project.egg-info
|
||||
Writing project-0.1.0/setup.cfg
|
||||
Creating tar archive
|
||||
removing 'project-0.1.0' (and everything under it)
|
||||
Building wheel from source distribution...
|
||||
running egg_info
|
||||
writing src/project.egg-info/PKG-INFO
|
||||
writing dependency_links to src/project.egg-info/dependency_links.txt
|
||||
writing requirements to src/project.egg-info/requires.txt
|
||||
writing top-level names to src/project.egg-info/top_level.txt
|
||||
reading manifest file 'src/project.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/project.egg-info/SOURCES.txt'
|
||||
running bdist_wheel
|
||||
running build
|
||||
running build_py
|
||||
creating build
|
||||
creating build/lib
|
||||
copying src/__init__.py -> build/lib
|
||||
running egg_info
|
||||
writing src/project.egg-info/PKG-INFO
|
||||
writing dependency_links to src/project.egg-info/dependency_links.txt
|
||||
writing requirements to src/project.egg-info/requires.txt
|
||||
writing top-level names to src/project.egg-info/top_level.txt
|
||||
reading manifest file 'src/project.egg-info/SOURCES.txt'
|
||||
writing manifest file 'src/project.egg-info/SOURCES.txt'
|
||||
installing to build/bdist.linux-x86_64/wheel
|
||||
running install
|
||||
running install_lib
|
||||
creating build/bdist.linux-x86_64
|
||||
creating build/bdist.linux-x86_64/wheel
|
||||
copying build/lib/__init__.py -> build/bdist.linux-x86_64/wheel
|
||||
running install_egg_info
|
||||
Copying src/project.egg-info to build/bdist.linux-x86_64/wheel/project-0.1.0-py3.8.egg-info
|
||||
running install_scripts
|
||||
creating build/bdist.linux-x86_64/wheel/project-0.1.0.dist-info/WHEEL
|
||||
creating '[TEMP_DIR]/project/dist/[TMP]/wheel' to it
|
||||
adding '__init__.py'
|
||||
adding 'project-0.1.0.dist-info/METADATA'
|
||||
adding 'project-0.1.0.dist-info/WHEEL'
|
||||
adding 'project-0.1.0.dist-info/top_level.txt'
|
||||
adding 'project-0.1.0.dist-info/RECORD'
|
||||
removing build/bdist.linux-x86_64/wheel
|
||||
Successfully built dist/project-0.1.0.tar.gz and dist/project-0.1.0-py3-none-any.whl
|
||||
"###);
|
||||
|
||||
project
|
||||
.child("dist")
|
||||
.child("project-0.1.0.tar.gz")
|
||||
.assert(predicate::path::is_file());
|
||||
project
|
||||
.child("dist")
|
||||
.child("project-0.1.0-py3-none-any.whl")
|
||||
.assert(predicate::path::is_file());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -585,6 +585,23 @@ You can limit `uv build` to building a source distribution with `uv build --sour
|
|||
distribution with `uv build --binary`, or build both distributions from source with
|
||||
`uv build --source --binary`.
|
||||
|
||||
`uv build` accepts `--build-constraints`, which can be used to constrain the versions of any build
|
||||
requirements during the build process. When coupled with `--require-hashes`, uv will enforce that
|
||||
the requirement used to build the project match specific, known hashes, for reproducibility.
|
||||
|
||||
For example, given the following `constraints.txt`:
|
||||
|
||||
```text
|
||||
setuptools==68.2.2 --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a
|
||||
```
|
||||
|
||||
Running the following would build the project with the specified version of `setuptools`, and verify
|
||||
that the downloaded `setuptools` distribution matches the specified hash:
|
||||
|
||||
```console
|
||||
$ uv build --build-constraints constraints.txt --require-hashes
|
||||
```
|
||||
|
||||
## Build isolation
|
||||
|
||||
By default, uv builds all packages in isolated virtual environments, as per
|
||||
|
|
|
|||
|
|
@ -6225,6 +6225,11 @@ uv build [OPTIONS] [SRC]
|
|||
<p>WARNING: Hosts included in this list will not be verified against the system’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><code>--build-constraint</code>, <code>-b</code> <i>build-constraint</i></dt><dd><p>Constrain build dependencies using the given requirements files when building distributions.</p>
|
||||
|
||||
<p>Constraints files are <code>requirements.txt</code>-like files that only control the <em>version</em> of a build dependency that’s installed. However, including a package in a constraints file will <em>not</em> trigger the inclusion of that package on its own.</p>
|
||||
|
||||
<p>May also be set with the <code>UV_BUILD_CONSTRAINT</code> environment variable.</p>
|
||||
</dd><dt><code>--cache-dir</code> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
|
||||
|
||||
<p>Defaults to <code>$HOME/Library/Caches/uv</code> on macOS, <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on Linux, and <code>%LOCALAPPDATA%\uv\cache</code> on Windows.</p>
|
||||
|
|
|
|||
Loading…
Reference in New Issue