From 8345f51d2f77ccb71baafe8aadc5eb1ee9f0c076 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Fri, 6 Feb 2026 12:04:25 +0100 Subject: [PATCH] Multiple changes: - deb: can use ppa as dependency - deb: cross and regular are using parallel nocheck builds - deb: ephemeral will not pull keyring if no root powers --- Cargo.toml | 1 + src/apt/keyring.rs | 117 +++++++++++++++++++++++++++++++++++++++-- src/context/unshare.rs | 2 +- src/deb/cross.rs | 1 - src/deb/ephemeral.rs | 11 +++- src/deb/local.rs | 88 +++++++++++++++++++++++++++++-- src/deb/mod.rs | 7 ++- src/main.rs | 4 +- src/package_info.rs | 2 +- src/utils/mod.rs | 1 + src/utils/root.rs | 15 ++++++ 11 files changed, 233 insertions(+), 16 deletions(-) create mode 100644 src/utils/root.rs diff --git a/Cargo.toml b/Cargo.toml index 787b788..064a6e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ clap = { version = "4.5.51", features = ["cargo"] } cmd_lib = "2.0.0" flate2 = "1.1.5" serde = { version = "1.0.228", features = ["derive"] } +libc = "0.2" csv = "1.3.0" reqwest = { version = "0.12.9", features = ["blocking", "json", "stream"] } git2 = "0.20.2" diff --git a/src/apt/keyring.rs b/src/apt/keyring.rs index 5688ce3..0384e6e 100644 --- a/src/apt/keyring.rs +++ b/src/apt/keyring.rs @@ -1,14 +1,21 @@ -//! APT keyring management for mmdebstrap +//! APT keyring management for mmdebstrap and PPA packages //! -//! Provides a simple function to ensure that archive keyrings are available -//! for mmdebstrap operations by downloading them from specified URLs. +//! Provides functions to ensure that archive keyrings are available +//! for mmdebstrap operations and for PPA packages by downloading them. use crate::context; use crate::distro_info; +use serde::Deserialize; use std::error::Error; use std::path::Path; use std::sync::Arc; +/// Launchpad API response structure for PPA information +#[derive(Deserialize)] +struct LaunchpadPpaResponse { + signing_key_fingerprint: String, +} + /// Download a keyring into apt trusted.gpg.d directory, trusting that keyring pub async fn download_trust_keyring( ctx: Option>, @@ -56,3 +63,107 @@ pub async fn download_trust_keyring( ); Ok(()) } + +/// Download and import a PPA key using Launchpad API +/// +/// # Arguments +/// * `ctx` - Optional context to use +/// * `ppa_owner` - PPA owner (username) +/// * `ppa_name` - PPA name +/// +/// # Returns +/// Result indicating success or failure +pub async fn download_trust_ppa_key( + ctx: Option>, + ppa_owner: &str, + ppa_name: &str, +) -> Result<(), Box> { + let ctx = ctx.unwrap_or_else(context::current); + + // Create trusted.gpg.d directory if it doesn't exist + let trusted_gpg_d = "/etc/apt/trusted.gpg.d"; + if !ctx.exists(Path::new(trusted_gpg_d))? { + ctx.command("mkdir").arg("-p").arg(trusted_gpg_d).status()?; + } + + let key_filename = format!("{}-{}.asc", ppa_owner, ppa_name); + let key_path = format!("{}/{}", trusted_gpg_d, key_filename); + + log::debug!( + "Retrieving PPA key for {}/{} using Launchpad API", + ppa_owner, + ppa_name + ); + + // Get PPA information from Launchpad API to get signing key fingerprint + // Use the correct devel API endpoint + let api_url = format!( + "https://api.launchpad.net/1.0/~{}/+archive/ubuntu/{}", + ppa_owner, ppa_name + ); + log::debug!("Querying Launchpad API: {}", api_url); + + let api_response = ctx + .command("curl") + .arg("-s") + .arg("-f") + .arg("-H") + .arg("Accept: application/json") + .arg(&api_url) + .output()?; + + if !api_response.status.success() { + return Err(format!( + "Failed to query Launchpad API for PPA {}/{}", + ppa_owner, ppa_name + ) + .into()); + } + + // Parse the JSON response to extract the signing key fingerprint + let api_response_str = String::from_utf8_lossy(&api_response.stdout); + let ppa_response: LaunchpadPpaResponse = + serde_json::from_str(&api_response_str).map_err(|e| { + format!( + "Failed to parse JSON response from Launchpad API for {}/{}: {}", + ppa_owner, ppa_name, e + ) + })?; + let fingerprint = ppa_response.signing_key_fingerprint; + + log::debug!("Found PPA signing key fingerprint: {}", fingerprint); + + // Download the actual key from the keyserver using the fingerprint + let keyserver_url = format!( + "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x{}", + fingerprint + ); + log::debug!("Downloading key from keyserver: {}", keyserver_url); + + let mut curl_cmd = ctx.command("curl"); + curl_cmd + .arg("-s") + .arg("-f") + .arg("-L") + .arg(&keyserver_url) + .arg("--output") + .arg(&key_path); + + let status = curl_cmd.status()?; + if !status.success() { + return Err(format!( + "Failed to download PPA key from keyserver for fingerprint {}", + fingerprint + ) + .into()); + } + + log::info!( + "Successfully downloaded and installed PPA key for {}/{} (fingerprint: {}) to {}", + ppa_owner, + ppa_name, + fingerprint, + key_path + ); + Ok(()) +} diff --git a/src/context/unshare.rs b/src/context/unshare.rs index 9a79277..8b80c6a 100644 --- a/src/context/unshare.rs +++ b/src/context/unshare.rs @@ -210,7 +210,7 @@ impl UnshareDriver { cmd.arg("--").arg("bash").arg("-c").arg(format!( "mount -t proc proc /proc; mkdir /dev/pts; mount -t devpts devpts /dev/pts; touch /dev/ptmx; mount --bind /dev/pts/ptmx /dev/ptmx; {} {}", program, - args.join(" ") + args.iter().map(|a| format!("\"{a}\"")).collect::>().join(" ") )); cmd diff --git a/src/deb/cross.rs b/src/deb/cross.rs index b62249b..8d41034 100644 --- a/src/deb/cross.rs +++ b/src/deb/cross.rs @@ -28,7 +28,6 @@ pub fn setup_environment( } } env.insert("DEB_BUILD_PROFILES".to_string(), "cross".to_string()); - env.insert("DEB_BUILD_OPTIONS".to_string(), "nocheck".to_string()); Ok(()) } diff --git a/src/deb/ephemeral.rs b/src/deb/ephemeral.rs index d611072..c11c125 100644 --- a/src/deb/ephemeral.rs +++ b/src/deb/ephemeral.rs @@ -124,14 +124,21 @@ impl EphemeralContextGuard { .status()?; // Make sure we have the right apt keyrings to mmdebstrap the chroot - crate::apt::keyring::download_trust_keyring(Some(ctx.clone()), series).await?; + // Check for root privileges before downloading keyring + if crate::utils::root::is_root()? { + crate::apt::keyring::download_trust_keyring(Some(ctx.clone()), series).await?; + } else { + log::info!( + "Lacking root privileges. Please ensure that the keyrings for the target distribution are present on your system." + ); + } // Use mmdebstrap to download the tarball to the cache directory let status = ctx .command("mmdebstrap") .arg("--variant=buildd") .arg("--mode=unshare") - .arg("--include=mount") + .arg("--include=mount,curl,ca-certificates") .arg("--format=tar") .arg(series) .arg(tarball_path.to_string_lossy().to_string()) diff --git a/src/deb/local.rs b/src/deb/local.rs index bcf7f30..4c87251 100644 --- a/src/deb/local.rs +++ b/src/deb/local.rs @@ -2,6 +2,7 @@ /// Directly calling 'debian/rules' in current context use crate::context; use crate::deb::find_dsc_file; +use log::warn; use std::collections::HashMap; use std::error::Error; use std::path::Path; @@ -9,13 +10,14 @@ use std::path::Path; use crate::apt; use crate::deb::cross; -pub fn build( +pub async fn build( package: &str, version: &str, arch: &str, series: &str, build_root: &str, cross: bool, + ppa: Option<&str>, ) -> Result<(), Box> { // Environment let mut env = HashMap::::new(); @@ -39,9 +41,11 @@ pub fn build( } }) .unwrap_or(1); // Default to 1 if we can't execute the command + + // Build options: parallel, disable tests by default env.insert( "DEB_BUILD_OPTIONS".to_string(), - format!("parallel={}", num_cores), + format!("parallel={} nocheck", num_cores), ); if cross { @@ -50,17 +54,73 @@ pub fn build( cross::ensure_repositories(arch, series)?; } - // UBUNTU: Ensure 'universe' repository is enabled let mut sources = apt::sources::load(None)?; let mut modified = false; + + // Add PPA repository if specified + if let Some(ppa_str) = ppa { + // PPA format: user/ppa_name + let parts: Vec<&str> = ppa_str.split('/').collect(); + if parts.len() == 2 { + let base_url = crate::package_info::ppa_to_base_url(parts[0], parts[1]); + + // Add new PPA source if not found + if !sources.iter().any(|s| s.uri.contains(&base_url)) { + // Get host and target architectures + let host_arch = crate::get_current_arch(); + let target_arch = if cross { arch } else { &host_arch }; + + // Create architectures list with both host and target if different + let mut architectures = vec![host_arch.clone()]; + if host_arch != *target_arch { + architectures.push(target_arch.to_string()); + } + + // Create suite list with all Ubuntu series + let suites = vec![format!("{}", series)]; + + let new_source = crate::apt::sources::SourceEntry { + enabled: true, + components: vec!["main".to_string()], + architectures: architectures.clone(), + suite: suites, + uri: base_url, + }; + sources.push(new_source); + modified = true; + log::info!( + "Added PPA: {} for series {} with architectures {:?}", + ppa_str, + series, + architectures + ); + } + } else { + return Err("Invalid PPA format. Expected: user/ppa_name".into()); + } + } + + // UBUNTU: Ensure 'universe' repository is enabled for source in &mut sources { if source.uri.contains("ubuntu") && !source.components.contains(&"universe".to_string()) { source.components.push("universe".to_string()); modified = true; } } + if modified { apt::sources::save_legacy(None, sources, "/etc/apt/sources.list")?; + + // Download and import PPA key if we added a PPA + if let Some(ppa_str) = ppa { + let parts: Vec<&str> = ppa_str.split('/').collect(); + if parts.len() == 2 + && let Err(e) = + crate::apt::keyring::download_trust_ppa_key(None, parts[0], parts[1]).await + { + warn!("Failed to download PPA key for {}: {}", ppa_str, e); + } + } } // Update package lists @@ -106,8 +166,8 @@ pub fn build( .to_str() .ok_or("Invalid package directory path")?; - // Install build dependencies - log::debug!("Installing build dependencies..."); + // Install arch-specific build dependencies + log::debug!("Installing arch-specific build dependencies..."); let mut cmd = ctx.command("apt-get"); cmd.current_dir(package_dir_str) .envs(env.clone()) @@ -116,6 +176,7 @@ pub fn build( if cross { cmd.arg(format!("--host-architecture={arch}")); } + cmd.arg("--arch-only"); let status = cmd.arg("./").status()?; // If build-dep fails, we try to explain the failure using dose-debcheck @@ -124,6 +185,23 @@ pub fn build( return Err("Could not install build-dependencies for the build".into()); } + // // Install arch-independant build dependencies + // log::debug!("Installing arch-independant build dependencies..."); + // let status = ctx.command("apt-get") + // .current_dir(package_dir_str) + // .envs(env.clone()) + // .arg("-y") + // .arg("build-dep") + // .arg("--indep-only") + // .arg("./") + // .status()?; + + // // 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)?; + // return Err("Could not install build-dependencies for the build".into()); + // } + // Run the build step log::debug!("Building (debian/rules build) package..."); let status = ctx diff --git a/src/deb/mod.rs b/src/deb/mod.rs index e6dea62..60d3aa4 100644 --- a/src/deb/mod.rs +++ b/src/deb/mod.rs @@ -23,6 +23,7 @@ pub async fn build_binary_package( cwd: Option<&Path>, cross: bool, mode: Option, + ppa: Option<&str>, ) -> Result<(), Box> { let cwd = cwd.unwrap_or_else(|| Path::new(".")); @@ -68,7 +69,9 @@ pub async fn build_binary_package( // Run the build using target build mode match mode { - BuildMode::Local => local::build(&package, &version, arch, series, &build_root, cross)?, + BuildMode::Local => { + local::build(&package, &version, arch, series, &build_root, cross, ppa).await? + } BuildMode::Sbuild => sbuild::build(&package, &version, arch, series, &build_root, cross)?, }; @@ -227,7 +230,7 @@ mod tests { log::debug!("Package directory: {}", cwd.display()); log::info!("Starting binary package build..."); - crate::deb::build_binary_package(arch, Some(series), Some(&cwd), cross, None) + crate::deb::build_binary_package(arch, Some(series), Some(&cwd), cross, None, None) .await .expect("Cannot build binary package (deb)"); log::info!("Successfully built binary package"); diff --git a/src/main.rs b/src/main.rs index 1e475fb..121a867 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,7 @@ fn main() { .about("Build the source package into binary package (.deb)") .arg(arg!(-s --series "Target distribution series").required(false)) .arg(arg!(-a --arch "Target architecture").required(false)) + .arg(arg!(--ppa "Build the package adding a specific PPA for dependencies").required(false)) .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)) .arg(arg!(--mode "Change build mode [sbuild, local]").required(false) @@ -159,6 +160,7 @@ fn main() { let series = sub_matches.get_one::("series").map(|s| s.as_str()); let arch = sub_matches.get_one::("arch").map(|s| s.as_str()); let cross = sub_matches.get_one::("cross").unwrap_or(&false); + let ppa = sub_matches.get_one::("ppa").map(|s| s.as_str()); let mode: Option<&str> = sub_matches.get_one::("mode").map(|s| s.as_str()); let mode: Option = match mode { Some("sbuild") => Some(pkh::deb::BuildMode::Sbuild), @@ -167,7 +169,7 @@ fn main() { }; if let Err(e) = rt.block_on(async { - pkh::deb::build_binary_package(arch, series, Some(cwd.as_path()), *cross, mode) + pkh::deb::build_binary_package(arch, series, Some(cwd.as_path()), *cross, mode, ppa) .await }) { error!("{}", e); diff --git a/src/package_info.rs b/src/package_info.rs index c0c9113..432b12d 100644 --- a/src/package_info.rs +++ b/src/package_info.rs @@ -15,7 +15,7 @@ use log::{debug, warn}; /// # Returns /// * The base URL for the PPA (e.g., "https://ppa.launchpadcontent.net/user/ppa_name/ubuntu/") pub fn ppa_to_base_url(user: &str, name: &str) -> String { - format!("https://ppa.launchpadcontent.net/{}/{}/ubuntu", user, name) + format!("http://ppa.launchpadcontent.net/{}/{}/ubuntu", user, name) } async fn check_launchpad_repo(package: &str) -> Result, Box> { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 0538510..cbf24ba 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1 +1,2 @@ pub mod gpg; +pub mod root; diff --git a/src/utils/root.rs b/src/utils/root.rs new file mode 100644 index 0000000..c551021 --- /dev/null +++ b/src/utils/root.rs @@ -0,0 +1,15 @@ +//! Root privilege checking utilities + +use std::error::Error; + +/// Check if the current process has root privileges +/// +/// # Returns +/// * `Ok(true)` - Running as root +/// * `Ok(false)` - Not running as root +/// * `Err` - Failed to check privileges +pub fn is_root() -> Result> { + // Check if we're running as root by checking the effective user ID + let uid = unsafe { libc::geteuid() }; + Ok(uid == 0) +}