Skip to content

Commit

Permalink
Add support for parameterized link modes (#164)
Browse files Browse the repository at this point in the history
Allows the user to select between clone, hardlink, and copy semantics
for installs. (The pnpm documentation has a decent description of what
these mean: https://pnpm.io/npmrc#package-import-method.)

Closes #159.
  • Loading branch information
charliermarsh authored Oct 22, 2023
1 parent 9bcc7fe commit 49a27ff
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 88 deletions.
42 changes: 21 additions & 21 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ platform-info = { version = "2.0.2" }
plist = { version = "1.5.0" }
pyproject-toml = { version = "0.7.0" }
rayon = { version = "1.8.0" }
reflink-copy = { version = "0.1.9" }
reflink-copy = { version = "0.1.10" }
regex = { version = "1.9.6" }
reqwest = { version = "0.11.22", features = ["json", "gzip", "brotli", "stream"] }
reqwest-middleware = { version = "0.2.3" }
Expand Down
4 changes: 1 addition & 3 deletions crates/install-wheel-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ pub use wheel::{
};

mod install_location;
pub mod linker;
#[cfg(feature = "python_bindings")]
mod python_bindings;
mod record;
#[cfg(any(target_os = "macos", target_os = "ios"))]
mod reflink;
mod script;
mod uninstall;
pub mod unpacked;
mod wheel;

#[derive(Error, Debug)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use crate::{read_record_file, Error, Script};
pub fn install_wheel(
location: &InstallLocation<impl AsRef<Path>>,
wheel: impl AsRef<Path>,
link_mode: LinkMode,
) -> Result<(), Error> {
let base_location = location.venv_base();

Expand Down Expand Up @@ -65,7 +66,7 @@ pub fn install_wheel(
// > 1.d Else unpack archive into platlib (site-packages).
// We always install in the same virtualenv site packages
debug!(name, "Extracting file");
let num_unpacked = unpack_wheel_files(&site_packages, &wheel)?;
let num_unpacked = link_mode.link_wheel_files(&site_packages, &wheel)?;
debug!(name, "Extracted {num_unpacked} files");

// Read the RECORD file.
Expand Down Expand Up @@ -243,14 +244,51 @@ fn parse_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(
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
pub enum LinkMode {
/// Clone (i.e., copy-on-write) packages from the wheel into the site packages.
Clone,
/// Copy packages from the wheel into the site packages.
Copy,
/// Hard link packages from the wheel into the site packages.
Hardlink,
}

impl Default for LinkMode {
fn default() -> Self {
if cfg!(any(target_os = "macos", target_os = "ios")) {
Self::Clone
} else {
Self::Hardlink
}
}
}

impl LinkMode {
/// Extract a wheel by linking all of its files into site packages.
pub fn link_wheel_files(
self,
site_packages: impl AsRef<Path>,
wheel: impl AsRef<Path>,
) -> Result<usize, Error> {
match self {
Self::Clone => clone_wheel_files(site_packages, wheel),
Self::Copy => copy_wheel_files(site_packages, wheel),
Self::Hardlink => hardlink_wheel_files(site_packages, wheel),
}
}
}

/// Extract a wheel by cloning all of its files into site packages. The files will be cloned
/// via copy-on-write, which is similar to a hard link, but allows the files to be modified
/// independently (that is, the file is copied upon modification).
///
/// This method uses `clonefile` on macOS, and `reflink` on Linux.
fn clone_wheel_files(
site_packages: impl AsRef<Path>,
wheel: impl AsRef<Path>,
) -> Result<usize, Error> {
use crate::reflink::reflink;

let mut count = 0usize;

// On macOS, directly can be recursively copied with a single `clonefile` call.
Expand All @@ -264,26 +302,21 @@ fn unpack_wheel_files(
.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)?;
}
}
fs::remove_dir_all(&to)
.or_else(|_| fs::remove_file(&to))
.ok();

// Copy the file.
reflink(&from, &to)?;
reflink_copy::reflink(&from, &to)?;

count += 1;
}

Ok(count)
}

/// Extract all files from the wheel into the site packages
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
fn unpack_wheel_files(
/// Extract a wheel by copying all of its files into site packages.
fn copy_wheel_files(
site_packages: impl AsRef<Path>,
wheel: impl AsRef<Path>,
) -> Result<usize, Error> {
Expand All @@ -300,7 +333,8 @@ fn unpack_wheel_files(
continue;
}

reflink_copy::reflink_or_copy(entry.path(), &out_path)?;
// Copy the file.
fs::copy(entry.path(), &out_path)?;

#[cfg(unix)]
{
Expand All @@ -320,3 +354,30 @@ fn unpack_wheel_files(

Ok(count)
}

/// Extract a wheel by hard-linking all of its files into site packages.
fn hardlink_wheel_files(
site_packages: impl AsRef<Path>,
wheel: impl AsRef<Path>,
) -> Result<usize, Error> {
let mut count = 0usize;

// Walk over the directory.
for entry in walkdir::WalkDir::new(&wheel) {
let entry = entry?;
let relative = entry.path().strip_prefix(&wheel).unwrap();
let out_path = site_packages.as_ref().join(relative);

if entry.file_type().is_dir() {
fs::create_dir_all(&out_path)?;
continue;
}

// Copy the file.
fs::hard_link(entry.path(), &out_path)?;

count += 1;
}

Ok(count)
}
39 changes: 0 additions & 39 deletions crates/install-wheel-rs/src/reflink.rs

This file was deleted.

6 changes: 5 additions & 1 deletion crates/puffin-cli/src/commands/pip_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::path::Path;

use anyhow::{Context, Result};
use colored::Colorize;
use install_wheel_rs::linker::LinkMode;
use itertools::Itertools;
use tracing::debug;

Expand All @@ -27,6 +28,7 @@ use crate::requirements::RequirementsSource;
/// Install a set of locked requirements into the current Python environment.
pub(crate) async fn pip_sync(
sources: &[RequirementsSource],
link_mode: LinkMode,
cache: Option<&Path>,
mut printer: Printer,
) -> Result<ExitStatus> {
Expand All @@ -42,12 +44,13 @@ pub(crate) async fn pip_sync(
return Ok(ExitStatus::Success);
}

sync_requirements(&requirements, cache, printer).await
sync_requirements(&requirements, link_mode, cache, printer).await
}

/// Install a set of locked requirements into the current Python environment.
pub(crate) async fn sync_requirements(
requirements: &[Requirement],
link_mode: LinkMode,
cache: Option<&Path>,
mut printer: Printer,
) -> Result<ExitStatus> {
Expand Down Expand Up @@ -211,6 +214,7 @@ pub(crate) async fn sync_requirements(
if !wheels.is_empty() {
let start = std::time::Instant::now();
puffin_installer::Installer::new(&python)
.with_link_mode(link_mode)
.with_reporter(InstallReporter::from(printer).with_length(wheels.len() as u64))
.install(&wheels)?;

Expand Down
Loading

0 comments on commit 49a27ff

Please sign in to comment.