Compare commits

..

4 Commits

Author SHA1 Message Date
e57a6fb457 ci-test: restore tempfile as dev-dep for tests
Some checks failed
CI / build (push) Has been cancelled
CI / build (pull_request) Has been cancelled
2026-01-11 01:29:52 +01:00
42ef14e17a ci-test: make deb tests sequential 2026-01-11 01:29:52 +01:00
d89606ded2 ci-test: ensure unique directory name for parallel testing 2026-01-11 01:29:52 +01:00
acb8a6657a ci-test: test running only cross test 2026-01-11 01:29:52 +01:00
8 changed files with 101 additions and 217 deletions

View File

@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
components: rustfmt, clippy components: rustfmt
- name: Check format - name: Check format
run: cargo fmt --check run: cargo fmt --check
- name: Install build dependencies - name: Install build dependencies
@@ -32,12 +32,6 @@ jobs:
sudo apt-get install -y pkg-config libssl-dev libgpg-error-dev libgpgme-dev sudo apt-get install -y pkg-config libssl-dev libgpg-error-dev libgpgme-dev
- name: Build - name: Build
run: cargo build run: cargo build
env:
RUSTFLAGS: -Dwarnings
- name: Lint
run: cargo clippy --all-targets --all-features
env:
RUSTFLAGS: -Dwarnings
- name: Install runtime system dependencies - name: Install runtime system dependencies
run: | run: |
sudo apt-get update sudo apt-get update

View File

@@ -24,10 +24,8 @@ Options:
Commands and workflows include: Commands and workflows include:
``` ```
Commands: Commands:
pull Pull a source package from the archive or git pull Get a source package from the archive or git
chlog Auto-generate changelog entry, editing it, committing it afterwards chlog Auto-generate changelog entry, editing it, committing it afterwards
build Build the source package (into a .dsc)
deb Build the source package into binary package (.deb)
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
``` ```
@@ -98,7 +96,7 @@ Missing features:
- [ ] Three build modes: - [ ] Three build modes:
- [ ] Build locally (discouraged) - [ ] Build locally (discouraged)
- [x] Build using sbuild+unshare, with binary emulation (default) - [x] Build using sbuild+unshare, with binary emulation (default)
- [x] Cross-compilation - [ ] Cross-compilation
- [ ] Async build - [ ] Async build
- [ ] `pkh status` - [ ] `pkh status`
- [ ] Show build status - [ ] Show build status

View File

