context: copy symlinks as symlinks and improve error messages

This commit is contained in:
2026-07-17 10:12:46 +02:00
parent 2b27b7b06e
commit c4b59a4376
11 changed files with 383 additions and 55 deletions
+15 -2
View File
@@ -46,10 +46,23 @@ pub fn build_source_package(cwd: Option<&Path>) -> Result<(), Box<dyn Error>> {
); );
} }
let status = command.status()?; let status = command.status().map_err(|e| {
format!(
"Failed to run 'dpkg-buildpackage': {}. \
Is 'dpkg-dev' (which provides dpkg-buildpackage) installed?",
e
)
})?;
if !status.success() { if !status.success() {
return Err(format!("dpkg-buildpackage failed with status: {}", status).into()); return Err(format!(
"dpkg-buildpackage failed with status: {}. \
Re-run with 'RUST_LOG=debug' for more details, or run \
'dpkg-buildpackage -S -I -i -nc -d' manually in '{}' to see the full output.",
status,
cwd.display()
)
.into());
} }
if signing_key.is_some() { if signing_key.is_some() {
+72 -9
View File
@@ -119,10 +119,24 @@ fn increment_suffix(version: &str, suffix: &str) -> String {
pub fn parse_changelog_header( pub fn parse_changelog_header(
path: &Path, path: &Path,
) -> Result<(String, String, String), Box<dyn std::error::Error>> { ) -> Result<(String, String, String), Box<dyn std::error::Error>> {
let file = File::open(path)?; let file = File::open(path).map_err(|e| {
format!(
"Failed to read changelog '{}': {}. \
Make sure you are running this command from the root of a source package \
(a directory containing a 'debian/' subdirectory with a 'changelog' file).",
path.display(),
e
)
})?;
let mut reader = io::BufReader::new(file); let mut reader = io::BufReader::new(file);
let mut first_line = String::new(); let mut first_line = String::new();
reader.read_line(&mut first_line)?; reader.read_line(&mut first_line).map_err(|e| {
format!(
"Failed to read first line of changelog '{}': {}",
path.display(),
e
)
})?;
// Format: package (version) series; urgency=urgency // Format: package (version) series; urgency=urgency
let re = Regex::new(r"^(\S+) \(([^)]+)\) (.*); .*")?; let re = Regex::new(r"^(\S+) \(([^)]+)\) (.*); .*")?;
@@ -132,16 +146,37 @@ pub fn parse_changelog_header(
let series = caps.get(3).map_or("", |m| m.as_str()).to_string(); let series = caps.get(3).map_or("", |m| m.as_str()).to_string();
Ok((package, version, series)) Ok((package, version, series))
} else { } else {
Err(format!("Invalid changelog header format in {}", path.display()).into()) Err(format!(
"Invalid changelog header format in '{}'. \
The first line must look like: `package (version) series; urgency=...`, \
but got: {:?}",
path.display(),
first_line.trim_end()
)
.into())
} }
} }
/// Parse a changelog file footer to extract maintainer information /// Parse a changelog file footer to extract maintainer information
/// Returns (name, email) tuple from the last modification entry /// Returns (name, email) tuple from the last modification entry
pub fn parse_changelog_footer(path: &Path) -> Result<(String, String), Box<dyn std::error::Error>> { pub fn parse_changelog_footer(path: &Path) -> Result<(String, String), Box<dyn std::error::Error>> {
let mut file = File::open(path)?; let mut file = File::open(path).map_err(|e| {
format!(
"Failed to read changelog '{}': {}. \
Make sure you are running this command from the root of a source package \
(a directory containing a 'debian/' subdirectory with a 'changelog' file).",
path.display(),
e
)
})?;
let mut content = String::new(); let mut content = String::new();
file.read_to_string(&mut content)?; file.read_to_string(&mut content).map_err(|e| {
format!(
"Failed to read changelog '{}': {}",
path.display(),
e
)
})?;
// Find the last maintainer line (format: -- Name <email> Date) // Find the last maintainer line (format: -- Name <email> Date)
let re = Regex::new(r"--\s*([^<]+?)\s*<([^>]+)>\s*")?; let re = Regex::new(r"--\s*([^<]+?)\s*<([^>]+)>\s*")?;
@@ -159,7 +194,13 @@ pub fn parse_changelog_footer(path: &Path) -> Result<(String, String), Box<dyn s
.to_string(); .to_string();
Ok((name, email)) Ok((name, email))
} else { } else {
Err(format!("No maintainer information found in {}", path.display()).into()) Err(format!(
"No maintainer information found in '{}'. \
The changelog must contain a line of the form '-- Name <email> Date', \
but none was found.",
path.display()
)
.into())
} }
} }
@@ -283,9 +324,31 @@ fn get_maintainer_info() -> Result<(String, String), Box<dyn std::error::Error>>
} }
// From git config // From git config
let config = git2::Config::open_default()?; let config = git2::Config::open_default().map_err(|e| {
let name = config.get_string("user.name")?; format!(
let email = config.get_string("user.email")?; "Could not determine maintainer information. \
Neither $DEBFULLNAME/$DEBEMAIL nor git configuration is available: {}. \
Set the DEBFULLNAME and DEBEMAIL environment variables, \
or configure git with `git config --global user.name` and `git config --global user.email`.",
e
)
})?;
let name = config.get_string("user.name").map_err(|e| {
format!(
"Could not find git 'user.name' configuration: {}. \
Set it with `git config --global user.name \"Your Name\"` \
or define the DEBFULLNAME environment variable.",
e
)
})?;
let email = config.get_string("user.email").map_err(|e| {
format!(
"Could not find git 'user.email' configuration: {}. \
Set it with `git config --global user.email \"you@example.com\"` \
or define the DEBEMAIL environment variable.",
e
)
})?;
Ok((name, email)) Ok((name, email))
} }
+34 -12
View File
@@ -292,21 +292,43 @@ impl<'a> ContextCommand<'a> {
/// Run command and obtain exit status /// Run command and obtain exit status
pub fn status(&mut self) -> io::Result<std::process::ExitStatus> { pub fn status(&mut self) -> io::Result<std::process::ExitStatus> {
self.context.driver().as_ref().unwrap().run( let program = self.program.clone();
&self.program, self.context
&self.args, .driver()
&self.env, .as_ref()
self.cwd.as_deref(), .unwrap()
) .run(&self.program, &self.args, &self.env, self.cwd.as_deref())
.map_err(|e| contextualize_spawn_error(&program, e))
} }
/// Run command, capturing output /// Run command, capturing output
pub fn output(&mut self) -> io::Result<std::process::Output> { pub fn output(&mut self) -> io::Result<std::process::Output> {
self.context.driver().as_ref().unwrap().run_output( let program = self.program.clone();
&self.program, self.context
&self.args, .driver()
&self.env, .as_ref()
self.cwd.as_deref(), .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}"))
} }
} }
+9
View File
@@ -2,6 +2,7 @@
/// Context driver: Does nothing /// Context driver: Does nothing
use super::api::ContextDriver; use super::api::ContextDriver;
use std::io; use std::io;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::time::SystemTime; use std::time::SystemTime;
@@ -112,6 +113,14 @@ impl ContextDriver for LocalDriver {
} }
fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> { 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() { if src.is_dir() {
std::fs::create_dir_all(dest)?; std::fs::create_dir_all(dest)?;
for entry in std::fs::read_dir(src)? { for entry in std::fs::read_dir(src)? {
+33
View File
@@ -147,4 +147,37 @@ mod tests {
"subcontent" "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
View File
@@ -1,6 +1,8 @@
use super::api::{Context, ContextCommand, ContextDriver}; use super::api::{Context, ContextCommand, ContextDriver};
use log::debug; use log::debug;
use std::fs;
use std::io; use std::io;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
@@ -9,23 +11,88 @@ pub struct UnshareDriver {
pub parent: Option<Arc<super::api::Context>>, 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<()> { fn copy_dir_recursive(src: &Path, dest: &Path) -> io::Result<()> {
// Create the destination directory // 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 // Iterate through the source directory
for entry in std::fs::read_dir(src)? { let read = std::fs::read_dir(src).map_err(|e| {
let entry = entry?; 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 src_path = entry.path();
let dest_path = dest.join(entry.file_name()); 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 // Recursively copy subdirectories
copy_dir_recursive(&src_path, &dest_path)?; copy_dir_recursive(&src_path, &dest_path)?;
} else { } else {
// Copy files // Copy regular files
std::fs::copy(&src_path, &dest_path)?; 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
),
)
})?;
} }
} }
+12 -2
View File
@@ -138,10 +138,20 @@ pub async fn build(
.command("apt-get") .command("apt-get")
.envs(env.clone()) .envs(env.clone())
.arg("update") .arg("update")
.status()?; .status()
.map_err(|e| {
format!(
"Failed to run 'apt-get update' inside the build context: {}. \
If this is a local build, make sure apt-get is available and \
try executing with sudo.",
e
)
})?;
if !status.success() { if !status.success() {
return Err( return Err(
"Could not execute apt-get update. If this is a local build, try executing with sudo." "apt-get update failed inside the build context. \
If this is a local build, try executing with sudo, \
or re-run with RUST_LOG=debug for more details."
.into(), .into(),
); );
} }
+67 -10
View File
@@ -110,11 +110,30 @@ fn parse_series_csv(content: &str) -> Result<Vec<SeriesInformation>, Box<dyn Err
Ok(series_info_list) Ok(series_info_list)
} }
/// List the distributions known to pkh (e.g. "debian", "ubuntu")
pub fn supported_dists() -> Vec<String> {
DATA.dist.keys().cloned().collect()
}
/// Get time-ordered list of series information for a distribution, development series first /// Get time-ordered list of series information for a distribution, development series first
pub async fn get_ordered_series(dist: &str) -> Result<Vec<SeriesInformation>, Box<dyn Error>> { pub async fn get_ordered_series(dist: &str) -> Result<Vec<SeriesInformation>, Box<dyn Error>> {
let series_info = &DATA.dist.get(dist).unwrap().series; let dist_data = DATA.dist.get(dist).ok_or_else(|| {
format!(
"Unknown distribution '{}'. Supported distributions are: {}.",
dist,
supported_dists().join(", ")
)
})?;
let series_info = &dist_data.series;
let content = if Path::new(series_info.local.as_str()).exists() { let content = if Path::new(series_info.local.as_str()).exists() {
std::fs::read_to_string(format!("/usr/share/distro-info/{dist}.csv"))? std::fs::read_to_string(format!("/usr/share/distro-info/{dist}.csv"))
.map_err(|e| {
format!(
"Failed to read distribution series data for '{dist}' \
from '{}': {}. The 'distro-info' package provides these CSV files.",
series_info.local, e
)
})?
} else { } else {
reqwest::get(series_info.network.as_str()) reqwest::get(series_info.network.as_str())
.await? .await?
@@ -188,13 +207,20 @@ pub async fn get_dist_from_series(series: &str) -> Result<String, Box<dyn Error>
/// Get the package pockets available for a given distribution /// Get the package pockets available for a given distribution
/// ///
/// Example: get_dist_pockets(ubuntu) => ["proposed", "updates", ""] /// Example: get_dist_pockets(ubuntu) => ["proposed", "updates", ""]
pub fn get_dist_pockets(dist: &str) -> Vec<String> { pub fn get_dist_pockets(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
let mut pockets = DATA.dist.get(dist).unwrap().pockets.clone(); let dist_data = DATA.dist.get(dist).ok_or_else(|| {
format!(
"Unknown distribution '{}'. Supported distributions are: {}.",
dist,
supported_dists().join(", ")
)
})?;
let mut pockets = dist_data.pockets.clone();
// Explicitely add 'main' pocket, which is just the empty string // Explicitely add 'main' pocket, which is just the empty string
pockets.push("".to_string()); pockets.push("".to_string());
pockets Ok(pockets)
} }
/// Get the sources URL for a distribution, series, pocket, and component /// Get the sources URL for a distribution, series, pocket, and component
@@ -210,8 +236,17 @@ pub fn get_sources_url(base_url: &str, series: &str, pocket: &str, component: &s
/// Get the archive base URL for a distribution /// Get the archive base URL for a distribution
/// ///
/// Example: ubuntu => http://archive.ubuntu.com/ubuntu /// Example: ubuntu => http://archive.ubuntu.com/ubuntu
pub fn get_base_url(dist: &str) -> String { pub fn get_base_url(dist: &str) -> Result<String, Box<dyn Error>> {
DATA.dist.get(dist).unwrap().base_url.clone() DATA.dist.get(dist)
.map(|d| d.base_url.clone())
.ok_or_else(|| {
format!(
"Unknown distribution '{}'. Supported distributions are: {}.",
dist,
supported_dists().join(", ")
)
.into()
})
} }
/// Obtain the URLs for the archive keyrings of a distribution series /// Obtain the URLs for the archive keyrings of a distribution series
@@ -246,7 +281,15 @@ pub async fn get_keyring_urls(series: &str) -> Result<Vec<String>, Box<dyn Error
Ok(urls) Ok(urls)
} }
} else { } else {
let series_num = get_debian_series_number(series).await?.unwrap(); let series_num = get_debian_series_number(series)
.await?
.ok_or_else(|| {
format!(
"Could not determine the version number for Debian series '{series}'. \
Make sure the 'distro-info' package is installed, which provides the \
series CSV data used to map series names to version numbers."
)
})?;
// Replace {series_num} placeholder with the actual series number // Replace {series_num} placeholder with the actual series number
Ok(vec![ Ok(vec![
dist_data dist_data
@@ -297,9 +340,23 @@ pub async fn get_components(
/// Map a Debian series name to its version number /// Map a Debian series name to its version number
pub async fn get_debian_series_number(series: &str) -> Result<Option<String>, Box<dyn Error>> { pub async fn get_debian_series_number(series: &str) -> Result<Option<String>, Box<dyn Error>> {
let series_info = &DATA.dist.get("debian").unwrap().series; let dist_data = DATA.dist.get("debian").ok_or_else(|| {
format!(
"Debian distribution data is missing from the built-in configuration. \
This is a bug; supported distributions are: {}.",
supported_dists().join(", ")
)
})?;
let series_info = &dist_data.series;
let content = if Path::new(series_info.local.as_str()).exists() { let content = if Path::new(series_info.local.as_str()).exists() {
std::fs::read_to_string(series_info.local.as_str())? std::fs::read_to_string(series_info.local.as_str())
.map_err(|e| {
format!(
"Failed to read Debian series data from '{}': {}. \
The 'distro-info' package provides this file.",
series_info.local, e
)
})?
} else { } else {
reqwest::get(series_info.network.as_str()) reqwest::get(series_info.network.as_str())
.await? .await?
+41 -7
View File
@@ -14,6 +14,17 @@ use log::{error, info};
mod ui; mod ui;
/// Obtain the current working directory, exiting with a helpful message on failure.
fn current_dir_or_exit() -> std::path::PathBuf {
match std::env::current_dir() {
Ok(p) => p,
Err(e) => {
error!("Could not determine the current working directory: {}", e);
std::process::exit(1);
}
}
}
fn main() { fn main() {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
let logger = let logger =
@@ -137,7 +148,7 @@ fn main() {
info!("Done."); info!("Done.");
} }
Some(("chlog", sub_matches)) => { Some(("chlog", sub_matches)) => {
let cwd = std::env::current_dir().unwrap(); let cwd = current_dir_or_exit();
let version = sub_matches.get_one::<String>("version").map(|s| s.as_str()); let version = sub_matches.get_one::<String>("version").map(|s| s.as_str());
if let Err(e) = generate_entry("debian/changelog", Some(&cwd), version) { if let Err(e) = generate_entry("debian/changelog", Some(&cwd), version) {
@@ -145,21 +156,38 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
let editor = std::env::var("EDITOR").unwrap(); let editor = match std::env::var("EDITOR") {
let _status = std::process::Command::new(editor) Ok(e) => e,
Err(_) => {
error!(
"No editor configured. Set the EDITOR environment variable \
(e.g. `EDITOR=nano` or `export EDITOR=vim`) and retry."
);
std::process::exit(1);
}
};
let _status = std::process::Command::new(&editor)
.current_dir(&cwd) .current_dir(&cwd)
.args(["debian/changelog"]) .args(["debian/changelog"])
.status(); .status()
.map_err(|e| {
error!(
"Could not launch editor '{}': {}. \
Make sure it is installed and available on PATH.",
editor, e
);
std::process::exit(1);
});
} }
Some(("build", _sub_matches)) => { Some(("build", _sub_matches)) => {
let cwd = std::env::current_dir().unwrap(); let cwd = current_dir_or_exit();
if let Err(e) = pkh::build::build_source_package(Some(&cwd)) { if let Err(e) = pkh::build::build_source_package(Some(&cwd)) {
error!("{}", e); error!("{}", e);
std::process::exit(1); std::process::exit(1);
} }
} }
Some(("deb", sub_matches)) => { Some(("deb", sub_matches)) => {
let cwd = std::env::current_dir().unwrap(); let cwd = current_dir_or_exit();
let series = sub_matches.get_one::<String>("series").map(|s| s.as_str()); let series = sub_matches.get_one::<String>("series").map(|s| s.as_str());
let arch = sub_matches.get_one::<String>("arch").map(|s| s.as_str()); let arch = sub_matches.get_one::<String>("arch").map(|s| s.as_str());
let cross = sub_matches.get_one::<bool>("cross").unwrap_or(&false); let cross = sub_matches.get_one::<bool>("cross").unwrap_or(&false);
@@ -221,7 +249,13 @@ fn main() {
"ssh" => { "ssh" => {
let endpoint = args let endpoint = args
.get_one::<String>("endpoint") .get_one::<String>("endpoint")
.expect("Endpoint is required for ssh context"); .unwrap_or_else(|| {
error!(
"An --endpoint is required to create an ssh context. \
Expected format: [ssh://][user@]host[:port]"
);
std::process::exit(1);
});
// Parse host, user, port from endpoint // Parse host, user, port from endpoint
// Formats: [ssh://][user@]host[:port] // Formats: [ssh://][user@]host[:port]
+2 -2
View File
@@ -213,7 +213,7 @@ async fn get(
} }
// Determine the base URL to use (either provided PPA URL or default archive) // Determine the base URL to use (either provided PPA URL or default archive)
let distro_base_url = crate::distro_info::get_base_url(&dist); let distro_base_url = crate::distro_info::get_base_url(&dist)?;
let base_url = if let Some(ppa_url) = base_url { let base_url = if let Some(ppa_url) = base_url {
ppa_url.to_string() ppa_url.to_string()
} else { } else {
@@ -298,7 +298,7 @@ async fn find_package(
} }
let pockets = if pocket.is_empty() { let pockets = if pocket.is_empty() {
crate::distro_info::get_dist_pockets(dist) crate::distro_info::get_dist_pockets(dist)?
} else { } else {
vec![pocket.to_string()] vec![pocket.to_string()]
}; };
+24 -4
View File
@@ -1,5 +1,6 @@
use std::cmp::min; use std::cmp::min;
use std::error::Error; use std::error::Error;
use std::os::unix::fs::symlink;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
@@ -101,7 +102,13 @@ fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), Box<dyn Error>> {
let src_path = entry.path(); let src_path = entry.path();
let dst_path = dst.join(entry.file_name()); let dst_path = dst.join(entry.file_name());
if src_path.is_dir() { // Reproduce symlinks as symlinks rather than following them, so that
// dangling/absolute symlinks do not abort the copy.
if std::fs::symlink_metadata(&src_path)?.file_type().is_symlink() {
let target = std::fs::read_link(&src_path)?;
let _ = std::fs::remove_file(&dst_path);
symlink(&target, &dst_path)?;
} else if src_path.is_dir() {
copy_dir_all(&src_path, &dst_path)?; copy_dir_all(&src_path, &dst_path)?;
} else { } else {
std::fs::copy(&src_path, &dst_path)?; std::fs::copy(&src_path, &dst_path)?;
@@ -183,12 +190,25 @@ fn checkout_pristine_tar(package_dir: &Path, filename: &str) -> Result<(), Box<d
.current_dir(package_dir) .current_dir(package_dir)
.args(["checkout", format!("../{filename}").as_str()]) .args(["checkout", format!("../{filename}").as_str()])
.output() .output()
.expect("pristine-tar checkout failed"); .map_err(|e| {
format!(
"Failed to run 'pristine-tar' to check out '{filename}': {}. \
Is 'pristine-tar' installed? It is required to reconstruct \
the upstream orig tarball from a git repository.",
e
)
})?;
if !output.status.success() { if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!( return Err(format!(
"pristine-tar checkout failed with status: {}", "pristine-tar checkout of '{filename}' failed with status: {}.{}",
output.status output.status,
if stderr.trim().is_empty() {
String::new()
} else {
format!("\npristine-tar output:\n{}", stderr.trim())
}
) )
.into()); .into());
} }