deb: cross-compilation, ephemeral contexts, local builds
All checks were successful
CI / build (push) Successful in 7m18s
All checks were successful
CI / build (push) Successful in 7m18s
Multiple changes: - New contexts (schroot, unshare) - Cross-building quirks, with ephemeral contexts and repositories management - Contexts with parents, global context manager, better lifetime handling - Local building of binary packages - Pull: pulling dsc files by default - Many small bugfixes and changes Co-authored-by: Valentin Haudiquet <valentin.haudiquet@canonical.com> Co-committed-by: Valentin Haudiquet <valentin.haudiquet@canonical.com>
This commit was merged in pull request #1.
This commit is contained in:
338
src/deb/cross.rs
Normal file
338
src/deb/cross.rs
Normal file
@@ -0,0 +1,338 @@
|
||||
use crate::context;
|
||||
use crate::context::{Context, ContextConfig};
|
||||
use directories::ProjectDirs;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tar::Archive;
|
||||
use xz2::read::XzDecoder;
|
||||
|
||||
pub struct EphemeralContextGuard {
|
||||
previous_context: String,
|
||||
chroot_path: PathBuf,
|
||||
}
|
||||
|
||||
impl EphemeralContextGuard {
|
||||
pub fn new(series: &str) -> Result<Self, Box<dyn Error>> {
|
||||
let current_context_name = context::manager().current_name();
|
||||
|
||||
// Create a temporary directory for the chroot
|
||||
let chroot_path_str = context::current().create_temp_dir()?;
|
||||
let chroot_path = PathBuf::from(chroot_path_str);
|
||||
|
||||
log::debug!(
|
||||
"Creating new chroot for {} at {}...",
|
||||
series,
|
||||
chroot_path.display()
|
||||
);
|
||||
|
||||
// Download and extract the chroot tarball
|
||||
Self::download_and_extract_chroot(series, &chroot_path)?;
|
||||
|
||||
// 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(current_context_name.clone()),
|
||||
}));
|
||||
|
||||
Ok(Self {
|
||||
previous_context: current_context_name,
|
||||
chroot_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn download_and_extract_chroot(
|
||||
series: &str,
|
||||
chroot_path: &PathBuf,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Get project directories for caching
|
||||
let proj_dirs = ProjectDirs::from("com", "pkh", "pkh")
|
||||
.ok_or("Could not determine project directories")?;
|
||||
let cache_dir = proj_dirs.cache_dir();
|
||||
fs::create_dir_all(cache_dir)?;
|
||||
|
||||
// Create tarball filename based on series
|
||||
let tarball_filename = format!("{}-buildd.tar.xz", series);
|
||||
let tarball_path = cache_dir.join(&tarball_filename);
|
||||
|
||||
// Download tarball if it doesn't exist
|
||||
if !tarball_path.exists() {
|
||||
log::debug!("Downloading chroot tarball for {}...", series);
|
||||
Self::download_chroot_tarball(series, &tarball_path)?;
|
||||
} else {
|
||||
log::debug!("Using cached chroot tarball for {}", series);
|
||||
}
|
||||
|
||||
// Extract tarball to chroot directory
|
||||
log::debug!("Extracting chroot tarball to {}...", chroot_path.display());
|
||||
Self::extract_tarball(&tarball_path, chroot_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn download_chroot_tarball(series: &str, tarball_path: &Path) -> Result<(), Box<dyn Error>> {
|
||||
// Use mmdebstrap to download the tarball to the cache directory
|
||||
let status = context::current()
|
||||
.command("mmdebstrap")
|
||||
.arg("--variant=buildd")
|
||||
.arg("--mode=unshare")
|
||||
.arg("--format=tar")
|
||||
.arg(series)
|
||||
.arg(tarball_path.to_string_lossy().to_string())
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(format!("Failed to download chroot tarball for series {}", series).into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_tarball(
|
||||
tarball_path: &PathBuf,
|
||||
chroot_path: &PathBuf,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Create the chroot directory
|
||||
fs::create_dir_all(chroot_path)?;
|
||||
|
||||
// Open the tarball file
|
||||
let tarball_file = std::fs::File::open(tarball_path)?;
|
||||
let xz_decoder = XzDecoder::new(tarball_file);
|
||||
let mut archive = Archive::new(xz_decoder);
|
||||
|
||||
// Extract all files to the chroot directory
|
||||
archive.unpack(chroot_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EphemeralContextGuard {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("Cleaning up ephemeral context...");
|
||||
// Reset to normal context
|
||||
if let Err(e) = context::manager().set_current(&self.previous_context) {
|
||||
log::error!("Failed to restore context {}: {}", self.previous_context, e);
|
||||
}
|
||||
|
||||
// Remove chroot directory
|
||||
// We use the restored context to execute the cleanup command
|
||||
let result = context::current()
|
||||
.command("sudo")
|
||||
.arg("rm")
|
||||
.arg("-rf")
|
||||
.arg(&self.chroot_path)
|
||||
.status();
|
||||
|
||||
match result {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
log::error!(
|
||||
"Failed to remove chroot directory {}",
|
||||
self.chroot_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to execute cleanup command for {}: {}",
|
||||
self.chroot_path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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("-a")
|
||||
.arg(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();
|
||||
let local_arch = crate::get_current_arch();
|
||||
|
||||
// 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 {
|
||||
ensure_repositories_deb822(&ctx, arch, &local_arch, series, deb822_path)?;
|
||||
} else {
|
||||
ensure_repositories_legacy(&ctx, arch, &local_arch, series, "/etc/apt/sources.list")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_repositories_deb822(
|
||||
ctx: &context::Context,
|
||||
arch: &str,
|
||||
local_arch: &str,
|
||||
series: &str,
|
||||
deb822_path: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Scope existing to local_arch if not already scoped
|
||||
ctx.command("sed")
|
||||
.arg("-i")
|
||||
.arg(format!("/URIs:.*\\(archive\\|security\\)\\.ubuntu\\.com/ {{ n; /^Architectures:/ ! i Architectures: {} }}", local_arch))
|
||||
.arg(deb822_path)
|
||||
.status()?;
|
||||
|
||||
// Ensure all components are enabled for the primary architecture
|
||||
ctx.command("sed")
|
||||
.arg("-i")
|
||||
.arg("/URIs:.*\\(archive\\|security\\)\\.ubuntu\\.com/,/Components:/ s/^Components:.*/Components: main restricted universe multiverse/")
|
||||
.arg(deb822_path)
|
||||
.status()?;
|
||||
|
||||
// Ensure all suites (pockets) are enabled for the primary architecture
|
||||
// Excluding 'proposed' as it contains unstable software
|
||||
let suites = format!("{series} {series}-updates {series}-backports {series}-security");
|
||||
ctx.command("sed")
|
||||
.arg("-i")
|
||||
.arg(format!(
|
||||
"/URIs:.*\\(archive\\|security\\)\\.ubuntu\\.com/,/Suites:/ s/^Suites:.*/Suites: {}/",
|
||||
suites
|
||||
))
|
||||
.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\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()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_repositories_legacy(
|
||||
ctx: &context::Context,
|
||||
arch: &str,
|
||||
local_arch: &str,
|
||||
series: &str,
|
||||
sources_path: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Scope archive.ubuntu.com and security.ubuntu.com to local_arch if not already scoped
|
||||
ctx.command("sed")
|
||||
.arg("-i")
|
||||
.arg(format!(
|
||||
r"/archive.ubuntu.com\|security.ubuntu.com/ {{ /arch=/ ! {{ /^deb \[/ ! s/^deb /deb [arch={}] /; /^deb \[/ s/^deb \[\([^]]*\)\]/deb [arch={} \1]/ }} }}",
|
||||
local_arch, local_arch
|
||||
))
|
||||
.arg(sources_path)
|
||||
.status()?;
|
||||
|
||||
// Ensure all components (main restricted universe multiverse) are present for all archive/security lines
|
||||
ctx.command("sed")
|
||||
.arg("-i")
|
||||
.arg(r"/archive.ubuntu.com\|security.ubuntu.com/ s/\( main\)\?\([ ]\+restricted\)\?\([ ]\+universe\)\?\([ ]\+multiverse\)\?$/ main restricted universe multiverse/")
|
||||
.arg(sources_path)
|
||||
.status()?;
|
||||
|
||||
// Ensure all pockets exist. If not, we append them.
|
||||
for pocket in ["", "-updates", "-backports", "-security"] {
|
||||
let suite = format!("{}{}", series, pocket);
|
||||
let has_suite = ctx
|
||||
.command("grep")
|
||||
.arg("-q")
|
||||
.arg(format!(" {}", suite))
|
||||
.arg(sources_path)
|
||||
.status()?
|
||||
.success();
|
||||
|
||||
if !has_suite {
|
||||
let line = format!(
|
||||
"deb [arch={}] http://archive.ubuntu.com/ubuntu/ {} main restricted universe multiverse",
|
||||
local_arch, suite
|
||||
);
|
||||
ctx.command("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo '{}' >> {}", line, 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"
|
||||
);
|
||||
ctx.command("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo '{}' >> {}", ports_lines, sources_path))
|
||||
.status()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
171
src/deb/local.rs
Normal file
171
src/deb/local.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
/// Local binary package building
|
||||
/// Directly calling 'debian/rules' in current context
|
||||
use crate::context;
|
||||
use crate::deb::find_dsc_file;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::deb::cross;
|
||||
|
||||
pub fn build(
|
||||
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());
|
||||
env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string());
|
||||
|
||||
let ctx = context::current();
|
||||
|
||||
if cross {
|
||||
log::debug!("Setting up environment for local cross build...");
|
||||
cross::setup_environment(&mut env, arch)?;
|
||||
cross::ensure_repositories(arch, series)?;
|
||||
}
|
||||
|
||||
// Update package lists
|
||||
log::debug!("Updating package lists for local build...");
|
||||
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
|
||||
log::debug!("Installing essential packages for local build...");
|
||||
let mut cmd = ctx.command("apt-get");
|
||||
|
||||
cmd.envs(env.clone())
|
||||
.arg("-y")
|
||||
.arg("install")
|
||||
.arg("build-essential")
|
||||
.arg("dose-builddebcheck")
|
||||
.arg("fakeroot");
|
||||
if cross {
|
||||
cmd.arg(format!("crossbuild-essential-{arch}"));
|
||||
cmd.arg(format!("libc6-{arch}-cross"));
|
||||
cmd.arg(format!("libc6-dev-{arch}-cross"));
|
||||
cmd.arg("dpkg-cross");
|
||||
cmd.arg(format!("libc6:{arch}"));
|
||||
cmd.arg(format!("libc6-dev:{arch}"));
|
||||
}
|
||||
let status = cmd.status()?;
|
||||
if !status.success() {
|
||||
return Err("Could not install essential packages for the build".into());
|
||||
}
|
||||
|
||||
// Install build dependencies
|
||||
log::debug!("Installing 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 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
|
||||
.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(())
|
||||
}
|
||||
|
||||
fn dose3_explain_dependencies(
|
||||
package: &str,
|
||||
version: &str,
|
||||
arch: &str,
|
||||
build_root: &str,
|
||||
cross: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let ctx = context::current();
|
||||
|
||||
// Construct the list of Packages files
|
||||
let mut bg_args = Vec::new();
|
||||
let mut cmd = ctx.command("apt-get");
|
||||
cmd.arg("indextargets")
|
||||
.arg("--format")
|
||||
.arg("$(FILENAME)")
|
||||
.arg("Created-By: Packages");
|
||||
|
||||
let output = cmd.output()?;
|
||||
if output.status.success() {
|
||||
let filenames = String::from_utf8_lossy(&output.stdout);
|
||||
for file in filenames.lines() {
|
||||
let file = file.trim();
|
||||
if !file.is_empty() {
|
||||
bg_args.push(file.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transform the dsc file into a 'Source' stanza (replacing 'Source' with 'Package')
|
||||
let dsc_path = find_dsc_file(build_root, package, version)?;
|
||||
let mut dsc_content = ctx.read_file(&dsc_path)?;
|
||||
dsc_content = dsc_content.replace("Source", "Package");
|
||||
ctx.write_file(
|
||||
Path::new(&format!("{build_root}/dsc-processed")),
|
||||
&dsc_content,
|
||||
)?;
|
||||
|
||||
// Call dose-builddebcheck
|
||||
let local_arch = crate::get_current_arch();
|
||||
let mut cmd = ctx.command("dose-builddebcheck");
|
||||
cmd.arg("--verbose")
|
||||
.arg("--failures")
|
||||
.arg("--explain")
|
||||
.arg("--summary")
|
||||
.arg(format!("--deb-native-arch={}", local_arch));
|
||||
|
||||
if cross {
|
||||
cmd.arg(format!("--deb-host-arch={}", arch))
|
||||
.arg("--deb-profiles=cross")
|
||||
.arg(format!("--deb-foreign-archs={}", arch));
|
||||
}
|
||||
|
||||
cmd.args(bg_args).arg(format!("{build_root}/dsc-processed"));
|
||||
cmd.status()?;
|
||||
Ok(())
|
||||
}
|
||||
178
src/deb/mod.rs
Normal file
178
src/deb/mod.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
mod cross;
|
||||
mod local;
|
||||
mod sbuild;
|
||||
|
||||
use crate::context;
|
||||
use std::error::Error;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum BuildMode {
|
||||
Sbuild,
|
||||
Local,
|
||||
}
|
||||
|
||||
pub fn build_binary_package(
|
||||
arch: Option<&str>,
|
||||
series: Option<&str>,
|
||||
cwd: Option<&Path>,
|
||||
cross: bool,
|
||||
mode: Option<BuildMode>,
|
||||
) -> 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(¤t_arch);
|
||||
|
||||
// Make sure we select a specific mode, either using user-requested
|
||||
// or by using default for user-supplied parameters
|
||||
let mode = if let Some(m) = mode {
|
||||
m
|
||||
} else {
|
||||
// For cross-compilation, we use local with an ephemeral context
|
||||
// created by the cross-compilation handler (see below)
|
||||
if cross {
|
||||
BuildMode::Local
|
||||
} else {
|
||||
// By default, we use sbuild
|
||||
BuildMode::Sbuild
|
||||
}
|
||||
};
|
||||
|
||||
// Specific case: native cross-compilation, we don't allow that
|
||||
// instead this wraps to an automatic unshare chroot
|
||||
// using an ephemeral context
|
||||
let _guard = if cross && mode == BuildMode::Local {
|
||||
Some(cross::EphemeralContextGuard::new(series)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Prepare build directory
|
||||
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 the build using target build mode
|
||||
match mode {
|
||||
BuildMode::Local => local::build(&package, &version, arch, series, &build_root, cross)?,
|
||||
BuildMode::Sbuild => sbuild::build(&package, &version, arch, series, &build_root, cross)?,
|
||||
};
|
||||
|
||||
// Retrieve produced .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(())
|
||||
}
|
||||
|
||||
fn find_dsc_file(
|
||||
build_root: &str,
|
||||
package: &str,
|
||||
version: &str,
|
||||
) -> Result<PathBuf, Box<dyn Error>> {
|
||||
// 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 = PathBuf::from(build_root).join(&dsc_name);
|
||||
|
||||
if !dsc_path.exists() {
|
||||
return Err(format!("Could not find .dsc file at {}", dsc_path.display()).into());
|
||||
}
|
||||
Ok(dsc_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
async fn test_build_end_to_end(package: &str, series: &str, arch: Option<&str>, cross: bool) {
|
||||
log::info!(
|
||||
"Starting end-to-end test for package: {} (series: {}, arch: {:?}, cross: {})",
|
||||
package,
|
||||
series,
|
||||
arch,
|
||||
cross
|
||||
);
|
||||
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let cwd = temp_dir.path();
|
||||
log::debug!("Created temporary directory: {}", cwd.display());
|
||||
|
||||
log::info!("Pulling package {} from Ubuntu {}...", package, series);
|
||||
crate::pull::pull(
|
||||
package,
|
||||
"",
|
||||
Some(series),
|
||||
"",
|
||||
"",
|
||||
Some("ubuntu"),
|
||||
Some(cwd),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Cannot pull package");
|
||||
log::info!("Successfully pulled package {}", package);
|
||||
|
||||
// Change directory to the package directory
|
||||
let cwd = cwd.join(package).join(package);
|
||||
log::debug!("Package directory: {}", cwd.display());
|
||||
|
||||
log::info!("Starting binary package build...");
|
||||
crate::deb::build_binary_package(arch, Some(series), Some(&cwd), cross, None)
|
||||
.expect("Cannot build binary package (deb)");
|
||||
log::info!("Successfully built binary package");
|
||||
|
||||
// Check that the .deb files are present
|
||||
let parent_dir = cwd.parent().expect("Cannot find parent directory");
|
||||
let deb_files = std::fs::read_dir(parent_dir)
|
||||
.expect("Cannot read build directory")
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "deb"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
log::info!("Found {} .deb files after build", deb_files.len());
|
||||
for file in &deb_files {
|
||||
log::debug!(" - {}", file.path().display());
|
||||
}
|
||||
|
||||
assert!(!deb_files.is_empty(), "No .deb files found after build");
|
||||
log::info!(
|
||||
"End-to-end test completed successfully for package: {}",
|
||||
package
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: We don't end-to-end test for a non-cross build for now.
|
||||
// This is an issue that we need to solve.
|
||||
// It seems that sbuild cannot by default be used inside of a (docker) container,
|
||||
// but our tests are currently running in one in CI.
|
||||
// TODO: Investigate on how to fix that.
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
async fn test_deb_hello_ubuntu_cross_end_to_end() {
|
||||
test_build_end_to_end("hello", "noble", Some("riscv64"), true).await;
|
||||
}
|
||||
}
|
||||
35
src/deb/sbuild.rs
Normal file
35
src/deb/sbuild.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
/// Sbuild binary package building
|
||||
/// Call 'sbuild' with the dsc file to build the package with unshare
|
||||
use crate::context;
|
||||
use std::error::Error;
|
||||
|
||||
pub fn build(
|
||||
package: &str,
|
||||
_version: &str,
|
||||
arch: &str,
|
||||
series: &str,
|
||||
build_root: &str,
|
||||
cross: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let ctx = context::current();
|
||||
let mut cmd = ctx.command("sbuild");
|
||||
cmd.current_dir(format!("{}/{}", build_root, package));
|
||||
cmd.arg("--chroot-mode=unshare");
|
||||
cmd.arg("--no-clean-source");
|
||||
|
||||
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.status()?;
|
||||
if !status.success() {
|
||||
return Err(format!("sbuild failed with status: {}", status).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user