context: copy symlinks as symlinks and improve error messages
This commit is contained in:
+33
-11
@@ -292,21 +292,43 @@ impl<'a> ContextCommand<'a> {
|
||||
|
||||
/// Run command and obtain exit status
|
||||
pub fn status(&mut self) -> io::Result<std::process::ExitStatus> {
|
||||
self.context.driver().as_ref().unwrap().run(
|
||||
&self.program,
|
||||
&self.args,
|
||||
&self.env,
|
||||
self.cwd.as_deref(),
|
||||
)
|
||||
let program = self.program.clone();
|
||||
self.context
|
||||
.driver()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.run(&self.program, &self.args, &self.env, self.cwd.as_deref())
|
||||
.map_err(|e| contextualize_spawn_error(&program, e))
|
||||
}
|
||||
|
||||
/// Run command, capturing output
|
||||
pub fn output(&mut self) -> io::Result<std::process::Output> {
|
||||
self.context.driver().as_ref().unwrap().run_output(
|
||||
&self.program,
|
||||
&self.args,
|
||||
&self.env,
|
||||
self.cwd.as_deref(),
|
||||
let program = self.program.clone();
|
||||
self.context
|
||||
.driver()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.run_output(&self.program, &self.args, &self.env, self.cwd.as_deref())
|
||||
.map_err(|e| contextualize_spawn_error(&program, e))
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap an I/O error from launching a command with a more helpful message.
|
||||
///
|
||||
/// In particular, a `NotFound` error (e.g. "No such file or directory") is almost always
|
||||
/// caused by the requested program not being installed or not on `PATH`; we make that
|
||||
/// explicit instead of leaking the raw OS error.
|
||||
fn contextualize_spawn_error(program: &str, e: io::Error) -> io::Error {
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
io::Error::new(
|
||||
e.kind(),
|
||||
format!(
|
||||
"Could not run '{program}': {e}. \
|
||||
The program does not seem to be installed or is not on PATH; \
|
||||
install the corresponding package and retry.",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
io::Error::new(e.kind(), format!("Could not run '{program}': {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/// Context driver: Does nothing
|
||||
use super::api::ContextDriver;
|
||||
use std::io;
|
||||
use std::os::unix::fs::symlink;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::SystemTime;
|
||||
@@ -112,6 +113,14 @@ impl ContextDriver for LocalDriver {
|
||||
}
|
||||
|
||||
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
|
||||
// Reproduce symlinks as symlinks rather than following them, so that
|
||||
// dangling/absolute symlinks do not abort the copy.
|
||||
if std::fs::symlink_metadata(src)?.file_type().is_symlink() {
|
||||
let target = std::fs::read_link(src)?;
|
||||
let _ = std::fs::remove_file(dest);
|
||||
return symlink(&target, dest);
|
||||
}
|
||||
|
||||
if src.is_dir() {
|
||||
std::fs::create_dir_all(dest)?;
|
||||
for entry in std::fs::read_dir(src)? {
|
||||
|
||||
@@ -147,4 +147,37 @@ mod tests {
|
||||
"subcontent"
|
||||
);
|
||||
}
|
||||
|
||||
/// Copying a tree containing a dangling symlink must reproduce the link
|
||||
/// itself rather than failing to follow it (which previously surfaced as
|
||||
/// a bare "No such file or directory (os error 2)").
|
||||
#[test]
|
||||
fn test_context_copy_preserves_dangling_symlink() {
|
||||
use std::os::unix::fs::symlink;
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let ctx = Context::new(ContextConfig::Local);
|
||||
|
||||
let src_dir = temp_dir.path().join("src");
|
||||
std::fs::create_dir_all(&src_dir).unwrap();
|
||||
// A regular file alongside the symlink, to ensure normal copies still work.
|
||||
std::fs::write(src_dir.join("real.txt"), "data").unwrap();
|
||||
// A dangling symlink pointing to a non-existent target.
|
||||
symlink("/nonexistent/target", src_dir.join("dangling")).unwrap();
|
||||
|
||||
let dest_dir = temp_dir.path().join("dest");
|
||||
ctx.copy_path(&src_dir, &dest_dir).unwrap();
|
||||
|
||||
// The regular file was copied.
|
||||
assert_eq!(
|
||||
ctx.read_file(&dest_dir.join("real.txt")).unwrap(),
|
||||
"data"
|
||||
);
|
||||
// The symlink was reproduced as a symlink (not followed).
|
||||
let meta = std::fs::symlink_metadata(dest_dir.join("dangling")).unwrap();
|
||||
assert!(meta.file_type().is_symlink());
|
||||
assert_eq!(
|
||||
std::fs::read_link(dest_dir.join("dangling")).unwrap(),
|
||||
std::path::Path::new("/nonexistent/target")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+74
-7
@@ -1,6 +1,8 @@
|
||||
use super::api::{Context, ContextCommand, ContextDriver};
|
||||
use log::debug;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::os::unix::fs::symlink;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -9,23 +11,88 @@ pub struct UnshareDriver {
|
||||
pub parent: Option<Arc<super::api::Context>>,
|
||||
}
|
||||
|
||||
/// Recursively copy a directory and all its contents
|
||||
/// Recursively copy a directory and all its contents.
|
||||
///
|
||||
/// Symlinks are copied as symlinks (preserving the link target rather than
|
||||
/// following it), so that dangling links do not abort the copy.
|
||||
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
|
||||
// Create the destination directory
|
||||
std::fs::create_dir_all(dest)?;
|
||||
std::fs::create_dir_all(dest).map_err(|e| {
|
||||
io::Error::new(
|
||||
e.kind(),
|
||||
format!("Failed to create directory '{}': {}", dest.display(), e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// Iterate through the source directory
|
||||
for entry in std::fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let read = std::fs::read_dir(src).map_err(|e| {
|
||||
io::Error::new(
|
||||
e.kind(),
|
||||
format!("Failed to read directory '{}': {}", src.display(), e),
|
||||
)
|
||||
})?;
|
||||
for entry in read {
|
||||
let entry = entry.map_err(|e| {
|
||||
io::Error::new(
|
||||
e.kind(),
|
||||
format!("Failed to read entry in '{}': {}", src.display(), e),
|
||||
)
|
||||
})?;
|
||||
let src_path = entry.path();
|
||||
let dest_path = dest.join(entry.file_name());
|
||||
|
||||
if src_path.is_dir() {
|
||||
// Handle symlinks explicitly: reproduce the link itself instead of
|
||||
// following it. This avoids failing on dangling/absolute symlinks.
|
||||
let metadata = match std::fs::symlink_metadata(&src_path) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
return Err(io::Error::new(
|
||||
e.kind(),
|
||||
format!(
|
||||
"Failed to read metadata of '{}': {}",
|
||||
src_path.display(),
|
||||
e
|
||||
),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if metadata.file_type().is_symlink() {
|
||||
let target = std::fs::read_link(&src_path).map_err(|e| {
|
||||
io::Error::new(
|
||||
e.kind(),
|
||||
format!("Failed to read symlink '{}': {}", src_path.display(), e),
|
||||
)
|
||||
})?;
|
||||
// Remove an existing destination entry (e.g. from a previous attempt)
|
||||
let _ = fs::remove_file(&dest_path);
|
||||
symlink(&target, &dest_path).map_err(|e| {
|
||||
io::Error::new(
|
||||
e.kind(),
|
||||
format!(
|
||||
"Failed to create symlink '{}' -> '{}': {}",
|
||||
dest_path.display(),
|
||||
target.display(),
|
||||
e
|
||||
),
|
||||
)
|
||||
})?;
|
||||
} else if src_path.is_dir() {
|
||||
// Recursively copy subdirectories
|
||||
copy_dir_recursive(&src_path, &dest_path)?;
|
||||
} else {
|
||||
// Copy files
|
||||
std::fs::copy(&src_path, &dest_path)?;
|
||||
// Copy regular files
|
||||
std::fs::copy(&src_path, &dest_path).map_err(|e| {
|
||||
io::Error::new(
|
||||
e.kind(),
|
||||
format!(
|
||||
"Failed to copy '{}' to '{}': {}",
|
||||
src_path.display(),
|
||||
dest_path.display(),
|
||||
e
|
||||
),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user