diff --git a/src/deb/cross.rs b/src/deb/cross.rs index 8d41034..5612461 100644 --- a/src/deb/cross.rs +++ b/src/deb/cross.rs @@ -1,15 +1,16 @@ -use crate::context; +use crate::context::Context; use std::collections::HashMap; use std::error::Error; +use std::sync::Arc; /// Set environment variables for cross-compilation pub fn setup_environment( env: &mut HashMap, arch: &str, + ctx: Arc, ) -> Result<(), Box> { let dpkg_architecture = String::from_utf8( - context::current() - .command("dpkg-architecture") + ctx.command("dpkg-architecture") .arg("-a") .arg(arch) .output()? @@ -34,8 +35,11 @@ pub fn setup_environment( /// Ensure that repositories for target architecture are available /// 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(); +pub fn ensure_repositories( + arch: &str, + series: &str, + ctx: Arc, +) -> Result<(), Box> { let local_arch = crate::get_current_arch(); // Add target ('host') architecture diff --git a/src/deb/ephemeral.rs b/src/deb/ephemeral.rs index 1672189..58ebc00 100644 --- a/src/deb/ephemeral.rs +++ b/src/deb/ephemeral.rs @@ -1,5 +1,4 @@ -use crate::context; -use crate::context::{Context, ContextConfig}; +use crate::context::{self, Context, ContextConfig}; use directories::ProjectDirs; use std::error::Error; use std::fs; @@ -14,24 +13,26 @@ pub struct EphemeralContextGuard { previous_context: String, chroot_path: PathBuf, build_succeeded: bool, + base_ctx: Arc, } impl EphemeralContextGuard { - /// Create a new ephemeral unshare context for the specified series + /// Create a new ephemeral unshare context with an explicit base context /// /// # Arguments /// * `series` - The distribution series (e.g., "noble", "sid") /// * `arch` - Optional target architecture. If provided and different from host, /// downloads a chroot for that architecture (uses qemu_binfmt transparently) - pub async fn new(series: &str, arch: Option<&str>) -> Result> { + /// * `base_ctx` - The base context to use for creating the chroot + pub async fn new_with_context( + series: &str, + arch: Option<&str>, + base_ctx: Arc, + ) -> Result> { let current_context_name = context::manager().current_name(); - // Capture the current context once to avoid race conditions - // with other threads modifying the global context state - let ctx = context::current(); - // Create a temporary directory for the chroot - let chroot_path_str = ctx.create_temp_dir()?; + let chroot_path_str = base_ctx.create_temp_dir()?; let chroot_path = PathBuf::from(chroot_path_str); log::debug!( @@ -42,7 +43,7 @@ impl EphemeralContextGuard { ); // Download and extract the chroot tarball - Self::download_and_extract_chroot(series, arch, &chroot_path, ctx.clone()).await?; + Self::download_and_extract_chroot(series, arch, &chroot_path, base_ctx.clone()).await?; // Switch to an ephemeral context to build the package in the chroot context::manager().set_current_ephemeral(Context::new(ContextConfig::Unshare { @@ -54,6 +55,7 @@ impl EphemeralContextGuard { previous_context: current_context_name, chroot_path, build_succeeded: false, + base_ctx, }) } @@ -309,13 +311,13 @@ impl Drop for EphemeralContextGuard { let is_root = crate::utils::root::is_root().unwrap_or(false); let result = if is_root { - context::current() + self.base_ctx .command("rm") .arg("-rf") .arg(&self.chroot_path) .status() } else { - context::current() + self.base_ctx .command("sudo") .arg("rm") .arg("-rf") diff --git a/src/deb/local.rs b/src/deb/local.rs index 82c3fd3..b44dccb 100644 --- a/src/deb/local.rs +++ b/src/deb/local.rs @@ -1,11 +1,12 @@ /// Local binary package building /// Directly calling 'debian/rules' in current context -use crate::context; +use crate::context::Context; use crate::deb::find_dsc_file; use log::warn; use std::collections::HashMap; use std::error::Error; use std::path::Path; +use std::sync::Arc; use crate::apt; use crate::deb::cross; @@ -20,14 +21,13 @@ pub async fn build( cross: bool, ppa: Option<&[&str]>, inject_packages: Option<&[&str]>, + ctx: Arc, ) -> Result<(), Box> { // Environment let mut env = HashMap::::new(); env.insert("LANG".to_string(), "C".to_string()); env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()); - let ctx = context::current(); - // Parallel building: find local number of cores, and use that let num_cores = ctx .command("nproc") @@ -52,11 +52,11 @@ pub async fn build( if cross { log::debug!("Setting up environment for local cross build..."); - cross::setup_environment(&mut env, arch)?; - cross::ensure_repositories(arch, series)?; + cross::setup_environment(&mut env, arch, ctx.clone())?; + cross::ensure_repositories(arch, series, ctx.clone())?; } - let mut sources = apt::sources::load(None)?; + let mut sources = apt::sources::load(Some(ctx.clone()))?; let mut modified = false; let mut added_ppas: Vec<(&str, &str)> = Vec::new(); @@ -117,11 +117,12 @@ pub async fn build( } if modified { - apt::sources::save_legacy(None, sources, "/etc/apt/sources.list")?; + apt::sources::save_legacy(Some(ctx.clone()), sources, "/etc/apt/sources.list")?; // Download and import PPA keys for all added PPAs for (user, ppa_name) in added_ppas { - if let Err(e) = crate::apt::keyring::download_trust_ppa_key(None, user, ppa_name).await + if let Err(e) = + crate::apt::keyring::download_trust_ppa_key(Some(ctx.clone()), user, ppa_name).await { warn!( "Failed to download PPA key for {}/{}: {}", @@ -205,7 +206,7 @@ pub async fn build( // If build-dep fails, we try to explain the failure using dose-debcheck if !status.success() { - dose3_explain_dependencies(package, version, arch, build_root, cross)?; + dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?; return Err("Could not install build-dependencies for the build".into()); } @@ -222,7 +223,7 @@ pub async fn build( // If build-dep fails, we try to explain the failure using dose-debcheck if !status.success() { - dose3_explain_dependencies(package, version, arch, build_root, cross)?; + dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?; return Err("Could not install build-dependencies for the build".into()); } @@ -261,9 +262,8 @@ fn dose3_explain_dependencies( arch: &str, build_root: &str, cross: bool, + ctx: Arc, ) -> Result<(), Box> { - let ctx = context::current(); - // Construct the list of Packages files let mut bg_args = Vec::new(); let mut cmd = ctx.command("apt-get"); @@ -285,7 +285,7 @@ fn dose3_explain_dependencies( // Transform the dsc file into a 'Source' stanza (replacing 'Source' with 'Package') // TODO: Remove potential GPG headers/signature - let dsc_path = find_dsc_file(build_root, package, version)?; + let dsc_path = find_dsc_file(build_root, package, version, &ctx)?; let mut dsc_content = ctx.read_file(&dsc_path)?; dsc_content = dsc_content.replace("Source", "Package"); ctx.write_file( diff --git a/src/deb/mod.rs b/src/deb/mod.rs index 2abbcf1..b17ead2 100644 --- a/src/deb/mod.rs +++ b/src/deb/mod.rs @@ -3,9 +3,10 @@ mod ephemeral; mod local; mod sbuild; -use crate::context; +use crate::context::{self, Context}; use std::error::Error; use std::path::{Path, PathBuf}; +use std::sync::Arc; /// Build mode for the binary build #[derive(PartialEq)] @@ -17,6 +18,7 @@ pub enum BuildMode { } /// Build package in 'cwd' to a .deb +#[allow(clippy::too_many_arguments)] pub async fn build_binary_package( arch: Option<&str>, series: Option<&str>, @@ -25,6 +27,7 @@ pub async fn build_binary_package( mode: Option, ppa: Option<&[&str]>, inject_packages: Option<&[&str]>, + ctx: Option>, ) -> Result<(), Box> { let cwd = cwd.unwrap_or_else(|| Path::new(".")); @@ -57,19 +60,35 @@ pub async fn build_binary_package( None }; + // Use provided context or get current + let base_ctx = ctx.unwrap_or_else(context::current); + let mut guard = if mode == BuildMode::Local { - Some(ephemeral::EphemeralContextGuard::new(series, chroot_arch).await?) + Some( + ephemeral::EphemeralContextGuard::new_with_context( + series, + chroot_arch, + base_ctx.clone(), + ) + .await?, + ) } else { None }; + // Get the build context - either the ephemeral context or the base context + let build_ctx = if mode == BuildMode::Local { + context::current() + } else { + base_ctx.clone() + }; + // Prepare build directory - let ctx = context::current(); - let build_root = ctx.create_temp_dir()?; + let build_root = build_ctx.create_temp_dir()?; // Ensure availability of all needed files for the build let parent_dir = cwd.parent().ok_or("Cannot find parent directory")?; - ctx.ensure_available(parent_dir, &build_root)?; + build_ctx.ensure_available(parent_dir, &build_root)?; let parent_dir_name = parent_dir .file_name() .ok_or("Cannot find parent directory name")?; @@ -87,19 +106,28 @@ pub async fn build_binary_package( cross, ppa, inject_packages, + build_ctx.clone(), ) .await? } - BuildMode::Sbuild => sbuild::build(&package, &version, arch, series, &build_root, cross)?, + BuildMode::Sbuild => sbuild::build( + &package, + &version, + arch, + series, + &build_root, + cross, + build_ctx.clone(), + )?, }; // Retrieve produced .deb files - let remote_files = ctx.list_files(Path::new(&build_root))?; + let remote_files = build_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 = parent_dir.join(file_name); - ctx.retrieve_path(&remote_file, &local_dest)?; + build_ctx.retrieve_path(&remote_file, &local_dest)?; } } @@ -195,14 +223,14 @@ fn find_dsc_file( build_root: &str, package: &str, version: &str, + ctx: &Arc, ) -> Result> { // Strip epoch from version (e.g., "1:2.3.4-5" -> "2.3.4-5") let version_without_epoch = version.split_once(':').map(|(_, v)| v).unwrap_or(version); let dsc_name = format!("{}_{}.dsc", package, version_without_epoch); let dsc_path = PathBuf::from(build_root).join(&dsc_name); - // Check if the .dsc file exists in current context - let ctx = context::current(); + // Check if the .dsc file exists in context if !ctx.exists(&dsc_path)? { return Err(format!("Could not find .dsc file at {}", dsc_path.display()).into()); } @@ -211,7 +239,9 @@ fn find_dsc_file( #[cfg(test)] mod tests { - use serial_test::serial; + use super::*; + use std::sync::Arc; + async fn test_build_end_to_end( package: &str, series: &str, @@ -241,17 +271,28 @@ mod tests { .expect("Cannot pull package"); log::info!("Successfully pulled package {}", package); + // Create a fresh local context for this test + let ctx = Arc::new(Context::new(crate::context::ContextConfig::Local)); + // Change directory to the package directory - let ctx = crate::context::current(); let cwd = crate::deb::find_package_directory(cwd, package, &package_info.stanza.version, &ctx) .expect("Cannot find package directory"); log::debug!("Package directory: {}", cwd.display()); log::info!("Starting binary package build..."); - crate::deb::build_binary_package(arch, Some(series), Some(&cwd), cross, None, None, None) - .await - .expect("Cannot build binary package (deb)"); + crate::deb::build_binary_package( + arch, + Some(series), + Some(&cwd), + cross, + None, + None, + None, + Some(ctx), + ) + .await + .expect("Cannot build binary package (deb)"); log::info!("Successfully built binary package"); // Check that the .deb files are present @@ -274,16 +315,10 @@ mod tests { ); } - // Tests below will be marked 'serial' - // As builds are using ephemeral contexts, tests running on the same - // process could use the ephemeral context of another thread and - // interfere with each other. - // FIXME: This is not ideal. In the future, we might want to - // either explicitely pass context (instead of shared state) or - // fork for building? + // Tests no longer need to be 'serial' since each test uses its own + // explicit context instead of shared global state. #[tokio::test] #[test_log::test] - #[serial] async fn test_deb_hello_ubuntu_end_to_end() { test_build_end_to_end("hello", "noble", None, None, false).await; } @@ -292,7 +327,6 @@ mod tests { #[tokio::test] #[test_log::test] #[cfg(target_arch = "x86_64")] - #[serial] async fn test_deb_hello_ubuntu_cross_end_to_end() { test_build_end_to_end("hello", "noble", None, Some("riscv64"), true).await; } @@ -302,7 +336,6 @@ mod tests { /// for example. #[tokio::test] #[test_log::test] - #[serial] async fn test_deb_hello_debian_sid_end_to_end() { test_build_end_to_end("hello", "sid", None, None, false).await; } @@ -329,7 +362,6 @@ mod tests { #[cfg(target_arch = "x86_64")] #[tokio::test] #[test_log::test] - #[serial] async fn test_deb_gcc_debian_end_to_end() { test_build_end_to_end("gcc-15", "sid", None, None, false).await; } diff --git a/src/deb/sbuild.rs b/src/deb/sbuild.rs index 42c38dc..0458085 100644 --- a/src/deb/sbuild.rs +++ b/src/deb/sbuild.rs @@ -1,8 +1,9 @@ /// Sbuild binary package building /// Call 'sbuild' with the dsc file to build the package with unshare -use crate::context; +use crate::context::Context; use std::error::Error; use std::path::Path; +use std::sync::Arc; pub fn build( package: &str, @@ -11,9 +12,8 @@ pub fn build( series: &str, build_root: &str, cross: bool, + ctx: Arc, ) -> Result<(), Box> { - let ctx = context::current(); - // Find the actual package directory let package_dir = crate::deb::find_package_directory(Path::new(build_root), package, version, &ctx)?; diff --git a/src/main.rs b/src/main.rs index f43ad7a..7dbb07d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -197,6 +197,7 @@ fn main() { mode, ppa, inject_packages, + None, ) .await }) { diff --git a/src/put.rs b/src/put.rs new file mode 100644 index 0000000..38b78d9 --- /dev/null +++ b/src/put.rs @@ -0,0 +1,82 @@ +use std::path::Path; +use std::process::Command; +use crate::ProgressCallback; +use std::fs; +use pkh::package_info::parse_control_file; + +/// Execute the `put` subcommand to upload package to PPA or archive +/// +/// # Arguments +/// - series: Target distribution series (e.g. "focal") +/// - dist: Target distribution (e.g. "ubuntu") +/// - version: Package version override +/// - ppa: Target PPA in "user/ppa-name" format +/// - archive: Set to true for official archive uploads +/// - cwd: Current working directory containing source package +/// - progress: Progress callback for UI updates +pub async fn put( + series: Option<&str>, + dist: Option<&str>, + version: Option<&str>, + ppa: Option<&str>, + archive: bool, + cwd: Option<&Path>, + progress: ProgressCallback<'_>, +) -> Result<(), Box> { + let current_dir = cwd.unwrap_or_else(|| Path::new(".")); + let control_path = current_dir.join("debian/control"); + + let control_content = fs::read_to_string(&control_path).map_err(|e| { + format!("Failed to read debian/control: {}. Are you in a source package directory?", e) + })?; + + let package_info = parse_control_file(&control_content)?; + let package = package_info.source.ok_or("Could not determine package name from debian/control")?; + + if let Some(cb) = progress { + cb(&package, "Uploading package...", 0, 1); + } + + // Find .dsc file in current directory + let dsc_files: Vec<_> = current_dir.read_dir()? + .filter_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + if path.extension()? == "dsc" { + Some(path) + } else { + None + } + }) + .collect(); + + let dsc_file = dsc_files.first().ok_or("No .dsc file found in current directory")?; + + if dsc_files.len() > 1 { + return Err("Multiple .dsc files found - please make sure only one exists".into()); + } + + if archive { + println!("Uploading {} to official archive", dsc_file.display()); + // Execute dput with official archive config + Command::new("dput") + .arg("ubuntu") + .arg(dsc_file) + .status()?; + } else if let Some(ppa) = ppa { + println!("Uploading {} to PPA: {}", dsc_file.display(), ppa); + // Execute dput with PPA target + Command::new("dput") + .arg(format!("ppa:{}", ppa)) + .arg(dsc_file) + .status()?; + } else { + return Err("Must specify either --ppa for PPA upload or --archive for official archive".into()); + } + + if let Some(cb) = progress { + cb(&package, "Upload complete", 1, 1); + } + + Ok(()) +}