Skip to content

Commit

Permalink
Accept --build-constraints in uv build (#7085)
Browse files Browse the repository at this point in the history
## Summary

Closes #7082.

Closes #7065.
  • Loading branch information
charliermarsh committed Sep 5, 2024
1 parent b36b7ba commit 80f51ce
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 9 deletions.
9 changes: 9 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 28 additions & 8 deletions crates/uv/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@ 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;
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;
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};
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/pip/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -1643,6 +1644,7 @@ impl BuildSettings {
package,
sdist,
wheel,
build_constraint,
python,
build,
refresh,
Expand All @@ -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),
Expand Down
222 changes: 222 additions & 0 deletions crates/uv/tests/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Loading

0 comments on commit 80f51ce

Please sign in to comment.