Compare commits
7 Commits
73511c258b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
62ce4e3696
|
|||
|
a0d35bb18e
|
|||
|
e1668d5d80
|
|||
|
f9e11e951b
|
|||
| 768e1c4f78 | |||
| 48248fdf9c | |||
|
c4b59a4376
|
@@ -41,7 +41,7 @@ jobs:
|
|||||||
- name: Install runtime system dependencies
|
- name: Install runtime system dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y git pristine-tar sbuild mmdebstrap util-linux dpkg-dev
|
sudo apt-get install -y git pristine-tar mmdebstrap util-linux dpkg-dev
|
||||||
- name: Setup subuid/subgid
|
- name: Setup subuid/subgid
|
||||||
run: |
|
run: |
|
||||||
usermod --add-subuids 100000-200000 --add-subgids 100000-200000 ${USER:-root}
|
usermod --add-subuids 100000-200000 --add-subgids 100000-200000 ${USER:-root}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ regex = "1"
|
|||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
tokio = { version = "1.41.1", features = ["full"] }
|
tokio = { version = "1.41.1", features = ["full"] }
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
|
md-5 = "0.10"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
indicatif = "0.18.3"
|
indicatif = "0.18.3"
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ Missing features:
|
|||||||
- [x] Build for a specific architecture
|
- [x] Build for a specific architecture
|
||||||
- [ ] Three build modes:
|
- [ ] Three build modes:
|
||||||
- [ ] Build locally (discouraged)
|
- [ ] Build locally (discouraged)
|
||||||
- [x] Build using sbuild+unshare, with binary emulation (default)
|
- [x] Build using unshare chroot, with binary emulation (default)
|
||||||
- [x] Cross-compilation
|
- [x] Cross-compilation
|
||||||
- [ ] Async build
|
- [ ] Async build
|
||||||
- [ ] `pkh status`
|
- [ ] `pkh status`
|
||||||
@@ -111,7 +111,7 @@ Missing features:
|
|||||||
- [ ] Lint the package
|
- [ ] Lint the package
|
||||||
- [ ] `pkh test`
|
- [ ] `pkh test`
|
||||||
- [ ] Run autopkgtest
|
- [ ] Run autopkgtest
|
||||||
- [ ] Provide options: local (discouraged), sbuild/VM?, ppa
|
- [ ] Provide options: local (discouraged), chroot, VM?, ppa
|
||||||
- [ ] Async test
|
- [ ] Async test
|
||||||
|
|
||||||
## Nice-to-have features
|
## Nice-to-have features
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ parts:
|
|||||||
- git
|
- git
|
||||||
- curl
|
- curl
|
||||||
- pristine-tar
|
- pristine-tar
|
||||||
- sbuild
|
|
||||||
- mmdebstrap
|
- mmdebstrap
|
||||||
- util-linux
|
- util-linux
|
||||||
- dpkg-dev
|
- dpkg-dev
|
||||||
|
|||||||
+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() {
|
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() {
|
||||||
|
|||||||
+67
-9
@@ -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,32 @@ 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 +189,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 +319,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
@@ -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}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)? {
|
||||||
|
|||||||
@@ -147,4 +147,34 @@ 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")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-8
@@ -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,29 +11,136 @@ 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, preserving the source modification and
|
||||||
std::fs::copy(&src_path, &dest_path)?;
|
// access times. This is important for autotools/gnulib-based
|
||||||
|
// packages (e.g. 'hello' from Debian sid) that ship pre-generated
|
||||||
|
// files alongside their prerequisites: if the copy resets the
|
||||||
|
// mtime to "now", the prerequisites appear as new as the
|
||||||
|
// generated targets and `make` tries to regenerate them using
|
||||||
|
// tools (like gperf) that are not declared build-dependencies.
|
||||||
|
copy_file_with_times(&src_path, &dest_path)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Copy a single regular file from `src` to `dest`, preserving the source's
|
||||||
|
/// modification and access times.
|
||||||
|
///
|
||||||
|
/// `std::fs::copy` resets the destination mtime to "now", which breaks
|
||||||
|
/// timestamp-based build systems (autotools/gnulib) that ship pre-generated
|
||||||
|
/// files alongside their prerequisites. Restoring the original timestamps
|
||||||
|
/// prevents `make` from needlessly regenerating those files with tools that
|
||||||
|
/// may not be installed (e.g. `gperf`).
|
||||||
|
fn copy_file_with_times(src: &Path, dest: &Path) -> io::Result<()> {
|
||||||
|
std::fs::copy(src, dest).map_err(|e| {
|
||||||
|
io::Error::new(
|
||||||
|
e.kind(),
|
||||||
|
format!(
|
||||||
|
"Failed to copy '{}' to '{}': {}",
|
||||||
|
src.display(),
|
||||||
|
dest.display(),
|
||||||
|
e
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Restore the original timestamps on the destination file.
|
||||||
|
let metadata = std::fs::metadata(src).map_err(|e| {
|
||||||
|
io::Error::new(
|
||||||
|
e.kind(),
|
||||||
|
format!("Failed to read metadata of '{}': {}", src.display(), e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let mut times = std::fs::FileTimes::new();
|
||||||
|
let mut have_times = false;
|
||||||
|
if let Ok(mtime) = metadata.modified() {
|
||||||
|
times = times.set_modified(mtime);
|
||||||
|
have_times = true;
|
||||||
|
}
|
||||||
|
if let Ok(atime) = metadata.accessed() {
|
||||||
|
times = times.set_accessed(atime);
|
||||||
|
have_times = true;
|
||||||
|
}
|
||||||
|
if have_times && let Ok(dest_file) = std::fs::File::open(dest) {
|
||||||
|
let _ = dest_file.set_times(times).map_err(|e| {
|
||||||
|
io::Error::new(
|
||||||
|
e.kind(),
|
||||||
|
format!("Failed to set times on '{}': {}", dest.display(), e),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
impl ContextDriver for UnshareDriver {
|
impl ContextDriver for UnshareDriver {
|
||||||
fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result<PathBuf> {
|
fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result<PathBuf> {
|
||||||
// Construct the destination path inside the chroot
|
// Construct the destination path inside the chroot
|
||||||
@@ -62,7 +171,7 @@ impl ContextDriver for UnshareDriver {
|
|||||||
dest_path.display()
|
dest_path.display()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
std::fs::copy(src, &dest_path)?;
|
copy_file_with_times(src, &dest_path)?;
|
||||||
debug!("Copied file {} to {}", src.display(), dest_path.display());
|
debug!("Copied file {} to {}", src.display(), dest_path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ impl EphemeralContextGuard {
|
|||||||
|
|
||||||
impl Drop for EphemeralContextGuard {
|
impl Drop for EphemeralContextGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
log::debug!("Cleaning up ephemeral context ({:?})...", &self.chroot_path);
|
log::debug!("Cleaning up ephemeral context ({:?})...", self.chroot_path);
|
||||||
// Reset to normal context
|
// Reset to normal context
|
||||||
if let Err(e) = context::manager().set_current(&self.previous_context) {
|
if let Err(e) = context::manager().set_current(&self.previous_context) {
|
||||||
log::error!("Failed to restore context {}: {}", self.previous_context, e);
|
log::error!("Failed to restore context {}: {}", self.previous_context, e);
|
||||||
|
|||||||
+13
-5
@@ -138,12 +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("apt-get update failed inside the build context. \
|
||||||
"Could not execute apt-get update. If this is a local build, try executing with sudo."
|
If this is a local build, try executing with sudo, \
|
||||||
.into(),
|
or re-run with RUST_LOG=debug for more details."
|
||||||
);
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install essential packages
|
// Install essential packages
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
mod cross;
|
mod cross;
|
||||||
mod ephemeral;
|
mod ephemeral;
|
||||||
mod local;
|
mod local;
|
||||||
mod sbuild;
|
|
||||||
|
|
||||||
use crate::context::{self, Context};
|
use crate::context::{self, Context};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
@@ -11,8 +10,6 @@ use std::sync::Arc;
|
|||||||
/// Build mode for the binary build
|
/// Build mode for the binary build
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub enum BuildMode {
|
pub enum BuildMode {
|
||||||
/// Use `sbuild` for the build, configured in unshare mode
|
|
||||||
Sbuild,
|
|
||||||
/// Local build, directly on the context
|
/// Local build, directly on the context
|
||||||
Local,
|
Local,
|
||||||
}
|
}
|
||||||
@@ -110,15 +107,6 @@ pub async fn build_binary_package(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
BuildMode::Sbuild => sbuild::build(
|
|
||||||
&package,
|
|
||||||
&version,
|
|
||||||
arch,
|
|
||||||
series,
|
|
||||||
&build_root,
|
|
||||||
cross,
|
|
||||||
build_ctx.clone(),
|
|
||||||
)?,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Retrieve produced .deb files
|
// Retrieve produced .deb files
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
/// Sbuild binary package building
|
|
||||||
/// Call 'sbuild' with the dsc file to build the package with unshare
|
|
||||||
use crate::context::Context;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub fn build(
|
|
||||||
package: &str,
|
|
||||||
version: &str,
|
|
||||||
arch: &str,
|
|
||||||
series: &str,
|
|
||||||
build_root: &str,
|
|
||||||
cross: bool,
|
|
||||||
ctx: Arc<Context>,
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
// Find the actual package directory
|
|
||||||
let package_dir =
|
|
||||||
crate::deb::find_package_directory(Path::new(build_root), package, version, &ctx)?;
|
|
||||||
let package_dir_str = package_dir
|
|
||||||
.to_str()
|
|
||||||
.ok_or("Invalid package directory path")?;
|
|
||||||
|
|
||||||
let mut cmd = ctx.command("sbuild");
|
|
||||||
cmd.current_dir(package_dir_str);
|
|
||||||
cmd.arg("--chroot-mode=unshare");
|
|
||||||
cmd.arg("--no-clean-source");
|
|
||||||
|
|
||||||
if cross {
|
|
||||||
cmd.arg(format!("--host={}", arch));
|
|
||||||
} else {
|
|
||||||
cmd.arg(format!("--arch={}", arch));
|
|
||||||
}
|
|
||||||
cmd.arg(format!("--dist={}", series));
|
|
||||||
|
|
||||||
// Add output directory argument
|
|
||||||
cmd.arg(format!("--build-dir={}", build_root));
|
|
||||||
|
|
||||||
let status = cmd.status()?;
|
|
||||||
if !status.success() {
|
|
||||||
return Err(format!("sbuild failed with status: {}", status).into());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
+65
-11
@@ -110,11 +110,29 @@ 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?
|
||||||
@@ -163,7 +181,7 @@ pub async fn get_n_latest_released_series(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort by release date descending (newest first)
|
// Sort by release date descending (newest first)
|
||||||
released_series.sort_by(|a, b| b.release.cmp(&a.release));
|
released_series.sort_by_key(|b| std::cmp::Reverse(b.release));
|
||||||
|
|
||||||
Ok(released_series
|
Ok(released_series
|
||||||
.iter()
|
.iter()
|
||||||
@@ -188,13 +206,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 +235,18 @@ 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,13 @@ 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 +338,22 @@ 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?
|
||||||
|
|||||||
+44
-12
@@ -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 =
|
||||||
@@ -60,8 +71,8 @@ fn main() {
|
|||||||
.long_help("Inject a package into the build environment before build-dep. Can be a .deb file path, a package name from the archive, or a package from a previously added PPA. Can be specified multiple times.").required(false).action(clap::ArgAction::Append))
|
.long_help("Inject a package into the build environment before build-dep. Can be a .deb file path, a package name from the archive, or a package from a previously added PPA. Can be specified multiple times.").required(false).action(clap::ArgAction::Append))
|
||||||
.arg(arg!(--cross "Cross-compile for target architecture (instead of qemu-binfmt)")
|
.arg(arg!(--cross "Cross-compile for target architecture (instead of qemu-binfmt)")
|
||||||
.long_help("Cross-compile for target architecture (instead of using qemu-binfmt)\nNote that most packages cannot be cross-compiled").required(false))
|
.long_help("Cross-compile for target architecture (instead of using qemu-binfmt)\nNote that most packages cannot be cross-compiled").required(false))
|
||||||
.arg(arg!(--mode <mode> "Change build mode [sbuild, local]").required(false)
|
.arg(arg!(--mode <mode> "Change build mode [local]").required(false)
|
||||||
.long_help("Change build mode [sbuild, local]\nDefault will chose depending on other parameters, don't provide if unsure")),
|
.long_help("Change build mode [local]\nDefault will chose depending on other parameters, don't provide if unsure")),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("context")
|
Command::new("context")
|
||||||
@@ -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);
|
||||||
@@ -183,7 +211,6 @@ fn main() {
|
|||||||
};
|
};
|
||||||
let mode: Option<&str> = sub_matches.get_one::<String>("mode").map(|s| s.as_str());
|
let mode: Option<&str> = sub_matches.get_one::<String>("mode").map(|s| s.as_str());
|
||||||
let mode: Option<pkh::deb::BuildMode> = match mode {
|
let mode: Option<pkh::deb::BuildMode> = match mode {
|
||||||
Some("sbuild") => Some(pkh::deb::BuildMode::Sbuild),
|
|
||||||
Some("local") => Some(pkh::deb::BuildMode::Local),
|
Some("local") => Some(pkh::deb::BuildMode::Local),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
@@ -219,9 +246,14 @@ fn main() {
|
|||||||
let context = match type_str {
|
let context = match type_str {
|
||||||
"local" => ContextConfig::Local,
|
"local" => ContextConfig::Local,
|
||||||
"ssh" => {
|
"ssh" => {
|
||||||
let endpoint = args
|
let endpoint =
|
||||||
.get_one::<String>("endpoint")
|
args.get_one::<String>("endpoint").unwrap_or_else(|| {
|
||||||
.expect("Endpoint is required for ssh context");
|
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]
|
||||||
|
|||||||
+58
-7
@@ -39,8 +39,47 @@ pub struct FileEntry {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
/// Size of the file
|
/// Size of the file
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
/// SHA256 hash for the file
|
/// Checksum hash for the file
|
||||||
pub sha256: String,
|
pub checksum: String,
|
||||||
|
/// Algorithm used for the checksum
|
||||||
|
pub checksum_algo: ChecksumAlgo,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checksum algorithm used for a file entry
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ChecksumAlgo {
|
||||||
|
/// MD5 (legacy 'Files' field)
|
||||||
|
Md5,
|
||||||
|
/// SHA-256 ('Checksums-Sha256' field)
|
||||||
|
Sha256,
|
||||||
|
/// SHA-512 ('Checksums-Sha512' field)
|
||||||
|
Sha512,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChecksumAlgo {
|
||||||
|
/// Compute the hex-encoded digest of the given data using this algorithm
|
||||||
|
pub fn hex_digest(&self, data: &[u8]) -> String {
|
||||||
|
match self {
|
||||||
|
ChecksumAlgo::Md5 => {
|
||||||
|
use md5::{Digest, Md5};
|
||||||
|
let mut hasher = Md5::new();
|
||||||
|
hasher.update(data);
|
||||||
|
hex::encode(hasher.finalize())
|
||||||
|
}
|
||||||
|
ChecksumAlgo::Sha256 => {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(data);
|
||||||
|
hex::encode(hasher.finalize())
|
||||||
|
}
|
||||||
|
ChecksumAlgo::Sha512 => {
|
||||||
|
use sha2::{Digest, Sha512};
|
||||||
|
let mut hasher = Sha512::new();
|
||||||
|
hasher.update(data);
|
||||||
|
hex::encode(hasher.finalize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A package 'stanza' as found is 'Sources.gz' files, containing basic information about a source package
|
/// A package 'stanza' as found is 'Sources.gz' files, containing basic information about a source package
|
||||||
@@ -138,16 +177,28 @@ impl Iterator for DebianSources {
|
|||||||
return self.next();
|
return self.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse package files
|
// Parse package files.
|
||||||
|
// Prefer the strongest available checksum field: Checksums-Sha256,
|
||||||
|
// then Checksums-Sha512, then the legacy 'Files' (MD5) field.
|
||||||
|
// Some archives (e.g. the Ubuntu development series) no longer ship
|
||||||
|
// Checksums-Sha256, so falling back is required to keep working.
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
if let Some(checksums) = fields.get("Checksums-Sha256") {
|
let (checksum_field, algo) = if fields.contains_key("Checksums-Sha256") {
|
||||||
|
("Checksums-Sha256", ChecksumAlgo::Sha256)
|
||||||
|
} else if fields.contains_key("Checksums-Sha512") {
|
||||||
|
("Checksums-Sha512", ChecksumAlgo::Sha512)
|
||||||
|
} else {
|
||||||
|
("Files", ChecksumAlgo::Md5)
|
||||||
|
};
|
||||||
|
if let Some(checksums) = fields.get(checksum_field) {
|
||||||
for line in checksums.lines() {
|
for line in checksums.lines() {
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
if parts.len() >= 3 {
|
if parts.len() >= 3 {
|
||||||
files.push(FileEntry {
|
files.push(FileEntry {
|
||||||
sha256: parts[0].to_string(),
|
checksum: parts[0].to_string(),
|
||||||
size: parts[1].parse().unwrap_or(0),
|
size: parts[1].parse().unwrap_or(0),
|
||||||
name: parts[2].to_string(),
|
name: parts[2].to_string(),
|
||||||
|
checksum_algo: algo,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +264,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 +349,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()]
|
||||||
};
|
};
|
||||||
|
|||||||
+96
-18
@@ -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;
|
||||||
|
|
||||||
@@ -82,7 +83,6 @@ fn clone_repo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
@@ -101,16 +101,58 @@ 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 {
|
||||||
|
// Copy the file, preserving the source modification and access
|
||||||
|
// times. This is important for autotools/gnulib-based packages
|
||||||
|
// (e.g. 'hello' from Debian sid) that ship pre-generated files
|
||||||
|
// alongside their prerequisites: if the copy resets the mtime to
|
||||||
|
// "now", the prerequisites appear as new as the generated targets
|
||||||
|
// and `make` tries to regenerate them using tools (like gperf)
|
||||||
|
// that are not declared build-dependencies.
|
||||||
std::fs::copy(&src_path, &dst_path)?;
|
std::fs::copy(&src_path, &dst_path)?;
|
||||||
|
copy_file_times(&src_path, &dst_path)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restore the modification and access times of `dest` to match `src`.
|
||||||
|
///
|
||||||
|
/// `std::fs::copy` resets the destination mtime to "now", which breaks
|
||||||
|
/// timestamp-based build systems (autotools/gnulib) that ship pre-generated
|
||||||
|
/// files alongside their prerequisites. Restoring the original timestamps
|
||||||
|
/// prevents `make` from needlessly regenerating those files with tools that
|
||||||
|
/// may not be installed (e.g. `gperf`).
|
||||||
|
fn copy_file_times(src: &Path, dest: &Path) -> Result<(), Box<dyn Error>> {
|
||||||
|
let metadata = std::fs::metadata(src)?;
|
||||||
|
let mut times = std::fs::FileTimes::new();
|
||||||
|
let mut have_times = false;
|
||||||
|
if let Ok(mtime) = metadata.modified() {
|
||||||
|
times = times.set_modified(mtime);
|
||||||
|
have_times = true;
|
||||||
|
}
|
||||||
|
if let Ok(atime) = metadata.accessed() {
|
||||||
|
times = times.set_accessed(atime);
|
||||||
|
have_times = true;
|
||||||
|
}
|
||||||
|
if have_times && let Ok(dest_file) = std::fs::File::open(dest) {
|
||||||
|
let _ = dest_file.set_times(times);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper function to extract tar archive with progress tracking
|
/// Helper function to extract tar archive with progress tracking
|
||||||
fn extract_tar_archive<D, F>(
|
fn extract_tar_archive<D, F>(
|
||||||
file_path: &Path,
|
file_path: &Path,
|
||||||
@@ -183,12 +225,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());
|
||||||
}
|
}
|
||||||
@@ -198,18 +253,19 @@ fn checkout_pristine_tar(package_dir: &Path, filename: &str) -> Result<(), Box<d
|
|||||||
async fn download_file_checksum(
|
async fn download_file_checksum(
|
||||||
url: &str,
|
url: &str,
|
||||||
checksum: &str,
|
checksum: &str,
|
||||||
|
algo: crate::package_info::ChecksumAlgo,
|
||||||
target_dir: &Path,
|
target_dir: &Path,
|
||||||
progress: ProgressCallback<'_>,
|
progress: ProgressCallback<'_>,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
// Download with reqwest
|
// Download with reqwest
|
||||||
let response = reqwest::get(url).await?;
|
let response = reqwest::get(url).await?;
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(format!("Failed to download '{}' : {}", &url, response.status()).into());
|
return Err(format!("Failed to download '{}' : {}", url, response.status()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_size = response
|
let total_size = response
|
||||||
.content_length()
|
.content_length()
|
||||||
.ok_or(format!("Failed to get content length from '{}'", &url))?;
|
.ok_or(format!("Failed to get content length from '{}'", url))?;
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
|
|
||||||
// Target file: extract file name from URL
|
// Target file: extract file name from URL
|
||||||
@@ -219,11 +275,13 @@ async fn download_file_checksum(
|
|||||||
|
|
||||||
// Download chunk by chunk to disk, while updating hasher for checksum
|
// Download chunk by chunk to disk, while updating hasher for checksum
|
||||||
let mut stream = response.bytes_stream();
|
let mut stream = response.bytes_stream();
|
||||||
let mut hasher = Sha256::new();
|
// Accumulate the downloaded bytes so we can compute the final digest with the
|
||||||
|
// correct algorithm once the download is complete.
|
||||||
|
let mut buffer: Vec<u8> = Vec::with_capacity(total_size as usize);
|
||||||
while let Some(item) = stream.next().await {
|
while let Some(item) = stream.next().await {
|
||||||
let chunk = item?;
|
let chunk = item?;
|
||||||
file.write_all(&chunk)?;
|
file.write_all(&chunk)?;
|
||||||
hasher.update(&chunk);
|
buffer.extend_from_slice(&chunk);
|
||||||
|
|
||||||
if let Some(cb) = progress {
|
if let Some(cb) = progress {
|
||||||
index = min(index + chunk.len(), total_size as usize);
|
index = min(index + chunk.len(), total_size as usize);
|
||||||
@@ -231,9 +289,8 @@ async fn download_file_checksum(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify checksum
|
// Verify checksum using the algorithm specified for this file
|
||||||
let result = hasher.finalize();
|
let calculated_checksum = algo.hex_digest(&buffer);
|
||||||
let calculated_checksum = hex::encode(result);
|
|
||||||
if calculated_checksum != checksum {
|
if calculated_checksum != checksum {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Checksum mismatch! Expected {}, got {}",
|
"Checksum mismatch! Expected {}, got {}",
|
||||||
@@ -300,7 +357,18 @@ async fn fetch_orig_tarball(
|
|||||||
.files
|
.files
|
||||||
.iter()
|
.iter()
|
||||||
.find(|f| f.name.contains(".orig.tar."))
|
.find(|f| f.name.contains(".orig.tar."))
|
||||||
.unwrap();
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"Could not find orig tarball in file list for package '{}'. \
|
||||||
|
Available files: {:?}",
|
||||||
|
info.stanza.package,
|
||||||
|
info.stanza
|
||||||
|
.files
|
||||||
|
.iter()
|
||||||
|
.map(|f| &f.name)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let filename = &orig_file.name;
|
let filename = &orig_file.name;
|
||||||
|
|
||||||
// 1. Try executing pristine-tar
|
// 1. Try executing pristine-tar
|
||||||
@@ -319,8 +387,9 @@ async fn fetch_orig_tarball(
|
|||||||
// or the current directory if cwd is None (which effectively is the parent of the package dir)
|
// or the current directory if cwd is None (which effectively is the parent of the package dir)
|
||||||
let target_dir = cwd.unwrap_or_else(|| Path::new("."));
|
let target_dir = cwd.unwrap_or_else(|| Path::new("."));
|
||||||
download_file_checksum(
|
download_file_checksum(
|
||||||
format!("{}/{}", &info.archive_url, filename).as_str(),
|
format!("{}/{}", info.archive_url, filename).as_str(),
|
||||||
&orig_file.sha256,
|
&orig_file.checksum,
|
||||||
|
orig_file.checksum_algo,
|
||||||
target_dir,
|
target_dir,
|
||||||
progress,
|
progress,
|
||||||
)
|
)
|
||||||
@@ -349,8 +418,9 @@ async fn fetch_dsc_file(
|
|||||||
debug!("Fetching dsc file: {}", filename);
|
debug!("Fetching dsc file: {}", filename);
|
||||||
|
|
||||||
download_file_checksum(
|
download_file_checksum(
|
||||||
format!("{}/{}", &info.archive_url, filename).as_str(),
|
format!("{}/{}", info.archive_url, filename).as_str(),
|
||||||
&dsc_file.sha256,
|
&dsc_file.checksum,
|
||||||
|
dsc_file.checksum_algo,
|
||||||
target_dir,
|
target_dir,
|
||||||
progress,
|
progress,
|
||||||
)
|
)
|
||||||
@@ -374,7 +444,14 @@ async fn fetch_archive_sources(
|
|||||||
|
|
||||||
for file in &info.stanza.files {
|
for file in &info.stanza.files {
|
||||||
let url = format!("{}/{}", info.archive_url, file.name);
|
let url = format!("{}/{}", info.archive_url, file.name);
|
||||||
download_file_checksum(&url, &file.sha256, package_dir, progress).await?;
|
download_file_checksum(
|
||||||
|
&url,
|
||||||
|
&file.checksum,
|
||||||
|
file.checksum_algo,
|
||||||
|
package_dir,
|
||||||
|
progress,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Extract all tar archives, merging extracted directories
|
// Extract all tar archives, merging extracted directories
|
||||||
if file.name.ends_with(".tar.gz") || file.name.ends_with(".tar.xz") {
|
if file.name.ends_with(".tar.gz") || file.name.ends_with(".tar.xz") {
|
||||||
@@ -423,6 +500,7 @@ async fn fetch_archive_sources(
|
|||||||
copy_dir_all(&sub_path, &target_path)?;
|
copy_dir_all(&sub_path, &target_path)?;
|
||||||
} else {
|
} else {
|
||||||
std::fs::copy(&sub_path, &target_path)?;
|
std::fs::copy(&sub_path, &target_path)?;
|
||||||
|
copy_file_times(&sub_path, &target_path)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::fs::remove_dir_all(&src_dir)?;
|
std::fs::remove_dir_all(&src_dir)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user