mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Use recursive clonefile
calls on macOS (#67)
It turns out that on macOS, you can pass `clonefile` a directory to recursively copy an entire directory. This speeds up wheel installation dramatically, by about 3x.
This commit is contained in:
parent
1c942ab8fe
commit
adbee4fb32
3 changed files with 77 additions and 7 deletions
|
@ -21,6 +21,8 @@ mod install_location;
|
||||||
#[cfg(feature = "python_bindings")]
|
#[cfg(feature = "python_bindings")]
|
||||||
mod python_bindings;
|
mod python_bindings;
|
||||||
mod record;
|
mod record;
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
|
mod reflink;
|
||||||
mod script;
|
mod script;
|
||||||
pub mod unpacked;
|
pub mod unpacked;
|
||||||
mod wheel;
|
mod wheel;
|
||||||
|
|
39
crates/install-wheel-rs/src/reflink.rs
Normal file
39
crates/install-wheel-rs/src/reflink.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
//! Reflink a file on macOS via `clonefile`.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
ffi::CString,
|
||||||
|
io,
|
||||||
|
os::{
|
||||||
|
raw::{c_char, c_int},
|
||||||
|
unix::ffi::OsStrExt,
|
||||||
|
},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn cstr(path: &Path) -> io::Result<CString> {
|
||||||
|
Ok(CString::new(path.as_os_str().as_bytes())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// const CLONE_NOFOLLOW: c_int = 0x0001;
|
||||||
|
const CLONE_NOOWNERCOPY: c_int = 0x0002;
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
// http://www.manpagez.com/man/2/clonefileat/
|
||||||
|
// https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/bsd/sys/clonefile.h
|
||||||
|
// TODO We need weak linkage here (OSX > 10.12, iOS > 10.0), otherwise compilation will fail on older versions
|
||||||
|
fn clonefile(src: *const c_char, dest: *const c_char, flags: c_int) -> c_int;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reflink(from: &Path, to: &Path) -> io::Result<()> {
|
||||||
|
let src = cstr(from)?;
|
||||||
|
let dest = cstr(to)?;
|
||||||
|
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
let ret = unsafe { clonefile(src.as_ptr(), dest.as_ptr(), CLONE_NOOWNERCOPY) };
|
||||||
|
|
||||||
|
if ret == -1 {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,6 @@ use fs_err as fs;
|
||||||
use fs_err::File;
|
use fs_err::File;
|
||||||
use mailparse::MailHeaderMap;
|
use mailparse::MailHeaderMap;
|
||||||
use tracing::{debug, span, Level};
|
use tracing::{debug, span, Level};
|
||||||
use walkdir::WalkDir;
|
|
||||||
|
|
||||||
use wheel_filename::WheelFilename;
|
use wheel_filename::WheelFilename;
|
||||||
|
|
||||||
|
@ -78,7 +77,7 @@ pub fn install_wheel(
|
||||||
// We always install in the same virtualenv site packages
|
// We always install in the same virtualenv site packages
|
||||||
debug!(name = name.as_str(), "Extracting file");
|
debug!(name = name.as_str(), "Extracting file");
|
||||||
let num_unpacked = unpack_wheel_files(&site_packages, wheel)?;
|
let num_unpacked = unpack_wheel_files(&site_packages, wheel)?;
|
||||||
debug!(name = name.as_str(), "Extracted {num_unpacked} files",);
|
debug!(name = name.as_str(), "Extracted {num_unpacked} files");
|
||||||
|
|
||||||
// Read the RECORD file.
|
// Read the RECORD file.
|
||||||
let mut record_file = File::open(&wheel.join(format!("{dist_info_prefix}.dist-info/RECORD")))?;
|
let mut record_file = File::open(&wheel.join(format!("{dist_info_prefix}.dist-info/RECORD")))?;
|
||||||
|
@ -244,16 +243,46 @@ fn parse_scripts(
|
||||||
Ok((console_scripts, gui_scripts))
|
Ok((console_scripts, gui_scripts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract all files from the wheel into the site packages.
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
|
fn unpack_wheel_files(site_packages: &Path, wheel: &Path) -> Result<usize, Error> {
|
||||||
|
use crate::reflink::reflink;
|
||||||
|
|
||||||
|
let mut count = 0usize;
|
||||||
|
|
||||||
|
// On macOS, directly can be recursively copied with a single `clonefile` call.
|
||||||
|
// So we only need to iterate over the top-level of the directory, and copy each file or
|
||||||
|
// subdirectory.
|
||||||
|
for entry in std::fs::read_dir(wheel)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let from = entry.path();
|
||||||
|
let to = site_packages.join(from.strip_prefix(wheel).unwrap());
|
||||||
|
|
||||||
|
// Delete the destination if it already exists.
|
||||||
|
if let Ok(metadata) = to.metadata() {
|
||||||
|
if metadata.is_dir() {
|
||||||
|
fs::remove_dir_all(&to)?;
|
||||||
|
} else if metadata.is_file() {
|
||||||
|
fs::remove_file(&to)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file.
|
||||||
|
reflink(&from, &to)?;
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
/// Extract all files from the wheel into the site packages
|
/// Extract all files from the wheel into the site packages
|
||||||
///
|
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
|
||||||
/// Matches with the RECORD entries
|
|
||||||
///
|
|
||||||
/// Returns paths relative to site packages
|
|
||||||
fn unpack_wheel_files(site_packages: &Path, wheel: &Path) -> Result<usize, Error> {
|
fn unpack_wheel_files(site_packages: &Path, wheel: &Path) -> Result<usize, Error> {
|
||||||
let mut count = 0usize;
|
let mut count = 0usize;
|
||||||
|
|
||||||
// Walk over the directory.
|
// Walk over the directory.
|
||||||
for entry in WalkDir::new(wheel) {
|
for entry in walkdir::WalkDir::new(wheel) {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
let relative = entry.path().strip_prefix(wheel).unwrap();
|
let relative = entry.path().strip_prefix(wheel).unwrap();
|
||||||
let out_path = site_packages.join(relative);
|
let out_path = site_packages.join(relative);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue