From fa24d9a5e20434ea8c142f36d13942a9645b5cdf Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 31 Jul 2025 10:20:49 -0400 Subject: [PATCH] Include wheel hashes from local Simple indexes (#14993) ## Summary This just looks like an oversight. We weren't including hashes from local Simple API indexes if a package had both a wheel and a source distribution. Closes https://github.com/astral-sh/uv/issues/14883 --- crates/uv-resolver/src/lock/mod.rs | 61 +++++---------- crates/uv/tests/it/lock.rs | 74 +++++++++++------- .../basic_package-0.1.0-py3-none-any.whl | Bin 0 -> 1548 bytes scripts/links/basic_package-0.1.0.tar.gz | Bin 0 -> 616 bytes 4 files changed, 66 insertions(+), 69 deletions(-) create mode 100644 scripts/links/basic_package-0.1.0-py3-none-any.whl create mode 100644 scripts/links/basic_package-0.1.0.tar.gz diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index b68336fd7..84a44532d 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -4396,27 +4396,12 @@ impl Wheel { } fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result { - let filename = wheel.filename.clone(); - match &wheel.index { + let url = match &wheel.index { IndexUrl::Pypi(_) | IndexUrl::Url(_) => { let url = normalize_file_location(&wheel.file.url) .map_err(LockErrorKind::InvalidUrl) .map_err(LockError::from)?; - let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from); - let size = wheel.file.size; - let upload_time = wheel - .file - .upload_time_utc_ms - .map(Timestamp::from_millisecond) - .transpose() - .map_err(LockErrorKind::InvalidTimestamp)?; - Ok(Wheel { - url: WheelWireSource::Url { url }, - hash, - size, - filename, - upload_time, - }) + WheelWireSource::Url { url } } IndexUrl::Path(path) => { let index_path = path @@ -4432,35 +4417,31 @@ impl Wheel { .or_else(|_| std::path::absolute(&wheel_path)) .map_err(LockErrorKind::DistributionRelativePath)? .into_boxed_path(); - Ok(Wheel { - url: WheelWireSource::Path { path }, - hash: None, - size: None, - upload_time: None, - filename, - }) + WheelWireSource::Path { path } } else { let url = normalize_file_location(&wheel.file.url) .map_err(LockErrorKind::InvalidUrl) .map_err(LockError::from)?; - let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from); - let size = wheel.file.size; - let upload_time = wheel - .file - .upload_time_utc_ms - .map(Timestamp::from_millisecond) - .transpose() - .map_err(LockErrorKind::InvalidTimestamp)?; - Ok(Wheel { - url: WheelWireSource::Url { url }, - hash, - size, - filename, - upload_time, - }) + WheelWireSource::Url { url } } } - } + }; + let filename = wheel.filename.clone(); + let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from); + let size = wheel.file.size; + let upload_time = wheel + .file + .upload_time_utc_ms + .map(Timestamp::from_millisecond) + .transpose() + .map_err(LockErrorKind::InvalidTimestamp)?; + Ok(Wheel { + url, + hash, + size, + upload_time, + filename, + }) } fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Wheel { diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 5e9542fa6..084a1dc01 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -10434,23 +10434,31 @@ fn lock_find_links_lower_priority_index() -> Result<()> { /// Lock against a local directory laid out as a PEP 503-compatible index. #[test] fn lock_local_index() -> Result<()> { - let context = TestContext::new("3.12"); + let context = TestContext::new("3.13"); let root = context.temp_dir.child("simple-html"); fs_err::create_dir_all(&root)?; - let tqdm = root.child("tqdm"); - fs_err::create_dir_all(&tqdm)?; + let basic_package = root.child("basic-package"); + fs_err::create_dir_all(&basic_package)?; - let wheel = tqdm.child("tqdm-1000.0.0-py3-none-any.whl"); + let sdist = basic_package.child("basic_package-0.1.0.tar.gz"); fs_err::copy( context .workspace_root - .join("scripts/links/tqdm-1000.0.0-py3-none-any.whl"), + .join("scripts/links/basic_package-0.1.0.tar.gz"), + &sdist, + )?; + + let wheel = basic_package.child("basic_package-0.1.0-py3-none-any.whl"); + fs_err::copy( + context + .workspace_root + .join("scripts/links/basic_package-0.1.0-py3-none-any.whl"), &wheel, )?; - let index = tqdm.child("index.html"); + let index = basic_package.child("index.html"); index.write_str(&formatdoc! {r#" @@ -10458,26 +10466,33 @@ fn lock_local_index() -> Result<()> { -

Links for tqdm

+

Links for basic-package

- tqdm-1000.0.0-py3-none-any.whl + basic_package-0.1.0-py3-none-any.whl + + + basic_package-0.1.0.tar.gz - "#, Url::from_file_path(wheel).unwrap().as_str()})?; - - let context = TestContext::new("3.12"); + "#, + Url::from_file_path(wheel).unwrap().as_str(), + Url::from_file_path(sdist).unwrap().as_str(), + })?; let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(&formatdoc! { r#" [project] name = "project" version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["tqdm"] + requires-python = ">=3.13" + dependencies = ["basic-package"] [tool.uv] extra-index-url = ["{}"] @@ -10509,26 +10524,27 @@ fn lock_local_index() -> Result<()> { lock, @r#" version = 1 revision = 3 - requires-python = ">=3.12" + requires-python = ">=3.13" + + [[package]] + name = "basic-package" + version = "0.1.0" + source = { registry = "simple-html" } + sdist = { path = "basic-package/basic_package-0.1.0.tar.gz", hash = "sha256:af478ff91ec60856c99a540b8df13d756513bebb65bc301fb27e0d1f974532b4" } + wheels = [ + { path = "basic-package/basic_package-0.1.0-py3-none-any.whl", hash = "sha256:7b6229db79b5800e4e98a351b5628c1c8a944533a2d428aeeaa7275a30d4ea82" }, + ] [[package]] name = "project" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "tqdm" }, + { name = "basic-package" }, ] [package.metadata] - requires-dist = [{ name = "tqdm" }] - - [[package]] - name = "tqdm" - version = "1000.0.0" - source = { registry = "../../[TMP]/simple-html" } - wheels = [ - { path = "tqdm/tqdm-1000.0.0-py3-none-any.whl" }, - ] + requires-dist = [{ name = "basic-package" }] "# ); }); @@ -10544,7 +10560,7 @@ fn lock_local_index() -> Result<()> { "###); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + uv_snapshot!(context.filters(), context.sync().arg("--frozen").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r" success: true exit_code: 0 ----- stdout ----- @@ -10552,8 +10568,8 @@ fn lock_local_index() -> Result<()> { ----- stderr ----- Prepared 1 package in [TIME] Installed 1 package in [TIME] - + tqdm==1000.0.0 - "###); + + basic-package==0.1.0 + "); Ok(()) } diff --git a/scripts/links/basic_package-0.1.0-py3-none-any.whl b/scripts/links/basic_package-0.1.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..22d9ff8a776ca57bce7d3e8f3bee115487e8c3fa GIT binary patch literal 1548 zcmWIWW@Zs#fB;1(4WameR8nGbW^#N%Vsdt3da8Z^R4oTkMv=j>Ku*aT$O2(W47Ks` znR%Hd@$q^EmENAd!6!V1yhp3J&u!`0P?`@Dzm*^9j1x?1PXoZlQ| zaK-pRk^fn5T`!%J`kR6@bb}2pY`$RpjO)~S{fl39uJLN9ajZ zV};4Jv!~B{U-dn+ng#9|NT7dOo};@O=#bYytVYm5zOEsTE{-9Nv3&=-m<$D6-v8|S z*XVM0Cu_LK_f;KT-_0_8BqsOFl`Hud$Ck?4|MivQ)ixGeu2;f^y{gea40~@SDRHN- zx%iSVwp%>t|Dh{q&V|wWZ85Sx%qMkPN@%^!DX7_hqZfn_E5}f3)3jv)kX=w6D8T7V7-6C`oaA zp*Xqa&aa~LUbc*=F<#cXsz8d7f#Ez5s}S^VkgKzQkW0(ijeg3GJPa4+{;K_2+M>=86jY4|mS}zTszP_a@&a=k~vy@!N83W2b+% zU)@_Tsjj8GJSjgUxygI7%i=E8i7MF=EFDb`E@*{bZ8J%EG_TG3vi1LIbJo3nTGdr4 z&9s+i(JEVZ9)+!6mRtC;lw;S^k^1G;=F<7E1npLtC%sU0XsIzS%3V# z{`&sYMc?Ha82tzh)xEEC5<{5dXdTk zbYno;kh3(xn0Xk+z%n?(43KKf3=T8nt>YDRGq7ZLSdc)xg`VMIW-VzH0470{Ai*#T yoc$4w!=7Og1}-FMAT0Z$n~9z)5N1vQh9!PSLh?v}H!B-R7dsGg0rk9L2JrwAT-Oc& literal 0 HcmV?d00001 diff --git a/scripts/links/basic_package-0.1.0.tar.gz b/scripts/links/basic_package-0.1.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..be7582b4764c8f9ac72d8c208fdb09c10b6c4496 GIT binary patch literal 616 zcmV-u0+;_LS1@5t$mUiN^rri12ta?Y`HtbFJAJI!jInmBwbQQQxXyCS7&92NS=+UZN}zJd1%Pp+7@*`Km)!b-B9GFgY1|{%_)SFubL*;*6TOdK*P*sU&R1crvow*INb7Tv zp$8Iw$VSY?U6_7P2s!kpev%mPsQ<@~p^LiXx;yQwnqM1t+Kyvvuh;|Bao7JY{;x0k z=YtCxhZUXw+x*)$Ycit}sG9#x{<9(z=~oOSm1#WPKmRUkZt~x3wYTT*1?sr@AI_;B zk%Y$xT`<>F-z4`l+1H*jJ52JY&Ue9lQ2$LLkcYFEE{_zG@KXPC=)#Zy0Q`hSomJvsU496;(hE`tW664ik?KRcC zUwV68Mqg6rvspl}NyR zr+siE(s=!Mz_Wi{_-gz`{bvQ0MTTKz_rI3gc8p4(j=TPA)Tm(`Eq()4(AIVU8UO$Z CBs+fq literal 0 HcmV?d00001