mirror of https://github.com/astral-sh/uv
Add `METADATA.json` and `WHEEL.json` to build backend
Add an experimental JSON format for METADATA and WHEEL files in the uv build backend to have a reference what it would look like and to demonstrate that the actual change is just a few lines, how much easier it is than the existing email-ish format.
This commit is contained in:
parent
6fa8204efe
commit
4b8ab58e3a
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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<BuildResults, Error> {
|
||||
fn build(source_root: &Path, dist: &Path, preview: Preview) -> Result<BuildResults, Error> {
|
||||
// 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
|
||||
");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: <https://github.com/pypi/warehouse/blob/main/warehouse/templates/packaging/detail.html>
|
||||
/// archived: <https://github.com/pypi/warehouse/blob/e3bd3c3805ff47fff32b67a899c1ce11c16f3c31/warehouse/templates/packaging/detail.html>
|
||||
urls: Option<BTreeMap<String, String>>,
|
||||
urls: Option<IndexMap<String, String>>,
|
||||
/// 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.
|
||||
|
|
|
|||
|
|
@ -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<WheelFilename, Error> {
|
||||
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<WheelFilename, Error> {
|
||||
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<String, Error> {
|
||||
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<String, Error> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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")?;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
//! Vendored from <https://github.com/PyO3/python-pkginfo-rs>
|
||||
|
||||
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
|
||||
/// <https://packaging.python.org/specifications/core-metadata/>.
|
||||
#[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<String>,
|
||||
/// A list of additional keywords, separated by commas, to be used to
|
||||
/// assist searching for the distribution in a larger catalog.
|
||||
pub keywords: Option<String>,
|
||||
pub keywords: Option<Keywords>,
|
||||
/// 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<String>,
|
||||
/// A string containing a browsable URL for the project and a label for it, separated by a
|
||||
/// comma.
|
||||
pub project_urls: Vec<String>,
|
||||
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<String>);
|
||||
|
||||
impl Keywords {
|
||||
pub fn new(keywords: Vec<String>) -> 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<String, String>);
|
||||
|
||||
impl ProjectUrls {
|
||||
pub fn new(project_urls: IndexMap<String, String>) -> Self {
|
||||
Self(project_urls)
|
||||
}
|
||||
|
||||
/// Read the `METADATA` format.
|
||||
pub fn from_iter_str(project_urls: impl IntoIterator<Item = String>) -> 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<String> {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|(label, url)| format!("{label}, {url}"))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Option<bool>, 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<str>) -> Option<bool> {
|
||||
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<Option<String>, Error> {
|
||||
match std::env::var(name) {
|
||||
|
|
@ -723,11 +667,13 @@ fn parse_string_environment_variable(name: &'static str) -> Result<Option<String
|
|||
}
|
||||
Err(e) => 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::<T>() {
|
||||
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(),
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,3 +17,5 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
uv-macros = { workspace = true }
|
||||
|
||||
thiserror = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -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<Option<bool>, 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<str>) -> Option<bool> {
|
||||
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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ExitStatus> {
|
||||
|
|
@ -21,6 +22,7 @@ pub(crate) fn build_sdist(sdist_directory: &Path) -> Result<ExitStatus> {
|
|||
pub(crate) fn build_wheel(
|
||||
wheel_directory: &Path,
|
||||
metadata_directory: Option<&Path>,
|
||||
preview: Preview,
|
||||
) -> Result<ExitStatus> {
|
||||
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<ExitStatus> {
|
||||
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<ExitStatus> {
|
|||
}
|
||||
|
||||
/// PEP 517 hook to just emit metadata through `.dist-info`.
|
||||
pub(crate) fn prepare_metadata_for_build_wheel(metadata_directory: &Path) -> Result<ExitStatus> {
|
||||
pub(crate) fn prepare_metadata_for_build_wheel(
|
||||
metadata_directory: &Path,
|
||||
preview: Preview,
|
||||
) -> Result<ExitStatus> {
|
||||
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<ExitStatus> {
|
|||
}
|
||||
|
||||
/// PEP 660 hook to just emit metadata through `.dist-info`.
|
||||
pub(crate) fn prepare_metadata_for_build_editable(metadata_directory: &Path) -> Result<ExitStatus> {
|
||||
pub(crate) fn prepare_metadata_for_build_editable(
|
||||
metadata_directory: &Path,
|
||||
preview: Preview,
|
||||
) -> Result<ExitStatus> {
|
||||
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")?;
|
||||
|
|
|
|||
|
|
@ -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<BuildMessage, Error> {
|
||||
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??;
|
||||
|
|
|
|||
|
|
@ -1818,6 +1818,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
} => 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<ExitStatus> {
|
|||
} => 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<ExitStatus> {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue