Multiple changes:
All checks were successful
CI / build (push) Successful in 16m23s
CI / snap (push) Successful in 4m1s

- 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
This commit is contained in:
2026-02-06 12:04:25 +01:00
parent 225157be63
commit 8345f51d2f
11 changed files with 233 additions and 16 deletions

View File

@@ -9,6 +9,7 @@ clap = { version = "4.5.51", features = ["cargo"] }
cmd_lib = "2.0.0" cmd_lib = "2.0.0"
flate2 = "1.1.5" flate2 = "1.1.5"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
libc = "0.2"
csv = "1.3.0" csv = "1.3.0"
reqwest = { version = "0.12.9", features = ["blocking", "json", "stream"] } reqwest = { version = "0.12.9", features = ["blocking", "json", "stream"] }
git2 = "0.20.2" git2 = "0.20.2"

View File

@@ -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 //! Provides functions to ensure that archive keyrings are available
//! for mmdebstrap operations by downloading them from specified URLs. //! for mmdebstrap operations and for PPA packages by downloading them.
use crate::context; use crate::context;
use crate::distro_info; use crate::distro_info;
use serde::Deserialize;
use std::error::Error; use std::error::Error;
use std::path::Path; use std::path::Path;
use std::sync::Arc; 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 /// Download a keyring into apt trusted.gpg.d directory, trusting that keyring
pub async fn download_trust_keyring( pub async fn download_trust_keyring(
ctx: Option<Arc<context::Context>>, ctx: Option<Arc<context::Context>>,
@@ -56,3 +63,107 @@ pub async fn download_trust_keyring(
); );
Ok(()) 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<Arc<context::Context>>,
ppa_owner: &str,
ppa_name: &str,
) -> Result<(), Box<dyn Error>> {
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(())
}

View File

@@ -210,7 +210,7 @@ impl UnshareDriver {
cmd.arg("--").arg("bash").arg("-c").arg(format!( 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; {} {}", "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, program,
args.join(" ") args.iter().map(|a| format!("\"{a}\"")).collect::<Vec<_>>().join(" ")
)); ));
cmd cmd

View File

@@ -28,7 +28,6 @@ pub fn setup_environment(
} }
} }
env.insert("DEB_BUILD_PROFILES".to_string(), "cross".to_string()); env.insert("DEB_BUILD_PROFILES".to_string(), "cross".to_string());
env.insert("DEB_BUILD_OPTIONS".to_string(), "nocheck".to_string());
Ok(()) Ok(())
} }

View File

