diff --git a/Cargo.lock b/Cargo.lock index 7be6f474d473..4422667ec76a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4271,7 +4271,6 @@ dependencies = [ name = "uv-build-backend" version = "0.1.0" dependencies = [ - "async_zip", "fs-err", "glob", "indoc", @@ -4281,8 +4280,6 @@ dependencies = [ "spdx", "tempfile", "thiserror", - "tokio", - "tokio-util", "toml", "tracing", "uv-distribution-filename", @@ -4294,6 +4291,7 @@ dependencies = [ "uv-pypi-types", "uv-version", "uv-warnings", + "zip", ] [[package]] diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index 9ecdcbf60942..2ba5734f2cb5 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -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 diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index f1e747ae09a2..7f27c98e090b 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -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 { @@ -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>); - -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, + 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(()) } } @@ -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 { - let contents = fs_err::tokio::read_to_string(source_tree.join("pyproject.toml")).await?; +pub fn build(source_tree: &Path, wheel_dir: &Path) -> Result { + let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; let pyproject_toml = PyProjectToml::parse(&contents)?; pyproject_toml.check_build_system(); @@ -110,25 +122,24 @@ pub async fn build(source_tree: &Path, wheel_dir: &Path) -> Result Result { - let contents = fs_err::tokio::read_to_string(source_tree.join("pyproject.toml")).await?; +pub fn metadata(source_tree: &Path, metadata_directory: &Path) -> Result { + 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", @@ -138,29 +149,27 @@ pub async fn metadata(source_tree: &Path, metadata_directory: &Path) -> Result 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(()) } diff --git a/crates/uv-build-backend/src/metadata.rs b/crates/uv-build-backend/src/metadata.rs index 6322aaec0b8c..638cd2be55ac 100644 --- a/crates/uv-build-backend/src/metadata.rs +++ b/crates/uv-build-backend/src/metadata.rs @@ -161,7 +161,7 @@ impl PyProjectToml { /// /// /// - pub(crate) async fn to_metadata(&self, root: &Path) -> Result { + pub(crate) fn to_metadata(&self, root: &Path) -> Result { let summary = if let Some(description) = &self.project.description { if description.contains('\n') { return Err(ValidationError::DescriptionNewlines.into()); @@ -174,7 +174,7 @@ impl PyProjectToml { let supported_content_types = ["text/plain", "text/x-rst", "text/markdown"]; let (description, description_content_type) = match &self.project.readme { Some(Readme::String(path)) => { - let content = fs_err::tokio::read_to_string(root.join(path)).await?; + let content = fs_err::read_to_string(root.join(path))?; let content_type = match path.extension().and_then(OsStr::to_str) { Some("txt") => "text/plain", Some("rst") => "text/x-rst", @@ -192,7 +192,7 @@ impl PyProjectToml { content_type, charset, }) => { - let content = fs_err::tokio::read_to_string(root.join(file)).await?; + let content = fs_err::read_to_string(root.join(file))?; if !supported_content_types.contains(&content_type.as_str()) { return Err( ValidationError::UnsupportedContentType(content_type.clone()).into(), @@ -346,7 +346,7 @@ impl PyProjectToml { } Some(License::Text { text }) => (Some(text.clone()), None, Vec::new()), Some(License::File { file }) => { - let text = fs_err::tokio::read_to_string(root.join(file)).await?; + let text = fs_err::read_to_string(root.join(file))?; (Some(text), None, Vec::new()) } } @@ -653,8 +653,8 @@ mod tests { formatted } - #[tokio::test] - async fn valid() { + #[test] + fn valid() { let temp_dir = TempDir::new().unwrap(); fs_err::write( @@ -728,7 +728,7 @@ mod tests { }; let pyproject_toml = PyProjectToml::parse(contents).unwrap(); - let metadata = pyproject_toml.to_metadata(temp_dir.path()).await.unwrap(); + let metadata = pyproject_toml.to_metadata(temp_dir.path()).unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r###" Metadata-Version: 2.3 @@ -841,14 +841,13 @@ mod tests { assert!(!pyproject_toml.check_build_system()); } - #[tokio::test] - async fn minimal() { + #[test] + fn minimal() { let contents = extend_project(""); let metadata = PyProjectToml::parse(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) - .await .unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r###" @@ -876,8 +875,8 @@ mod tests { "###); } - #[tokio::test] - async fn missing_readme() { + #[test] + fn missing_readme() { let contents = extend_project(indoc! {r#" readme = "Readme.md" "# @@ -886,16 +885,13 @@ mod tests { let err = PyProjectToml::parse(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) - .await .unwrap_err(); // Simplified for windows compatibility. - assert_snapshot!(err.to_string().replace('\\', "/"), @r###" - failed to read from file `/do/not/read/Readme.md` - "###); + assert_snapshot!(err.to_string().replace('\\', "/"), @"failed to open file `/do/not/read/Readme.md`"); } - #[tokio::test] - async fn multiline_description() { + #[test] + fn multiline_description() { let contents = extend_project(indoc! {r#" description = "Hi :)\nThis is my project" "# @@ -904,7 +900,6 @@ mod tests { let err = PyProjectToml::parse(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) - .await .unwrap_err(); assert_snapshot!(format_err(err), @r###" Invalid pyproject.toml @@ -912,8 +907,8 @@ mod tests { "###); } - #[tokio::test] - async fn mixed_licenses() { + #[test] + fn mixed_licenses() { let contents = extend_project(indoc! {r#" license-files = ["licenses/*"] license = { text = "MIT" } @@ -923,7 +918,6 @@ mod tests { let err = PyProjectToml::parse(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) - .await .unwrap_err(); assert_snapshot!(format_err(err), @r###" Invalid pyproject.toml @@ -931,8 +925,8 @@ mod tests { "###); } - #[tokio::test] - async fn valid_license() { + #[test] + fn valid_license() { let contents = extend_project(indoc! {r#" license = "MIT OR Apache-2.0" "# @@ -940,7 +934,6 @@ mod tests { let metadata = PyProjectToml::parse(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) - .await .unwrap(); assert_snapshot!(metadata.core_metadata_format(), @r###" Metadata-Version: 2.4 @@ -950,8 +943,8 @@ mod tests { "###); } - #[tokio::test] - async fn invalid_license() { + #[test] + fn invalid_license() { let contents = extend_project(indoc! {r#" license = "MIT XOR Apache-2" "# @@ -959,7 +952,6 @@ mod tests { let err = PyProjectToml::parse(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) - .await .unwrap_err(); // TODO(konsti): We mess up the indentation in the error. assert_snapshot!(format_err(err), @r###" @@ -970,8 +962,8 @@ mod tests { "###); } - #[tokio::test] - async fn dynamic() { + #[test] + fn dynamic() { let contents = extend_project(indoc! {r#" dynamic = ["dependencies"] "# @@ -980,7 +972,6 @@ mod tests { let err = PyProjectToml::parse(&contents) .unwrap() .to_metadata(Path::new("/do/not/read")) - .await .unwrap_err(); assert_snapshot!(format_err(err), @r###" Invalid pyproject.toml diff --git a/crates/uv/src/commands/build_backend.rs b/crates/uv/src/commands/build_backend.rs index f8b159e2a1bd..aa62ce47c280 100644 --- a/crates/uv/src/commands/build_backend.rs +++ b/crates/uv/src/commands/build_backend.rs @@ -5,54 +5,42 @@ use anyhow::Result; use std::env; use std::path::Path; -#[expect(clippy::unused_async)] -pub(crate) async fn build_sdist(_sdist_directory: &Path) -> Result { +pub(crate) fn build_sdist(_sdist_directory: &Path) -> Result { todo!() } - -pub(crate) async fn build_wheel( +pub(crate) fn build_wheel( wheel_directory: &Path, _metadata_directory: Option<&Path>, ) -> Result { - let filename = uv_build_backend::build(&env::current_dir()?, wheel_directory).await?; + let filename = uv_build_backend::build(&env::current_dir()?, wheel_directory)?; println!("{filename}"); Ok(ExitStatus::Success) } -#[expect(clippy::unused_async)] -pub(crate) async fn build_editable( +pub(crate) fn build_editable( _wheel_directory: &Path, _metadata_directory: Option<&Path>, ) -> Result { todo!() } -#[expect(clippy::unused_async)] -pub(crate) async fn get_requires_for_build_sdist() -> Result { +pub(crate) fn get_requires_for_build_sdist() -> Result { todo!() } -#[expect(clippy::unused_async)] -pub(crate) async fn get_requires_for_build_wheel() -> Result { +pub(crate) fn get_requires_for_build_wheel() -> Result { todo!() } - -pub(crate) async fn prepare_metadata_for_build_wheel( - metadata_directory: &Path, -) -> Result { - let filename = uv_build_backend::metadata(&env::current_dir()?, metadata_directory).await?; +pub(crate) fn prepare_metadata_for_build_wheel(metadata_directory: &Path) -> Result { + let filename = uv_build_backend::metadata(&env::current_dir()?, metadata_directory)?; println!("{filename}"); Ok(ExitStatus::Success) } -#[expect(clippy::unused_async)] -pub(crate) async fn get_requires_for_build_editable() -> Result { +pub(crate) fn get_requires_for_build_editable() -> Result { todo!() } -#[expect(clippy::unused_async)] -pub(crate) async fn prepare_metadata_for_build_editable( - _wheel_directory: &Path, -) -> Result { +pub(crate) fn prepare_metadata_for_build_editable(_wheel_directory: &Path) -> Result { todo!() } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 4b36cc4e6dc6..ef7e9513fd8f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -11,6 +11,7 @@ use clap::error::{ContextKind, ContextValue}; use clap::{CommandFactory, Parser}; use owo_colors::OwoColorize; use settings::PipTreeSettings; +use tokio::task::spawn_blocking; use tracing::{debug, instrument}; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; @@ -1113,46 +1114,42 @@ async fn run(cli: Cli) -> Result { ) .await } - Commands::BuildBackend { command } => match command { + Commands::BuildBackend { command } => spawn_blocking(move || match command { BuildBackendCommand::BuildSdist { sdist_directory } => { - commands::build_backend::build_sdist(&sdist_directory).await + commands::build_backend::build_sdist(&sdist_directory) } BuildBackendCommand::BuildWheel { wheel_directory, metadata_directory, - } => { - commands::build_backend::build_wheel( - &wheel_directory, - metadata_directory.as_deref(), - ) - .await - } + } => commands::build_backend::build_wheel( + &wheel_directory, + metadata_directory.as_deref(), + ), BuildBackendCommand::BuildEditable { wheel_directory, metadata_directory, - } => { - commands::build_backend::build_editable( - &wheel_directory, - metadata_directory.as_deref(), - ) - .await - } + } => commands::build_backend::build_editable( + &wheel_directory, + metadata_directory.as_deref(), + ), BuildBackendCommand::GetRequiresForBuildSdist => { - commands::build_backend::get_requires_for_build_sdist().await + commands::build_backend::get_requires_for_build_sdist() } BuildBackendCommand::GetRequiresForBuildWheel => { - commands::build_backend::get_requires_for_build_wheel().await + commands::build_backend::get_requires_for_build_wheel() } BuildBackendCommand::PrepareMetadataForBuildWheel { wheel_directory } => { - commands::build_backend::prepare_metadata_for_build_wheel(&wheel_directory).await + commands::build_backend::prepare_metadata_for_build_wheel(&wheel_directory) } BuildBackendCommand::GetRequiresForBuildEditable => { - commands::build_backend::get_requires_for_build_editable().await + commands::build_backend::get_requires_for_build_editable() } BuildBackendCommand::PrepareMetadataForBuildEditable { wheel_directory } => { - commands::build_backend::prepare_metadata_for_build_editable(&wheel_directory).await + commands::build_backend::prepare_metadata_for_build_editable(&wheel_directory) } - }, + }) + .await + .expect("tokio threadpool exited unexpectedly"), } }