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

Add --require-hashes and --verify-hashes to uv build #7094

Merged
merged 1 commit 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
40 changes: 40 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Maybe<PathBuf>>,

/// 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
Expand Down
30 changes: 18 additions & 12 deletions crates/uv/src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub(crate) async fn build(
sdist: bool,
wheel: bool,
build_constraints: Vec<RequirementsSource>,
hash_checking: Option<HashCheckingMode>,
python: Option<String>,
settings: ResolverSettings,
no_config: bool,
Expand All @@ -53,6 +54,7 @@ pub(crate) async fn build(
sdist,
wheel,
&build_constraints,
hash_checking,
python.as_deref(),
settings.as_ref(),
no_config,
Expand Down Expand Up @@ -93,6 +95,7 @@ async fn build_impl(
sdist: bool,
wheel: bool,
build_constraints: &[RequirementsSource],
hash_checking: Option<HashCheckingMode>,
python_request: Option<&str>,
settings: ResolverSettingsRef<'_>,
no_config: bool,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand All @@ -301,7 +307,7 @@ async fn build_impl(
build_isolation,
link_mode,
build_options,
&build_hasher,
&hasher,
exclude_newer,
sources,
concurrency,
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
args.sdist,
args.wheel,
build_constraints,
args.hash_checking,
args.python,
args.settings,
cli.no_config,
Expand Down
9 changes: 9 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,7 @@ pub(crate) struct BuildSettings {
pub(crate) sdist: bool,
pub(crate) wheel: bool,
pub(crate) build_constraint: Vec<PathBuf>,
pub(crate) hash_checking: Option<HashCheckingMode>,
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
pub(crate) settings: ResolverSettings,
Expand All @@ -1645,6 +1646,10 @@ impl BuildSettings {
sdist,
wheel,
build_constraint,
require_hashes,
no_require_hashes,
verify_hashes,
no_verify_hashes,
python,
build,
refresh,
Expand All @@ -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),
Expand Down
124 changes: 121 additions & 3 deletions crates/uv/tests/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----
Expand All @@ -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")
Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -6434,6 +6434,17 @@ uv build [OPTIONS] [SRC]

</dd><dt><code>--refresh-package</code> <i>refresh-package</i></dt><dd><p>Refresh cached data for a specific package</p>

</dd><dt><code>--require-hashes</code></dt><dd><p>Require a matching hash for each build requirement.</p>

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

<p>Hash-checking mode introduces a number of additional constraints:</p>

<ul>
<li>Git dependencies are not supported. - Editable installs are not supported. - Local dependencies are not supported, unless they point to a specific wheel (<code>.whl</code>) or source archive (<code>.zip</code>, <code>.tar.gz</code>), as opposed to a directory.</li>
</ul>

<p>May also be set with the <code>UV_REQUIRE_HASHES</code> environment variable.</p>
</dd><dt><code>--resolution</code> <i>resolution</i></dt><dd><p>The strategy to use when selecting between the different compatible versions for a given package requirement.</p>

<p>By default, uv will use the latest compatible version of each package (<code>highest</code>).</p>
Expand All @@ -6458,6 +6469,11 @@ uv build [OPTIONS] [SRC]

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

</dd><dt><code>--verify-hashes</code></dt><dd><p>Validate any hashes provided in the build constraints file.</p>

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

<p>May also be set with the <code>UV_VERIFY_HASHES</code> environment variable.</p>
</dd><dt><code>--version</code>, <code>-V</code></dt><dd><p>Display the uv version</p>

</dd><dt><code>--wheel</code></dt><dd><p>Build a binary distribution (&quot;wheel&quot;) from the given directory</p>
Expand Down
Loading