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"
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"

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
//! 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<Arc<context::Context>>,
@@ -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<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!(
"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::<Vec<_>>().join(" ")
));
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_OPTIONS".to_string(), "nocheck".to_string());
Ok(())
}

View File

@@ -124,14 +124,21 @@ impl EphemeralContextGuard {
.status()?;
// 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?;
} 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())

View File

@@ -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<dyn Error>> {
// Environment
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
// 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

View File

@@ -23,6 +23,7 @@ pub async fn build_binary_package(
cwd: Option<&Path>,
cross: bool,
mode: Option<BuildMode>,
ppa: Option<&str>,
) -> Result<(), Box<dyn Error>> {
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");

View File

@@ -54,6 +54,7 @@ fn main() {
.about("Build the source package into binary package (.deb)")
.arg(arg!(-s --series <series> "Target distribution series").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)")
.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)
@@ -159,6 +160,7 @@ fn main() {
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 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<pkh::deb::BuildMode> = 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);

View File

@@ -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<Option<String>, Box<dyn Error>> {

View File

@@ -1 +1,2 @@
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)
}