Build basic source distributions (#8886)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / build binary | linux (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on linux (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on opensuse (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86_64 (push) Blocked by required conditions
CI / check system | python3.10 on windows (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on linux (push) Blocked by required conditions
CI / check system | conda3.8 on linux (push) Blocked by required conditions
CI / check system | conda3.11 on macos (push) Blocked by required conditions
CI / check system | conda3.8 on macos (push) Blocked by required conditions
CI / check system | conda3.11 on windows (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
CI / check system | conda3.8 on windows (push) Blocked by required conditions

Very basic source distribution support. What's included:

- Include and exclude patterns (hard-coded): Currently, we have
globset+walkdir in one part and glob in the other. I'll migrate
everything to globset+walkset and some custom perf optimizations to
avoid traversing irrelevant directories on top. I'll also pick a glob
syntax (or subset), PEP 639 seems like a good candidate since it's
consistent with what we already have to support.
- Add the `PKG-INFO` file with metadata: Thanks to Code Metadata 2.2,
this metadata is reliable and can be read statically by external tools.

Example output:

```
$ tar -ztvf dist/dummy-0.1.0.tar.gz
-rw-r--r-- 0/0             154 1970-01-01 01:00 dummy-0.1.0/PKG-INFO
-rw-rw-r-- 0/0             509 1970-01-01 01:00 dummy-0.1.0/pyproject.toml
drwxrwxr-x 0/0               0 1970-01-01 01:00 dummy-0.1.0/src/dummy
drwxrwxr-x 0/0               0 1970-01-01 01:00 dummy-0.1.0/src/dummy/submodule
-rw-rw-r-- 0/0              30 1970-01-01 01:00 dummy-0.1.0/src/dummy/submodule/impl.py
-rw-rw-r-- 0/0              14 1970-01-01 01:00 dummy-0.1.0/src/dummy/submodule/__init__.py
-rw-rw-r-- 0/0              12 1970-01-01 01:00 dummy-0.1.0/src/dummy/__init__.py
```

No tests since the source distributions don't build valid wheels yet.
This commit is contained in:
konsti 2024-11-07 14:29:54 +01:00 committed by GitHub
parent 5eba64a641
commit 107ab3d71c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 171 additions and 14 deletions

14
Cargo.lock generated
View file

@ -3523,6 +3523,17 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddb6b06d20fba9ed21fca3d696ee1b6e870bca0bcf9fa2971f6ae2436de576a" checksum = "eddb6b06d20fba9ed21fca3d696ee1b6e870bca0bcf9fa2971f6ae2436de576a"
[[package]]
name = "tar"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.16" version = "0.12.16"
@ -4322,14 +4333,17 @@ name = "uv-build-backend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"csv", "csv",
"flate2",
"fs-err", "fs-err",
"glob", "glob",
"globset",
"indoc", "indoc",
"insta", "insta",
"itertools 0.13.0", "itertools 0.13.0",
"serde", "serde",
"sha2", "sha2",
"spdx", "spdx",
"tar",
"tempfile", "tempfile",
"thiserror", "thiserror",
"toml", "toml",

View file

@ -103,6 +103,7 @@ fs-err = { version = "2.11.0" }
fs2 = { version = "0.4.3" } fs2 = { version = "0.4.3" }
futures = { version = "0.3.30" } futures = { version = "0.3.30" }
glob = { version = "0.3.1" } glob = { version = "0.3.1" }
globset = { version = "0.4.15" }
globwalk = { version = "0.9.1" } globwalk = { version = "0.9.1" }
goblin = { version = "0.9.0", default-features = false, features = ["std", "elf32", "elf64", "endian_fd"] } goblin = { version = "0.9.0", default-features = false, features = ["std", "elf32", "elf64", "endian_fd"] }
hex = { version = "0.4.3" } hex = { version = "0.4.3" }
@ -126,7 +127,7 @@ path-slash = { version = "0.2.1" }
pathdiff = { version = "0.2.1" } pathdiff = { version = "0.2.1" }
petgraph = { version = "0.6.5" } petgraph = { version = "0.6.5" }
platform-info = { version = "2.0.3" } platform-info = { version = "2.0.3" }
procfs = { version = "0.17.0" , default-features = false, features = ["flate2"] } procfs = { version = "0.17.0", default-features = false, features = ["flate2"] }
proc-macro2 = { version = "1.0.86" } proc-macro2 = { version = "1.0.86" }
pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "95e1390399cdddee986b658be19587eb1fdb2d79" } pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "95e1390399cdddee986b658be19587eb1fdb2d79" }
version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "95e1390399cdddee986b658be19587eb1fdb2d79" } version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "95e1390399cdddee986b658be19587eb1fdb2d79" }
@ -153,6 +154,7 @@ smallvec = { version = "1.13.2" }
spdx = { version = "0.10.6" } spdx = { version = "0.10.6" }
syn = { version = "2.0.77" } syn = { version = "2.0.77" }
sys-info = { version = "0.9.1" } sys-info = { version = "0.9.1" }
tar = { version = "0.4.43" }
target-lexicon = { version = "0.12.16" } target-lexicon = { version = "0.12.16" }
tempfile = { version = "3.12.0" } tempfile = { version = "3.12.0" }
textwrap = { version = "0.16.1" } textwrap = { version = "0.16.1" }

