Skip to content

Commit

Permalink
Implement build backend metadata (#7961)
Browse files Browse the repository at this point in the history
  • Loading branch information
konstin authored Oct 7, 2024
1 parent 92538ad commit 5d789c6
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 152 deletions.
4 changes: 1 addition & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions crates/uv-build-backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,15 @@ uv-pypi-types = { workspace = true }
uv-version = { workspace = true }
uv-warnings = { workspace = true }

async_zip = { workspace = true }
fs-err = { workspace = true, features = ["tokio"] }
fs-err = { workspace = true }
glob = { workspace = true }
itertools = { workspace = true }
serde = { workspace = true }
spdx = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tokio-util = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
zip = { workspace = true }

[lints]
workspace = true
Expand Down
149 changes: 79 additions & 70 deletions crates/uv-build-backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ mod pep639_glob;

use crate::metadata::{PyProjectToml, ValidationError};
use crate::pep639_glob::Pep639GlobError;
use async_zip::base::write::ZipFileWriter;
use async_zip::error::ZipError;
use async_zip::{Compression, ZipEntryBuilder, ZipString};
use glob::{GlobError, PatternError};
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use thiserror::Error;
use uv_distribution_filename::WheelFilename;
use zip::{CompressionMethod, ZipWriter};

#[derive(Debug, Error)]
pub enum Error {
Expand All @@ -28,51 +27,67 @@ pub enum Error {
#[error(transparent)]
Glob(#[from] GlobError),
#[error("Failed to write wheel zip archive")]
Zip(#[from] ZipError),
Zip(#[from] zip::result::ZipError),
}

/// Allow dispatching between writing to a directory, writing to zip and writing to a `.tar.gz`.
trait AsyncDirectoryWrite: Sized {
async fn write_bytes(
&mut self,
directory: &Path,
filename: &str,
bytes: &[u8],
) -> Result<(), Error>;
///
/// All paths are string types instead of path types since wheel are portable between platforms.
trait DirectoryWriter: Sized {
/// Add a file with the given content.
fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error>;

/// Create a directory.
fn write_directory(&mut self, directory: &str) -> Result<(), Error>;

#[allow(clippy::unused_async)] // https://github.com/rust-lang/rust-clippy/issues/11660
async fn close(self) -> Result<(), Error> {
fn close(self) -> Result<(), Error> {
Ok(())
}
}

/// Zip archive (wheel) writer.
struct AsyncZipWriter(ZipFileWriter<tokio_util::compat::Compat<fs_err::tokio::File>>);

impl AsyncDirectoryWrite for AsyncZipWriter {
async fn write_bytes(
&mut self,
directory: &Path,
filename: &str,
bytes: &[u8],
) -> Result<(), Error> {
self.0
.write_entry_whole(
ZipEntryBuilder::new(
ZipString::from(format!("{}/{}", directory.display(), filename)),
// TODO(konsti): Editables use stored.
Compression::Deflate,
)
// https://github.com/Majored/rs-async-zip/issues/150
.unix_permissions(0o644),
bytes,
)
.await?;
struct ZipDirectoryWriter {
writer: ZipWriter<fs_err::File>,
compression: CompressionMethod,
}

impl ZipDirectoryWriter {
/// A wheel writer with deflate compression.
fn new_wheel(file: fs_err::File) -> Self {
Self {
writer: ZipWriter::new(file),
compression: CompressionMethod::Deflated,
}
}

/// A wheel writer with no (stored) compression.
///
/// Since editables are temporary, we save time be skipping compression and decompression.
#[expect(dead_code)]
fn new_editable(file: fs_err::File) -> Self {
Self {
writer: ZipWriter::new(file),
compression: CompressionMethod::Stored,
}
}
}

impl DirectoryWriter for ZipDirectoryWriter {
fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> {
let options = zip::write::FileOptions::default().compression_method(self.compression);
self.writer.start_file(path, options)?;
self.writer.write_all(bytes)?;
Ok(())
}

async fn close(self) -> Result<(), Error> {
self.0.close().await?;
fn write_directory(&mut self, directory: &str) -> Result<(), Error> {
let options = zip::write::FileOptions::default().compression_method(self.compression);
Ok(self.writer.add_directory(directory, options)?)
}

fn close(mut self) -> Result<(), Error> {
self.writer.finish()?;
Ok(())
}
}
Expand All @@ -82,22 +97,19 @@ struct AsyncFsWriter {
}

/// File system writer.
impl AsyncDirectoryWrite for AsyncFsWriter {
async fn write_bytes(
&mut self,
directory: &Path,
filename: &str,
bytes: &[u8],
) -> Result<(), Error> {
fs_err::tokio::create_dir_all(self.root.join(directory)).await?;
fs_err::tokio::write(self.root.join(directory).join(filename), bytes).await?;
Ok(())
impl DirectoryWriter for AsyncFsWriter {
fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> {
Ok(fs_err::write(path, bytes)?)
}

fn write_directory(&mut self, directory: &str) -> Result<(), Error> {
Ok(fs_err::create_dir(self.root.join(directory))?)
}
}

/// Build a wheel from the source tree and place it in the output directory.
pub async fn build(source_tree: &Path, wheel_dir: &Path) -> Result<WheelFilename, Error> {
let contents = fs_err::tokio::read_to_string(source_tree.join("pyproject.toml")).await?;
pub fn build(source_tree: &Path, wheel_dir: &Path) -> Result<WheelFilename, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
pyproject_toml.check_build_system();

Expand All @@ -110,25 +122,24 @@ pub async fn build(source_tree: &Path, wheel_dir: &Path) -> Result<WheelFilename
platform_tag: vec!["any".to_string()],
};

// TODO(konsti): async-zip doesn't like a buffered writer
let wheel_file = fs_err::tokio::File::create(wheel_dir.join(filename.to_string())).await?;
let mut wheel_writer = AsyncZipWriter(ZipFileWriter::with_tokio(wheel_file));
write_dist_info(&mut wheel_writer, &pyproject_toml, source_tree).await?;
wheel_writer.close().await?;
let mut wheel_writer =
ZipDirectoryWriter::new_wheel(fs_err::File::create(wheel_dir.join(filename.to_string()))?);
write_dist_info(&mut wheel_writer, &pyproject_toml, source_tree)?;
wheel_writer.close()?;
Ok(filename)
}

/// Write the dist-info directory to the output directory without building the wheel.
pub async fn metadata(source_tree: &Path, metadata_directory: &Path) -> Result<String, Error> {
let contents = fs_err::tokio::read_to_string(source_tree.join("pyproject.toml")).await?;
pub fn metadata(source_tree: &Path, metadata_directory: &Path) -> Result<String, Error> {
let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?;
let pyproject_toml = PyProjectToml::parse(&contents)?;
pyproject_toml.check_build_system();

let mut wheel_writer = AsyncFsWriter {
root: metadata_directory.to_path_buf(),
};
write_dist_info(&mut wheel_writer, &pyproject_toml, source_tree).await?;
wheel_writer.close().await?;
write_dist_info(&mut wheel_writer, &pyproject_toml, source_tree)?;
wheel_writer.close()?;

Ok(format!(
"{}-{}.dist-info",
Expand All @@ -138,29 +149,27 @@ pub async fn metadata(source_tree: &Path, metadata_directory: &Path) -> Result<S
}

/// Add `METADATA` and `entry_points.txt` to the dist-info directory.
async fn write_dist_info(
writer: &mut impl AsyncDirectoryWrite,
fn write_dist_info(
writer: &mut impl DirectoryWriter,
pyproject_toml: &PyProjectToml,
root: &Path,
) -> Result<(), Error> {
let dist_info_dir = PathBuf::from(format!(
let dist_info_dir = format!(
"{}-{}.dist-info",
pyproject_toml.name().as_dist_info_name(),
pyproject_toml.version()
));
);

writer.write_directory(&dist_info_dir)?;

let metadata = pyproject_toml
.to_metadata(root)
.await?
.core_metadata_format();
writer
.write_bytes(&dist_info_dir, "METADATA", metadata.as_bytes())
.await?;
let metadata = pyproject_toml.to_metadata(root)?.core_metadata_format();
writer.write_bytes(&format!("{dist_info_dir}/METADATA"), metadata.as_bytes())?;

let entrypoint = pyproject_toml.to_entry_points()?;
writer
.write_bytes(&dist_info_dir, "entry_points.txt", entrypoint.as_bytes())
.await?;
writer.write_bytes(
&format!("{dist_info_dir}/entry_points.txt"),
entrypoint.as_bytes(),
)?;

Ok(())
}
Loading

0 comments on commit 5d789c6

Please sign in to comment.