From b5ac76a29304b00ed7fda2900b1fc22bdcce5d6c Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Tue, 16 Dec 2025 20:12:08 +0100 Subject: [PATCH] deb: retrieve .deb artifacts sbuild options: unshare, build directory context: retrieve files --- src/context/api.rs | 15 ++++++++++++++ src/context/local.rs | 16 ++++++++++++++- src/context/ssh.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++ src/deb.rs | 25 ++++++++++++++++++++++- 4 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/context/api.rs b/src/context/api.rs index bfabc0a..fbb2158 100644 --- a/src/context/api.rs +++ b/src/context/api.rs @@ -18,6 +18,8 @@ pub trait CommandRunner { pub trait ContextDriver { fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result; + fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()>; + fn list_files(&self, path: &Path) -> io::Result>; fn create_runner(&self, program: String) -> Box; fn prepare_work_dir(&self) -> io::Result; } @@ -60,6 +62,19 @@ impl Context { self.driver().prepare_work_dir() } + /// Retrieve a file or directory from the context to the local filesystem. + /// + /// The `src` path is on the context, `dest` is on the local machine. + /// If `src` is a directory, it is copied recursively. + pub fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()> { + self.driver().retrieve_path(src, dest) + } + + /// List files in a directory on the context. + pub fn list_files(&self, path: &Path) -> io::Result> { + self.driver().list_files(path) + } + fn driver(&self) -> Box { match self { Context::Local => Box::new(LocalDriver), diff --git a/src/context/local.rs b/src/context/local.rs index ab2ba94..1f1c2d1 100644 --- a/src/context/local.rs +++ b/src/context/local.rs @@ -21,7 +21,21 @@ impl ContextDriver for LocalDriver { } fn prepare_work_dir(&self) -> io::Result { - Ok(".".to_string()) + // TODO: Fix that, we should not always use '..' as work directory locally + Ok("..".to_string()) + } + + fn retrieve_path(&self, _src: &Path, _dest: &Path) -> io::Result<()> { + Ok(()) + } + + fn list_files(&self, path: &Path) -> io::Result> { + let mut entries = Vec::new(); + for entry in std::fs::read_dir(path)? { + let entry = entry?; + entries.push(entry.path()); + } + Ok(entries) } } diff --git a/src/context/ssh.rs b/src/context/ssh.rs index d44449a..eda9fb0 100644 --- a/src/context/ssh.rs +++ b/src/context/ssh.rs @@ -77,6 +77,30 @@ impl ContextDriver for SshDriver { }) } + fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()> { + let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?; + let sftp = sess.sftp().map_err(io::Error::other)?; + + debug!("Downloading remote {:?} to {:?}", src, dest); + Self::download_recursive(&sftp, src, dest) + } + + fn list_files(&self, path: &Path) -> io::Result> { + let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?; + let sftp = sess.sftp().map_err(io::Error::other)?; + + let mut files = Vec::new(); + let entries = sftp.readdir(path).map_err(io::Error::other)?; + for (p, _) in entries { + let file_name = p.file_name().unwrap(); + if file_name == "." || file_name == ".." { + continue; + } + files.push(p); + } + Ok(files) + } + fn prepare_work_dir(&self) -> io::Result { let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?; let mut channel = sess.channel_session().map_err(io::Error::other)?; @@ -118,6 +142,30 @@ impl SshDriver { } Ok(()) } + + fn download_recursive(sftp: &ssh2::Sftp, src: &Path, dest: &Path) -> io::Result<()> { + let stat = sftp + .stat(src) + .map_err(|e| io::Error::other(format!("Remote stat failed for {:?}: {}", src, e)))?; + + if stat.is_dir() { + fs::create_dir_all(dest)?; + let entries = sftp.readdir(src).map_err(io::Error::other)?; + for (path, _) in entries { + let file_name = path.file_name().unwrap(); + if file_name == "." || file_name == ".." { + continue; + } + let new_dest = dest.join(file_name); + Self::download_recursive(sftp, &path, &new_dest)?; + } + } else { + let mut remote_file = sftp.open(src).map_err(io::Error::other)?; + let mut local_file = fs::File::create(dest)?; + io::copy(&mut remote_file, &mut local_file)?; + } + Ok(()) + } } pub struct SshRunner { diff --git a/src/deb.rs b/src/deb.rs index 94e278c..08a9727 100644 --- a/src/deb.rs +++ b/src/deb.rs @@ -37,7 +37,24 @@ pub fn build_binary_package( ); // Run sbuild - run_sbuild(&ctx, &remote_dsc_path, arch, series)?; + run_sbuild(&ctx, &remote_dsc_path, arch, series, &build_root)?; + + // Retrieve artifacts + // Always retrieve to the directory containing the .dsc file + let local_output_dir = dsc_path + .parent() + .ok_or("Could not determine parent directory of dsc file")?; + println!("Retrieving artifacts to {}...", local_output_dir.display()); + + // Only retrieve .deb files + let remote_files = ctx.list_files(Path::new(&build_root))?; + for remote_file in remote_files { + if remote_file.extension().is_some_and(|ext| ext == "deb") { + let file_name = remote_file.file_name().ok_or("Invalid remote filename")?; + let local_dest = local_output_dir.join(file_name); + ctx.retrieve_path(&remote_file, &local_dest)?; + } + } Ok(()) } @@ -116,8 +133,11 @@ fn run_sbuild( dsc_path: &Path, arch: Option<&str>, series: Option<&str>, + output_dir: &str, ) -> Result<(), Box> { let mut cmd = ctx.command("sbuild"); + cmd.arg("--chroot-mode=unshare"); + if let Some(a) = arch { cmd.arg(format!("--arch={}", a)); } @@ -125,6 +145,9 @@ fn run_sbuild( cmd.arg(format!("--dist={}", s)); } + // Add output directory argument + cmd.arg(format!("--build-dir={}", output_dir)); + let status = cmd.arg(dsc_path).status()?; if !status.success() {