@@ -80,16 +80,10 @@ pub fn build(
return Err("Could not install essential packages for the build".into()); return Err("Could not install essential packages for the build".into());
} }
// Find the actual package directory
let package_dir = crate::deb::find_package_directory(Path::new(build_root), package, version)?;
let package_dir_str = package_dir
.to_str()
.ok_or("Invalid package directory path")?;
// Install build dependencies // Install build dependencies
log::debug!("Installing build dependencies..."); log::debug!("Installing build dependencies...");
let mut cmd = ctx.command("apt-get"); let mut cmd = ctx.command("apt-get");
cmd.current_dir(package_dir_str) cmd.current_dir(format!("{build_root}/{package}"))
.envs(env.clone()) .envs(env.clone())
.arg("-y") .arg("-y")
.arg("build-dep"); .arg("build-dep");
@@ -108,7 +102,7 @@ pub fn build(
log::debug!("Building (debian/rules build) package..."); log::debug!("Building (debian/rules build) package...");
let status = ctx let status = ctx
.command("debian/rules") .command("debian/rules")
.current_dir(package_dir_str) .current_dir(format!("{build_root}/{package}"))
.envs(env.clone()) .envs(env.clone())
.arg("build") .arg("build")
.status()?; .status()?;
@@ -119,7 +113,7 @@ pub fn build(
// Run the 'binary' step to produce deb // Run the 'binary' step to produce deb
let status = ctx let status = ctx
.command("fakeroot") .command("fakeroot")
.current_dir(package_dir_str) .current_dir(format!("{build_root}/{package}"))
.envs(env.clone()) .envs(env.clone())
.arg("debian/rules") .arg("debian/rules")
.arg("binary") .arg("binary")

View File

@@ -85,87 +85,6 @@ pub fn build_binary_package(
Ok(()) Ok(())
} }
/// Find the current package directory by trying both patterns:
/// - package/package
/// - package/package-origversion
pub(crate) fn find_package_directory(
parent_dir: &Path,
package: &str,
version: &str,
) -> Result<PathBuf, Box<dyn Error>> {
let ctx = context::current();
// Try package/package pattern first
let package_dir = parent_dir.join(package).join(package);
if ctx.exists(&package_dir)? {
return Ok(package_dir);
}
// Compute origversion from version: remove everything after first '-', after stripping epoch
let version_without_epoch = version.split_once(':').map(|(_, v)| v).unwrap_or(version);
let origversion = version_without_epoch
.split_once('-')
.map(|(v, _)| v)
.unwrap_or(version);
// Try package/package-origversion pattern
let package_dir = parent_dir
.join(package)
.join(format!("{}-{}", package, origversion));
if ctx.exists(&package_dir)? {
return Ok(package_dir);
}
// Try 'package' only
let package_dir = parent_dir.join(package);
if ctx.exists(&package_dir)? {
return Ok(package_dir);
}
// Try package-origversion only
let package_dir = parent_dir.join(format!("{}-{}", package, origversion));
if ctx.exists(&package_dir)? {
return Ok(package_dir);
}
// List all directories under 'package/' and log them
let package_parent = parent_dir;
if ctx.exists(package_parent)? {
log::debug!(
"Listing all directories under '{}':",
package_parent.display()
);
let entries = ctx.list_files(package_parent)?;
let mut found_dirs = Vec::new();
for entry in entries {
if entry.is_dir() {
if let Some(file_name) = entry.file_name() {
found_dirs.push(file_name.to_string_lossy().into_owned());
}
log::debug!(" - {}", entry.display());
}
}
// If we found directories but none matched our patterns, provide helpful error
if !found_dirs.is_empty() {
return Err(format!(
"Could not find package directory for {} in {}. Found directories: {}",
package,
parent_dir.display(),
found_dirs.join(", ")
)
.into());
}
}
Err(format!(
"Could not find package directory for {} in {}",
package,
parent_dir.display()
)
.into())
}
fn find_dsc_file( fn find_dsc_file(
build_root: &str, build_root: &str,
package: &str, package: &str,
@@ -206,18 +125,14 @@ mod tests {
let cwd = temp_dir.path(); let cwd = temp_dir.path();
log::debug!("Created temporary directory: {}", cwd.display()); log::debug!("Created temporary directory: {}", cwd.display());
log::info!("Pulling package {} from {}...", package, series); log::info!("Pulling package {} from Ubuntu {}...", package, series);
let package_info = crate::package_info::lookup(package, None, Some(series), "", dist, None) crate::pull::pull(package, "", Some(series), "", "", dist, Some(cwd), None)
.await
.expect("Cannot lookup package information");
crate::pull::pull(&package_info, Some(cwd), None, true)
.await .await
.expect("Cannot pull package"); .expect("Cannot pull package");
log::info!("Successfully pulled package {}", package); log::info!("Successfully pulled package {}", package);
// Change directory to the package directory // Change directory to the package directory
let cwd = crate::deb::find_package_directory(cwd, package, &package_info.stanza.version) let cwd = cwd.join(package).join(package);
.expect("Cannot find package directory");
log::debug!("Package directory: {}", cwd.display()); log::debug!("Package directory: {}", cwd.display());
log::info!("Starting binary package build..."); log::info!("Starting binary package build...");

View File

@@ -2,26 +2,18 @@
/// Call 'sbuild' with the dsc file to build the package with unshare /// Call 'sbuild' with the dsc file to build the package with unshare
use crate::context; use crate::context;
use std::error::Error; use std::error::Error;
use std::path::Path;
pub fn build( pub fn build(
package: &str, package: &str,
version: &str, _version: &str,
arch: &str, arch: &str,
series: &str, series: &str,
build_root: &str, build_root: &str,
cross: bool, cross: bool,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let ctx = context::current(); let ctx = context::current();
// Find the actual package directory
let package_dir = crate::deb::find_package_directory(Path::new(build_root), package, version)?;
let package_dir_str = package_dir
.to_str()
.ok_or("Invalid package directory path")?;
let mut cmd = ctx.command("sbuild"); let mut cmd = ctx.command("sbuild");
cmd.current_dir(package_dir_str); cmd.current_dir(format!("{}/{}", build_root, package));
cmd.arg("--chroot-mode=unshare"); cmd.arg("--chroot-mode=unshare");
cmd.arg("--no-clean-source"); cmd.arg("--no-clean-source");

View File

@@ -7,6 +7,8 @@ use pkh::context::ContextConfig;
extern crate flate2; extern crate flate2;
use pkh::pull::pull;
use pkh::changelog::generate_entry; use pkh::changelog::generate_entry;
use indicatif_log_bridge::LogWrapper; use indicatif_log_bridge::LogWrapper;
@@ -47,10 +49,10 @@ fn main() {
.arg(arg!(--backport "This changelog is for a backport entry").required(false)) .arg(arg!(--backport "This changelog is for a backport entry").required(false))
.arg(arg!(-v --version <version> "Target version").required(false)), .arg(arg!(-v --version <version> "Target version").required(false)),
) )
.subcommand(Command::new("build").about("Build the source package (into a .dsc)")) .subcommand(Command::new("build").about("Build the source package"))
.subcommand( .subcommand(
Command::new("deb") Command::new("deb")
.about("Build the source package into binary package (.deb)") .about("Build the binary package")
.arg(arg!(-s --series <series> "Target distribution series").required(false)) .arg(arg!(-s --series <series> "Target distribution series").required(false))
.arg(arg!(-a --arch <arch> "Target architecture").required(false)) .arg(arg!(-a --arch <arch> "Target architecture").required(false))
.arg(arg!(--cross "Cross-compile for target architecture (instead of qemu-binfmt)") .arg(arg!(--cross "Cross-compile for target architecture (instead of qemu-binfmt)")
@@ -92,8 +94,11 @@ fn main() {
let package = sub_matches.get_one::<String>("package").expect("required"); let package = sub_matches.get_one::<String>("package").expect("required");
let series = sub_matches.get_one::<String>("series").map(|s| s.as_str()); let series = sub_matches.get_one::<String>("series").map(|s| s.as_str());
let dist = sub_matches.get_one::<String>("dist").map(|s| s.as_str()); let dist = sub_matches.get_one::<String>("dist").map(|s| s.as_str());
let version = sub_matches.get_one::<String>("version").map(|s| s.as_str()); let version = sub_matches
let _ppa = sub_matches .get_one::<String>("version")
.map(|s| s.as_str())
.unwrap_or("");
let ppa = sub_matches
.get_one::<String>("ppa") .get_one::<String>("ppa")
.map(|s| s.as_str()) .map(|s| s.as_str())
.unwrap_or(""); .unwrap_or("");
@@ -101,18 +106,16 @@ fn main() {
let (pb, progress_callback) = ui::create_progress_bar(&multi); let (pb, progress_callback) = ui::create_progress_bar(&multi);
// Since pull is async, we need to block on it // Since pull is async, we need to block on it
if let Err(e) = rt.block_on(async { if let Err(e) = rt.block_on(pull(
let package_info = pkh::package_info::lookup( package,
package, version,
version, series,
series, "",
"", ppa,
dist, dist,
Some(&progress_callback), None,
) Some(&progress_callback),
.await?; )) {
pkh::pull::pull(&package_info, None, Some(&progress_callback), false).await
}) {
pb.finish_and_clear(); pb.finish_and_clear();
error!("{}", e); error!("{}", e);
std::process::exit(1); std::process::exit(1);

View File

@@ -56,8 +56,7 @@ fn parse_series_csv(content: &str) -> Result<Vec<String>, Box<dyn Error>> {
Ok(entries.into_iter().map(|(s, _)| s).collect()) Ok(entries.into_iter().map(|(s, _)| s).collect())
} }
/// Get time-ordered list of series for a distribution, development series first async fn get_ordered_series(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
pub async fn get_ordered_series(dist: &str) -> Result<Vec<String>, Box<dyn Error>> {
let content = if Path::new(format!("/usr/share/distro-info/{dist}.csv").as_str()).exists() { let content = if Path::new(format!("/usr/share/distro-info/{dist}.csv").as_str()).exists() {
std::fs::read_to_string(format!("/usr/share/distro-info/{dist}.csv"))? std::fs::read_to_string(format!("/usr/share/distro-info/{dist}.csv"))?
} else { } else {
@@ -72,8 +71,9 @@ pub async fn get_ordered_series(dist: &str) -> Result<Vec<String>, Box<dyn Error
let mut series = parse_series_csv(&content)?; let mut series = parse_series_csv(&content)?;
// For Debian, ensure 'sid' is first if it's not // For Debian, ensure 'sid' is first if it's not (it usually doesn't have a date or is very old/new depending on file)
// We want to try 'sid' (unstable) first for Debian. // Actually in the file sid has 1993 date.
// But we want to try 'sid' (unstable) first for Debian.
if dist == "debian" { if dist == "debian" {
series.retain(|s| s != "sid"); series.retain(|s| s != "sid");
series.insert(0, "sid".to_string()); series.insert(0, "sid".to_string());
@@ -332,7 +332,7 @@ fn parse_sources(
} }
/// Get package information from a package, distribution series, and pocket /// Get package information from a package, distribution series, and pocket
async fn get( pub async fn get(
package_name: &str, package_name: &str,
series: &str, series: &str,
pocket: &str, pocket: &str,
@@ -406,7 +406,7 @@ async fn get(
} }
/// Try to find package information in a distribution, trying all series and pockets /// Try to find package information in a distribution, trying all series and pockets
async fn find_package( pub async fn find_package(
package_name: &str, package_name: &str,
dist: &str, dist: &str,
pocket: &str, pocket: &str,
@@ -452,58 +452,6 @@ async fn find_package(
Err(format!("Package '{}' not found.", package_name).into()) Err(format!("Package '{}' not found.", package_name).into())
} }
/// Lookup package information for a source package
///
/// This function obtains package information either directly from a specific series
/// or by searching across all series in a distribution.
pub async fn lookup(
package: &str,
version: Option<&str>,
series: Option<&str>,
pocket: &str,
dist: Option<&str>,
progress: ProgressCallback<'_>,
) -> Result<PackageInfo, Box<dyn Error>> {
// Obtain the package information, either directly in a series or with a search in all series
let package_info = if let Some(s) = series {
if let Some(cb) = progress {
cb(
&format!("Resolving package info for {}...", package),
"",
0,
0,
);
}
// Get the package information from that series and pocket
get(package, s, pocket, version).await?
} else {
let dist = dist.unwrap_or_else(||
// Use auto-detection to see if current distro is ubuntu, or fallback to debian by default
if std::process::Command::new("lsb_release").arg("-i").arg("-s").output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_lowercase()).unwrap_or_default() == "ubuntu" {
"ubuntu"
} else {
"debian"
}
);
if let Some(cb) = progress {
cb(
&format!("Searching for package {} in {}...", package, dist),
"",
0,
0,
);
}
// Try to find the package in all series from that dist
find_package(package, dist, pocket, version, progress).await?
};
Ok(package_info)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -2,6 +2,7 @@ use std::cmp::min;
use std::error::Error; use std::error::Error;
use std::path::Path; use std::path::Path;
use crate::package_info;
use crate::package_info::PackageInfo; use crate::package_info::PackageInfo;
use std::process::Command; use std::process::Command;
@@ -332,19 +333,64 @@ async fn fetch_archive_sources(
Ok(()) Ok(())
} }
/// Pull a source package locally using pre-retrieved package information /// Pull a source package locally
/// ///
/// This function takes a PackageInfo struct and downloads the package using the preferred method /// Will try to find the package information, and use it to download it over prefered way
/// (either git or direct archive download), as well as orig tarball, inside 'package' directory. /// (either git or direct archive download), as well as orig tarball, inside 'package' directory
/// The source will be extracted under 'package/package'. /// The source will be extracted under 'package/package'
pub async fn pull( pub async fn pull(
package_info: &PackageInfo, package: &str,
_version: &str,
series: Option<&str>,
pocket: &str,
_ppa: &str,
dist: Option<&str>,
cwd: Option<&Path>, cwd: Option<&Path>,
progress: ProgressCallback<'_>, progress: ProgressCallback<'_>,
force_archive: bool, ) -> Result<PackageInfo, Box<dyn Error>> {
) -> Result<(), Box<dyn Error>> { let version_opt = if _version.is_empty() {
let package = &package_info.stanza.package; None
let series = &package_info.series; } else {
Some(_version)
};
/* Obtain the package information, either directly in a series or with a search in all series */
let package_info = if let Some(s) = series {
if let Some(cb) = progress {
cb(
&format!("Resolving package info for {}...", package),
"",
0,
0,
);
}
// Get the package information from that series and pocket
package_info::get(package, s, pocket, version_opt).await?
} else {
let dist = dist.unwrap_or_else(||
// Use auto-detection to see if current distro is ubuntu, or fallback to debian by default
if std::process::Command::new("lsb_release").arg("-i").arg("-s").output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_lowercase()).unwrap_or_default() == "ubuntu" {
"ubuntu"
} else {
"debian"
}
);
if let Some(cb) = progress {
cb(
&format!("Searching for package {} in {}...", package, dist),
"",
0,
0,
);
}
// Try to find the package in all series from that dist
package_info::find_package(package, dist, pocket, version_opt, progress).await?
};
let package_dir = if let Some(path) = cwd { let package_dir = if let Some(path) = cwd {
path.join(package) path.join(package)
} else { } else {
@@ -352,20 +398,15 @@ pub async fn pull(
}; };
/* Fetch the package: either via git (preferred VCS) or the archive */ /* Fetch the package: either via git (preferred VCS) or the archive */
if let Some(ref url) = package_info.preferred_vcs if let Some(ref url) = package_info.preferred_vcs {
&& !force_archive
{
// We have found a preferred VCS (git repository) for the package, so // We have found a preferred VCS (git repository) for the package, so
// we fetch the package from that repo. // we fetch the package from that repo.
// Depending on target series, we pick target branch; if latest series is specified, // Depending on target series, we pick target branch; if no series is specified,
// we target the development branch, i.e. the default branch // we target the development branch, i.e. the default branch
let branch_name = if crate::package_info::get_ordered_series(package_info.dist.as_str()) let branch_name = if let Some(s) = series {
.await?[0]
!= *series
{
if package_info.dist == "ubuntu" { if package_info.dist == "ubuntu" {
Some(format!("{}/{}", package_info.dist, series)) Some(format!("{}/{}", package_info.dist, s))
} else { } else {
// Debian does not have reliable branch naming... // Debian does not have reliable branch naming...
// For now, we skip that part and clone default // For now, we skip that part and clone default
@@ -405,7 +446,7 @@ pub async fn pull(
if let Some(cb) = progress { if let Some(cb) = progress {
cb("Fetching orig tarball...", "", 0, 0); cb("Fetching orig tarball...", "", 0, 0);
} }
fetch_orig_tarball(package_info, Some(&package_dir), progress).await?; fetch_orig_tarball(&package_info, Some(&package_dir), progress).await?;
} else { } else {
debug!("Native package, skipping orig tarball fetch."); debug!("Native package, skipping orig tarball fetch.");
} }
@@ -413,16 +454,16 @@ pub async fn pull(
if let Some(cb) = progress { if let Some(cb) = progress {
cb("Fetching dsc file...", "", 0, 0); cb("Fetching dsc file...", "", 0, 0);
} }
fetch_dsc_file(package_info, Some(&package_dir), progress).await?; fetch_dsc_file(&package_info, Some(&package_dir), progress).await?;
} else { } else {
// Fallback to archive fetching // Fallback to archive fetching
if let Some(cb) = progress { if let Some(cb) = progress {
cb("Downloading from archive...", "", 0, 0); cb("Downloading from archive...", "", 0, 0);
} }
fetch_archive_sources(package_info, Some(&package_dir), progress).await?; fetch_archive_sources(&package_info, Some(&package_dir), progress).await?;
} }
Ok(()) Ok(package_info)
} }
#[cfg(test)] #[cfg(test)]
@@ -434,17 +475,16 @@ mod tests {
// For determinism, we require for tests that either a distro or series is specified, // For determinism, we require for tests that either a distro or series is specified,
// as no distribution would mean fallback to system distro // as no distribution would mean fallback to system distro
assert!(dist.is_some() || series.is_some()); assert!(dist != None || series != None);
// Use a temp directory as working directory // Use a temp directory as working directory
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let cwd = temp_dir.path(); let cwd = temp_dir.path();
// Main 'pull' command: the one we want to test // Main 'pull' command: the one we want to test
let info = crate::package_info::lookup(package, None, series, "", dist, None) let info = pull(package, "", series, "", "", dist, Some(cwd), None)
.await .await
.unwrap(); .unwrap();
pull(&info, Some(cwd), None, false).await.unwrap();
let package_dir = cwd.join(package); let package_dir = cwd.join(package);
assert!(package_dir.exists()); assert!(package_dir.exists());