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
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod gpg;
|
||||
pub mod root;
|
||||
|
||||
15
src/utils/root.rs
Normal file
15
src/utils/root.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user