Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept --build-constraints in uv build #7085

Merged
merged 2 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So... this will validate any hashes that are provided in the constraints file. But it doesn't require hashes. Should we add a separate flag to require them?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love that.

❤️ ❤️ ❤️

)?;
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
Loading