From 3878c00dbd3f64e3b16820347c005a5745d9fed3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 28 Jan 2025 20:27:09 -0500 Subject: [PATCH] Mark metadata as dynamic when reading from built wheel cache (#11046) ## Summary The issue here boils down to: when we write metadata that came from building the wheel itself, we aren't setting the `dynamic` field. We now _always_ set the dynamic field when reading, even when we read cached data. Closes https://github.com/astral-sh/uv/issues/11047. --- crates/uv-distribution/src/source/mod.rs | 39 +++++--- crates/uv/tests/it/lock.rs | 119 +++++++++++++++++++++++ 2 files changed, 143 insertions(+), 15 deletions(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 9921855eb..7b5190d94 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1187,10 +1187,19 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await? .filter(|metadata| metadata.matches(source.name(), source.version())) { - debug!("Using cached metadata for: {source}"); + // If necessary, mark the metadata as dynamic. + let metadata = if dynamic { + ResolutionMetadata { + dynamic: true, + ..metadata.into() + } + } else { + metadata.into() + }; + return Ok(ArchiveMetadata::from( Metadata::from_workspace( - metadata.into(), + metadata, resource.install_path.as_ref(), None, self.build_context.locations(), @@ -1212,6 +1221,14 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .boxed_local() .await? { + // Store the metadata. + fs::create_dir_all(metadata_entry.dir()) + .await + .map_err(Error::CacheWrite)?; + write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) + .await + .map_err(Error::CacheWrite)?; + // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { @@ -1222,14 +1239,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { metadata }; - // Store the metadata. - fs::create_dir_all(metadata_entry.dir()) - .await - .map_err(Error::CacheWrite)?; - write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) - .await - .map_err(Error::CacheWrite)?; - return Ok(ArchiveMetadata::from( Metadata::from_workspace( metadata, @@ -1273,6 +1282,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } } + // Store the metadata. + write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) + .await + .map_err(Error::CacheWrite)?; + // If necessary, mark the metadata as dynamic. let metadata = if dynamic { ResolutionMetadata { @@ -1283,11 +1297,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { metadata }; - // Store the metadata. - write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) - .await - .map_err(Error::CacheWrite)?; - Ok(ArchiveMetadata::from( Metadata::from_workspace( metadata, diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index c6106bf7d..97cb0ea3e 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -20363,6 +20363,125 @@ fn lock_dynamic_version_self_extra_setuptools() -> Result<()> { Ok(()) } +/// See: +#[test] +fn lock_dynamic_built_cache() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + requires-python = ">=3.12" + dynamic = ["version"] + dependencies = [] + + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + + [tool.uv] + cache-keys = [{ file = "pyproject.toml" }, { file = "src/__about__.py" }] + + [tool.hatch.version] + path = "src/__about__.py" + scheme = "standard" + "#, + )?; + + context + .temp_dir + .child("src") + .child("__about__.py") + .write_str("__version__ = '0.1.0'")?; + + context + .temp_dir + .child("src") + .child("project") + .child("__init__.py") + .touch()?; + + // Lock the project, which should omit the dynamic version. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "project" + source = { editable = "." } + "### + ); + }); + + // Install the project, to force a build. + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + // Remove the lockfile. + fs_err::remove_file(context.temp_dir.join("uv.lock"))?; + + // Lock the project, which should omit the dynamic version. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "project" + source = { editable = "." } + "### + ); + }); + + Ok(()) +} + /// Re-lock after converting a package from dynamic to static. #[test] fn lock_dynamic_to_static() -> Result<()> {