Files
pkh/src/deb/mod.rs
Valentin Haudiquet 9524cae5ef
Some checks failed
CI / build (push) Failing after 1m56s
ci-test: make deb tests sequential
2026-01-11 01:14:43 +01:00

185 lines
6.1 KiB
Rust

mod cross;
mod ephemeral;
mod local;
mod sbuild;
use crate::context;
use std::error::Error;
use std::path::{Path, PathBuf};
/// Build mode for the binary build
#[derive(PartialEq)]
pub enum BuildMode {
/// Use `sbuild` for the build, configured in unshare mode
Sbuild,
/// Local build, directly on the context
Local,
}
/// Build package in 'cwd' to a .deb
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(&current_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 {
// By default, we use local build
BuildMode::Local
};
// Create an ephemeral unshare context for all Local builds
let _guard = if mode == BuildMode::Local {
Some(ephemeral::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);
// Check if the .dsc file exists in current context
let ctx = context::current();
if !ctx.exists(&dsc_path)? {
return Err(format!("Could not find .dsc file at {}", dsc_path.display()).into());
}
Ok(dsc_path)
}
#[cfg(test)]
mod tests {
use serial_test::serial;
async fn test_build_end_to_end(
package: &str,
series: &str,
dist: Option<&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), "", "", dist, 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
);
}
// Tests below will be marked 'serial'
// As builds are using ephemeral contexts, tests running on the same
// process could use the ephemeral context of another thread and
// interfere with each other.
// FIXME: This is not ideal. In the future, we might want to
// either explicitely pass context (instead of shared state) or
// fork for building?
#[tokio::test]
#[test_log::test]
#[serial]
async fn test_deb_hello_ubuntu_end_to_end() {
test_build_end_to_end("hello", "noble", None, None, false).await;
}
#[tokio::test]
#[test_log::test]
#[cfg(target_arch = "x86_64")]
#[serial]
async fn test_deb_hello_ubuntu_cross_end_to_end() {
test_build_end_to_end("hello", "noble", None, Some("riscv64"), true).await;
}
}