Skip to content

Commit da3a7ec

Browse files
authored
Linker copies files as a fallback when ref-linking fails (#1773)
## Summary Fixes #1444. In situations where the installer fails to perform a reflink, a regular file copy is also attempted, as a fallback. This circumvents issues with linking files across filesystems or volumes. ## Test Plan N/A
1 parent 1652844 commit da3a7ec

File tree

1 file changed

+90
-38
lines changed

1 file changed

+90
-38
lines changed

crates/install-wheel-rs/src/linker.rs

+90-38
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::str::FromStr;
77
use configparser::ini::Ini;
88
use fs_err as fs;
99
use fs_err::{DirEntry, File};
10+
use reflink_copy as reflink;
1011
use tempfile::tempdir_in;
1112
use tracing::{debug, instrument};
1213

@@ -280,55 +281,118 @@ fn clone_wheel_files(
280281
wheel: impl AsRef<Path>,
281282
) -> Result<usize, Error> {
282283
let mut count = 0usize;
284+
let mut attempt = Attempt::default();
283285

284286
// On macOS, directly can be recursively copied with a single `clonefile` call.
285287
// So we only need to iterate over the top-level of the directory, and copy each file or
286288
// subdirectory unless the subdirectory exists already in which case we'll need to recursively
287289
// merge its contents with the existing directory.
288290
for entry in fs::read_dir(wheel.as_ref())? {
289-
clone_recursive(site_packages.as_ref(), wheel.as_ref(), &entry?)?;
291+
clone_recursive(
292+
site_packages.as_ref(),
293+
wheel.as_ref(),
294+
&entry?,
295+
&mut attempt,
296+
)?;
290297
count += 1;
291298
}
292299

293300
Ok(count)
294301
}
295302

303+
// Hard linking / reflinking might not be supported but we (afaik) can't detect this ahead of time,
304+
// so we'll try hard linking / reflinking the first file - if this succeeds we'll know later
305+
// errors are not due to lack of os/fs support. If it fails, we'll switch to copying for the rest of the
306+
// install.
307+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
308+
enum Attempt {
309+
#[default]
310+
Initial,
311+
Subsequent,
312+
UseCopyFallback,
313+
}
314+
296315
/// Recursively clone the contents of `from` into `to`.
297-
fn clone_recursive(site_packages: &Path, wheel: &Path, entry: &DirEntry) -> Result<(), Error> {
316+
fn clone_recursive(
317+
site_packages: &Path,
318+
wheel: &Path,
319+
entry: &DirEntry,
320+
attempt: &mut Attempt,
321+
) -> Result<(), Error> {
298322
// Determine the existing and destination paths.
299323
let from = entry.path();
300324
let to = site_packages.join(from.strip_prefix(wheel).unwrap());
301325

302326
debug!("Cloning {} to {}", from.display(), to.display());
303327

304-
// Attempt to copy the file or directory
305-
let reflink = reflink_copy::reflink(&from, &to);
306-
307-
if reflink
308-
.as_ref()
309-
.is_err_and(|err| matches!(err.kind(), std::io::ErrorKind::AlreadyExists))
310-
{
311-
// If copying fails and the directory exists already, it must be merged recursively.
312-
if entry.file_type()?.is_dir() {
313-
for entry in fs::read_dir(from)? {
314-
clone_recursive(site_packages, wheel, &entry?)?;
328+
match attempt {
329+
Attempt::Initial => {
330+
if let Err(err) = reflink::reflink(&from, &to) {
331+
if matches!(err.kind(), std::io::ErrorKind::AlreadyExists) {
332+
// If cloning/copying fails and the directory exists already, it must be merged recursively.
333+
if entry.file_type()?.is_dir() {
334+
for entry in fs::read_dir(from)? {
335+
clone_recursive(site_packages, wheel, &entry?, attempt)?;
336+
}
337+
} else {
338+
// If file already exists, overwrite it.
339+
let tempdir = tempdir_in(site_packages)?;
340+
let tempfile = tempdir.path().join(from.file_name().unwrap());
341+
if reflink::reflink(&from, &tempfile).is_ok() {
342+
fs::rename(&tempfile, to)?;
343+
} else {
344+
debug!("Failed to clone {} to temporary location {} - attempting to copy files as a fallback", from.display(), tempfile.display());
345+
*attempt = Attempt::UseCopyFallback;
346+
fs::copy(&from, &to)?;
347+
}
348+
}
349+
} else {
350+
debug!(
351+
"Failed to clone {} to {} - attempting to copy files as a fallback",
352+
from.display(),
353+
to.display()
354+
);
355+
// switch to copy fallback
356+
*attempt = Attempt::UseCopyFallback;
357+
clone_recursive(site_packages, wheel, entry, attempt)?;
358+
}
359+
}
360+
}
361+
Attempt::Subsequent => {
362+
if let Err(err) = reflink::reflink(&from, &to) {
363+
if matches!(err.kind(), std::io::ErrorKind::AlreadyExists) {
364+
// If cloning/copying fails and the directory exists already, it must be merged recursively.
365+
if entry.file_type()?.is_dir() {
366+
for entry in fs::read_dir(from)? {
367+
clone_recursive(site_packages, wheel, &entry?, attempt)?;
368+
}
369+
} else {
370+
// If file already exists, overwrite it.
371+
let tempdir = tempdir_in(site_packages)?;
372+
let tempfile = tempdir.path().join(from.file_name().unwrap());
373+
reflink::reflink(&from, &tempfile)?;
374+
fs::rename(&tempfile, to)?;
375+
}
376+
} else {
377+
return Err(Error::Reflink { from, to, err });
378+
}
379+
}
380+
}
381+
Attempt::UseCopyFallback => {
382+
if entry.file_type()?.is_dir() {
383+
fs::create_dir_all(&to)?;
384+
for entry in fs::read_dir(from)? {
385+
clone_recursive(site_packages, wheel, &entry?, attempt)?;
386+
}
387+
} else {
388+
fs::copy(&from, &to)?;
315389
}
316-
} else {
317-
// If file already exists, overwrite it.
318-
let tempdir = tempdir_in(site_packages)?;
319-
let tempfile = tempdir.path().join(from.file_name().unwrap());
320-
reflink_copy::reflink(from, &tempfile)?;
321-
fs::rename(&tempfile, to)?;
322390
}
323-
} else {
324-
// Other errors should be tracked
325-
reflink.map_err(|err| Error::Reflink {
326-
from: from.clone(),
327-
to: to.clone(),
328-
err,
329-
})?;
330391
}
331392

393+
if *attempt == Attempt::Initial {
394+
*attempt = Attempt::Subsequent;
395+
}
332396
Ok(())
333397
}
334398

@@ -366,18 +430,6 @@ fn hardlink_wheel_files(
366430
site_packages: impl AsRef<Path>,
367431
wheel: impl AsRef<Path>,
368432
) -> Result<usize, Error> {
369-
// Hard linking might not be supported but we (afaik) can't detect this ahead of time, so we'll
370-
// try hard linking the first file, if this succeeds we'll know later hard linking errors are
371-
// not due to lack of os/fs support, if it fails we'll switch to copying for the rest of the
372-
// install
373-
#[derive(Debug, Default, Clone, Copy)]
374-
enum Attempt {
375-
#[default]
376-
Initial,
377-
Subsequent,
378-
UseCopyFallback,
379-
}
380-
381433
let mut attempt = Attempt::default();
382434
let mut count = 0usize;
383435

0 commit comments

Comments
 (0)