From 0919247c6d0e37918cad3840e511094389014344 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 5 Sep 2024 14:36:30 -0400 Subject: [PATCH] Add --require-hashes and --verify-hashes to uv build --- crates/uv-cli/src/lib.rs | 40 +++++++++++ crates/uv/src/commands/build.rs | 30 ++++---- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 9 +++ crates/uv/tests/build.rs | 124 +++++++++++++++++++++++++++++++- docs/reference/cli.md | 16 +++++ 6 files changed, 205 insertions(+), 15 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index fe91416972ea..b617b648b598 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1988,6 +1988,46 @@ pub struct BuildArgs { #[arg(long, short, env = "UV_BUILD_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub build_constraint: Vec>, + /// Require a matching hash for each build requirement. + /// + /// Hash-checking mode is all or nothing. If enabled, _all_ build requirements must be provided + /// with a corresponding hash or set of hashes via the `--build-constraints` argument. + /// Additionally, if enabled, _all_ requirements must either be pinned to exact versions + /// (e.g., `==1.0.0`), or be specified via direct URL. + /// + /// Hash-checking mode introduces a number of additional constraints: + /// + /// - Git dependencies are not supported. + /// - Editable installs are not supported. + /// - Local dependencies are not supported, unless they point to a specific wheel (`.whl`) or + /// source archive (`.zip`, `.tar.gz`), as opposed to a directory. + #[arg( + long, + env = "UV_REQUIRE_HASHES", + value_parser = clap::builder::BoolishValueParser::new(), + overrides_with("no_require_hashes"), + )] + pub require_hashes: bool, + + #[arg(long, overrides_with("require_hashes"), hide = true)] + pub no_require_hashes: bool, + + /// Validate any hashes provided in the build constraints file. + /// + /// Unlike `--require-hashes`, `--verify-hashes` does not require that all requirements have + /// hashes; instead, it will limit itself to verifying the hashes of those requirements that do + /// include them. + #[arg( + long, + env = "UV_VERIFY_HASHES", + value_parser = clap::builder::BoolishValueParser::new(), + overrides_with("no_verify_hashes"), + )] + pub verify_hashes: bool, + + #[arg(long, overrides_with("verify_hashes"), hide = true)] + pub no_verify_hashes: bool, + /// The Python interpreter to use for the build environment. /// /// By default, builds are executed in isolated virtual environments. The diff --git a/crates/uv/src/commands/build.rs b/crates/uv/src/commands/build.rs index bf080531b697..d76dcc654674 100644 --- a/crates/uv/src/commands/build.rs +++ b/crates/uv/src/commands/build.rs @@ -35,6 +35,7 @@ pub(crate) async fn build( sdist: bool, wheel: bool, build_constraints: Vec, + hash_checking: Option, python: Option, settings: ResolverSettings, no_config: bool, @@ -53,6 +54,7 @@ pub(crate) async fn build( sdist, wheel, &build_constraints, + hash_checking, python.as_deref(), settings.as_ref(), no_config, @@ -93,6 +95,7 @@ async fn build_impl( sdist: bool, wheel: bool, build_constraints: &[RequirementsSource], + hash_checking: Option, python_request: Option<&str>, settings: ResolverSettingsRef<'_>, no_config: bool, @@ -235,16 +238,19 @@ async fn build_impl( 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 hasher = if let Some(hash_checking) = hash_checking { + HashStrategy::from_requirements( + std::iter::empty(), + build_constraints + .iter() + .map(|entry| (&entry.requirement, entry.hashes.as_slice())), + Some(&interpreter.resolver_markers()), + hash_checking, + )? + } else { + HashStrategy::None + }; + let build_constraints = Constraints::from_requirements( build_constraints .iter() @@ -279,7 +285,7 @@ async fn build_impl( let flat_index = { let client = FlatIndexClient::new(&client, cache); let entries = client.fetch(index_locations.flat_index()).await?; - FlatIndex::from_entries(entries, None, &build_hasher, build_options) + FlatIndex::from_entries(entries, None, &hasher, build_options) }; // Initialize any shared state. @@ -301,7 +307,7 @@ async fn build_impl( build_isolation, link_mode, build_options, - &build_hasher, + &hasher, exclude_newer, sources, concurrency, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e10abee99561..f237b60d03e9 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -684,6 +684,7 @@ async fn run(cli: Cli) -> Result { args.sdist, args.wheel, build_constraints, + args.hash_checking, args.python, args.settings, cli.no_config, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 4063d91f005b..1e14742c9363 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1630,6 +1630,7 @@ pub(crate) struct BuildSettings { pub(crate) sdist: bool, pub(crate) wheel: bool, pub(crate) build_constraint: Vec, + pub(crate) hash_checking: Option, pub(crate) python: Option, pub(crate) refresh: Refresh, pub(crate) settings: ResolverSettings, @@ -1645,6 +1646,10 @@ impl BuildSettings { sdist, wheel, build_constraint, + require_hashes, + no_require_hashes, + verify_hashes, + no_verify_hashes, python, build, refresh, @@ -1661,6 +1666,10 @@ impl BuildSettings { .into_iter() .filter_map(Maybe::into_option) .collect(), + hash_checking: HashCheckingMode::from_args( + flag(require_hashes, no_require_hashes).unwrap_or_default(), + flag(verify_hashes, no_verify_hashes).unwrap_or_default(), + ), python, refresh: Refresh::from(refresh), settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem), diff --git a/crates/uv/tests/build.rs b/crates/uv/tests/build.rs index 1eaa5d7a3d90..bc12234bedf0 100644 --- a/crates/uv/tests/build.rs +++ b/crates/uv/tests/build.rs @@ -1240,11 +1240,104 @@ fn sha() -> Result<()> { project.child("src").child("__init__.py").touch()?; project.child("README").touch()?; - // Reject an incorrect hash. + // Ignore an incorrect hash, if `--require-hashes` is not provided. 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: 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()); + + fs_err::remove_dir_all(project.child("dist"))?; + + // Reject an incorrect hash. + uv_snapshot!(&filters, context.build().arg("--build-constraint").arg("constraints.txt").arg("--require-hashes").current_dir(&project), @r###" success: false exit_code: 2 ----- stdout ----- @@ -1263,6 +1356,33 @@ fn sha() -> Result<()> { 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()); + + fs_err::remove_dir_all(project.child("dist"))?; + + // Reject a missing hash. + let constraints = project.child("constraints.txt"); + constraints.write_str("setuptools==68.2.2")?; + + uv_snapshot!(&filters, context.build().arg("--build-constraint").arg("constraints.txt").arg("--require-hashes").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: In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: `setuptools` + "###); + project .child("dist") .child("project-0.1.0.tar.gz") @@ -1284,12 +1404,10 @@ fn sha() -> Result<()> { ----- 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 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 5e6839e9123d..ee9debfc70f4 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -6434,6 +6434,17 @@ uv build [OPTIONS] [SRC]
--refresh-package refresh-package

Refresh cached data for a specific package

+
--require-hashes

Require a matching hash for each build requirement.

+ +

Hash-checking mode is all or nothing. If enabled, all build requirements must be provided with a corresponding hash or set of hashes via the --build-constraints argument. Additionally, if enabled, all requirements must either be pinned to exact versions (e.g., ==1.0.0), or be specified via direct URL.

+ +

Hash-checking mode introduces a number of additional constraints:

+ +
    +
  • Git dependencies are not supported. - Editable installs are not supported. - Local dependencies are not supported, unless they point to a specific wheel (.whl) or source archive (.zip, .tar.gz), as opposed to a directory.
  • +
+ +

May also be set with the UV_REQUIRE_HASHES environment variable.

--resolution resolution

The strategy to use when selecting between the different compatible versions for a given package requirement.

By default, uv will use the latest compatible version of each package (highest).

@@ -6458,6 +6469,11 @@ uv build [OPTIONS] [SRC]

You can configure fine-grained logging using the RUST_LOG environment variable. (<https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>)

+
--verify-hashes

Validate any hashes provided in the build constraints file.

+ +

Unlike --require-hashes, --verify-hashes does not require that all requirements have hashes; instead, it will limit itself to verifying the hashes of those requirements that do include them.

+ +

May also be set with the UV_VERIFY_HASHES environment variable.

--version, -V

Display the uv version

--wheel

Build a binary distribution ("wheel") from the given directory