exp: cross #2

This commit is contained in:
2025-12-20 00:06:07 +01:00
parent 8e9e19a6ca
commit 31bcd28c72
16 changed files with 1209 additions and 438 deletions

169
src/deb/cross.rs Normal file
View File

@@ -0,0 +1,169 @@
use crate::context;
use crate::context::{Context, ContextConfig};
use std::collections::HashMap;
use std::error::Error;
/// Setup a specific chroot context for native cross builds
pub fn setup_native_context(series: &str) -> Result<(), Box<dyn Error>> {
// Use the system cache directory to store the chroot
let proj_dirs = directories::ProjectDirs::from("com", "pkh", "pkh")
.expect("Could not determine cache directory");
let cache_dir = proj_dirs.cache_dir();
let chroot_path = cache_dir.join(format!("pkh-cross-{series}"));
// Check if the chroot already exists
if !chroot_path.exists() {
log::debug!(
"Creating new chroot for {} at {}...",
series,
chroot_path.display()
);
std::fs::create_dir_all(&chroot_path)?;
let status = context::current()
.command("sudo")
.arg("mmdebstrap")
.arg("--variant=buildd")
.arg(series)
.arg(chroot_path.to_string_lossy().to_string())
.status()?;
if !status.success() {
// Clean up on failure
let _ = std::fs::remove_dir_all(&chroot_path);
return Err(format!("mmdebstrap failed for series {}", series).into());
}
}
// Switch to an ephemeral context to build the package in the chroot
context::manager().set_current_ephemeral(Context::new(ContextConfig::Unshare {
path: chroot_path.to_string_lossy().to_string(),
parent: Some(context::manager().current_name()),
}));
Ok(())
}
/// Set environment variables for cross-compilation
pub fn setup_environment(
env: &mut HashMap<String, String>,
arch: &str,
) -> Result<(), Box<dyn Error>> {
let dpkg_architecture = String::from_utf8(
context::current()
.command("dpkg-architecture")
.arg(format!("-a{}", arch))
.output()?
.stdout,
)?;
let env_var_regex = regex::Regex::new(r"(?<key>.*)=(?<value>.*)").unwrap();
for l in dpkg_architecture.lines() {
let capture = env_var_regex.captures(l).unwrap();
let key = capture.name("key").unwrap().as_str().to_string();
let value = capture.name("value").unwrap().as_str().to_string();
env.insert(key.clone(), value.clone());
if key == "DEB_HOST_GNU_TYPE" {
env.insert("CROSS_COMPILE".to_string(), format!("{value}-"));
}
}
env.insert("DEB_BUILD_PROFILES".to_string(), "cross".to_string());
env.insert("DEB_BUILD_OPTIONS".to_string(), "nocheck".to_string());
Ok(())
}
/// 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<dyn Error>> {
let ctx = context::current();
// Add target ('host') architecture
ctx.command("dpkg")
.arg("--add-architecture")
.arg(arch)
.status()?;
// Check if we are on Ubuntu
let os_release = String::from_utf8(ctx.command("cat").arg("/etc/os-release").output()?.stdout)?;
if !os_release.contains("ID=ubuntu") {
return Ok(());
}
// Handle DEB822 format (Ubuntu 24.04+)
let deb822_path = "/etc/apt/sources.list.d/ubuntu.sources";
let has_deb822 = ctx
.command("test")
.arg("-f")
.arg(deb822_path)
.status()?
.success();
if has_deb822 {
// Scope existing to amd64 if not already scoped
// This looks for URIs lines for archive/security and adds Architectures: amd64 on the next line if missing
ctx.command("sed")
.arg("-i")
.arg("/URIs:.*\\(archive\\|security\\)\\.ubuntu\\.com/ { n; /^Architectures:/ ! i Architectures: amd64 }")
.arg(deb822_path)
.status()?;
// Add ports if not already present
let has_ports = ctx
.command("grep")
.arg("-q")
.arg("ports.ubuntu.com")
.arg(deb822_path)
.status()?
.success();
if !has_ports {
let ports_block = format!(
"\nTypes: deb\nURIs: http://ports.ubuntu.com/ubuntu-ports\nSuites: {series} {series}-updates {series}-backports {series}-security {series}-proposed\nComponents: main restricted universe multiverse\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\nArchitectures: {arch}\n"
);
ctx.command("sh")
.arg("-c")
.arg(format!("echo '{}' >> {}", ports_block, deb822_path))
.status()?;
}
} else {
// Traditional sources.list
let sources_path = "/etc/apt/sources.list";
// Scope archive.ubuntu.com and security.ubuntu.com to amd64 if not already scoped
// 1. For lines without [], insert [arch=amd64]
// 2. For lines with [], insert arch=amd64 inside the brackets
// Both only if arch= is not already present on the line
ctx.command("sed")
.arg("-i")
.arg(r"/archive.ubuntu.com\|security.ubuntu.com/ { /arch=/ ! { /^deb \[/ ! s/^deb /deb [arch=amd64] /; /^deb \[/ s/^deb \[\([^]]*\)\]/deb [arch=amd64 \1]/ } }")
.arg(sources_path)
.status()?;
// Add ports repository to sources.list if not already present
let has_ports = ctx
.command("grep")
.arg("-q")
.arg("ports.ubuntu.com")
.arg(sources_path)
.status()?
.success();
if !has_ports {
let ports_lines = format!(
"deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series} main restricted universe multiverse\n\
deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-updates main restricted universe multiverse\n\
deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-backports main restricted universe multiverse\n\
deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-security main restricted universe multiverse\n\
deb [arch={arch}] http://ports.ubuntu.com/ubuntu-ports {series}-proposed main restricted universe multiverse"
);
ctx.command("sh")
.arg("-c")
.arg(format!("echo '{}' >> {}", ports_lines, sources_path))
.status()?;
}
}
Ok(())
}

