Skip to content

Commit

Permalink
Split build backend into modules
Browse files Browse the repository at this point in the history
`lib.rs` has grown to large
  • Loading branch information
konstin committed Dec 1, 2024
1 parent 71601ee commit e701e2e
Show file tree
Hide file tree
Showing 4 changed files with 1,025 additions and 978 deletions.
263 changes: 263 additions & 0 deletions crates/uv-build-backend/src/fs_write_dispatcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
//! Dispatcher between writing to a directory, writing to a zip, writing to a `.tar.gz` and
//! listing files.
use crate::wheel::{write_hashed, RecordEntry};
use crate::Error;
use flate2::write::GzEncoder;
use flate2::Compression;
use fs_err::File;
use sha2::{Digest, Sha256};
use std::io::{BufReader, Cursor, Write};
use std::path::{Path, PathBuf};
use std::{io, mem};
use tar::{EntryType, Header};
use tracing::trace;
use uv_fs::Simplified;
use zip::{CompressionMethod, ZipWriter};

/// Dispatcher between writing to a directory, writing to a zip, writing to a `.tar.gz` and
/// listing files.
///
/// All paths are string types instead of path types since wheels are portable between platforms.
///
/// Contract: You must call close before dropping to obtain a valid output (dropping is fine in the
/// error case).
pub(crate) trait FsWriteDispatcher {
/// Add a file with the given content.
fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error>;

/// Add a local file.
fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error>;

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

/// Write the `RECORD` file and if applicable, the central directory.
fn close(self, dist_info_dir: &str) -> Result<(), Error>;
}

/// Zip archive (wheel) writer.
pub(crate) struct ZipDirectoryWriter {
writer: ZipWriter<File>,
compression: CompressionMethod,
/// The entries in the `RECORD` file.
record: Vec<RecordEntry>,
}

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

/// 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: File) -> Self {
Self {
writer: ZipWriter::new(file),
compression: CompressionMethod::Stored,
record: Vec::new(),
}
}

/// Add a file with the given name and return a writer for it.
fn new_writer<'slf>(&'slf mut self, path: &str) -> Result<Box<dyn Write + 'slf>, Error> {
// TODO(konsti): We need to preserve permissions, at least the executable bit.
self.writer.start_file(
path,
zip::write::FileOptions::default().compression_method(self.compression),
)?;
Ok(Box::new(&mut self.writer))
}
}

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

let hash = format!("{:x}", Sha256::new().chain_update(bytes).finalize());
self.record.push(RecordEntry {
path: path.to_string(),
hash,
size: bytes.len(),
});

Ok(())
}

fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> {
trace!("Adding {} from {}", path, file.user_display());
let mut reader = BufReader::new(File::open(file)?);
let mut writer = self.new_writer(path)?;
let record = write_hashed(path, &mut reader, &mut writer)?;
drop(writer);
self.record.push(record);
Ok(())
}

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

/// Write the `RECORD` file and the central directory.
fn close(mut self, dist_info_dir: &str) -> Result<(), Error> {
let record_path = format!("{dist_info_dir}/RECORD");
trace!("Adding {record_path}");
let record = mem::take(&mut self.record);
crate::wheel::write_record(&mut self.new_writer(&record_path)?, dist_info_dir, record)?;

trace!("Adding central directory");
self.writer.finish()?;
Ok(())
}
}

pub(crate) struct FilesystemWriter {
/// The virtualenv or metadata directory that add file paths are relative to.
root: PathBuf,
/// The entries in the `RECORD` file.
record: Vec<RecordEntry>,
}

impl FilesystemWriter {
pub(crate) fn new(root: &Path) -> Self {
Self {
root: root.to_owned(),
record: Vec::new(),
}
}

/// Add a file with the given name and return a writer for it.
fn new_writer<'slf>(&'slf mut self, path: &str) -> Result<Box<dyn Write + 'slf>, Error> {
trace!("Adding {}", path);
Ok(Box::new(File::create(self.root.join(path))?))
}
}

/// File system writer.
impl FsWriteDispatcher for FilesystemWriter {
fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> {
trace!("Adding {}", path);
let hash = format!("{:x}", Sha256::new().chain_update(bytes).finalize());
self.record.push(RecordEntry {
path: path.to_string(),
hash,
size: bytes.len(),
});

Ok(fs_err::write(self.root.join(path), bytes)?)
}
fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> {
trace!("Adding {} from {}", path, file.user_display());
let mut reader = BufReader::new(File::open(file)?);
let mut writer = self.new_writer(path)?;
let record = write_hashed(path, &mut reader, &mut writer)?;
drop(writer);
self.record.push(record);
Ok(())
}

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

/// Write the `RECORD` file.
fn close(mut self, dist_info_dir: &str) -> Result<(), Error> {
let record = mem::take(&mut self.record);
crate::wheel::write_record(
&mut self.new_writer(&format!("{dist_info_dir}/RECORD"))?,
dist_info_dir,
record,
)?;

Ok(())
}
}

pub(crate) struct TarGzWriter {
path: PathBuf,
tar: tar::Builder<GzEncoder<File>>,
}

impl TarGzWriter {
pub(crate) fn new(path: impl Into<PathBuf>) -> Result<Self, Error> {
let path = path.into();
let file = File::create(&path)?;
let enc = GzEncoder::new(file, Compression::default());
let tar = tar::Builder::new(enc);
Ok(Self { path, tar })
}
}

impl FsWriteDispatcher for TarGzWriter {
fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> {
let mut header = Header::new_gnu();
header.set_size(bytes.len() as u64);
// Reasonable default to avoid 0o000 permissions, the user's umask will be applied on
// unpacking.
header.set_mode(0o644);
header.set_cksum();
self.tar
.append_data(&mut header, path, Cursor::new(bytes))
.map_err(|err| Error::TarWrite(self.path.clone(), err))?;
Ok(())
}

fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> {
let metadata = fs_err::metadata(file)?;
let mut header = Header::new_gnu();
#[cfg(unix)]
{
// Preserve for example an executable bit.
header.set_mode(std::os::unix::fs::MetadataExt::mode(&metadata));
}
#[cfg(not(unix))]
{
// Reasonable default to avoid 0o000 permissions, the user's umask will be applied on
// unpacking.
header.set_mode(0o644);
}
header.set_size(metadata.len());
header.set_cksum();
let reader = BufReader::new(File::open(file)?);
self.tar
.append_data(&mut header, path, reader)
.map_err(|err| Error::TarWrite(self.path.clone(), err))?;
Ok(())
}

fn write_directory(&mut self, directory: &str) -> Result<(), Error> {
let mut header = Header::new_gnu();
// Directories are always executable, which means they can be listed.
header.set_mode(0o755);
header.set_entry_type(EntryType::Directory);
header
.set_path(directory)
.map_err(|err| Error::TarWrite(self.path.clone(), err))?;
header.set_size(0);
header.set_cksum();
self.tar
.append(&header, io::empty())
.map_err(|err| Error::TarWrite(self.path.clone(), err))?;
Ok(())
}

fn close(mut self, _dist_info_dir: &str) -> Result<(), Error> {
self.tar
.finish()
.map_err(|err| Error::TarWrite(self.path.clone(), err))?;
Ok(())
}
}
Loading

0 comments on commit e701e2e

Please sign in to comment.