View file

@ -21,13 +21,16 @@ uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }
csv = { workspace = true} csv = { workspace = true }
flate2 = { workspace = true }
fs-err = { workspace = true } fs-err = { workspace = true }
glob = { workspace = true } glob = { workspace = true }
globset = { workspace = true }
itertools = { workspace = true } itertools = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
sha2 = { workspace = true } sha2 = { workspace = true }
spdx = { workspace = true } spdx = { workspace = true }
tar = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View file

@ -3,17 +3,21 @@ mod pep639_glob;
use crate::metadata::{PyProjectToml, ValidationError}; use crate::metadata::{PyProjectToml, ValidationError};
use crate::pep639_glob::Pep639GlobError; use crate::pep639_glob::Pep639GlobError;
use flate2::write::GzEncoder;
use flate2::Compression;
use fs_err::File; use fs_err::File;
use glob::{GlobError, PatternError}; use glob::{GlobError, PatternError};
use globset::{Glob, GlobSetBuilder};
use itertools::Itertools; use itertools::Itertools;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::fs::FileType; use std::fs::FileType;
use std::io::{BufReader, Read, Write}; use std::io::{BufReader, Cursor, Read, Write};
use std::path::{Path, PathBuf, StripPrefixError}; use std::path::{Path, PathBuf, StripPrefixError};
use std::{io, mem}; use std::{io, mem};
use tar::{EntryType, Header};
use thiserror::Error; use thiserror::Error;
use tracing::{debug, trace}; use tracing::{debug, trace};
use uv_distribution_filename::WheelFilename; use uv_distribution_filename::{SourceDistExtension, SourceDistFilename, WheelFilename};
use uv_fs::Simplified; use uv_fs::Simplified;
use walkdir::WalkDir; use walkdir::WalkDir;
use zip::{CompressionMethod, ZipWriter}; use zip::{CompressionMethod, ZipWriter};
@ -33,6 +37,9 @@ pub enum Error {
/// [`GlobError`] is a wrapped io error. /// [`GlobError`] is a wrapped io error.
#[error(transparent)] #[error(transparent)]
Glob(#[from] GlobError), Glob(#[from] GlobError),
/// [`globset::Error`] shows the glob that failed to parse.
#[error(transparent)]
GlobSet(#[from] globset::Error),
#[error("Failed to walk source tree: `{}`", root.user_display())] #[error("Failed to walk source tree: `{}`", root.user_display())]
WalkDir { WalkDir {
root: PathBuf, root: PathBuf,
@ -43,8 +50,8 @@ pub enum Error {
NotUtf8Path(PathBuf), NotUtf8Path(PathBuf),
#[error("Failed to walk source tree")] #[error("Failed to walk source tree")]
StripPrefix(#[from] StripPrefixError), StripPrefix(#[from] StripPrefixError),
#[error("Unsupported file type: {0:?}")] #[error("Unsupported file type {1:?}: `{}`", _0.user_display())]
UnsupportedFileType(FileType), UnsupportedFileType(PathBuf, FileType),
#[error("Failed to write wheel zip archive")] #[error("Failed to write wheel zip archive")]
Zip(#[from] zip::result::ZipError), Zip(#[from] zip::result::ZipError),
#[error("Failed to write RECORD file")] #[error("Failed to write RECORD file")]
@ -53,6 +60,8 @@ pub enum Error {
MissingModule(PathBuf), MissingModule(PathBuf),
#[error("Inconsistent metadata between prepare and build step: `{0}`")] #[error("Inconsistent metadata between prepare and build step: `{0}`")]
InconsistentSteps(&'static str), InconsistentSteps(&'static str),
#[error("Failed to write to {}", _0.user_display())]
TarWrite(PathBuf, #[source] io::Error),
} }
/// Allow dispatching between writing to a directory, writing to zip and writing to a `.tar.gz`. /// Allow dispatching between writing to a directory, writing to zip and writing to a `.tar.gz`.
@ -276,7 +285,7 @@ fn write_hashed(
} }
/// Build a wheel from the source tree and place it in the output directory. /// Build a wheel from the source tree and place it in the output directory.
pub fn build( pub fn build_wheel(
source_tree: &Path, source_tree: &Path,
wheel_dir: &Path, wheel_dir: &Path,
metadata_directory: Option<&Path>, metadata_directory: Option<&Path>,
@ -323,7 +332,10 @@ pub fn build(
wheel_writer.write_file(relative_path_str, entry.path())?; wheel_writer.write_file(relative_path_str, entry.path())?;
} else { } else {
// TODO(konsti): We may want to support symlinks, there is support for installing them. // TODO(konsti): We may want to support symlinks, there is support for installing them.
return Err(Error::UnsupportedFileType(entry.file_type())); return Err(Error::UnsupportedFileType(
entry.path().to_path_buf(),
entry.file_type(),
));
} }
entry.path(); entry.path();
@ -342,6 +354,126 @@ pub fn build(
Ok(filename) Ok(filename)
} }
/// Build a source distribution from the source tree and place it in the output directory.
pub fn build_source_dist(
source_tree: &Path,
source_dist_directory: &Path,
uv_version: &str,
) -> Result<SourceDistFilename, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
pyproject_toml.check_build_system(uv_version);
let filename = SourceDistFilename {
name: pyproject_toml.name().clone(),
version: pyproject_toml.version().clone(),
extension: SourceDistExtension::TarGz,
};
let top_level = format!("{}-{}", pyproject_toml.name(), pyproject_toml.version());
let source_dist_path = source_dist_directory.join(filename.to_string());
let tar_gz = File::create(&source_dist_path)?;
let enc = GzEncoder::new(tar_gz, Compression::default());
let mut tar = tar::Builder::new(enc);
let metadata = pyproject_toml
.to_metadata(source_tree)?
.core_metadata_format();
let mut header = Header::new_gnu();
header.set_size(metadata.bytes().len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(
&mut header,
Path::new(&top_level).join("PKG-INFO"),
Cursor::new(metadata),
)
.map_err(|err| Error::TarWrite(source_dist_path.clone(), err))?;
let includes = ["src/**/*", "pyproject.toml"];
let mut include_builder = GlobSetBuilder::new();
for include in includes {
include_builder.add(Glob::new(include)?);
}
let include_matcher = include_builder.build()?;
let excludes = ["__pycache__", "*.pyc", "*.pyo"];
let mut exclude_builder = GlobSetBuilder::new();
for exclude in excludes {
exclude_builder.add(Glob::new(exclude)?);
}
let exclude_matcher = exclude_builder.build()?;
// TODO(konsti): Add files linked by pyproject.toml
for file in WalkDir::new(source_tree).into_iter().filter_entry(|dir| {
let relative = dir
.path()
.strip_prefix(source_tree)
.expect("walkdir starts with root");
// TODO(konsti): Also check that we're matching at least a prefix of an include matcher.
!exclude_matcher.is_match(relative)
}) {
let entry = file.map_err(|err| Error::WalkDir {
root: source_tree.to_path_buf(),
err,
})?;
let relative = entry
.path()
.strip_prefix(source_tree)
.expect("walkdir starts with root");
if !include_matcher.is_match(relative) {
trace!("Excluding {}", relative.user_display());
continue;
}
debug!("Including {}", relative.user_display());
let metadata = fs_err::metadata(entry.path())?;
let mut header = Header::new_gnu();
#[cfg(unix)]
{
header.set_mode(std::os::unix::fs::MetadataExt::mode(&metadata));
}
#[cfg(not(unix))]
{
header.set_mode(0o644);
}
if entry.file_type().is_dir() {
header.set_entry_type(EntryType::Directory);
header
.set_path(Path::new(&top_level).join(relative))
.map_err(|err| Error::TarWrite(source_dist_path.clone(), err))?;
header.set_size(0);
header.set_cksum();
tar.append(&header, io::empty())
.map_err(|err| Error::TarWrite(source_dist_path.clone(), err))?;
continue;
} else if entry.file_type().is_file() {
header.set_size(metadata.len());
header.set_cksum();
tar.append_data(
&mut header,
Path::new(&top_level).join(relative),
BufReader::new(File::open(entry.path())?),
)
.map_err(|err| Error::TarWrite(source_dist_path.clone(), err))?;
} else {
return Err(Error::UnsupportedFileType(
relative.to_path_buf(),
entry.file_type(),
));
}
}
tar.finish()
.map_err(|err| Error::TarWrite(source_dist_path.clone(), err))?;
Ok(filename)
}
/// Write the dist-info directory to the output directory without building the wheel. /// Write the dist-info directory to the output directory without building the wheel.
pub fn metadata( pub fn metadata(
source_tree: &Path, source_tree: &Path,
@ -350,7 +482,7 @@ pub fn metadata(
) -> Result<String, Error> { ) -> Result<String, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?; let pyproject_toml = PyProjectToml::parse(&contents)?;
pyproject_toml.check_build_system("1.0.0+test"); pyproject_toml.check_build_system(uv_version);
let filename = WheelFilename { let filename = WheelFilename {
name: pyproject_toml.name().clone(), name: pyproject_toml.name().clone(),

View file

@ -46,7 +46,7 @@ fn test_record() {
fn test_determinism() { fn test_determinism() {
let temp1 = TempDir::new().unwrap(); let temp1 = TempDir::new().unwrap();
let uv_backend = Path::new("../../scripts/packages/uv_backend"); let uv_backend = Path::new("../../scripts/packages/uv_backend");
build(uv_backend, temp1.path(), None, "1.0.0+test").unwrap(); build_wheel(uv_backend, temp1.path(), None, "1.0.0+test").unwrap();
// Touch the file to check that we don't serialize the last modified date. // Touch the file to check that we don't serialize the last modified date.
fs_err::write( fs_err::write(
@ -56,7 +56,7 @@ fn test_determinism() {
.unwrap(); .unwrap();
let temp2 = TempDir::new().unwrap(); let temp2 = TempDir::new().unwrap();
build(uv_backend, temp2.path(), None, "1.0.0+test").unwrap(); build_wheel(uv_backend, temp2.path(), None, "1.0.0+test").unwrap();
let wheel_filename = "uv_backend-0.1.0-py3-none-any.whl"; let wheel_filename = "uv_backend-0.1.0-py3-none-any.whl";
assert_eq!( assert_eq!(

View file

@ -5,14 +5,20 @@ use anyhow::Result;
use std::env; use std::env;
use std::path::Path; use std::path::Path;
pub(crate) fn build_sdist(_sdist_directory: &Path) -> Result<ExitStatus> { pub(crate) fn build_sdist(sdist_directory: &Path) -> Result<ExitStatus> {
todo!() let filename = uv_build_backend::build_source_dist(
&env::current_dir()?,
sdist_directory,
uv_version::version(),
)?;
println!("{filename}");
Ok(ExitStatus::Success)
} }
pub(crate) fn build_wheel( pub(crate) fn build_wheel(
wheel_directory: &Path, wheel_directory: &Path,
metadata_directory: Option<&Path>, metadata_directory: Option<&Path>,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
let filename = uv_build_backend::build( let filename = uv_build_backend::build_wheel(
&env::current_dir()?, &env::current_dir()?,
wheel_directory, wheel_directory,
metadata_directory, metadata_directory,