diff --git a/Cargo.lock b/Cargo.lock index 380328072..eff7190b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5626,6 +5626,8 @@ dependencies = [ "tracing-subscriber", "uv-build-backend", "uv-logging", + "uv-preview", + "uv-static", "uv-version", ] @@ -5639,6 +5641,7 @@ dependencies = [ "flate2", "fs-err", "globset", + "indexmap", "indoc", "insta", "itertools 0.14.0", @@ -5646,6 +5649,7 @@ dependencies = [ "rustc-hash", "schemars", "serde", + "serde_json", "sha2", "spdx 0.13.2", "tar", @@ -5662,6 +5666,7 @@ dependencies = [ "uv-pep440", "uv-pep508", "uv-platform-tags", + "uv-preview", "uv-pypi-types", "uv-version", "uv-warnings", @@ -6838,6 +6843,7 @@ dependencies = [ name = "uv-static" version = "0.0.8" dependencies = [ + "thiserror 2.0.17", "uv-macros", ] diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index 71b24a60a..661c6cfeb 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -22,6 +22,7 @@ uv-options-metadata = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } +uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-version = { workspace = true } uv-warnings = { workspace = true } @@ -31,10 +32,12 @@ csv = { workspace = true } flate2 = { workspace = true, default-features = false } fs-err = { workspace = true } globset = { workspace = true } +indexmap = { workspace = true } itertools = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true } +serde_json = { workspace = true } sha2 = { workspace = true } spdx = { workspace = true } tar = { workspace = true } diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 1bdb68a67..6b39ed960 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -63,6 +63,8 @@ pub enum Error { Zip(#[from] zip::result::ZipError), #[error("Failed to write RECORD file")] Csv(#[from] csv::Error), + #[error("Failed to write JSON metadata file")] + Json(#[source] serde_json::Error), #[error("Expected a Python module at: {}", _0.user_display())] MissingInitPy(PathBuf), #[error("For namespace packages, `__init__.py[i]` is not allowed in parent directory: {}", _0.user_display())] @@ -449,6 +451,7 @@ mod tests { use tempfile::TempDir; use uv_distribution_filename::{SourceDistFilename, WheelFilename}; use uv_fs::{copy_dir_all, relative_to}; + use uv_preview::{Preview, PreviewFeatures}; const MOCK_UV_VERSION: &str = "1.0.0+test"; @@ -475,11 +478,13 @@ mod tests { /// Run both a direct wheel build and an indirect wheel build through a source distribution, /// while checking that directly built wheel and indirectly built wheel are the same. - fn build(source_root: &Path, dist: &Path) -> Result { + fn build(source_root: &Path, dist: &Path, preview: Preview) -> Result { // Build a direct wheel, capture all its properties to compare it with the indirect wheel // latest and remove it since it has the same filename as the indirect wheel. - let (_name, direct_wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false)?; - let direct_wheel_filename = build_wheel(source_root, dist, None, MOCK_UV_VERSION, false)?; + let (_name, direct_wheel_list_files) = + list_wheel(source_root, MOCK_UV_VERSION, false, preview)?; + let direct_wheel_filename = + build_wheel(source_root, dist, None, MOCK_UV_VERSION, false, preview)?; let direct_wheel_path = dist.join(direct_wheel_filename.to_string()); let direct_wheel_contents = wheel_contents(&direct_wheel_path); let direct_wheel_hash = sha2::Sha256::digest(fs_err::read(&direct_wheel_path)?); @@ -490,7 +495,7 @@ mod tests { list_source_dist(source_root, MOCK_UV_VERSION, false)?; // TODO(konsti): This should run in the unpacked source dist tempdir, but we need to // normalize the path. - let (_name, wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false)?; + let (_name, wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false, preview)?; let source_dist_filename = build_source_dist(source_root, dist, MOCK_UV_VERSION, false)?; let source_dist_path = dist.join(source_dist_filename.to_string()); let source_dist_contents = sdist_contents(&source_dist_path); @@ -511,6 +516,7 @@ mod tests { None, MOCK_UV_VERSION, false, + preview, )?; let wheel_contents = wheel_contents(&dist.join(wheel_filename.to_string())); @@ -535,7 +541,7 @@ mod tests { fn build_err(source_root: &Path) -> String { let dist = TempDir::new().unwrap(); - let build_err = build(source_root, dist.path()).unwrap_err(); + let build_err = build(source_root, dist.path(), Preview::default()).unwrap_err(); let err_message: String = format_err(&build_err) .replace(&source_root.user_display().to_string(), "[TEMP_PATH]") .replace('\\', "/"); @@ -652,7 +658,7 @@ mod tests { // Perform both the direct and the indirect build. let dist = TempDir::new().unwrap(); - let build = build(src.path(), dist.path()).unwrap(); + let build = build(src.path(), dist.path(), Preview::default()).unwrap(); let source_dist_path = dist.path().join(build.source_dist_filename.to_string()); assert_eq!( @@ -716,7 +722,7 @@ mod tests { // Check that the wheel is reproducible across platforms. assert_snapshot!( format!("{:x}", sha2::Sha256::digest(fs_err::read(&wheel_path).unwrap())), - @"319afb04e87caf894b1362b508ec745253c6d241423ea59021694d2015e821da" + @"dbe56fd8bd52184095b2e0ea3e83c95d1bc8b4aa53cf469cec5af62251b24abb" ); assert_snapshot!(build.wheel_contents.join("\n"), @r" built_by_uv-0.1.0.data/data/ @@ -767,7 +773,7 @@ mod tests { .unwrap() .read_to_string(&mut record) .unwrap(); - assert_snapshot!(record, @r###" + assert_snapshot!(record, @r" built_by_uv/__init__.py,sha256=AJ7XpTNWxYktP97ydb81UpnNqoebH7K4sHRakAMQKG4,44 built_by_uv/arithmetic/__init__.py,sha256=x2agwFbJAafc9Z6TdJ0K6b6bLMApQdvRSQjP4iy7IEI,67 built_by_uv/arithmetic/circle.py,sha256=FYZkv6KwrF9nJcwGOKigjke1dm1Fkie7qW1lWJoh3AE,287 @@ -779,11 +785,11 @@ mod tests { built_by_uv-0.1.0.data/headers/built_by_uv.h,sha256=p5-HBunJ1dY-xd4dMn03PnRClmGyRosScIp8rT46kg4,144 built_by_uv-0.1.0.data/scripts/whoami.sh,sha256=T2cmhuDFuX-dTkiSkuAmNyIzvv8AKopjnuTCcr9o-eE,20 built_by_uv-0.1.0.data/data/data.csv,sha256=7z7u-wXu7Qr2eBZFVpBILlNUiGSngv_1vYqZHVWOU94,265 - built_by_uv-0.1.0.dist-info/WHEEL,sha256=PaG_oOj9G2zCRqoLK0SjWBVZbGAMtIXDmm-MEGw9Wo0,83 + built_by_uv-0.1.0.dist-info/WHEEL,sha256=JBpLtoa_WBz5WPGpRsAUTD4Dz6H0KkkdiKWCkfMSS1U,84 built_by_uv-0.1.0.dist-info/entry_points.txt,sha256=-IO6yaq6x6HSl-zWH96rZmgYvfyHlH00L5WQoCpz-YI,50 built_by_uv-0.1.0.dist-info/METADATA,sha256=m6EkVvKrGmqx43b_VR45LHD37IZxPYC0NI6Qx9_UXLE,474 built_by_uv-0.1.0.dist-info/RECORD,, - "###); + "); } /// Test that `license = { file = "LICENSE" }` is supported. @@ -833,6 +839,7 @@ mod tests { None, "0.5.15", false, + Preview::default(), ) .unwrap(); let wheel = output_dir @@ -885,7 +892,13 @@ mod tests { // Prepare the metadata. let metadata_dir = TempDir::new().unwrap(); - let dist_info_dir = metadata(src.path(), metadata_dir.path(), "0.5.15").unwrap(); + let dist_info_dir = metadata( + src.path(), + metadata_dir.path(), + "0.5.15", + Preview::default(), + ) + .unwrap(); let metadata_prepared = fs_err::read_to_string(metadata_dir.path().join(&dist_info_dir).join("METADATA")) .unwrap(); @@ -898,6 +911,7 @@ mod tests { Some(&metadata_dir.path().join(&dist_info_dir)), "0.5.15", false, + Preview::default(), ) .unwrap(); let wheel = output_dir @@ -947,7 +961,7 @@ mod tests { File::create(src.path().join("two_step_build").join("__init__.py")).unwrap(); let dist = TempDir::new().unwrap(); - let build1 = build(src.path(), dist.path()).unwrap(); + let build1 = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_snapshot!(build1.source_dist_contents.join("\n"), @r" two_step_build-1.0.0/ @@ -986,7 +1000,7 @@ mod tests { .unwrap(); let dist = TempDir::new().unwrap(); - let build2 = build(src.path(), dist.path()).unwrap(); + let build2 = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_eq!(build1, build2); } @@ -1013,7 +1027,7 @@ mod tests { File::create(src.path().join("src").join("camelCase").join("__init__.py")).unwrap(); let dist = TempDir::new().unwrap(); - let build1 = build(src.path(), dist.path()).unwrap(); + let build1 = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_snapshot!(build1.wheel_contents.join("\n"), @r" camelCase/ @@ -1030,7 +1044,7 @@ mod tests { pyproject_toml.replace("camelCase", "camel_case"), ) .unwrap(); - let build_err = build(src.path(), dist.path()).unwrap_err(); + let build_err = build(src.path(), dist.path(), Preview::default()).unwrap_err(); let err_message = format_err(&build_err) .replace(&src.path().user_display().to_string(), "[TEMP_PATH]") .replace('\\', "/"); @@ -1059,7 +1073,7 @@ mod tests { fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); let dist = TempDir::new().unwrap(); - let build_err = build(src.path(), dist.path()).unwrap_err(); + let build_err = build(src.path(), dist.path(), Preview::default()).unwrap_err(); let err_message = format_err(&build_err); assert_snapshot!( err_message, @@ -1095,7 +1109,7 @@ mod tests { File::create(®ular_init_py).unwrap(); let dist = TempDir::new().unwrap(); - let build_err = build(src.path(), dist.path()).unwrap_err(); + let build_err = build(src.path(), dist.path(), Preview::default()).unwrap_err(); let err_message = format_err(&build_err) .replace(&src.path().user_display().to_string(), "[TEMP_PATH]") .replace('\\', "/"); @@ -1114,7 +1128,7 @@ mod tests { ) .unwrap(); - let build1 = build(src.path(), dist.path()).unwrap(); + let build1 = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_snapshot!(build1.wheel_contents.join("\n"), @r" stuffed_bird-stubs/ stuffed_bird-stubs/__init__.pyi @@ -1140,7 +1154,7 @@ mod tests { }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); - let build2 = build(src.path(), dist.path()).unwrap(); + let build2 = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_eq!(build1.wheel_contents, build2.wheel_contents); } @@ -1194,7 +1208,7 @@ mod tests { fs_err::remove_file(bogus_init_py).unwrap(); let dist = TempDir::new().unwrap(); - let build1 = build(src.path(), dist.path()).unwrap(); + let build1 = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_snapshot!(build1.source_dist_contents.join("\n"), @r" simple_namespace_part-1.0.0/ simple_namespace_part-1.0.0/PKG-INFO @@ -1231,7 +1245,7 @@ mod tests { }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); - let build2 = build(src.path(), dist.path()).unwrap(); + let build2 = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_eq!(build1, build2); } @@ -1285,7 +1299,7 @@ mod tests { .unwrap(); let dist = TempDir::new().unwrap(); - let build1 = build(src.path(), dist.path()).unwrap(); + let build1 = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_snapshot!(build1.wheel_contents.join("\n"), @r" complex_namespace-1.0.0.dist-info/ complex_namespace-1.0.0.dist-info/METADATA @@ -1315,7 +1329,7 @@ mod tests { }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); - let build2 = build(src.path(), dist.path()).unwrap(); + let build2 = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_eq!(build1, build2); } @@ -1356,7 +1370,7 @@ mod tests { .unwrap(); let dist = TempDir::new().unwrap(); - let build = build(src.path(), dist.path()).unwrap(); + let build = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_snapshot!(build.wheel_contents.join("\n"), @r" cloud-stubs/ cloud-stubs/db/ @@ -1453,7 +1467,7 @@ mod tests { fs_err::remove_file(bogus_init_py).unwrap(); let dist = TempDir::new().unwrap(); - let build = build(src.path(), dist.path()).unwrap(); + let build = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_snapshot!(build.source_dist_contents.join("\n"), @r" simple_namespace_part-1.0.0/ simple_namespace_part-1.0.0/PKG-INFO @@ -1567,7 +1581,7 @@ mod tests { .unwrap(); let dist = TempDir::new().unwrap(); - let build = build(src.path(), dist.path()).unwrap(); + let build = build(src.path(), dist.path(), Preview::default()).unwrap(); assert_snapshot!(build.source_dist_contents.join("\n"), @r" duplicate-1.0.0/ duplicate-1.0.0/PKG-INFO @@ -1591,4 +1605,51 @@ mod tests { foo/__init__.py "); } + + /// Check that JSON metadata files are present. + #[test] + fn metadata_json_preview() { + let src = TempDir::new().unwrap(); + fs_err::write( + src.path().join("pyproject.toml"), + indoc! {r#" + [project] + name = "metadata-json-preview" + version = "1.0.0" + + [build-system] + requires = ["uv_build>=0.5.15,<0.6.0"] + build-backend = "uv_build" + "# + }, + ) + .unwrap(); + fs_err::create_dir_all(src.path().join("src").join("metadata_json_preview")).unwrap(); + File::create( + src.path() + .join("src") + .join("metadata_json_preview") + .join("__init__.py"), + ) + .unwrap(); + + let dist = TempDir::new().unwrap(); + let build = build( + src.path(), + dist.path(), + Preview::new(PreviewFeatures::METADATA_JSON), + ) + .unwrap(); + + assert_snapshot!(build.wheel_contents.join("\n"), @r" + metadata_json_preview-1.0.0.dist-info/ + metadata_json_preview-1.0.0.dist-info/METADATA + metadata_json_preview-1.0.0.dist-info/METADATA.json + metadata_json_preview-1.0.0.dist-info/RECORD + metadata_json_preview-1.0.0.dist-info/WHEEL + metadata_json_preview-1.0.0.dist-info/WHEEL.json + metadata_json_preview/ + metadata_json_preview/__init__.py + "); + } } diff --git a/crates/uv-build-backend/src/metadata.rs b/crates/uv-build-backend/src/metadata.rs index e45670a6e..3401468fc 100644 --- a/crates/uv-build-backend/src/metadata.rs +++ b/crates/uv-build-backend/src/metadata.rs @@ -1,12 +1,12 @@ +use indexmap::IndexMap; +use itertools::Itertools; +use serde::{Deserialize, Deserializer}; use std::collections::{BTreeMap, Bound}; use std::ffi::OsStr; use std::fmt::Display; use std::fmt::Write; use std::path::{Path, PathBuf}; use std::str::{self, FromStr}; - -use itertools::Itertools; -use serde::{Deserialize, Deserializer}; use tracing::{debug, trace, warn}; use version_ranges::Ranges; use walkdir::WalkDir; @@ -18,7 +18,7 @@ use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::{ ExtraOperator, MarkerExpression, MarkerTree, MarkerValueExtra, Requirement, VersionOrUrl, }; -use uv_pypi_types::{Metadata23, VerbatimParsedUrl}; +use uv_pypi_types::{Keywords, Metadata23, ProjectUrls, VerbatimParsedUrl}; use crate::serde_verbatim::SerdeVerbatim; use crate::{BuildBackendSettings, Error, error_on_venv}; @@ -349,13 +349,7 @@ impl PyProjectToml { let (license, license_expression, license_files) = self.license_metadata(root)?; // TODO(konsti): https://peps.python.org/pep-0753/#label-normalization (Draft) - let project_urls = self - .project - .urls - .iter() - .flatten() - .map(|(key, value)| format!("{key}, {value}")) - .collect(); + let project_urls = ProjectUrls::new(self.project.urls.clone().unwrap_or_default()); let extras = self .project @@ -400,11 +394,7 @@ impl PyProjectToml { summary, description, description_content_type, - keywords: self - .project - .keywords - .as_ref() - .map(|keywords| keywords.join(",")), + keywords: self.project.keywords.clone().map(Keywords::new), home_page: None, download_url: None, author, @@ -695,7 +685,7 @@ struct Project { /// PyPI shows all URLs with their name. For some known patterns, they add favicons. /// main: /// archived: - urls: Option>, + urls: Option>, /// The console entrypoints of the project. /// /// The key of the table is the name of the entry point and the value is the object reference. diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index 5d60e32f0..e09bf2dd4 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -1,9 +1,9 @@ use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD as base64}; use fs_err::File; use globset::{GlobSet, GlobSetBuilder}; -use itertools::Itertools; use rustc_hash::FxHashSet; use sha2::{Digest, Sha256}; +use std::fmt::{Display, Formatter}; use std::io::{BufReader, Read, Write}; use std::path::{Component, Path, PathBuf}; use std::{io, mem}; @@ -15,6 +15,7 @@ use uv_distribution_filename::WheelFilename; use uv_fs::Simplified; use uv_globfilter::{GlobDirFilter, PortableGlobParser}; use uv_platform_tags::{AbiTag, LanguageTag, PlatformTag}; +use uv_preview::{Preview, PreviewFeatures}; use uv_warnings::warn_user_once; use crate::metadata::DEFAULT_EXCLUDES; @@ -30,6 +31,7 @@ pub fn build_wheel( metadata_directory: Option<&Path>, uv_version: &str, show_warnings: bool, + preview: Preview, ) -> Result { let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { @@ -59,6 +61,7 @@ pub fn build_wheel( uv_version, wheel_writer, show_warnings, + preview, )?; Ok(filename) @@ -69,6 +72,7 @@ pub fn list_wheel( source_tree: &Path, uv_version: &str, show_warnings: bool, + preview: Preview, ) -> Result<(WheelFilename, FileList), Error> { let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { @@ -95,6 +99,7 @@ pub fn list_wheel( uv_version, writer, show_warnings, + preview, )?; Ok((filename, files)) } @@ -106,6 +111,7 @@ fn write_wheel( uv_version: &str, mut wheel_writer: impl DirectoryWriter, show_warnings: bool, + preview: Preview, ) -> Result<(), Error> { let settings = pyproject_toml .settings() @@ -257,6 +263,7 @@ fn write_wheel( filename, source_tree, uv_version, + preview, )?; wheel_writer.close(&dist_info_dir)?; @@ -270,6 +277,7 @@ pub fn build_editable( metadata_directory: Option<&Path>, uv_version: &str, show_warnings: bool, + preview: Preview, ) -> Result { let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { @@ -320,6 +328,7 @@ pub fn build_editable( &filename, source_tree, uv_version, + preview, )?; wheel_writer.close(&dist_info_dir)?; @@ -331,6 +340,7 @@ pub fn metadata( source_tree: &Path, metadata_directory: &Path, uv_version: &str, + preview: Preview, ) -> Result { let pyproject_toml = PyProjectToml::parse(&source_tree.join("pyproject.toml"))?; for warning in pyproject_toml.check_build_system(uv_version) { @@ -359,6 +369,7 @@ pub fn metadata( &filename, source_tree, uv_version, + preview, )?; wheel_writer.close(&dist_info_dir)?; @@ -563,6 +574,7 @@ fn write_dist_info( filename: &WheelFilename, root: &Path, uv_version: &str, + preview: Preview, ) -> Result { let dist_info_dir = format!( "{}-{}.dist-info", @@ -572,9 +584,18 @@ fn write_dist_info( writer.write_directory(&dist_info_dir)?; - // Add `WHEEL`. - let wheel_info = wheel_info(filename, uv_version); - writer.write_bytes(&format!("{dist_info_dir}/WHEEL"), wheel_info.as_bytes())?; + // Add `WHEEL` and `WHEEL.json`. + let wheel_info = WheelInfo::new(filename, uv_version); + writer.write_bytes( + &format!("{dist_info_dir}/WHEEL"), + wheel_info.to_string().as_bytes(), + )?; + if preview.is_enabled(PreviewFeatures::METADATA_JSON) { + writer.write_bytes( + &format!("{dist_info_dir}/WHEEL.json"), + &serde_json::to_vec(&wheel_info).map_err(Error::Json)?, + )?; + } // Add `entry_points.txt`. if let Some(entrypoint) = pyproject_toml.to_entry_points()? { @@ -584,34 +605,64 @@ fn write_dist_info( )?; } - // Add `METADATA`. - let metadata = pyproject_toml.to_metadata(root)?.core_metadata_format(); - writer.write_bytes(&format!("{dist_info_dir}/METADATA"), metadata.as_bytes())?; + // Add `METADATA` and `METADATA.json`. + let metadata = pyproject_toml.to_metadata(root)?; + writer.write_bytes( + &format!("{dist_info_dir}/METADATA"), + metadata.core_metadata_format().as_bytes(), + )?; + if preview.is_enabled(PreviewFeatures::METADATA_JSON) { + writer.write_bytes( + &format!("{dist_info_dir}/METADATA.json"), + &serde_json::to_vec(&metadata).map_err(Error::Json)?, + )?; + } // `RECORD` is added on closing. Ok(dist_info_dir) } -/// Returns the `WHEEL` file contents. -fn wheel_info(filename: &WheelFilename, uv_version: &str) -> String { - // https://packaging.python.org/en/latest/specifications/binary-distribution-format/#file-contents - let mut wheel_info = vec![ - ("Wheel-Version", "1.0".to_string()), - ("Generator", format!("uv {uv_version}")), - ("Root-Is-Purelib", "true".to_string()), - ]; - for python_tag in filename.python_tags() { - for abi_tag in filename.abi_tags() { - for platform_tag in filename.platform_tags() { - wheel_info.push(("Tag", format!("{python_tag}-{abi_tag}-{platform_tag}"))); +/// The contents of the `WHEEL` and `WHEEL.json` files. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +struct WheelInfo { + wheel_version: String, + generator: String, + root_is_purelib: bool, + tags: Vec, +} + +impl WheelInfo { + fn new(filename: &WheelFilename, uv_version: &str) -> Self { + let mut tags = Vec::new(); + for python_tag in filename.python_tags() { + for abi_tag in filename.abi_tags() { + for platform_tag in filename.platform_tags() { + tags.push(format!("{python_tag}-{abi_tag}-{platform_tag}")); + } } } + Self { + wheel_version: "1.0".to_string(), + generator: format!("uv {uv_version}"), + root_is_purelib: true, + tags, + } + } +} + +impl Display for WheelInfo { + /// Returns the `WHEEL` file contents in its key-value format. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Wheel-Version: {}", self.wheel_version)?; + writeln!(f, "Generator: {}", self.generator)?; + writeln!(f, "Root-Is-Purelib: {}", self.root_is_purelib)?; + for tag in &self.tags { + writeln!(f, "Tag: {tag}")?; + } + Ok(()) } - wheel_info - .into_iter() - .map(|(key, value)| format!("{key}: {value}")) - .join("\n") } /// Zip archive (wheel) writer. @@ -812,7 +863,7 @@ mod test { PlatformTag::Any, ); - assert_snapshot!(wheel_info(&filename, "1.0.0+test"), @r" + assert_snapshot!(WheelInfo::new(&filename, "1.0.0+test").to_string(), @r" Wheel-Version: 1.0 Generator: uv 1.0.0+test Root-Is-Purelib: true @@ -841,7 +892,13 @@ mod test { fn test_prepare_metadata() { let metadata_dir = TempDir::new().unwrap(); let built_by_uv = Path::new("../../test/packages/built-by-uv"); - metadata(built_by_uv, metadata_dir.path(), "1.0.0+test").unwrap(); + metadata( + built_by_uv, + metadata_dir.path(), + "1.0.0+test", + Preview::default(), + ) + .unwrap(); let mut files: Vec<_> = WalkDir::new(metadata_dir.path()) .sort_by_file_name() @@ -889,12 +946,12 @@ mod test { let record_file = metadata_dir .path() .join("built_by_uv-0.1.0.dist-info/RECORD"); - assert_snapshot!(fs_err::read_to_string(record_file).unwrap(), @r###" - built_by_uv-0.1.0.dist-info/WHEEL,sha256=PaG_oOj9G2zCRqoLK0SjWBVZbGAMtIXDmm-MEGw9Wo0,83 + assert_snapshot!(fs_err::read_to_string(record_file).unwrap(), @r" + built_by_uv-0.1.0.dist-info/WHEEL,sha256=JBpLtoa_WBz5WPGpRsAUTD4Dz6H0KkkdiKWCkfMSS1U,84 built_by_uv-0.1.0.dist-info/entry_points.txt,sha256=-IO6yaq6x6HSl-zWH96rZmgYvfyHlH00L5WQoCpz-YI,50 built_by_uv-0.1.0.dist-info/METADATA,sha256=m6EkVvKrGmqx43b_VR45LHD37IZxPYC0NI6Qx9_UXLE,474 built_by_uv-0.1.0.dist-info/RECORD,, - "###); + "); let wheel_file = metadata_dir .path() diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index c3b173ee1..8b4727b23 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -12,7 +12,9 @@ license = { workspace = true } [dependencies] uv-build-backend = { workspace = true } uv-logging = { workspace = true } +uv-preview = { workspace = true } uv-version = { workspace = true } +uv-static = { workspace = true } anstream = { workspace = true } anyhow = { workspace = true } diff --git a/crates/uv-build/src/main.rs b/crates/uv-build/src/main.rs index b3211cb4c..f7fc0c5fc 100644 --- a/crates/uv-build/src/main.rs +++ b/crates/uv-build/src/main.rs @@ -1,6 +1,9 @@ use std::env; use std::io::Write; use std::path::PathBuf; +use std::str::FromStr; +use uv_preview::{Preview, PreviewFeatures}; +use uv_static::{EnvVars, parse_boolish_environment_variable}; use anyhow::{Context, Result, bail}; use tracing_subscriber::filter::LevelFilter; @@ -37,6 +40,25 @@ fn main() -> Result<()> { .to_str() .context("Invalid non-UTF8 command")? .to_string(); + + // Ad-hoc preview features parsing due to a lack of clap CLI in uv-build. + let preview_features = + if parse_boolish_environment_variable(EnvVars::UV_PREVIEW)?.unwrap_or(false) { + PreviewFeatures::all() + } else if let Some(preview_features) = env::var_os(EnvVars::UV_PREVIEW_FEATURES) { + let preview_features = preview_features.to_str().with_context(|| { + format!("`{}` is not valid UTF-8", EnvVars::UV_PREVIEW_FEATURES) + })?; + PreviewFeatures::from_str(preview_features).with_context(|| { + format!( + "Invalid preview features list in `{}`", + EnvVars::UV_PREVIEW_FEATURES + ) + })? + } else { + PreviewFeatures::default() + }; + let preview = Preview::new(preview_features); match command.as_str() { "build-sdist" => { let sdist_directory = PathBuf::from(args.next().context("Missing sdist directory")?); @@ -58,6 +80,7 @@ fn main() -> Result<()> { metadata_directory.as_deref(), uv_version::version(), false, + preview, )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; @@ -71,6 +94,7 @@ fn main() -> Result<()> { metadata_directory.as_deref(), uv_version::version(), false, + preview, )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; @@ -81,6 +105,7 @@ fn main() -> Result<()> { &env::current_dir()?, &wheel_directory, uv_version::version(), + preview, )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; @@ -91,6 +116,7 @@ fn main() -> Result<()> { &env::current_dir()?, &wheel_directory, uv_version::version(), + preview, )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index afcf38bd1..d55a63031 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -526,6 +526,7 @@ impl BuildContext for BuildDispatch<'_> { debug!("Performing direct build for {identifier}"); let output_dir = output_dir.to_path_buf(); + let preview = self.preview; let filename = tokio::task::spawn_blocking(move || -> Result<_> { let filename = match build_kind { BuildKind::Wheel => { @@ -535,6 +536,7 @@ impl BuildContext for BuildDispatch<'_> { None, uv_version::version(), sources == SourceStrategy::Enabled, + preview, )?; DistFilename::WheelFilename(wheel) } @@ -554,6 +556,7 @@ impl BuildContext for BuildDispatch<'_> { None, uv_version::version(), sources == SourceStrategy::Enabled, + preview, )?; DistFilename::WheelFilename(wheel) } diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index 146c6e580..25584a96e 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -27,6 +27,7 @@ bitflags::bitflags! { const WORKSPACE_LIST = 1 << 15; const SBOM_EXPORT = 1 << 16; const AUTH_HELPER = 1 << 17; + const METADATA_JSON = 1 << 18; } } @@ -54,6 +55,7 @@ impl PreviewFeatures { Self::WORKSPACE_LIST => "workspace-list", Self::SBOM_EXPORT => "sbom-export", Self::AUTH_HELPER => "auth-helper", + Self::METADATA_JSON => "metadata-json", _ => panic!("`flag_as_str` can only be used for exactly one feature flag"), } } @@ -109,6 +111,7 @@ impl FromStr for PreviewFeatures { "workspace-list" => Self::WORKSPACE_LIST, "sbom-export" => Self::SBOM_EXPORT, "auth-helper" => Self::AUTH_HELPER, + "metadata-json" => Self::METADATA_JSON, _ => { warn_user_once!("Unknown preview feature: `{part}`"); continue; @@ -285,6 +288,10 @@ mod tests { assert_eq!(PreviewFeatures::FORMAT.flag_as_str(), "format"); assert_eq!(PreviewFeatures::S3_ENDPOINT.flag_as_str(), "s3-endpoint"); assert_eq!(PreviewFeatures::SBOM_EXPORT.flag_as_str(), "sbom-export"); + assert_eq!( + PreviewFeatures::METADATA_JSON.flag_as_str(), + "metadata-json" + ); } #[test] diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index b743033bc..b078bad6f 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -942,7 +942,7 @@ impl FormMetadata { add_option("description_content_type", description_content_type); add_option("download_url", download_url); add_option("home_page", home_page); - add_option("keywords", keywords); + add_option("keywords", keywords.map(|keywords| keywords.to_string())); add_option("license", license); add_option("license_expression", license_expression); add_option("maintainer", maintainer); @@ -964,7 +964,7 @@ impl FormMetadata { add_vec("license_file", license_files); add_vec("obsoletes_dist", obsoletes_dist); add_vec("platform", platforms); - add_vec("project_urls", project_urls); + add_vec("project_urls", project_urls.to_vec_str()); add_vec("provides_dist", provides_dist); add_vec("provides_extra", provides_extra); add_vec("requires_dist", requires_dist); diff --git a/crates/uv-pypi-types/src/metadata/metadata23.rs b/crates/uv-pypi-types/src/metadata/metadata23.rs index 79c2043fc..0158707b0 100644 --- a/crates/uv-pypi-types/src/metadata/metadata23.rs +++ b/crates/uv-pypi-types/src/metadata/metadata23.rs @@ -1,16 +1,17 @@ //! Vendored from -use std::fmt::Display; -use std::fmt::Write; -use std::str; -use std::str::FromStr; - use crate::MetadataError; use crate::metadata::Headers; +use indexmap::IndexMap; +use std::fmt::Write; +use std::fmt::{Display, Formatter}; +use std::str; +use std::str::FromStr; /// Code Metadata 2.3 as specified in /// . -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] pub struct Metadata23 { /// Version of the file format; legal values are `1.0`, `1.1`, `1.2`, `2.1`, `2.2`, `2.3` and /// `2.4`. @@ -36,7 +37,7 @@ pub struct Metadata23 { pub description_content_type: Option, /// A list of additional keywords, separated by commas, to be used to /// assist searching for the distribution in a larger catalog. - pub keywords: Option, + pub keywords: Option, /// A string containing the URL for the distribution's home page. /// /// Deprecated by PEP 753. @@ -95,7 +96,7 @@ pub struct Metadata23 { pub requires_external: Vec, /// A string containing a browsable URL for the project and a label for it, separated by a /// comma. - pub project_urls: Vec, + pub project_urls: ProjectUrls, /// A string containing the name of an optional feature. Must be a valid Python identifier. /// May be used to make a dependency conditional on whether the optional feature has been /// requested. @@ -128,7 +129,10 @@ impl Metadata23 { } else { Some(body.to_string()) }; - let keywords = headers.get_first_value("Keywords"); + let keywords = headers + .get_first_value("Keywords") + .as_deref() + .map(Keywords::from_metadata); let home_page = headers.get_first_value("Home-Page"); let download_url = headers.get_first_value("Download-URL"); let author = headers.get_first_value("Author"); @@ -144,7 +148,7 @@ impl Metadata23 { let maintainer_email = headers.get_first_value("Maintainer-email"); let requires_python = headers.get_first_value("Requires-Python"); let requires_external = headers.get_all_values("Requires-External").collect(); - let project_urls = headers.get_all_values("Project-URL").collect(); + let project_urls = ProjectUrls::from_iter_str(headers.get_all_values("Project-URL")); let provides_extra = headers.get_all_values("Provides-Extra").collect(); let description_content_type = headers.get_first_value("Description-Content-Type"); let dynamic = headers.get_all_values("Dynamic").collect(); @@ -263,7 +267,7 @@ impl Metadata23 { self.requires_python.as_ref(), ); write_all(&mut writer, "Requires-External", &self.requires_external); - write_all(&mut writer, "Project-URL", &self.project_urls); + write_all(&mut writer, "Project-URL", self.project_urls.to_vec_str()); write_all(&mut writer, "Provides-Extra", &self.provides_extra); write_opt_str( &mut writer, @@ -288,6 +292,68 @@ impl FromStr for Metadata23 { } } +/// Handle the different keywords representation between `METADATA` and `METADATA.json`. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct Keywords(Vec); + +impl Keywords { + pub fn new(keywords: Vec) -> Self { + Self(keywords) + } + + /// Read the `METADATA` format. + pub fn from_metadata(keywords: &str) -> Self { + Self(keywords.split(',').map(ToString::to_string).collect()) + } +} + +impl Display for Keywords { + /// Write the `METADATA` format. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut keywords = self.0.iter(); + if let Some(keyword) = keywords.next() { + write!(f, "{keyword}")?; + } + for keyword in keywords { + write!(f, ",{keyword}")?; + } + Ok(()) + } +} + +/// Handle the different project URLs representation between `METADATA` and `METADATA.json`. +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct ProjectUrls(IndexMap); + +impl ProjectUrls { + pub fn new(project_urls: IndexMap) -> Self { + Self(project_urls) + } + + /// Read the `METADATA` format. + pub fn from_iter_str(project_urls: impl IntoIterator) -> Self { + Self( + project_urls + .into_iter() + .map(|project_url| { + let (label, url) = project_url.split_once(',').unwrap_or((&project_url, "")); + // TODO(konsti): The spec says separated by comma, but it's actually comma and a + // space. + (label.trim().to_string(), url.trim().to_string()) + }) + .collect(), + ) + } + + /// Write the `METADATA` format. + pub fn to_vec_str(&self) -> Vec { + self.0 + .iter() + .map(|(label, url)| format!("{label}, {url}")) + .collect() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/uv-pypi-types/src/metadata/mod.rs b/crates/uv-pypi-types/src/metadata/mod.rs index 0ef7f2321..3bf738b82 100644 --- a/crates/uv-pypi-types/src/metadata/mod.rs +++ b/crates/uv-pypi-types/src/metadata/mod.rs @@ -20,7 +20,7 @@ use crate::VerbatimParsedUrl; pub use build_requires::BuildRequires; pub use metadata_resolver::ResolutionMetadata; pub use metadata10::Metadata10; -pub use metadata23::Metadata23; +pub use metadata23::{Keywords, Metadata23, ProjectUrls}; pub use pyproject_toml::PyProjectToml; pub use requires_dist::RequiresDist; pub use requires_txt::RequiresTxt; diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index ecc687229..20163a950 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -6,7 +6,7 @@ use std::time::Duration; use uv_dirs::{system_config_file, user_config_dir}; use uv_flags::EnvironmentFlags; use uv_fs::Simplified; -use uv_static::EnvVars; +use uv_static::{EnvVars, InvalidEnvironmentVariable, parse_boolish_environment_variable}; use uv_warnings::warn_user; pub use crate::combine::*; @@ -567,12 +567,8 @@ pub enum Error { )] PyprojectOnlyField(PathBuf, &'static str), - #[error("Failed to parse environment variable `{name}` with invalid value `{value}`: {err}")] - InvalidEnvironmentVariable { - name: String, - value: String, - err: String, - }, + #[error(transparent)] + InvalidEnvironmentVariable(#[from] InvalidEnvironmentVariable), } #[derive(Copy, Clone, Debug)] @@ -659,58 +655,6 @@ impl EnvironmentOptions { } } -/// Parse a boolean environment variable. -/// -/// Adapted from Clap's `BoolishValueParser` which is dual licensed under the MIT and Apache-2.0. -fn parse_boolish_environment_variable(name: &'static str) -> Result, Error> { - // See `clap_builder/src/util/str_to_bool.rs` - // We want to match Clap's accepted values - - // True values are `y`, `yes`, `t`, `true`, `on`, and `1`. - const TRUE_LITERALS: [&str; 6] = ["y", "yes", "t", "true", "on", "1"]; - - // False values are `n`, `no`, `f`, `false`, `off`, and `0`. - const FALSE_LITERALS: [&str; 6] = ["n", "no", "f", "false", "off", "0"]; - - // Converts a string literal representation of truth to true or false. - // - // `false` values are `n`, `no`, `f`, `false`, `off`, and `0` (case insensitive). - // - // Any other value will be considered as `true`. - fn str_to_bool(val: impl AsRef) -> Option { - let pat: &str = &val.as_ref().to_lowercase(); - if TRUE_LITERALS.contains(&pat) { - Some(true) - } else if FALSE_LITERALS.contains(&pat) { - Some(false) - } else { - None - } - } - - let Some(value) = std::env::var_os(name) else { - return Ok(None); - }; - - let Some(value) = value.to_str() else { - return Err(Error::InvalidEnvironmentVariable { - name: name.to_string(), - value: value.to_string_lossy().to_string(), - err: "expected a valid UTF-8 string".to_string(), - }); - }; - - let Some(value) = str_to_bool(value) else { - return Err(Error::InvalidEnvironmentVariable { - name: name.to_string(), - value: value.to_string(), - err: "expected a boolish value".to_string(), - }); - }; - - Ok(Some(value)) -} - /// Parse a string environment variable. fn parse_string_environment_variable(name: &'static str) -> Result, Error> { match std::env::var(name) { @@ -723,11 +667,13 @@ fn parse_string_environment_variable(name: &'static str) -> Result match e { std::env::VarError::NotPresent => Ok(None), - std::env::VarError::NotUnicode(err) => Err(Error::InvalidEnvironmentVariable { - name: name.to_string(), - value: err.to_string_lossy().to_string(), - err: "expected a valid UTF-8 string".to_string(), - }), + std::env::VarError::NotUnicode(err) => Err(Error::InvalidEnvironmentVariable( + InvalidEnvironmentVariable { + name: name.to_string(), + value: err.to_string_lossy().to_string(), + err: "expected a valid UTF-8 string".to_string(), + }, + )), }, } } @@ -742,11 +688,13 @@ where Err(e) => { return match e { std::env::VarError::NotPresent => Ok(None), - std::env::VarError::NotUnicode(err) => Err(Error::InvalidEnvironmentVariable { - name: name.to_string(), - value: err.to_string_lossy().to_string(), - err: "expected a valid UTF-8 string".to_string(), - }), + std::env::VarError::NotUnicode(err) => Err(Error::InvalidEnvironmentVariable( + InvalidEnvironmentVariable { + name: name.to_string(), + value: err.to_string_lossy().to_string(), + err: "expected a valid UTF-8 string".to_string(), + }, + )), }; } }; @@ -756,11 +704,13 @@ where match value.parse::() { Ok(v) => Ok(Some(v)), - Err(err) => Err(Error::InvalidEnvironmentVariable { - name: name.to_string(), - value, - err: err.to_string(), - }), + Err(err) => Err(Error::InvalidEnvironmentVariable( + InvalidEnvironmentVariable { + name: name.to_string(), + value, + err: err.to_string(), + }, + )), } } diff --git a/crates/uv-static/Cargo.toml b/crates/uv-static/Cargo.toml index aa6a20170..67882bb77 100644 --- a/crates/uv-static/Cargo.toml +++ b/crates/uv-static/Cargo.toml @@ -17,3 +17,5 @@ workspace = true [dependencies] uv-macros = { workspace = true } + +thiserror = { workspace = true } diff --git a/crates/uv-static/src/lib.rs b/crates/uv-static/src/lib.rs index 153591db7..2a347d4c7 100644 --- a/crates/uv-static/src/lib.rs +++ b/crates/uv-static/src/lib.rs @@ -1,3 +1,67 @@ pub use env_vars::*; mod env_vars; + +use thiserror::Error; + +#[derive(Debug, Error)] +#[error("Failed to parse environment variable `{name}` with invalid value `{value}`: {err}")] +pub struct InvalidEnvironmentVariable { + pub name: String, + pub value: String, + pub err: String, +} + +/// Parse a boolean environment variable. +/// +/// Adapted from Clap's `BoolishValueParser` which is dual licensed under the MIT and Apache-2.0. +pub fn parse_boolish_environment_variable( + name: &'static str, +) -> Result, InvalidEnvironmentVariable> { + // See `clap_builder/src/util/str_to_bool.rs` + // We want to match Clap's accepted values + + // True values are `y`, `yes`, `t`, `true`, `on`, and `1`. + const TRUE_LITERALS: [&str; 6] = ["y", "yes", "t", "true", "on", "1"]; + + // False values are `n`, `no`, `f`, `false`, `off`, and `0`. + const FALSE_LITERALS: [&str; 6] = ["n", "no", "f", "false", "off", "0"]; + + // Converts a string literal representation of truth to true or false. + // + // `false` values are `n`, `no`, `f`, `false`, `off`, and `0` (case insensitive). + // + // Any other value will be considered as `true`. + fn str_to_bool(val: impl AsRef) -> Option { + let pat: &str = &val.as_ref().to_lowercase(); + if TRUE_LITERALS.contains(&pat) { + Some(true) + } else if FALSE_LITERALS.contains(&pat) { + Some(false) + } else { + None + } + } + + let Some(value) = std::env::var_os(name) else { + return Ok(None); + }; + + let Some(value) = value.to_str() else { + return Err(InvalidEnvironmentVariable { + name: name.to_string(), + value: value.to_string_lossy().to_string(), + err: "expected a valid UTF-8 string".to_string(), + }); + }; + + let Some(value) = str_to_bool(value) else { + return Err(InvalidEnvironmentVariable { + name: name.to_string(), + value: value.to_string(), + err: "expected a boolish value".to_string(), + }); + }; + + Ok(Some(value)) +} diff --git a/crates/uv-trampoline/Cargo.lock b/crates/uv-trampoline/Cargo.lock index 79a797d16..8603f5269 100644 --- a/crates/uv-trampoline/Cargo.lock +++ b/crates/uv-trampoline/Cargo.lock @@ -71,6 +71,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "ufmt" version = "0.2.0" @@ -130,6 +150,7 @@ dependencies = [ name = "uv-static" version = "0.0.8" dependencies = [ + "thiserror", "uv-macros", ] diff --git a/crates/uv/src/commands/build_backend.rs b/crates/uv/src/commands/build_backend.rs index c9f4e2ed5..cf4dc3ce7 100644 --- a/crates/uv/src/commands/build_backend.rs +++ b/crates/uv/src/commands/build_backend.rs @@ -3,6 +3,7 @@ use anyhow::{Context, Result}; use std::env; use std::io::Write; use std::path::Path; +use uv_preview::Preview; /// PEP 517 hook to build a source distribution. pub(crate) fn build_sdist(sdist_directory: &Path) -> Result { @@ -21,6 +22,7 @@ pub(crate) fn build_sdist(sdist_directory: &Path) -> Result { pub(crate) fn build_wheel( wheel_directory: &Path, metadata_directory: Option<&Path>, + preview: Preview, ) -> Result { let filename = uv_build_backend::build_wheel( &env::current_dir()?, @@ -28,6 +30,7 @@ pub(crate) fn build_wheel( metadata_directory, uv_version::version(), false, + preview, )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; @@ -38,6 +41,7 @@ pub(crate) fn build_wheel( pub(crate) fn build_editable( wheel_directory: &Path, metadata_directory: Option<&Path>, + preview: Preview, ) -> Result { let filename = uv_build_backend::build_editable( &env::current_dir()?, @@ -45,6 +49,7 @@ pub(crate) fn build_editable( metadata_directory, uv_version::version(), false, + preview, )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; @@ -62,11 +67,15 @@ pub(crate) fn get_requires_for_build_wheel() -> Result { } /// PEP 517 hook to just emit metadata through `.dist-info`. -pub(crate) fn prepare_metadata_for_build_wheel(metadata_directory: &Path) -> Result { +pub(crate) fn prepare_metadata_for_build_wheel( + metadata_directory: &Path, + preview: Preview, +) -> Result { let filename = uv_build_backend::metadata( &env::current_dir()?, metadata_directory, uv_version::version(), + preview, )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; @@ -79,11 +88,15 @@ pub(crate) fn get_requires_for_build_editable() -> Result { } /// PEP 660 hook to just emit metadata through `.dist-info`. -pub(crate) fn prepare_metadata_for_build_editable(metadata_directory: &Path) -> Result { +pub(crate) fn prepare_metadata_for_build_editable( + metadata_directory: &Path, + preview: Preview, +) -> Result { let filename = uv_build_backend::metadata( &env::current_dir()?, metadata_directory, uv_version::version(), + preview, )?; // Tell the build frontend about the name of the artifact we built writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 099ece65e..5ca26744b 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -742,6 +742,7 @@ async fn build_package( version_id, build_output, Some(sdist_build.normalized_filename().version()), + preview, ) .await?; build_results.push(wheel_build); @@ -779,6 +780,7 @@ async fn build_package( version_id, build_output, None, + preview, ) .await?; build_results.push(wheel_build); @@ -814,6 +816,7 @@ async fn build_package( version_id, build_output, Some(sdist_build.normalized_filename().version()), + preview, ) .await?; build_results.push(sdist_build); @@ -857,6 +860,7 @@ async fn build_package( version_id, build_output, version.as_ref(), + preview, ) .await?; build_results.push(wheel_build); @@ -1014,6 +1018,7 @@ async fn build_wheel( build_output: BuildOutput, // Used for checking version consistency version: Option<&Version>, + preview: Preview, ) -> Result { let build_message = match action { BuildAction::List => { @@ -1023,6 +1028,7 @@ async fn build_wheel( &source_tree_, uv_version::version(), sources == SourceStrategy::Enabled, + preview, ) }) .await??; @@ -1054,6 +1060,7 @@ async fn build_wheel( None, uv_version::version(), sources == SourceStrategy::Enabled, + preview, ) }) .await??; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 31ba18ce7..4ca84183b 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1818,6 +1818,7 @@ async fn run(mut cli: Cli) -> Result { } => commands::build_backend::build_wheel( &wheel_directory, metadata_directory.as_deref(), + globals.preview, ), BuildBackendCommand::BuildEditable { wheel_directory, @@ -1825,6 +1826,7 @@ async fn run(mut cli: Cli) -> Result { } => commands::build_backend::build_editable( &wheel_directory, metadata_directory.as_deref(), + globals.preview, ), BuildBackendCommand::GetRequiresForBuildSdist => { commands::build_backend::get_requires_for_build_sdist() @@ -1833,13 +1835,19 @@ async fn run(mut cli: Cli) -> Result { commands::build_backend::get_requires_for_build_wheel() } BuildBackendCommand::PrepareMetadataForBuildWheel { wheel_directory } => { - commands::build_backend::prepare_metadata_for_build_wheel(&wheel_directory) + commands::build_backend::prepare_metadata_for_build_wheel( + &wheel_directory, + globals.preview, + ) } BuildBackendCommand::GetRequiresForBuildEditable => { commands::build_backend::get_requires_for_build_editable() } BuildBackendCommand::PrepareMetadataForBuildEditable { wheel_directory } => { - commands::build_backend::prepare_metadata_for_build_editable(&wheel_directory) + commands::build_backend::prepare_metadata_for_build_editable( + &wheel_directory, + globals.preview, + ) } }) .await diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index f951d754d..51a1572e9 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -7834,7 +7834,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER | METADATA_JSON, ), }, python_preference: Managed, @@ -8064,7 +8064,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER | METADATA_JSON, ), }, python_preference: Managed,