diff --git a/src/context/schroot.rs b/src/context/schroot.rs index df19519..ddd284a 100644 --- a/src/context/schroot.rs +++ b/src/context/schroot.rs @@ -1,3 +1,5 @@ +/// Schroot context: execute commands in a schroot session +/// Not tested, will need more work! use super::api::ContextDriver; use std::io; use std::path::{Path, PathBuf}; @@ -9,25 +11,104 @@ pub struct SchrootDriver { pub parent: Option>, } +use super::api::{Context, ContextConfig}; + +impl SchrootDriver { + fn parent(&self) -> Arc { + self.parent + .clone() + .unwrap_or_else(|| Arc::new(Context::new(ContextConfig::Local))) + } + + fn ensure_session(&self) -> io::Result { + let mut session_lock = self.session.lock().unwrap(); + if let Some(id) = session_lock.as_ref() { + return Ok(id.clone()); + } + + // Create new session + let output = self + .parent() + .command("schroot") + .arg("-b") + .arg("-c") + .arg(&self.name) + .output()?; + + if !output.status.success() { + return Err(io::Error::other(format!( + "Failed to create schroot session: {}", + String::from_utf8_lossy(&output.stderr) + ))); + } + + let session_id = String::from_utf8(output.stdout) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? + .trim() + .to_string(); + + *session_lock = Some(session_id.clone()); + Ok(session_id) + } + + fn get_session_location(&self, session_id: &str) -> io::Result { + let output = self + .parent() + .command("schroot") + .arg("--location") + .arg("-c") + .arg(session_id) + .output()?; + + if !output.status.success() { + return Err(io::Error::other(format!( + "Failed to get schroot location: {}", + String::from_utf8_lossy(&output.stderr) + ))); + } + + Ok(String::from_utf8(output.stdout) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? + .trim() + .to_string()) + } +} + impl ContextDriver for SchrootDriver { fn ensure_available(&self, src: &Path, _dest_root: &str) -> io::Result { src.canonicalize() } - fn retrieve_path(&self, _src: &Path, _dest: &Path) -> io::Result<()> { - // TODO: Implement schroot file retrieval logic - Err(io::Error::new( - io::ErrorKind::Unsupported, - "retrieve_path not yet implemented for schroot", - )) + fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()> { + let session_id = self.ensure_session()?; + let location = self.get_session_location(&session_id)?; + + let path_in_chroot = src.strip_prefix("/").unwrap_or(src); + let host_src = Path::new(&location).join(path_in_chroot); + + self.parent().retrieve_path(&host_src, dest) } - fn list_files(&self, _path: &Path) -> io::Result> { - // TODO: Implement schroot file listing logic - Err(io::Error::new( - io::ErrorKind::Unsupported, - "list_files not yet implemented for schroot", - )) + fn list_files(&self, path: &Path) -> io::Result> { + let session_id = self.ensure_session()?; + let location = self.get_session_location(&session_id)?; + + let path_in_chroot = path.strip_prefix("/").unwrap_or(path); + let host_path = Path::new(&location).join(path_in_chroot); + + let files = self.parent().list_files(&host_path)?; + let mut chroot_files = Vec::new(); + + // TODO: Check if we *need* to strip the prefix. + // If we don't, we can just return `files`. + for file in files { + if let Ok(rel) = file.strip_prefix(&location) { + chroot_files.push(Path::new("/").join(rel)); + } else { + chroot_files.push(file); + } + } + Ok(chroot_files) } fn run( @@ -37,94 +118,48 @@ impl ContextDriver for SchrootDriver { env: &[(String, String)], cwd: Option<&str>, ) -> io::Result { - // Initialize session on first run - let mut session_lock = self.session.lock().unwrap(); - if session_lock.is_none() { - let session_output = if let Some(parent) = &self.parent { - parent - .command("schroot") - .arg("-b") - .arg("-c") - .arg(&self.name) - .output()? - } else { - std::process::Command::new("schroot") - .arg("-b") - .arg("-c") - .arg(&self.name) - .output()? - }; + let session_id = self.ensure_session()?; - if !session_output.status.success() { - return Err(io::Error::other(format!( - "Failed to create schroot session: {}", - String::from_utf8_lossy(&session_output.stderr) - ))); - } + // Construct the schroot command + // schroot -p -r -c session_id -- program args... + // If cwd is specified, we wrap in sh -c "cd cwd && ..." - let session_id = String::from_utf8(session_output.stdout) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? - .trim() - .to_string(); + let mut command_args = vec![ + "-p".to_string(), + "-r".to_string(), + "-c".to_string(), + session_id, + "--".to_string(), + ]; - *session_lock = Some(session_id); - } - drop(session_lock); + let mut actual_program = program.to_string(); + let mut actual_args = args.to_vec(); - let session_lock = self.session.lock().unwrap(); - let session_id = session_lock.as_ref().unwrap(); - - if let Some(parent) = &self.parent { - let mut cmd = parent.command("sudo"); - cmd.envs(env.iter().cloned()); + // Simplest: Wrap everything in `sh -c` if CWD or ENV is needed. + if cwd.is_some() || !env.is_empty() { + let mut shell_cmd = String::new(); if let Some(dir) = cwd { - cmd.arg("schroot") - .arg("-p") - .arg("-r") - .arg("-c") - .arg(session_id) - .arg("--") - .arg("sh") - .arg("-c") - .arg(format!("cd {} && {} {}", dir, program, args.join(" "))); - } else { - cmd.arg("schroot") - .arg("-p") - .arg("-r") - .arg("-c") - .arg(session_id) - .arg("--") - .arg(program) - .args(args); + shell_cmd.push_str(&format!("cd {} && ", dir)); } - cmd.status() - } else { - let mut cmd = std::process::Command::new("sudo"); - cmd.args(env.iter().map(|(k, v)| format!("{k}={v}"))); - if let Some(dir) = cwd { - cmd.arg("schroot") - .arg("-p") - .arg("-r") - .arg("-c") - .arg(session_id) - .arg("--") - .arg("sh") - .arg("-c") - .arg(format!("cd {} && {} {}", dir, program, args.join(" "))); - } else { - cmd.arg("schroot") - .arg("-p") - .arg("-r") - .arg("-c") - .arg(session_id) - .arg("--") - .arg(program) - .args(args); + if !env.is_empty() { + shell_cmd.push_str("env "); + for (k, v) in env { + shell_cmd.push_str(&format!("{}={} ", k, v)); + } } - cmd.status() + + shell_cmd.push_str(&format!("{} {}", program, args.join(" "))); + + actual_program = "sh".to_string(); + actual_args = vec!["-c".to_string(), shell_cmd]; } + + command_args.push(actual_program); + command_args.extend(actual_args); + + self.parent().command("schroot").args(command_args).status() } fn run_output( @@ -134,66 +169,42 @@ impl ContextDriver for SchrootDriver { env: &[(String, String)], cwd: Option<&str>, ) -> io::Result { - if let Some(parent) = &self.parent { - let mut cmd = parent.command("sudo"); - cmd.envs(env.iter().cloned()); + let session_id = self.ensure_session()?; + + let mut command_args = vec![ + "-r".to_string(), + "-c".to_string(), + session_id, + "--".to_string(), + ]; + + let mut actual_program = program.to_string(); + let mut actual_args = args.to_vec(); + + if cwd.is_some() || !env.is_empty() { + let mut shell_cmd = String::new(); if let Some(dir) = cwd { - cmd.arg("schroot") - .arg("-r") - .arg("-c") - .arg(&self.name) - .arg("--") - .arg("sh") - .arg("-c"); - - let mut cmd_str = String::new(); - cmd_str.push_str(&format!("cd {} && {} {}", dir, program, args.join(" "))); - cmd.arg(cmd_str); - } else { - cmd.arg("schroot") - .arg("-r") - .arg("-c") - .arg(&self.name) - .arg("--"); - - cmd.arg(program).args(args); + shell_cmd.push_str(&format!("cd {} && ", dir)); } - cmd.output() - } else { - let mut cmd = std::process::Command::new("sudo"); - if let Some(dir) = cwd { - cmd.arg("schroot") - .arg("-r") - .arg("-c") - .arg(&self.name) - .arg("--") - .arg("sh") - .arg("-c"); - let mut cmd_str = String::new(); + if !env.is_empty() { + shell_cmd.push_str("env "); for (k, v) in env { - cmd_str.push_str(&format!("{}={} ", k, v)); + shell_cmd.push_str(&format!("{}={} ", k, v)); } - cmd_str.push_str(&format!("cd {} && {} {}", dir, program, args.join(" "))); - cmd.arg(cmd_str); - } else { - cmd.arg("schroot") - .arg("-r") - .arg("-c") - .arg(&self.name) - .arg("--"); - - if !env.is_empty() { - cmd.arg("env"); - for (k, v) in env { - cmd.arg(format!("{}={}", k, v)); - } - } - cmd.arg(program).args(args); } - cmd.output() + + shell_cmd.push_str(&format!("{} {}", program, args.join(" "))); + + actual_program = "sh".to_string(); + actual_args = vec!["-c".to_string(), shell_cmd]; } + + command_args.push(actual_program); + command_args.extend(actual_args); + + self.parent().command("schroot").args(command_args).output() } fn create_temp_dir(&self) -> io::Result { @@ -233,7 +244,6 @@ impl ContextDriver for SchrootDriver { } fn write_file(&self, path: &Path, content: &str) -> io::Result<()> { - // TODO: change that command, it's not safe let status = self.run( "sh", &[ diff --git a/src/context/unshare.rs b/src/context/unshare.rs index 110e8a5..d88c478 100644 --- a/src/context/unshare.rs +++ b/src/context/unshare.rs @@ -70,12 +70,9 @@ impl ContextDriver for UnshareDriver { Ok(Path::new(dest_root).join(filename)) } - fn retrieve_path(&self, _src: &Path, _dest: &Path) -> io::Result<()> { - // TODO: Implement chroot file retrieval logic - Err(io::Error::new( - io::ErrorKind::Unsupported, - "retrieve_path not yet implemented for chroot", - )) + fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()> { + let host_src = Path::new(&self.path).join(src.to_string_lossy().trim_start_matches('/')); + self.parent().retrieve_path(&host_src, dest) } fn list_files(&self, path: &Path) -> io::Result> { diff --git a/src/deb/cross.rs b/src/deb/cross.rs index 1766ecd..3a8434b 100644 --- a/src/deb/cross.rs +++ b/src/deb/cross.rs @@ -3,59 +3,89 @@ use crate::context::{Context, ContextConfig}; use std::collections::HashMap; use std::error::Error; -/// Setup a specific chroot context for native cross builds -pub fn setup_native_context(series: &str) -> Result<(), Box> { - // Create a temporary directory for the chroot - let chroot_path_str = context::current().create_temp_dir()?; - let chroot_path = std::path::PathBuf::from(chroot_path_str); +use std::path::PathBuf; - log::debug!( - "Creating new chroot for {} at {}...", - series, - chroot_path.display() - ); - - let status = context::current() - .command("sudo") - .arg("mmdebstrap") - .arg("--variant=buildd") - .arg(series) - .arg(chroot_path.to_string_lossy().to_string()) - .status()?; - - if !status.success() { - // Clean up on failure - let _ = std::fs::remove_dir_all(&chroot_path); - return Err(format!("mmdebstrap failed for series {}", series).into()); - } - - // Switch to an ephemeral context to build the package in the chroot - context::manager().set_current_ephemeral(Context::new(ContextConfig::Unshare { - path: chroot_path.to_string_lossy().to_string(), - parent: Some(context::manager().current_name()), - })); - - Ok(()) +pub struct EphemeralContextGuard { + previous_context: String, + chroot_path: PathBuf, } -pub fn clean_native_context() -> Result<(), Box> { - let ctx = context::current(); - if let ContextConfig::Unshare { path, .. } = &ctx.config { - let chroot_path = path.clone(); +impl EphemeralContextGuard { + pub fn new(series: &str) -> Result> { + let current_context_name = context::manager().current_name(); + // Create a temporary directory for the chroot + let chroot_path_str = context::current().create_temp_dir()?; + let chroot_path = PathBuf::from(chroot_path_str); + + log::debug!( + "Creating new chroot for {} at {}...", + series, + chroot_path.display() + ); + + let status = context::current() + .command("sudo") + .arg("mmdebstrap") + .arg("--variant=buildd") + .arg(series) + .arg(chroot_path.to_string_lossy().to_string()) + .status()?; + + if !status.success() { + // Clean up on failure + let _ = std::fs::remove_dir_all(&chroot_path); + return Err(format!("mmdebstrap failed for series {}", series).into()); + } + + // Switch to an ephemeral context to build the package in the chroot + context::manager().set_current_ephemeral(Context::new(ContextConfig::Unshare { + path: chroot_path.to_string_lossy().to_string(), + parent: Some(current_context_name.clone()), + })); + + Ok(Self { + previous_context: current_context_name, + chroot_path, + }) + } +} + +impl Drop for EphemeralContextGuard { + fn drop(&mut self) { + log::debug!("Cleaning up ephemeral context..."); // Reset to normal context - context::manager().set_current(&context::manager().current_name())?; + if let Err(e) = context::manager().set_current(&self.previous_context) { + log::error!("Failed to restore context {}: {}", self.previous_context, e); + } // Remove chroot directory - context::current() + // We use the restored context to execute the cleanup command + let result = context::current() .command("sudo") .arg("rm") .arg("-rf") - .arg(chroot_path) - .status()?; - } + .arg(&self.chroot_path) + .status(); - Ok(()) + match result { + Ok(status) => { + if !status.success() { + log::error!( + "Failed to remove chroot directory {}", + self.chroot_path.display() + ); + } + } + Err(e) => { + log::error!( + "Failed to execute cleanup command for {}: {}", + self.chroot_path.display(), + e + ); + } + } + } } /// Set environment variables for cross-compilation @@ -92,6 +122,7 @@ pub fn setup_environment( /// This also handles the 'ports.ubuntu.com' vs 'archive.ubuntu.com' on Ubuntu pub fn ensure_repositories(arch: &str, series: &str) -> Result<(), Box> { let ctx = context::current(); + let local_arch = crate::get_current_arch(); // Add target ('host') architecture ctx.command("dpkg") @@ -115,114 +146,135 @@ pub fn ensure_repositories(arch: &str, series: &str) -> Result<(), Box> {}", ports_block, deb822_path)) - .status()?; - } + ensure_repositories_deb822(&ctx, arch, &local_arch, series, deb822_path)?; } else { - // Traditional sources.list - let sources_path = "/etc/apt/sources.list"; - - // Scope archive.ubuntu.com and security.ubuntu.com to amd64 if not already scoped - ctx.command("sed") - .arg("-i") - .arg(r"/archive.ubuntu.com\|security.ubuntu.com/ { /arch=/ ! { /^deb \[/ ! s/^deb /deb [arch=amd64] /; /^deb \[/ s/^deb \[\([^]]*\)\]/deb [arch=amd64 \1]/ } }") - .arg(sources_path) - .status()?; - - // Ensure all components (main restricted universe multiverse) are present for all archive/security lines - // We match any combination of components at the end and replace with the full set - ctx.command("sed") - .arg("-i") - .arg(r"/archive.ubuntu.com\|security.ubuntu.com/ s/\( main\)\?\([ ]\+restricted\)\?\([ ]\+universe\)\?\([ ]\+multiverse\)\?$/ main restricted universe multiverse/") - .arg(sources_path) - .status()?; - - // Ensure all pockets exist. If not, we append them. - // We ignore 'proposed' as it contains unstable software - for pocket in ["", "-updates", "-backports", "-security"] { - let suite = format!("{}{}", series, pocket); - let has_suite = ctx - .command("grep") - .arg("-q") - .arg(format!(" {}", suite)) - .arg(sources_path) - .status()? - .success(); - - if !has_suite { - let line = format!( - "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ {} main restricted universe multiverse", - suite - ); - ctx.command("sh") - .arg("-c") - .arg(format!("echo '{}' >> {}", line, sources_path)) - .status()?; - } - } - - // Add ports repository to sources.list if not already present - let has_ports = ctx - .command("grep") - .arg("-q") - .arg("ports.ubuntu.com") - .arg(sources_path) - .status()? - .success(); - - if !has_ports { - let ports_lines = format!( - "deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series} main restricted universe multiverse\n\ - deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-updates main restricted universe multiverse\n\ - deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-backports main restricted universe multiverse\n\ - deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-security main restricted universe multiverse" - ); - ctx.command("sh") - .arg("-c") - .arg(format!("echo '{}' >> {}", ports_lines, sources_path)) - .status()?; - } + ensure_repositories_legacy(&ctx, arch, &local_arch, series, "/etc/apt/sources.list")?; } Ok(()) } + +fn ensure_repositories_deb822( + ctx: &context::Context, + arch: &str, + local_arch: &str, + series: &str, + deb822_path: &str, +) -> Result<(), Box> { + // Scope existing to local_arch if not already scoped + ctx.command("sed") + .arg("-i") + .arg(format!("/URIs:.*\\(archive\\|security\\)\\.ubuntu\\.com/ {{ n; /^Architectures:/ ! i Architectures: {} }}", local_arch)) + .arg(deb822_path) + .status()?; + + // Ensure all components are enabled for the primary architecture + ctx.command("sed") + .arg("-i") + .arg("/URIs:.*\\(archive\\|security\\)\\.ubuntu\\.com/,/Components:/ s/^Components:.*/Components: main restricted universe multiverse/") + .arg(deb822_path) + .status()?; + + // Ensure all suites (pockets) are enabled for the primary architecture + // Excluding 'proposed' as it contains unstable software + let suites = format!("{series} {series}-updates {series}-backports {series}-security"); + ctx.command("sed") + .arg("-i") + .arg(format!( + "/URIs:.*\\(archive\\|security\\)\\.ubuntu\\.com/,/Suites:/ s/^Suites:.*/Suites: {}/", + suites + )) + .arg(deb822_path) + .status()?; + + // Add ports if not already present + let has_ports = ctx + .command("grep") + .arg("-q") + .arg("ports.ubuntu.com") + .arg(deb822_path) + .status()? + .success(); + + if !has_ports { + let ports_block = format!( + "\nTypes: deb\nURIs: http://ports.ubuntu.com/ubuntu-ports\nSuites: {series} {series}-updates {series}-backports {series}-security\nComponents: main restricted universe multiverse\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\nArchitectures: {arch}\n" + ); + ctx.command("sh") + .arg("-c") + .arg(format!("echo '{}' >> {}", ports_block, deb822_path)) + .status()?; + } + Ok(()) +} + +fn ensure_repositories_legacy( + ctx: &context::Context, + arch: &str, + local_arch: &str, + series: &str, + sources_path: &str, +) -> Result<(), Box> { + // Scope archive.ubuntu.com and security.ubuntu.com to local_arch if not already scoped + ctx.command("sed") + .arg("-i") + .arg(format!( + r"/archive.ubuntu.com\|security.ubuntu.com/ {{ /arch=/ ! {{ /^deb \[/ ! s/^deb /deb [arch={}] /; /^deb \[/ s/^deb \[\([^]]*\)\]/deb [arch={} \1]/ }} }}", + local_arch, local_arch + )) + .arg(sources_path) + .status()?; + + // Ensure all components (main restricted universe multiverse) are present for all archive/security lines + ctx.command("sed") + .arg("-i") + .arg(r"/archive.ubuntu.com\|security.ubuntu.com/ s/\( main\)\?\([ ]\+restricted\)\?\([ ]\+universe\)\?\([ ]\+multiverse\)\?$/ main restricted universe multiverse/") + .arg(sources_path) + .status()?; + + // Ensure all pockets exist. If not, we append them. + for pocket in ["", "-updates", "-backports", "-security"] { + let suite = format!("{}{}", series, pocket); + let has_suite = ctx + .command("grep") + .arg("-q") + .arg(format!(" {}", suite)) + .arg(sources_path) + .status()? + .success(); + + if !has_suite { + let line = format!( + "deb [arch={}] http://archive.ubuntu.com/ubuntu/ {} main restricted universe multiverse", + local_arch, suite + ); + ctx.command("sh") + .arg("-c") + .arg(format!("echo '{}' >> {}", line, sources_path)) + .status()?; + } + } + + // Add ports repository to sources.list if not already present + let has_ports = ctx + .command("grep") + .arg("-q") + .arg("ports.ubuntu.com") + .arg(sources_path) + .status()? + .success(); + + if !has_ports { + let ports_lines = format!( + "deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series} main restricted universe multiverse\n\ + deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-updates main restricted universe multiverse\n\ + deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-backports main restricted universe multiverse\n\ + deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-security main restricted universe multiverse" + ); + ctx.command("sh") + .arg("-c") + .arg(format!("echo '{}' >> {}", ports_lines, sources_path)) + .status()?; + } + Ok(()) +} diff --git a/src/deb/mod.rs b/src/deb/mod.rs index b6a3d9a..d5a53d0 100644 --- a/src/deb/mod.rs +++ b/src/deb/mod.rs @@ -51,9 +51,11 @@ pub fn build_binary_package( // Specific case: native cross-compilation, we don't allow that // instead this wraps to an automatic unshare chroot // using an ephemeral context - if cross && mode == BuildMode::Local { - cross::setup_native_context(series)?; - } + let _guard = if cross && mode == BuildMode::Local { + Some(cross::EphemeralContextGuard::new(series)?) + } else { + None + }; // Prepare build directory let ctx = context::current(); @@ -83,10 +85,6 @@ pub fn build_binary_package( } } - if cross && mode == BuildMode::Local { - cross::clean_native_context()?; - } - Ok(()) }