context: copy symlinks as symlinks and improve error messages
This commit is contained in:
+15
-2
@@ -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() {
|
||||
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() {
|
||||
|
||||
+72
-9
@@ -119,10 +119,24 @@ fn increment_suffix(version: &str, suffix: &str) -> String {
|
||||
pub fn parse_changelog_header(
|
||||
path: &Path,
|
||||
) -> 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 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
|
||||
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();
|
||||
Ok((package, version, series))
|
||||
} 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
|
||||
/// Returns (name, email) tuple from the last modification entry
|
||||
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();
|
||||
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)
|
||||
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();
|
||||
Ok((name, email))
|
||||
} 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
|
||||
let config = git2::Config::open_default()?;
|
||||
let name = config.get_string("user.name")?;
|
||||
let email = config.get_string("user.email")?;
|
||||
let config = git2::Config::open_default().map_err(|e| {
|
||||
format!(
|
||||
"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))
|
||||
}
|
||||
|
||||
|
||||
+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
|
||||
),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-2
@@ -138,10 +138,20 @@ pub async fn build(
|
||||
.command("apt-get")
|
||||
.envs(env.clone())
|
||||
.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() {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
+67
-10
@@ -110,11 +110,30 @@ fn parse_series_csv(content: &str) -> Result<Vec<SeriesInformation>, Box<dyn Err
|
||||
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
|
||||
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() {
|
||||
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 {
|
||||
reqwest::get(series_info.network.as_str())
|
||||
.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
|
||||
///
|
||||
/// Example: get_dist_pockets(ubuntu) => ["proposed", "updates", ""]
|
||||
pub fn get_dist_pockets(dist: &str) -> Vec<String> {
|
||||
let mut pockets = DATA.dist.get(dist).unwrap().pockets.clone();
|
||||
pub fn get_dist_pockets(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
|
||||
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
|
||||
pockets.push("".to_string());
|
||||
|
||||
pockets
|
||||
Ok(pockets)
|
||||
}
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// Example: ubuntu => http://archive.ubuntu.com/ubuntu
|
||||
pub fn get_base_url(dist: &str) -> String {
|
||||
DATA.dist.get(dist).unwrap().base_url.clone()
|
||||
pub fn get_base_url(dist: &str) -> Result<String, Box<dyn Error>> {
|
||||
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
|
||||
@@ -246,7 +281,15 @@ pub async fn get_keyring_urls(series: &str) -> Result<Vec<String>, Box<dyn Error
|
||||
Ok(urls)
|
||||
}
|
||||
} 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
|
||||
Ok(vec![
|
||||
dist_data
|
||||
@@ -297,9 +340,23 @@ pub async fn get_components(
|
||||
|
||||
/// Map a Debian series name to its version number
|
||||
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() {
|
||||
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 {
|
||||
reqwest::get(series_info.network.as_str())
|
||||
.await?
|
||||
|
||||
+41
-7
@@ -14,6 +14,17 @@ use log::{error, info};
|
||||
|
||||
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() {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let logger =
|
||||
@@ -137,7 +148,7 @@ fn main() {
|
||||
info!("Done.");
|
||||
}
|
||||
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());
|
||||
|
||||
if let Err(e) = generate_entry("debian/changelog", Some(&cwd), version) {
|
||||
@@ -145,21 +156,38 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let editor = std::env::var("EDITOR").unwrap();
|
||||
let _status = std::process::Command::new(editor)
|
||||
let editor = match std::env::var("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)
|
||||
.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)) => {
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let cwd = current_dir_or_exit();
|
||||
if let Err(e) = pkh::build::build_source_package(Some(&cwd)) {
|
||||
error!("{}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
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 arch = sub_matches.get_one::<String>("arch").map(|s| s.as_str());
|
||||
let cross = sub_matches.get_one::<bool>("cross").unwrap_or(&false);
|
||||
@@ -221,7 +249,13 @@ fn main() {
|
||||
"ssh" => {
|
||||
let endpoint = args
|
||||
.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
|
||||
// Formats: [ssh://][user@]host[:port]
|
||||
|
||||
+2
-2
@@ -213,7 +213,7 @@ async fn get(
|
||||
}
|
||||
|
||||
// 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 {
|
||||
ppa_url.to_string()
|
||||
} else {
|
||||
@@ -298,7 +298,7 @@ async fn find_package(
|
||||
}
|
||||
|
||||
let pockets = if pocket.is_empty() {
|
||||
crate::distro_info::get_dist_pockets(dist)
|
||||
crate::distro_info::get_dist_pockets(dist)?
|
||||
} else {
|
||||
vec![pocket.to_string()]
|
||||
};
|
||||
|
||||
+24
-4
@@ -1,5 +1,6 @@
|
||||
use std::cmp::min;
|
||||
use std::error::Error;
|
||||
use std::os::unix::fs::symlink;
|
||||
use std::path::Path;
|
||||
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 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)?;
|
||||
} else {
|
||||
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)
|
||||
.args(["checkout", format!("../{filename}").as_str()])
|
||||
.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() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!(
|
||||
"pristine-tar checkout failed with status: {}",
|
||||
output.status
|
||||
"pristine-tar checkout of '{filename}' failed with status: {}.{}",
|
||||
output.status,
|
||||
if stderr.trim().is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\npristine-tar output:\n{}", stderr.trim())
|
||||
}
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user