99
src/deb/local.rs Normal file
View File

@@ -0,0 +1,99 @@
/// Local binary package building
/// Directly calling 'debian/rules' in current context
use crate::context;
use std::collections::HashMap;
use std::error::Error;
use std::path::Path;
use crate::deb::cross;
pub fn build(
_cwd: &Path,
package: &str,
_version: &str,
arch: &str,
series: &str,
build_root: &str,
cross: bool,
) -> Result<(), Box<dyn Error>> {
// Environment
let mut env = HashMap::<String, String>::new();
env.insert("LANG".to_string(), "C".to_string());
let ctx = context::current();
if cross {
cross::setup_environment(&mut env, arch)?;
cross::ensure_repositories(arch, series)?;
}
// Update package lists
let status = ctx
.command("apt-get")
.envs(env.clone())
.arg("update")
.status()?;
if !status.success() {
return Err(
"Could not execute apt-get update. If this is a local build, try executing with sudo."
.into(),
);
}
// Install essential packages
let mut cmd = ctx.command("apt-get");
cmd.envs(env.clone())
.arg("-y")
.arg("install")
.arg("build-essential")
.arg("fakeroot");
if cross {
cmd.arg(format!("crossbuild-essential-{arch}"));
}
let status = cmd.status()?;
if !status.success() {
return Err("Could not install essential packages for the build".into());
}
// Install build dependencies
let mut cmd = ctx.command("apt-get");
cmd.current_dir(format!("{build_root}/{package}"))
.envs(env.clone())
.arg("-y")
.arg("build-dep");
if cross {
cmd.arg(format!("--host-architecture={arch}"));
}
let status = cmd.arg("./").status()?;
if !status.success() {
return Err("Could not install build-dependencies for the build".into());
}
// Run the build step
let status = ctx
.command("debian/rules")
.current_dir(format!("{build_root}/{package}"))
.envs(env.clone())
.arg("build")
.status()?;
if !status.success() {
return Err("Error while building the package".into());
}
// Run the 'binary' step to produce deb
let status = ctx
.command("fakeroot")
.current_dir(format!("{build_root}/{package}"))
.envs(env.clone())
.arg("debian/rules")
.arg("binary")
.status()?;
if !status.success() {
return Err(
"Error while building the binary artifacts (.deb) from the built package".into(),
);
}
Ok(())
}

70
src/deb/mod.rs Normal file
View File

@@ -0,0 +1,70 @@
mod cross;
mod local;
mod sbuild;
use crate::context;
use std::error::Error;
use std::path::Path;
pub fn build_binary_package(
arch: Option<&str>,
series: Option<&str>,
cwd: Option<&Path>,
cross: bool,
) -> Result<(), Box<dyn Error>> {
let cwd = cwd.unwrap_or_else(|| Path::new("."));
// Parse changelog to get package name, version and series
let changelog_path = cwd.join("debian/changelog");
let (package, version, package_series) =
crate::changelog::parse_changelog_header(&changelog_path)?;
let series = if let Some(s) = series {
s
} else {
&package_series
};
let current_arch = crate::get_current_arch();
let arch = arch.unwrap_or(&current_arch);
// Specific case: native cross-compilation, we don't allow that
// instead this wraps to an automatic unshare chroot
// using an ephemeral context
if cross {
cross::setup_native_context(series)?;
}
// Prepare Environment
let ctx = context::current();
let build_root = 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)?;
let parent_dir_name = parent_dir
.file_name()
.ok_or("Cannot find parent directory name")?;
let build_root = format!("{}/{}", build_root, parent_dir_name.to_str().unwrap());
// Run sbuild
if cross {
local::build(cwd, &package, &version, arch, series, &build_root, cross)?;
} else {
sbuild::build(cwd, &package, &version, arch, series, &build_root, cross)?;
}
// Retrieve artifacts
// Always retrieve to the directory containing the .dsc file
println!("Retrieving artifacts to {}...", parent_dir.display());
// Only retrieve .deb files
let remote_files = 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)?;
}
}
Ok(())
}

51
src/deb/sbuild.rs Normal file
View File

@@ -0,0 +1,51 @@
/// Sbuild binary package building
/// Call 'sbuild' with the dsc file to build the package with unshare
use crate::context;
use std::error::Error;
use std::path::{Path, PathBuf};
fn find_dsc_file(cwd: &Path, package: &str, version: &str) -> Result<PathBuf, Box<dyn Error>> {
let parent = cwd.parent().ok_or("Cannot find parent directory")?;
// 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 = parent.join(&dsc_name);
if !dsc_path.exists() {
return Err(format!("Could not find .dsc file at {}", dsc_path.display()).into());
}
Ok(dsc_path)
}
pub fn build(
cwd: &Path,
package: &str,
version: &str,
arch: &str,
series: &str,
build_root: &str,
cross: bool,
) -> Result<(), Box<dyn Error>> {
let dsc_path = find_dsc_file(cwd, package, version)?;
let ctx = context::current();
let mut cmd = ctx.command("sbuild");
cmd.arg("--chroot-mode=unshare");
if cross {
cmd.arg(format!("--host={}", arch));
} else {
cmd.arg(format!("--arch={}", arch));
}
cmd.arg(format!("--dist={}", series));
// Add output directory argument
cmd.arg(format!("--build-dir={}", build_root));
let status = cmd.arg(dsc_path).status()?;
if !status.success() {
return Err(format!("sbuild failed with status: {}", status).into());
}
Ok(())
}