@@ -124,14 +124,21 @@ impl EphemeralContextGuard {
.status()?; .status()?;
// Make sure we have the right apt keyrings to mmdebstrap the chroot // Make sure we have the right apt keyrings to mmdebstrap the chroot
// Check for root privileges before downloading keyring
if crate::utils::root::is_root()? {
crate::apt::keyring::download_trust_keyring(Some(ctx.clone()), series).await?; 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 // Use mmdebstrap to download the tarball to the cache directory
let status = ctx let status = ctx
.command("mmdebstrap") .command("mmdebstrap")
.arg("--variant=buildd") .arg("--variant=buildd")
.arg("--mode=unshare") .arg("--mode=unshare")
.arg("--include=mount") .arg("--include=mount,curl,ca-certificates")
.arg("--format=tar") .arg("--format=tar")
.arg(series) .arg(series)
.arg(tarball_path.to_string_lossy().to_string()) .arg(tarball_path.to_string_lossy().to_string())

View File

@@ -2,6 +2,7 @@
/// Directly calling 'debian/rules' in current context /// Directly calling 'debian/rules' in current context
use crate::context; use crate::context;
use crate::deb::find_dsc_file; use crate::deb::find_dsc_file;
use log::warn;
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::path::Path; use std::path::Path;
@@ -9,13 +10,14 @@ use std::path::Path;
use crate::apt; use crate::apt;
use crate::deb::cross; use crate::deb::cross;
pub fn build( pub async fn build(
package: &str, package: &str,
version: &str, version: &str,
arch: &str, arch: &str,
series: &str, series: &str,
build_root: &str, build_root: &str,
cross: bool, cross: bool,
ppa: Option<&str>,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
// Environment // Environment
let mut env = HashMap::<String, String>::new(); let mut env = HashMap::<String, String>::new();
@@ -39,9 +41,11 @@ pub fn build(
} }
}) })
.unwrap_or(1); // Default to 1 if we can't execute the command .unwrap_or(1); // Default to 1 if we can't execute the command
// Build options: parallel, disable tests by default
env.insert( env.insert(
"DEB_BUILD_OPTIONS".to_string(), "DEB_BUILD_OPTIONS".to_string(),
format!("parallel={}", num_cores), format!("parallel={} nocheck", num_cores),
); );
if cross { if cross {
@@ -50,17 +54,73 @@ pub fn build(
cross::ensure_repositories(arch, series)?; cross::ensure_repositories(arch, series)?;
} }
// UBUNTU: Ensure 'universe' repository is enabled
let mut sources = apt::sources::load(None)?; let mut sources = apt::sources::load(None)?;
let mut modified = false; 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 { for source in &mut sources {
if source.uri.contains("ubuntu") && !source.components.contains(&"universe".to_string()) { if source.uri.contains("ubuntu") && !source.components.contains(&"universe".to_string()) {
source.components.push("universe".to_string()); source.components.push("universe".to_string());
modified = true; modified = true;
} }
} }
if modified { if modified {
apt::sources::save_legacy(None, sources, "/etc/apt/sources.list")?; 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 // Update package lists
@@ -106,8 +166,8 @@ pub fn build(
.to_str() .to_str()
.ok_or("Invalid package directory path")?; .ok_or("Invalid package directory path")?;
// Install build dependencies // Install arch-specific build dependencies
log::debug!("Installing build dependencies..."); log::debug!("Installing arch-specific build dependencies...");
let mut cmd = ctx.command("apt-get"); let mut cmd = ctx.command("apt-get");
cmd.current_dir(package_dir_str) cmd.current_dir(package_dir_str)
.envs(env.clone()) .envs(env.clone())
@@ -116,6 +176,7 @@ pub fn build(
if cross { if cross {
cmd.arg(format!("--host-architecture={arch}")); cmd.arg(format!("--host-architecture={arch}"));
} }
cmd.arg("--arch-only");
let status = cmd.arg("./").status()?; let status = cmd.arg("./").status()?;
// If build-dep fails, we try to explain the failure using dose-debcheck // 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()); 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 // Run the build step
log::debug!("Building (debian/rules build) package..."); log::debug!("Building (debian/rules build) package...");
let status = ctx let status = ctx

View File

@@ -23,6 +23,7 @@ pub async fn build_binary_package(
cwd: Option<&Path>, cwd: Option<&Path>,
cross: bool, cross: bool,
mode: Option<BuildMode>, mode: Option<BuildMode>,
ppa: Option<&str>,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let cwd = cwd.unwrap_or_else(|| Path::new(".")); 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 // Run the build using target build mode
match 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)?, BuildMode::Sbuild => sbuild::build(&package, &version, arch, series, &build_root, cross)?,
}; };
@@ -227,7 +230,7 @@ mod tests {
log::debug!("Package directory: {}", cwd.display()); log::debug!("Package directory: {}", cwd.display());
log::info!("Starting binary package build..."); 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 .await
.expect("Cannot build binary package (deb)"); .expect("Cannot build binary package (deb)");
log::info!("Successfully built binary package"); log::info!("Successfully built binary package");

View File

@@ -54,6 +54,7 @@ fn main() {
.about("Build the source package into binary package (.deb)") .about("Build the source package into binary package (.deb)")
.arg(arg!(-s --series <series> "Target distribution series").required(false)) .arg(arg!(-s --series <series> "Target distribution series").required(false))
.arg(arg!(-a --arch <arch> "Target architecture").required(false)) .arg(arg!(-a --arch <arch> "Target architecture").required(false))
.arg(arg!(--ppa <ppa> "Build the package adding a specific PPA for dependencies").required(false))
.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 [sbuild, local]").required(false)
@@ -159,6 +160,7 @@ fn main() {
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);
let ppa = sub_matches.get_one::<String>("ppa").map(|s| s.as_str());
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("sbuild") => Some(pkh::deb::BuildMode::Sbuild),
@@ -167,7 +169,7 @@ fn main() {
}; };
if let Err(e) = rt.block_on(async { 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 .await
}) { }) {
error!("{}", e); error!("{}", e);

View File

@@ -15,7 +15,7 @@ use log::{debug, warn};
/// # Returns /// # Returns
/// * The base URL for the PPA (e.g., "https://ppa.launchpadcontent.net/user/ppa_name/ubuntu/") /// * 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 { 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<Option<String>, Box<dyn Error>> { async fn check_launchpad_repo(package: &str) -> Result<Option<String>, Box<dyn Error>> {

View File

@@ -1 +1,2 @@
pub mod gpg; pub mod gpg;
pub mod root;

15
src/utils/root.rs Normal file
View File

@@ -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<bool, Box<dyn Error>> {
// Check if we're running as root by checking the effective user ID
let uid = unsafe { libc::geteuid() };
Ok(uid == 0)
}