From e5dec657886f67e7efa47b64c737ebc9ffafcbe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Mon, 12 Jan 2026 23:25:41 +0100 Subject: [PATCH 01/21] feat(cli): infer package name and version from package dir for build --- Cargo.lock | 155 +++++++++++++++++++++++++++++++ Cargo.toml | 5 + debian/control | 3 +- packages/debmagic/Cargo.toml | 2 + packages/debmagic/src/main.rs | 1 + packages/debmagic/src/package.rs | 35 +++++++ 6 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 packages/debmagic/src/package.rs diff --git a/Cargo.lock b/Cargo.lock index 023bad8..5be7b0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,6 +253,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -278,6 +284,21 @@ dependencies = [ "typenum", ] +[[package]] +name = "debian-changelog" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b2dbdd00d57ff463e85d76ff81c1f28e64d8412d01ef5ae9bcd041eea982f9" +dependencies = [ + "chrono", + "debversion", + "lazy-regex", + "log", + "rowan", + "textwrap", + "whoami", +] + [[package]] name = "debmagic" version = "0.0.1-alpha1" @@ -285,6 +306,8 @@ dependencies = [ "anyhow", "clap", "config", + "debian-changelog", + "debmagic-common", "dirs", "glob", "libc", @@ -302,6 +325,17 @@ dependencies = [ "test-case", ] +[[package]] +name = "debversion" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f5cc9ce1d5067bee8060dd75208525dd0133ffea0b2960fef64ab85d58c4c5" +dependencies = [ + "chrono", + "lazy-regex", + "num-bigint", +] + [[package]] name = "digest" version = "0.10.7" @@ -500,6 +534,29 @@ dependencies = [ "serde", ] +[[package]] +name = "lazy-regex" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5c13b6857ade4c8ee05c3c3dc97d2ab5415d691213825b90d3211c425c1f907" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a95c68db5d41694cea563c86a4ba4dc02141c16ef64814108cb23def4d5438" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + [[package]] name = "libc" version = "0.2.178" @@ -514,6 +571,7 @@ checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" dependencies = [ "bitflags", "libc", + "redox_syscall", ] [[package]] @@ -528,6 +586,25 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -638,6 +715,15 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "redox_syscall" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -692,6 +778,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash", + "text-size", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -702,6 +800,12 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.22" @@ -789,6 +893,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "strsim" version = "0.11.1" @@ -839,6 +949,23 @@ dependencies = [ "test-case-core", ] +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "2.0.17" @@ -923,12 +1050,24 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "utf8parse" version = "0.2.2" @@ -967,6 +1106,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.106" @@ -1012,6 +1157,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "windows-core" version = "0.62.2" diff --git a/Cargo.toml b/Cargo.toml index aa675db..1dc2b15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,10 @@ edition = "2024" rust-version = "1.89" [workspace.dependencies] +# internal +debmagic-common = { path = "packages/debmagic-common" } + +# third party clap = { version = ">=4.5.23", features = ["derive"] } anyhow = { version = ">=1.0.95" } config = { version = ">=0.15.9", features = ["toml"] } @@ -19,6 +23,7 @@ serde_json = ">=1.0.139" uuid = { version = ">=1.10.0", features = ["v4"] } chrono = { version = ">=0.4.42" } regex = { version = ">=1.12.2" } +debian-changelog = { version = ">=0.2.14" } # dev dependencies test-case = { version = ">=3.3.1" } diff --git a/debian/control b/debian/control index a6c2c79..27f29ad 100644 --- a/debian/control +++ b/debian/control @@ -24,7 +24,8 @@ Build-Depends: librust-chrono-dev (>=0.4.42), librust-regex-dev (>=1.12.2), librust-test-case-dev (>=3.3.1), - librust-pyo3-dev (>=0.27.2) + librust-pyo3-dev (>=0.27.2), + librust-debian-changelog-dev (>=0.2.14) Rules-Requires-Root: no X-Style: black Standards-Version: 4.7.2 diff --git a/packages/debmagic/Cargo.toml b/packages/debmagic/Cargo.toml index 4df138b..98fe8e8 100644 --- a/packages/debmagic/Cargo.toml +++ b/packages/debmagic/Cargo.toml @@ -8,6 +8,7 @@ rust-version.workspace = true edition.workspace = true [dependencies] +debmagic-common = { workspace = true } clap = { workspace = true, features = ["derive"] } anyhow = { workspace = true } config = { workspace = true, features = ["toml"] } @@ -17,3 +18,4 @@ libc = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } uuid = { workspace = true, features = ["v4"] } +debian-changelog = { workspace = true } diff --git a/packages/debmagic/src/main.rs b/packages/debmagic/src/main.rs index a663c93..31becea 100644 --- a/packages/debmagic/src/main.rs +++ b/packages/debmagic/src/main.rs @@ -15,6 +15,7 @@ use crate::{ pub mod build; pub mod cli; pub mod config; +pub mod package; fn get_config(cli: &Cli, source_dir: &Option) -> anyhow::Result { let mut config_file_paths = vec![]; diff --git a/packages/debmagic/src/package.rs b/packages/debmagic/src/package.rs new file mode 100644 index 0000000..44a51a4 --- /dev/null +++ b/packages/debmagic/src/package.rs @@ -0,0 +1,35 @@ +use std::{path::PathBuf, str::FromStr}; + +use anyhow::anyhow; + +use debian_changelog::ChangeLog; +use debmagic_common::debian::version::PackageVersion; + +pub struct PackageDescription { + pub name: String, + pub version: PackageVersion, +} + +impl PackageDescription { + pub fn from_dir(dir: &PathBuf) -> anyhow::Result { + let changelog_file = dir.join("debian").join("changelog"); + let changelog_contents = std::fs::read_to_string(changelog_file)?; + let changelog: debian_changelog::ChangeLog = changelog_contents.parse()?; + + let first_entry = changelog + .into_iter() + .next() + .ok_or(anyhow!("changelog is empty"))?; + + let name = first_entry + .package() + .ok_or(anyhow!("empty package name in changelog entry"))?; + let version: PackageVersion = first_entry + .version() + .ok_or(anyhow!("no package version in changelog entry")) + .map(|v| PackageVersion::from_str(v)) + .map_err(|| anyhow!("invalid package version in changelog entry"))?; + + Self { name, version } + } +} From 100556dcd1ac2253f865754e0bd9f3ebc8ee3835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Tue, 13 Jan 2026 21:38:43 +0100 Subject: [PATCH 02/21] test(common): move python tests for py common to debmagic-pkg package --- .../tests/assets/pkg1/debian/changelog | 0 .../tests/assets/pkg1/debian/control | 0 .../{debmagic-common => debmagic-pkg}/tests/test_changelog.py | 0 packages/{debmagic-common => debmagic-pkg}/tests/test_exec.py | 0 packages/{debmagic-common => debmagic-pkg}/tests/test_package.py | 0 .../{debmagic-common => debmagic-pkg}/tests/test_versioning.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename packages/{debmagic-common => debmagic-pkg}/tests/assets/pkg1/debian/changelog (100%) rename packages/{debmagic-common => debmagic-pkg}/tests/assets/pkg1/debian/control (100%) rename packages/{debmagic-common => debmagic-pkg}/tests/test_changelog.py (100%) rename packages/{debmagic-common => debmagic-pkg}/tests/test_exec.py (100%) rename packages/{debmagic-common => debmagic-pkg}/tests/test_package.py (100%) rename packages/{debmagic-common => debmagic-pkg}/tests/test_versioning.py (100%) diff --git a/packages/debmagic-common/tests/assets/pkg1/debian/changelog b/packages/debmagic-pkg/tests/assets/pkg1/debian/changelog similarity index 100% rename from packages/debmagic-common/tests/assets/pkg1/debian/changelog rename to packages/debmagic-pkg/tests/assets/pkg1/debian/changelog diff --git a/packages/debmagic-common/tests/assets/pkg1/debian/control b/packages/debmagic-pkg/tests/assets/pkg1/debian/control similarity index 100% rename from packages/debmagic-common/tests/assets/pkg1/debian/control rename to packages/debmagic-pkg/tests/assets/pkg1/debian/control diff --git a/packages/debmagic-common/tests/test_changelog.py b/packages/debmagic-pkg/tests/test_changelog.py similarity index 100% rename from packages/debmagic-common/tests/test_changelog.py rename to packages/debmagic-pkg/tests/test_changelog.py diff --git a/packages/debmagic-common/tests/test_exec.py b/packages/debmagic-pkg/tests/test_exec.py similarity index 100% rename from packages/debmagic-common/tests/test_exec.py rename to packages/debmagic-pkg/tests/test_exec.py diff --git a/packages/debmagic-common/tests/test_package.py b/packages/debmagic-pkg/tests/test_package.py similarity index 100% rename from packages/debmagic-common/tests/test_package.py rename to packages/debmagic-pkg/tests/test_package.py diff --git a/packages/debmagic-common/tests/test_versioning.py b/packages/debmagic-pkg/tests/test_versioning.py similarity index 100% rename from packages/debmagic-common/tests/test_versioning.py rename to packages/debmagic-pkg/tests/test_versioning.py From 951e24d1292f2f482504ec647643e922e284914e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Tue, 13 Jan 2026 21:57:54 +0100 Subject: [PATCH 03/21] fix(cli): properly read package name and version from changelog in source tree --- debian/control | 1 - .../debmagic-common/src/debian/version.rs | 57 +++++++++++++------ packages/debmagic/src/build.rs | 3 +- packages/debmagic/src/build/common.rs | 7 --- packages/debmagic/src/main.rs | 19 +++---- packages/debmagic/src/package.rs | 18 +++--- 6 files changed, 60 insertions(+), 45 deletions(-) diff --git a/debian/control b/debian/control index 27f29ad..496cc9d 100644 --- a/debian/control +++ b/debian/control @@ -49,7 +49,6 @@ Package: debmagic Architecture: any Depends: ${misc:Depends}, - ${python3:Depends}, ${shlibs:Depends}, Description: Debian package building made easy. Holistic cli for the whole debian package building workflow. diff --git a/packages/debmagic-common/src/debian/version.rs b/packages/debmagic-common/src/debian/version.rs index ae4a425..36573f7 100644 --- a/packages/debmagic-common/src/debian/version.rs +++ b/packages/debmagic-common/src/debian/version.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::str::FromStr; use regex::Regex; @@ -5,45 +6,61 @@ use regex::Regex; #[derive(Debug, Clone, PartialEq, Eq)] pub struct PackageVersion { // distro packaging override base version (default is 0) - epoch: String, + epoch: Option, // upstream package version upstream: String, // packaging (linux distro) revision - revision: String, + revision: Option, } impl PackageVersion { + pub fn new(epoch: Option, upstream: String, revision: Option) -> Self { + Self { + epoch, + upstream, + revision, + } + } + pub fn version(&self) -> String { let mut ret: String = "".to_owned(); - if self.epoch != "0" { - ret.push_str(&format!("{}:", self.epoch)); + if let Some(epoch) = self.epoch + && epoch != 0 + { + ret.push_str(&format!("{}:", epoch)); } ret.push_str(&self.upstream); - if !self.revision.is_empty() { - ret.push_str(&format!("-{}", self.revision)); + if let Some(revision) = &self.revision { + ret.push_str(&format!("-{}", revision)); } ret } /// distro epoch plus upstream version pub fn epoch_upstream(&self) -> String { - if !self.epoch.is_empty() { - return format!("{}:{}", self.epoch, self.upstream); + if let Some(epoch) = self.epoch { + return format!("{}:{}", epoch, self.upstream); } self.upstream.clone() } /// distro epoch plus upstream version pub fn upstream_revision(&self) -> String { - if !self.revision.is_empty() { - return format!("{}-{}", self.upstream, self.revision); + if let Some(revision) = &self.revision { + return format!("{}-{}", self.upstream, revision); } self.upstream.clone() } } +impl fmt::Display for PackageVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.version()) + } +} + #[derive(Debug, PartialEq, Eq)] pub struct VersionParseError; @@ -58,10 +75,10 @@ impl FromStr for PackageVersion { // pkg-info.mk uses the full version if no epoch is in it. // instead, we return "0" as oritinally intended if no epoch is in version. let epoch = if !version.contains(':') { - "0".to_string() + Some(0) } else { let re_epoch = Regex::new(r"^([0-9]+):.*$").map_err(|_| VersionParseError)?; - re_epoch.replace(version, "$1").to_string() + re_epoch.replace(version, "$1").to_string().parse().ok() }; let re_upstream = Regex::new(r"^([0-9]*:)?(.*?)$").map_err(|_| VersionParseError)?; @@ -75,7 +92,11 @@ impl FromStr for PackageVersion { Ok(Self { epoch, upstream, - revision, + revision: if revision.is_empty() { + None + } else { + Some(revision) + }, }) } } @@ -88,19 +109,19 @@ mod tests { #[test_case( "1.2.3a.4-42.2-14ubuntu2~20.04.1", - &PackageVersion{epoch:"0".to_string(), upstream:"1.2.3a.4-42.2".to_string(), revision:"14ubuntu2~20.04.1".to_string()})] + &PackageVersion{epoch:Some(0), upstream:"1.2.3a.4-42.2".to_string(), revision:Some("14ubuntu2~20.04.1".to_string())})] #[test_case( "3:1.2.3a.4-42.2-14ubuntu2~20.04.1", - &PackageVersion{epoch:"3".to_string(), upstream:"1.2.3a.4-42.2".to_string(), revision:"14ubuntu2~20.04.1".to_string()})] + &PackageVersion{epoch:Some(3), upstream:"1.2.3a.4-42.2".to_string(), revision:Some("14ubuntu2~20.04.1".to_string())})] #[test_case( "3:1.2.3a.4ubuntu", - &PackageVersion{epoch:"3".to_string(), upstream:"1.2.3a.4ubuntu".to_string(), revision:"".to_string()})] + &PackageVersion{epoch:Some(3), upstream:"1.2.3a.4ubuntu".to_string(), revision:None})] #[test_case( "3:1.2.3a-4ubuntu", - &PackageVersion{epoch:"3".to_string(), upstream:"1.2.3a".to_string(), revision:"4ubuntu".to_string()})] + &PackageVersion{epoch:Some(3), upstream:"1.2.3a".to_string(), revision:Some("4ubuntu".to_string())})] #[test_case( "3:1.2.3a-4ubuntu1", - &PackageVersion{epoch:"3".to_string(), upstream:"1.2.3a".to_string(), revision:"4ubuntu1".to_string()})] + &PackageVersion{epoch:Some(3), upstream:"1.2.3a".to_string(), revision:Some("4ubuntu1".to_string())})] fn test_version_parsing(version: &str, expected: &PackageVersion) { let parsed_version = PackageVersion::from_str(version).unwrap(); diff --git a/packages/debmagic/src/build.rs b/packages/debmagic/src/build.rs index a407b4b..11d8c3b 100644 --- a/packages/debmagic/src/build.rs +++ b/packages/debmagic/src/build.rs @@ -9,12 +9,13 @@ use std::{ use crate::{ build::{ - common::{BuildConfig, BuildDriver, BuildDriverType, BuildMetadata, PackageDescription}, + common::{BuildConfig, BuildDriver, BuildDriverType, BuildMetadata}, config::DriverConfig, driver_bare::DriverBare, driver_docker::DriverDocker, }, config::Config, + package::PackageDescription, }; use anyhow::{Context, anyhow}; use glob::glob; diff --git a/packages/debmagic/src/build/common.rs b/packages/debmagic/src/build/common.rs index ddf075d..4a0d296 100644 --- a/packages/debmagic/src/build/common.rs +++ b/packages/debmagic/src/build/common.rs @@ -15,13 +15,6 @@ pub enum BuildDriverType { // Lxd } -#[derive(Debug, Clone)] -pub struct PackageDescription { - pub name: String, - pub version: String, - pub source_dir: PathBuf, -} - pub type DriverSpecificBuildMetadata = HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/packages/debmagic/src/main.rs b/packages/debmagic/src/main.rs index 31becea..bb7e4ab 100644 --- a/packages/debmagic/src/main.rs +++ b/packages/debmagic/src/main.rs @@ -7,9 +7,10 @@ use anyhow::Context; use clap::{CommandFactory, Parser}; use crate::{ - build::{build_package, common::PackageDescription, get_shell_in_build}, + build::{build_package, get_shell_in_build}, cli::{Cli, Commands}, config::Config, + package::PackageDescription, }; pub mod build; @@ -47,11 +48,9 @@ fn main() -> anyhow::Result<()> { Commands::Build(args) => { let source_dir = args.source_dir.as_deref().unwrap_or(¤t_dir); let config = get_config(&cli, &Some(source_dir.to_path_buf()))?; - let package = PackageDescription { - name: "debmagic".to_string(), - version: "0.1.0".to_string(), - source_dir: path::absolute(source_dir).context("resolving source dir failed")?, - }; + let package = PackageDescription::from_dir( + &path::absolute(source_dir).context("resolving source dir failed")?, + )?; let output_dir = args.output_dir.as_deref().unwrap_or(¤t_dir); build_package( &config, @@ -64,11 +63,9 @@ fn main() -> anyhow::Result<()> { Commands::Shell(args) => { let source_dir = args.source_dir.as_deref().unwrap_or(¤t_dir); let config = get_config(&cli, &Some(source_dir.to_path_buf()))?; - let package = PackageDescription { - name: "debmagic".to_string(), - version: "0.1.0".to_string(), - source_dir: path::absolute(source_dir).context("resolving source dir failed")?, - }; + let package = PackageDescription::from_dir( + &path::absolute(source_dir).context("resolving source dir failed")?, + )?; get_shell_in_build(&config, &package)?; } Commands::Test {} => { diff --git a/packages/debmagic/src/package.rs b/packages/debmagic/src/package.rs index 44a51a4..d3515aa 100644 --- a/packages/debmagic/src/package.rs +++ b/packages/debmagic/src/package.rs @@ -1,17 +1,18 @@ -use std::{path::PathBuf, str::FromStr}; +use std::path::{Path, PathBuf}; use anyhow::anyhow; -use debian_changelog::ChangeLog; use debmagic_common::debian::version::PackageVersion; +#[derive(Debug, Clone)] pub struct PackageDescription { pub name: String, pub version: PackageVersion, + pub source_dir: PathBuf, } impl PackageDescription { - pub fn from_dir(dir: &PathBuf) -> anyhow::Result { + pub fn from_dir(dir: &Path) -> anyhow::Result { let changelog_file = dir.join("debian").join("changelog"); let changelog_contents = std::fs::read_to_string(changelog_file)?; let changelog: debian_changelog::ChangeLog = changelog_contents.parse()?; @@ -24,12 +25,15 @@ impl PackageDescription { let name = first_entry .package() .ok_or(anyhow!("empty package name in changelog entry"))?; - let version: PackageVersion = first_entry + let version = first_entry .version() .ok_or(anyhow!("no package version in changelog entry")) - .map(|v| PackageVersion::from_str(v)) - .map_err(|| anyhow!("invalid package version in changelog entry"))?; + .map(|v| PackageVersion::new(v.epoch, v.upstream_version, v.debian_revision))?; - Self { name, version } + Ok(Self { + name, + version, + source_dir: dir.to_path_buf(), + }) } } From 6dd4e7102861341cef7e11b640b3eab7be03828e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Tue, 13 Jan 2026 21:59:27 +0100 Subject: [PATCH 04/21] chore: add clippy to pre-commit hooks --- .pre-commit-config.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4d980c5..b0b5a3e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,4 +11,9 @@ repos: name: ty check entry: uv run ty check . pass_filenames: false - language: python \ No newline at end of file + language: python + - id: clippy + name: clippy check + entry: cargo clippy --workspace --all-targets --all-features -- -D warnings + pass_filenames: false + language: rust \ No newline at end of file From d7c451585657c35fca63ac490a93391d7db5d66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Wed, 14 Jan 2026 19:19:45 +0100 Subject: [PATCH 05/21] fix(cli): don't nest shells in bare driver mode --- packages/debmagic/src/build/driver_bare.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/debmagic/src/build/driver_bare.rs b/packages/debmagic/src/build/driver_bare.rs index d8e5014..93ed175 100644 --- a/packages/debmagic/src/build/driver_bare.rs +++ b/packages/debmagic/src/build/driver_bare.rs @@ -1,4 +1,4 @@ -use std::{env, path::Path, process::Command}; +use std::{path::Path, process::Command}; use serde::{Deserialize, Serialize}; @@ -76,11 +76,10 @@ impl BuildDriver for DriverBare { } fn interactive_shell(&self) -> std::io::Result<()> { - let mut shell = Command::new("/usr/bin/env"); - let shell_type = env::var("SHELL").unwrap_or("bash".to_string()); - shell.arg(shell_type); - - let _ = shell.status()?; + println!( + "source directory of current package build in {}", + self.config.build_source_dir().display() + ); Ok(()) } From ad295aca66dbc5ee59428ee359481d8bc723188f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Wed, 14 Jan 2026 20:30:31 +0100 Subject: [PATCH 06/21] feat(cli): allow persisting build drivers - noop for the bare driver - will reuse the docker container instead of creating a new one -> enables reusing all kinds of caches, installed build deps etc while still having a fresh worktree and build --- Cargo.lock | 93 +++++++ Cargo.toml | 1 + debian/control | 3 +- packages/debmagic/Cargo.toml | 1 + packages/debmagic/src/build.rs | 60 +++-- packages/debmagic/src/build/common.rs | 1 - packages/debmagic/src/build/config.rs | 4 +- packages/debmagic/src/build/driver_bare.rs | 19 +- packages/debmagic/src/build/driver_docker.rs | 250 ++++++++++++------- packages/debmagic/src/cli.rs | 3 + packages/debmagic/src/config.rs | 19 +- packages/debmagic/src/main.rs | 13 +- packages/debmagic/tests/assets/config1.toml | 3 +- 13 files changed, 327 insertions(+), 143 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5be7b0c..e412c3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,6 +117,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -268,6 +278,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crunchy" version = "0.2.4" @@ -310,6 +345,7 @@ dependencies = [ "debmagic-common", "dirs", "glob", + "ignore", "libc", "serde", "serde_json", @@ -447,6 +483,19 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -501,6 +550,22 @@ dependencies = [ "cc", ] +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -812,6 +877,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.228" @@ -1091,6 +1165,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1167,6 +1251,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + [[package]] name = "windows-core" version = "0.62.2" diff --git a/Cargo.toml b/Cargo.toml index 1dc2b15..0a10d23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ uuid = { version = ">=1.10.0", features = ["v4"] } chrono = { version = ">=0.4.42" } regex = { version = ">=1.12.2" } debian-changelog = { version = ">=0.2.14" } +ignore = { version = ">=0.4.25" } # dev dependencies test-case = { version = ">=3.3.1" } diff --git a/debian/control b/debian/control index 496cc9d..827b876 100644 --- a/debian/control +++ b/debian/control @@ -25,7 +25,8 @@ Build-Depends: librust-regex-dev (>=1.12.2), librust-test-case-dev (>=3.3.1), librust-pyo3-dev (>=0.27.2), - librust-debian-changelog-dev (>=0.2.14) + librust-debian-changelog-dev (>=0.2.14), + librust-ignore-dev (>=0.4.25) Rules-Requires-Root: no X-Style: black Standards-Version: 4.7.2 diff --git a/packages/debmagic/Cargo.toml b/packages/debmagic/Cargo.toml index 98fe8e8..65e61b0 100644 --- a/packages/debmagic/Cargo.toml +++ b/packages/debmagic/Cargo.toml @@ -19,3 +19,4 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } uuid = { workspace = true, features = ["v4"] } debian-changelog = { workspace = true } +ignore = { workspace = true } diff --git a/packages/debmagic/src/build.rs b/packages/debmagic/src/build.rs index 11d8c3b..94e0eb4 100644 --- a/packages/debmagic/src/build.rs +++ b/packages/debmagic/src/build.rs @@ -35,11 +35,8 @@ fn get_build_driver( driver_config: &DriverConfig, ) -> anyhow::Result> { match config.driver { - BuildDriverType::Docker => Ok(Box::new(DriverDocker::create( - config, - &driver_config.docker, - )?)), - BuildDriverType::Bare => Ok(Box::new(DriverBare::create(config, &driver_config.bare))), + BuildDriverType::Docker => Ok(Box::new(DriverDocker::create(config, driver_config)?)), + BuildDriverType::Bare => Ok(Box::new(DriverBare::create(config, driver_config))), // BuildDriverType::Lxd => ... } } @@ -51,12 +48,12 @@ fn create_driver_from_metadata( let driver: anyhow::Result> = match &metadata.config.driver { BuildDriverType::Docker => Ok(Box::new(DriverDocker::from_build_metadata( &metadata.config, - &config.docker, + config, metadata, ))), BuildDriverType::Bare => Ok(Box::new(DriverBare::from_build_metadata( &metadata.config, - &config.bare, + config, metadata, ))), // BuildDriverType::Lxd => ... @@ -66,7 +63,8 @@ fn create_driver_from_metadata( impl Build { pub fn create(config: &BuildConfig, driver_config: &DriverConfig) -> anyhow::Result { - let driver = get_build_driver(config, driver_config)?; + let driver = get_build_driver(config, driver_config) + .context(format!("failed to create {:?} build driver", config.driver))?; Ok(Self { config: config.clone(), driver, @@ -248,18 +246,33 @@ fn copy_glob(src_dir: &Path, pattern: &str, dest_dir: &Path) -> anyhow::Result<( Ok(()) } -fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { - // TODO: properly handle gitignore / other ignore files when copying - // Use ignore crate - // Simple copy logic (for advanced gitignore support, look at the `ignore` crate) +fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> anyhow::Result<()> { fs::create_dir_all(&dst)?; - for entry in fs::read_dir(src)? { + + let walker = ignore::WalkBuilder::new(&src) + .standard_filters(true) + .hidden(false) + .filter_entry(|entry| !(entry.path().is_dir() && entry.path().ends_with(".git"))) + .build(); + + for entry in walker { let entry = entry?; - let file_type = entry.file_type()?; + let file_type = entry.file_type().ok_or(anyhow!( + "failed to get file type of {}", + entry.path().display() + ))?; + + // get path of entry relative to src + let relative_path = entry + .path() + .strip_prefix(src.as_ref()) + .context("failed to get relative path")?; + if file_type.is_dir() { - copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; + fs::create_dir_all(dst.as_ref().join(relative_path))?; } else if file_type.is_file() { - fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; + fs::copy(entry.path(), dst.as_ref().join(relative_path)) + .context(format!("failed to copy file: {}", entry.path().display()))?; } // handle hardlinks, symlinks and similar weird filetypes } @@ -294,13 +307,15 @@ fn prepare_build_env( build_root_dir: build_root, distro: "debian".to_string(), distro_version: "forky".to_string(), - dry_run: config.dry_run, sign_package: false, }; - build_config.create_dirs()?; + build_config + .create_dirs() + .context("failed to create build directories")?; - copy_dir_all(&build_config.source_dir, build_config.build_source_dir())?; + copy_dir_all(&build_config.source_dir, build_config.build_source_dir()) + .context("failed to copy source tree to build directory")?; let build = Build::create(&build_config, &config.driver)?; Ok(build) @@ -324,8 +339,11 @@ pub fn build_package( driver_type: BuildDriverType, output_dir: &Path, ) -> anyhow::Result<()> { - let build = prepare_build_env(config, package, driver_type, output_dir)?; - build.write_metadata()?; + let build = prepare_build_env(config, package, driver_type, output_dir) + .context("failed to prepare build environment")?; + build + .write_metadata() + .context("failed to write build metadata")?; let result = (|| -> anyhow::Result<()> { build.driver.run_command( diff --git a/packages/debmagic/src/build/common.rs b/packages/debmagic/src/build/common.rs index 4a0d296..2fc0393 100644 --- a/packages/debmagic/src/build/common.rs +++ b/packages/debmagic/src/build/common.rs @@ -34,7 +34,6 @@ pub struct BuildConfig { pub build_root_dir: PathBuf, pub source_dir: PathBuf, pub output_dir: PathBuf, - pub dry_run: bool, pub distro_version: String, pub distro: String, pub sign_package: bool, diff --git a/packages/debmagic/src/build/config.rs b/packages/debmagic/src/build/config.rs index 7e874af..46392b0 100644 --- a/packages/debmagic/src/build/config.rs +++ b/packages/debmagic/src/build/config.rs @@ -3,8 +3,10 @@ use serde::Deserialize; use crate::build::driver_bare::DriverBareConfig; use crate::build::driver_docker::DriverDockerConfig; -#[derive(Deserialize, Debug, Default, Clone)] +#[derive(Deserialize, Debug, Clone, Default)] +#[serde(default)] pub struct DriverConfig { + pub persistent: bool, pub docker: DriverDockerConfig, pub bare: DriverBareConfig, } diff --git a/packages/debmagic/src/build/driver_bare.rs b/packages/debmagic/src/build/driver_bare.rs index 93ed175..7ddfdb7 100644 --- a/packages/debmagic/src/build/driver_bare.rs +++ b/packages/debmagic/src/build/driver_bare.rs @@ -2,20 +2,24 @@ use std::{path::Path, process::Command}; use serde::{Deserialize, Serialize}; -use crate::build::common::{ - BuildConfig, BuildDriver, BuildDriverType, BuildMetadata, DriverSpecificBuildMetadata, +use crate::build::{ + common::{ + BuildConfig, BuildDriver, BuildDriverType, BuildMetadata, DriverSpecificBuildMetadata, + }, + config::DriverConfig, }; #[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default)] pub struct DriverBareConfig {} pub struct DriverBare { config: BuildConfig, - _driver_config: DriverBareConfig, + _driver_config: DriverConfig, } impl DriverBare { - pub fn create(config: &BuildConfig, driver_config: &DriverBareConfig) -> Self { + pub fn create(config: &BuildConfig, driver_config: &DriverConfig) -> Self { Self { config: config.clone(), _driver_config: driver_config.clone(), @@ -24,7 +28,7 @@ impl DriverBare { pub fn from_build_metadata( config: &BuildConfig, - driver_config: &DriverBareConfig, + driver_config: &DriverConfig, _build_metadata: &BuildMetadata, ) -> Self { Self { @@ -49,11 +53,6 @@ impl BuildDriver for DriverBare { full_cmd.extend(cmd.iter().map(|s| s.to_string())); - if self.config.dry_run { - println!("[dry-run] Would run: {full_cmd:?}"); - return Ok(()); - } - let mut command = Command::new(&full_cmd[0]); command.args(&full_cmd[1..]); diff --git a/packages/debmagic/src/build/driver_docker.rs b/packages/debmagic/src/build/driver_docker.rs index 8e14ff1..73ca610 100644 --- a/packages/debmagic/src/build/driver_docker.rs +++ b/packages/debmagic/src/build/driver_docker.rs @@ -6,13 +6,16 @@ use std::{ use anyhow::anyhow; use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use crate::build::common::{ - BuildConfig, BuildDriver, BuildDriverType, BuildMetadata, DriverSpecificBuildMetadata, +use crate::build::{ + common::{ + BuildConfig, BuildDriver, BuildDriverType, BuildMetadata, DriverSpecificBuildMetadata, + }, + config::DriverConfig, }; #[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default)] pub struct DriverDockerConfig { pub base_image: Option, } @@ -45,97 +48,163 @@ pub struct DockerDriverBuildMetadata { pub struct DriverDocker { config: BuildConfig, - _driver_config: DriverDockerConfig, + driver_config: DriverConfig, container_name: String, } -impl DriverDocker { - pub fn create( - config: &BuildConfig, - driver_config: &DriverDockerConfig, - ) -> anyhow::Result { - let base_image = driver_config - .base_image - .clone() - .unwrap_or_else(|| format!("docker.io/{}:{}", config.distro, config.distro_version)); - - let formatted_dockerfile = DOCKERFILE_TEMPLATE - .replace("{base_image}", &base_image) - .replace("{docker_user}", DOCKER_USER) - .replace("{build_dir}", BUILD_DIR_IN_CONTAINER) - .replace( - "{debian_control_file}", - &config - .build_source_dir() - .join("debian") - .join("control") - .to_string_lossy(), - ); - - let dockerfile_path = config.build_temp_dir().join("Dockerfile"); - fs::write(&dockerfile_path, formatted_dockerfile).expect("Failed to write Dockerfile"); - - fs::create_dir_all(config.build_temp_dir().join("debian"))?; - fs::copy( - config.build_source_dir().join("debian").join("control"), - config.build_temp_dir().join("debian").join("control"), - )?; - - let docker_image_name = format!("debmagic-{}", config.build_identifier()); - let mut build_args = Vec::new(); - - let uid = unsafe { libc::geteuid() }; - if uid != 0 { - build_args.extend(["--build-arg".to_string(), format!("USER_UID={uid}")]); - } - let gid = unsafe { libc::getegid() }; - if gid != 0 { - build_args.extend(["--build-arg".to_string(), format!("USER_GID={gid}")]); - } +fn error_from_command(cmd: &mut Command, message: &str) -> anyhow::Error { + anyhow!( + "{message}:\n{}", + cmd.output() + .map(|o| String::from_utf8_lossy(&o.stderr).to_string()) + .unwrap_or("".to_string()) + ) +} + +fn build_build_image(config: &BuildConfig, driver_config: &DriverConfig) -> anyhow::Result { + let base_image = driver_config + .docker + .base_image + .clone() + .unwrap_or_else(|| format!("docker.io/{}:{}", config.distro, config.distro_version)); + + let debian_control_file_path = config.build_source_dir().join("debian").join("control"); + + let formatted_dockerfile = DOCKERFILE_TEMPLATE + .replace("{base_image}", &base_image) + .replace("{docker_user}", DOCKER_USER) + .replace("{build_dir}", BUILD_DIR_IN_CONTAINER) + .replace( + "{debian_control_file}", + &debian_control_file_path.to_string_lossy(), + ); + + let dockerfile_path = config.build_temp_dir().join("Dockerfile"); + fs::write(&dockerfile_path, formatted_dockerfile) + .map_err(|e| anyhow!("Failed to write Dockerfile, {e}"))?; + + fs::create_dir_all(config.build_temp_dir().join("debian")) + .map_err(|e| anyhow!("Failed to create debian directory: {e}"))?; + fs::copy( + &debian_control_file_path, + config.build_temp_dir().join("debian").join("control"), + ) + .map_err(|e| anyhow!("Failed to copy debian control file: {e}"))?; + + let docker_image_name = format!("debmagic-{}", config.build_identifier()); + let mut build_args = Vec::new(); + + let uid = unsafe { libc::geteuid() }; + if uid != 0 { + build_args.extend(["--build-arg".to_string(), format!("USER_UID={uid}")]); + } + let gid = unsafe { libc::getegid() }; + if gid != 0 { + build_args.extend(["--build-arg".to_string(), format!("USER_GID={gid}")]); + } + + let mut build_cmd = Command::new("docker"); + build_cmd.args(["build"]).args(&build_args).args([ + "--tag", + &docker_image_name, + "-f", + &dockerfile_path.to_string_lossy(), + &config.build_temp_dir().to_string_lossy(), + ]); + + if !build_cmd.status().map(|s| s.success()).unwrap_or(false) { + return Err(error_from_command( + &mut build_cmd, + "Error creating docker image", + )); + } - let mut build_cmd = Command::new("docker"); - build_cmd.args(["build"]).args(&build_args).args([ - "--tag", - &docker_image_name, - "-f", - &dockerfile_path.to_string_lossy(), - &config.build_temp_dir().to_string_lossy(), - ]); - - if !config.dry_run && !build_cmd.status().map(|s| s.success()).unwrap_or(false) { - return Err(anyhow!("Error creating docker image")); + Ok(docker_image_name) +} + +fn does_container_exist(container_name: &str) -> anyhow::Result { + let mut ps_cmd = Command::new("docker"); + ps_cmd.args(["ps", "--all", "--format", "json"]); + ps_cmd.stdout(std::process::Stdio::piped()); + if !ps_cmd.status().map(|s| s.success()).unwrap_or(false) { + return Err(anyhow!("failed to query running docker containers")); + } + + let output = ps_cmd + .output() + .map_err(|_| anyhow!("Failed to read docker ps output"))?; + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + if let Ok(container) = serde_json::from_str::(line) + && let Some(names) = container.get("Names") + && names == container_name + { + return Ok(true); } + } + Ok(false) +} + +impl DriverDocker { + pub fn create(config: &BuildConfig, driver_config: &DriverConfig) -> anyhow::Result { + let container_name = format!("debmagic-{}", config.build_identifier()); + let container_exists = does_container_exist(&container_name)?; + + if driver_config.persistent && container_exists { + let mut start_cmd = Command::new("docker"); + start_cmd.args(["start", &container_name]); + if !start_cmd.status().map(|s| s.success()).unwrap_or(false) { + return Err(error_from_command( + &mut start_cmd, + "Error starting docker container", + )); + } + } else { + if container_exists { + let mut rm_cmd = Command::new("docker"); + rm_cmd.args(["rm", "-f", &container_name]); + if !rm_cmd.status().map(|s| s.success()).unwrap_or(false) { + return Err(error_from_command( + &mut rm_cmd, + "Error removing existing docker container", + )); + } + } - let container_uuid = Uuid::new_v4().to_string(); - let mut run_cmd = Command::new("docker"); - run_cmd.args([ - "run", - "--detach", - "--name", - &container_uuid, - "--mount", - &format!( - "type=bind,src={},dst={}", - config.build_root_dir.display(), - BUILD_DIR_IN_CONTAINER - ), - &docker_image_name, - ]); - - if !config.dry_run && !run_cmd.status().map(|s| s.success()).unwrap_or(false) { - return Err(anyhow!("Error starting docker container")); + let docker_image_name = build_build_image(config, driver_config)?; + let mut run_cmd = Command::new("docker"); + run_cmd.args([ + "run", + "--detach", + "--name", + &container_name, + "--mount", + &format!( + "type=bind,src={},dst={}", + config.build_root_dir.display(), + BUILD_DIR_IN_CONTAINER + ), + &docker_image_name, + ]); + + if !run_cmd.status().map(|s| s.success()).unwrap_or(false) { + return Err(error_from_command( + &mut run_cmd, + "Error starting docker container", + )); + } } Ok(Self { config: config.clone(), - _driver_config: driver_config.clone(), - container_name: container_uuid, + driver_config: driver_config.clone(), + container_name, }) } pub fn from_build_metadata( config: &BuildConfig, - driver_config: &DriverDockerConfig, + driver_config: &DriverConfig, build_metadata: &BuildMetadata, ) -> Self { let container_name = build_metadata @@ -146,7 +215,7 @@ impl DriverDocker { Self { config: config.clone(), - _driver_config: driver_config.clone(), + driver_config: driver_config.clone(), container_name, } } @@ -190,11 +259,6 @@ impl BuildDriver for DriverDocker { exec_args.push(self.container_name.clone()); exec_args.extend(cmd.iter().map(|s| s.to_string())); - if self.config.dry_run { - println!("[dry-run] docker {exec_args:?}"); - return Ok(()); - } - let status = Command::new("docker").args(exec_args).status()?; if !status.success() { return Err(std::io::Error::other("Docker exec failed")); @@ -203,16 +267,18 @@ impl BuildDriver for DriverDocker { } fn cleanup(&self) { - let _ = Command::new("docker") - .args(["rm", "-f", &self.container_name]) - .status(); + if self.driver_config.persistent { + let _ = Command::new("docker") + .args(["stop", &self.container_name]) + .status(); + } else { + let _ = Command::new("docker") + .args(["rm", "-f", &self.container_name]) + .status(); + } } fn interactive_shell(&self) -> std::io::Result<()> { - if self.config.dry_run { - return Ok(()); - } - let workdir = self.translate_path_in_container(&self.config.build_root_dir)?; let _ = Command::new("docker") .args([ diff --git a/packages/debmagic/src/cli.rs b/packages/debmagic/src/cli.rs index 7185c95..79af0a6 100644 --- a/packages/debmagic/src/cli.rs +++ b/packages/debmagic/src/cli.rs @@ -30,6 +30,9 @@ pub struct BuildSubcommandArgs { #[arg(long)] pub driver_docker_build_image: Option, + #[arg(long)] + pub driver_persistent: Option, + #[arg(short, long)] pub source_dir: Option, #[arg(short, long)] diff --git a/packages/debmagic/src/config.rs b/packages/debmagic/src/config.rs index 2fd6728..de779a8 100644 --- a/packages/debmagic/src/config.rs +++ b/packages/debmagic/src/config.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::{build::config::DriverConfig, cli::Cli}; +use crate::build::config::DriverConfig; use anyhow::{Context, anyhow}; use config::{Config as ConfigBuilder, File}; use serde::Deserialize; @@ -10,7 +10,6 @@ use serde::Deserialize; pub struct Config { pub driver: DriverConfig, pub temp_build_dir: PathBuf, - pub dry_run: bool, } impl Default for Config { @@ -18,13 +17,12 @@ impl Default for Config { Self { driver: DriverConfig::default(), temp_build_dir: PathBuf::from("/tmp/debmagic"), - dry_run: false, } } } impl Config { - pub fn new(config_files: &Vec, _cli_args: &Cli) -> anyhow::Result { + pub fn new(config_files: &Vec) -> anyhow::Result { let mut builder = ConfigBuilder::builder(); for file in config_files { @@ -40,14 +38,13 @@ impl Config { let config: anyhow::Result = build .try_deserialize() .map_err(|e| anyhow!("Failed to read config: {e}")); + config } } #[cfg(test)] mod tests { - use crate::cli::Commands; - use super::*; #[test] @@ -55,14 +52,8 @@ mod tests { let test_asset_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("assets"); - let cfg = Config::new( - &vec![test_asset_dir.join("config1.toml")], - &Cli { - config: None, - command: Commands::Version {}, - }, - )?; - assert!(cfg.dry_run); + let cfg = Config::new(&vec![test_asset_dir.join("config1.toml")])?; + assert!(cfg.driver.persistent); Ok(()) } diff --git a/packages/debmagic/src/main.rs b/packages/debmagic/src/main.rs index bb7e4ab..c78471f 100644 --- a/packages/debmagic/src/main.rs +++ b/packages/debmagic/src/main.rs @@ -36,7 +36,7 @@ fn get_config(cli: &Cli, source_dir: &Option) -> anyhow::Result config_file_paths.push(config_file_override.clone()); } - let config = Config::new(&config_file_paths, cli)?; + let config = Config::new(&config_file_paths)?; Ok(config) } @@ -47,7 +47,16 @@ fn main() -> anyhow::Result<()> { match &cli.command { Commands::Build(args) => { let source_dir = args.source_dir.as_deref().unwrap_or(¤t_dir); - let config = get_config(&cli, &Some(source_dir.to_path_buf()))?; + let mut config = get_config(&cli, &Some(source_dir.to_path_buf()))?; + + // TODO: figure out a better way to override config from CLI args - maybe more generic, if that is even possible since + // we want a nice cli which somewhat matches the config structure + // but some config options only make sense in some cli subcommands -> these flags don't make sense in all commands + // and should only be used in some + if let Some(driver_persistent) = args.driver_persistent { + config.driver.persistent = driver_persistent; + } + let package = PackageDescription::from_dir( &path::absolute(source_dir).context("resolving source dir failed")?, )?; diff --git a/packages/debmagic/tests/assets/config1.toml b/packages/debmagic/tests/assets/config1.toml index 3978981..41c650b 100644 --- a/packages/debmagic/tests/assets/config1.toml +++ b/packages/debmagic/tests/assets/config1.toml @@ -1 +1,2 @@ -dry_run = true +[driver] +persistent = true From 72ad8399bad6b13139754e7bc8e8b6e28e31e36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Thu, 12 Feb 2026 21:06:41 +0100 Subject: [PATCH 07/21] refactor(cli): rename build subcommand to pack --- .github/workflows/ci.yml | 4 ++-- packages/debmagic/src/cli.rs | 6 +++--- packages/debmagic/src/config.rs | 2 +- packages/debmagic/src/main.rs | 6 +++--- packages/debmagic/src/{build.rs => pack.rs} | 4 ++-- packages/debmagic/src/{build => pack}/common.rs | 0 packages/debmagic/src/{build => pack}/config.rs | 4 ++-- packages/debmagic/src/{build => pack}/driver_bare.rs | 2 +- packages/debmagic/src/{build => pack}/driver_docker.rs | 2 +- tests/integration/test_packages.py | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) rename packages/debmagic/src/{build.rs => pack.rs} (99%) rename packages/debmagic/src/{build => pack}/common.rs (100%) rename packages/debmagic/src/{build => pack}/config.rs (67%) rename packages/debmagic/src/{build => pack}/driver_bare.rs (99%) rename packages/debmagic/src/{build => pack}/driver_docker.rs (99%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 103c7b4..e3ddd21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,8 +157,8 @@ jobs: run: uv sync --locked --all-extras --dev - name: Install debmagic (cli) run: uv pip install packages/debmagic - - name: Run Debmagic build on ourself - run: uv run debmagic build --driver=docker + - name: Run Debmagic pack on ourself + run: uv run debmagic pack --driver=docker # TODO: integration tests currently don't work in the CI since they require running apt source on debian trixie -> CI runs on ubuntu # integration-tests: diff --git a/packages/debmagic/src/cli.rs b/packages/debmagic/src/cli.rs index 79af0a6..2492ea1 100644 --- a/packages/debmagic/src/cli.rs +++ b/packages/debmagic/src/cli.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::build::common::BuildDriverType; +use crate::pack::common::BuildDriverType; use clap::{Args, Parser, Subcommand}; #[derive(Parser, Debug)] @@ -15,7 +15,7 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Commands { - Build(BuildSubcommandArgs), + Pack(PackSubcommandArgs), Shell(ShellSubcommandArgs), Test {}, Check {}, @@ -23,7 +23,7 @@ pub enum Commands { } #[derive(Args, Debug)] -pub struct BuildSubcommandArgs { +pub struct PackSubcommandArgs { #[arg(short, long)] pub driver: BuildDriverType, diff --git a/packages/debmagic/src/config.rs b/packages/debmagic/src/config.rs index de779a8..68365a7 100644 --- a/packages/debmagic/src/config.rs +++ b/packages/debmagic/src/config.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::build::config::DriverConfig; +use crate::pack::config::DriverConfig; use anyhow::{Context, anyhow}; use config::{Config as ConfigBuilder, File}; use serde::Deserialize; diff --git a/packages/debmagic/src/main.rs b/packages/debmagic/src/main.rs index c78471f..0928b6d 100644 --- a/packages/debmagic/src/main.rs +++ b/packages/debmagic/src/main.rs @@ -7,15 +7,15 @@ use anyhow::Context; use clap::{CommandFactory, Parser}; use crate::{ - build::{build_package, get_shell_in_build}, cli::{Cli, Commands}, config::Config, + pack::{build_package, get_shell_in_build}, package::PackageDescription, }; -pub mod build; pub mod cli; pub mod config; +pub mod pack; pub mod package; fn get_config(cli: &Cli, source_dir: &Option) -> anyhow::Result { @@ -45,7 +45,7 @@ fn main() -> anyhow::Result<()> { let current_dir = env::current_dir()?; match &cli.command { - Commands::Build(args) => { + Commands::Pack(args) => { let source_dir = args.source_dir.as_deref().unwrap_or(¤t_dir); let mut config = get_config(&cli, &Some(source_dir.to_path_buf()))?; diff --git a/packages/debmagic/src/build.rs b/packages/debmagic/src/pack.rs similarity index 99% rename from packages/debmagic/src/build.rs rename to packages/debmagic/src/pack.rs index 94e0eb4..60163af 100644 --- a/packages/debmagic/src/build.rs +++ b/packages/debmagic/src/pack.rs @@ -8,13 +8,13 @@ use std::{ }; use crate::{ - build::{ + config::Config, + pack::{ common::{BuildConfig, BuildDriver, BuildDriverType, BuildMetadata}, config::DriverConfig, driver_bare::DriverBare, driver_docker::DriverDocker, }, - config::Config, package::PackageDescription, }; use anyhow::{Context, anyhow}; diff --git a/packages/debmagic/src/build/common.rs b/packages/debmagic/src/pack/common.rs similarity index 100% rename from packages/debmagic/src/build/common.rs rename to packages/debmagic/src/pack/common.rs diff --git a/packages/debmagic/src/build/config.rs b/packages/debmagic/src/pack/config.rs similarity index 67% rename from packages/debmagic/src/build/config.rs rename to packages/debmagic/src/pack/config.rs index 46392b0..b2b0154 100644 --- a/packages/debmagic/src/build/config.rs +++ b/packages/debmagic/src/pack/config.rs @@ -1,7 +1,7 @@ use serde::Deserialize; -use crate::build::driver_bare::DriverBareConfig; -use crate::build::driver_docker::DriverDockerConfig; +use crate::pack::driver_bare::DriverBareConfig; +use crate::pack::driver_docker::DriverDockerConfig; #[derive(Deserialize, Debug, Clone, Default)] #[serde(default)] diff --git a/packages/debmagic/src/build/driver_bare.rs b/packages/debmagic/src/pack/driver_bare.rs similarity index 99% rename from packages/debmagic/src/build/driver_bare.rs rename to packages/debmagic/src/pack/driver_bare.rs index 7ddfdb7..afa60fe 100644 --- a/packages/debmagic/src/build/driver_bare.rs +++ b/packages/debmagic/src/pack/driver_bare.rs @@ -2,7 +2,7 @@ use std::{path::Path, process::Command}; use serde::{Deserialize, Serialize}; -use crate::build::{ +use crate::pack::{ common::{ BuildConfig, BuildDriver, BuildDriverType, BuildMetadata, DriverSpecificBuildMetadata, }, diff --git a/packages/debmagic/src/build/driver_docker.rs b/packages/debmagic/src/pack/driver_docker.rs similarity index 99% rename from packages/debmagic/src/build/driver_docker.rs rename to packages/debmagic/src/pack/driver_docker.rs index 73ca610..a9f6315 100644 --- a/packages/debmagic/src/build/driver_docker.rs +++ b/packages/debmagic/src/pack/driver_docker.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::anyhow; use serde::{Deserialize, Serialize}; -use crate::build::{ +use crate::pack::{ common::{ BuildConfig, BuildDriver, BuildDriverType, BuildMetadata, DriverSpecificBuildMetadata, }, diff --git a/tests/integration/test_packages.py b/tests/integration/test_packages.py index 9e2a793..ad6d3aa 100644 --- a/tests/integration/test_packages.py +++ b/tests/integration/test_packages.py @@ -93,7 +93,7 @@ def test_build_package(test_env: Environment, package: str, version: str): "uv", "run", "debmagic", - "build", + "pack", "--driver", "docker", "--driver-config.docker.base-image", From 17e9f89be5af0bcfa18aaea320ec4b0acf6dfcd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Thu, 12 Feb 2026 21:11:59 +0100 Subject: [PATCH 08/21] feat(cli): start shells in build source dir --- packages/debmagic/src/pack.rs | 8 ++++++-- packages/debmagic/src/pack/common.rs | 2 +- packages/debmagic/src/pack/driver_bare.rs | 2 +- packages/debmagic/src/pack/driver_docker.rs | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/debmagic/src/pack.rs b/packages/debmagic/src/pack.rs index 60163af..795fbb1 100644 --- a/packages/debmagic/src/pack.rs +++ b/packages/debmagic/src/pack.rs @@ -324,7 +324,9 @@ fn prepare_build_env( pub fn get_shell_in_build(config: &Config, package: &PackageDescription) -> anyhow::Result<()> { let (_package_identifier, build_root) = get_build_root_and_identifier(config, package); let build = Build::from_build_root(&build_root, &config.driver)?; - let result = build.driver.interactive_shell(); + let result = build + .driver + .interactive_shell(&build.config.build_source_dir()); // TODO: detach - decrement num_attached_processes build.detach()?; @@ -376,7 +378,9 @@ pub fn build_package( if let Err(e) = result { if stdout().is_terminal() { eprintln!("Build failed: {e}. Dropping into shell..."); - let res = build.driver.interactive_shell(); + let res = build + .driver + .interactive_shell(&build.config.build_source_dir()); if let Err(shell_error) = res { eprintln!("Dropping into shell failed: {shell_error}"); } diff --git a/packages/debmagic/src/pack/common.rs b/packages/debmagic/src/pack/common.rs index 2fc0393..2b63a4d 100644 --- a/packages/debmagic/src/pack/common.rs +++ b/packages/debmagic/src/pack/common.rs @@ -74,7 +74,7 @@ pub trait BuildDriver { fn cleanup(&self); - fn interactive_shell(&self) -> std::io::Result<()>; + fn interactive_shell(&self, cwd: &Path) -> std::io::Result<()>; fn driver_type(&self) -> BuildDriverType; } diff --git a/packages/debmagic/src/pack/driver_bare.rs b/packages/debmagic/src/pack/driver_bare.rs index afa60fe..6b0fced 100644 --- a/packages/debmagic/src/pack/driver_bare.rs +++ b/packages/debmagic/src/pack/driver_bare.rs @@ -74,7 +74,7 @@ impl BuildDriver for DriverBare { // No-op for bare driver } - fn interactive_shell(&self) -> std::io::Result<()> { + fn interactive_shell(&self, _cwd: &Path) -> std::io::Result<()> { println!( "source directory of current package build in {}", self.config.build_source_dir().display() diff --git a/packages/debmagic/src/pack/driver_docker.rs b/packages/debmagic/src/pack/driver_docker.rs index a9f6315..44cc329 100644 --- a/packages/debmagic/src/pack/driver_docker.rs +++ b/packages/debmagic/src/pack/driver_docker.rs @@ -278,8 +278,8 @@ impl BuildDriver for DriverDocker { } } - fn interactive_shell(&self) -> std::io::Result<()> { - let workdir = self.translate_path_in_container(&self.config.build_root_dir)?; + fn interactive_shell(&self, cwd: &Path) -> std::io::Result<()> { + let workdir = self.translate_path_in_container(cwd)?; let _ = Command::new("docker") .args([ "exec", From 7c1d652e9384963c38043f6565b9a9a62dce08fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Thu, 12 Feb 2026 21:16:40 +0100 Subject: [PATCH 09/21] test(cli): correct debmagic installation during integration test --- tests/integration/test_packages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_packages.py b/tests/integration/test_packages.py index ad6d3aa..2bd53ed 100644 --- a/tests/integration/test_packages.py +++ b/tests/integration/test_packages.py @@ -12,7 +12,8 @@ FROM docker.io/{distro}:{distro_version} RUN apt-get update && apt-get -y install dpkg-dev python3 python3-pip python3-pydantic -RUN --mount=from=dist,target=/tmp/dist python3 -m pip install --break-system-packages /tmp/dist/debmagic_pkg*.whl +RUN --mount=from=dist,target=/tmp/dist python3 -m pip install --root-user-action=ignore \ + --break-system-packages /tmp/dist/debmagic_pkg*.whl """ @@ -34,7 +35,6 @@ def fetch_sources(package_name: str, version: str) -> Path: def _prepare_docker_image(test_tmp_dir: Path, distro: str, distro_version: str): debmagic_repo_root_dir = Path(__file__).parent.parent.parent - run_cmd(["uv", "build", "--package", "debmagic-common"], check=True) run_cmd(["uv", "build", "--package", "debmagic-pkg"], check=True) formatted_dockerfile = DOCKERFILE_TEMPLATE.format( From 8e0802968bde14fa8eaf1b4b2b5579b02c27d423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Thu, 12 Feb 2026 22:17:12 +0100 Subject: [PATCH 10/21] refactor(cli): improve driver subargs handling --- packages/debmagic/src/cli.rs | 48 +++++++++++++++++++++++------- packages/debmagic/src/config.rs | 2 ++ packages/debmagic/src/main.rs | 32 +++++++++++++++----- tests/integration/test_packages.py | 6 ++-- 4 files changed, 68 insertions(+), 20 deletions(-) diff --git a/packages/debmagic/src/cli.rs b/packages/debmagic/src/cli.rs index 2492ea1..fa560ec 100644 --- a/packages/debmagic/src/cli.rs +++ b/packages/debmagic/src/cli.rs @@ -17,30 +17,58 @@ pub struct Cli { pub enum Commands { Pack(PackSubcommandArgs), Shell(ShellSubcommandArgs), - Test {}, - Check {}, + Test(TestSubcommandArgs), + Check(CheckSubcommandArgs), Version {}, } +#[derive(Args, Debug)] +pub struct CommonCli { + #[arg(short, long)] + pub source_dir: Option, +} + +#[derive(Args, Debug)] +pub struct DockerArgs { + #[arg(long("driver-docker-base-image"))] + pub base_image: Option, +} + #[derive(Args, Debug)] pub struct PackSubcommandArgs { #[arg(short, long)] pub driver: BuildDriverType, - #[arg(long)] - pub driver_docker_build_image: Option, + #[arg(long, action = clap::ArgAction::SetTrue)] + pub persist_driver: Option, - #[arg(long)] - pub driver_persistent: Option, + #[command(flatten)] + pub docker: DockerArgs, + + #[arg(short, long, action = clap::ArgAction::SetTrue)] + pub incremental: Option, + + #[command(flatten)] + pub common: CommonCli, - #[arg(short, long)] - pub source_dir: Option, #[arg(short, long)] pub output_dir: Option, } #[derive(Args, Debug)] pub struct ShellSubcommandArgs { - #[arg(short, long)] - pub source_dir: Option, + #[command(flatten)] + pub common: CommonCli, +} + +#[derive(Args, Debug)] +pub struct TestSubcommandArgs { + #[command(flatten)] + pub common: CommonCli, +} + +#[derive(Args, Debug)] +pub struct CheckSubcommandArgs { + #[command(flatten)] + pub common: CommonCli, } diff --git a/packages/debmagic/src/config.rs b/packages/debmagic/src/config.rs index 68365a7..c527547 100644 --- a/packages/debmagic/src/config.rs +++ b/packages/debmagic/src/config.rs @@ -10,6 +10,7 @@ use serde::Deserialize; pub struct Config { pub driver: DriverConfig, pub temp_build_dir: PathBuf, + pub incremental: bool, } impl Default for Config { @@ -17,6 +18,7 @@ impl Default for Config { Self { driver: DriverConfig::default(), temp_build_dir: PathBuf::from("/tmp/debmagic"), + incremental: false, } } } diff --git a/packages/debmagic/src/main.rs b/packages/debmagic/src/main.rs index 0928b6d..b17bcda 100644 --- a/packages/debmagic/src/main.rs +++ b/packages/debmagic/src/main.rs @@ -18,6 +18,12 @@ pub mod config; pub mod pack; pub mod package; +/// Precedence of config files is: +/// +/// 1. explicit config file passed on the command line +/// 2. `/debian/debmagic.toml` +/// 3. `/debmagic/config.toml` +/// fn get_config(cli: &Cli, source_dir: &Option) -> anyhow::Result { let mut config_file_paths = vec![]; let xdg_config_file = dirs::config_dir().map(|p| p.join("debmagic").join("config.toml")); @@ -28,8 +34,7 @@ fn get_config(cli: &Cli, source_dir: &Option) -> anyhow::Result } if let Some(source_dir) = &source_dir { - config_file_paths.push(source_dir.join(".debmagic.toml")); - config_file_paths.push(source_dir.join("debmagic.toml")); + config_file_paths.push(source_dir.join("debian").join("debmagic.toml")); } if let Some(config_file_override) = &cli.config { @@ -46,15 +51,26 @@ fn main() -> anyhow::Result<()> { let current_dir = env::current_dir()?; match &cli.command { Commands::Pack(args) => { - let source_dir = args.source_dir.as_deref().unwrap_or(¤t_dir); + let source_dir = args.common.source_dir.as_deref().unwrap_or(¤t_dir); let mut config = get_config(&cli, &Some(source_dir.to_path_buf()))?; // TODO: figure out a better way to override config from CLI args - maybe more generic, if that is even possible since // we want a nice cli which somewhat matches the config structure // but some config options only make sense in some cli subcommands -> these flags don't make sense in all commands // and should only be used in some - if let Some(driver_persistent) = args.driver_persistent { - config.driver.persistent = driver_persistent; + if let Some(persist_driver) = args.persist_driver { + config.driver.persistent = persist_driver; + } + + if let Some(incremental) = args.incremental { + config.incremental = incremental; + } + if config.incremental { + // TODO: investigate if this is actually needed + config.driver.persistent = true; + } + if let Some(docker_base_image) = &args.docker.base_image { + config.driver.docker.base_image = Some(docker_base_image.clone()); } let package = PackageDescription::from_dir( @@ -70,17 +86,17 @@ fn main() -> anyhow::Result<()> { .context("Building the package failed")?; } Commands::Shell(args) => { - let source_dir = args.source_dir.as_deref().unwrap_or(¤t_dir); + let source_dir = args.common.source_dir.as_deref().unwrap_or(¤t_dir); let config = get_config(&cli, &Some(source_dir.to_path_buf()))?; let package = PackageDescription::from_dir( &path::absolute(source_dir).context("resolving source dir failed")?, )?; get_shell_in_build(&config, &package)?; } - Commands::Test {} => { + Commands::Test(_args) => { println!("Test subcommand! - not implemented"); } - Commands::Check {} => { + Commands::Check(_args) => { println!("Check subcommand! - not implemented"); } Commands::Version {} => { diff --git a/tests/integration/test_packages.py b/tests/integration/test_packages.py index 2bd53ed..a4d2ce9 100644 --- a/tests/integration/test_packages.py +++ b/tests/integration/test_packages.py @@ -90,13 +90,15 @@ def test_build_package(test_env: Environment, package: str, version: str): with tempfile.TemporaryDirectory() as output_dir: subprocess.run( [ - "uv", + "cargo", "run", + "--package", "debmagic", + "--", "pack", "--driver", "docker", - "--driver-config.docker.base-image", + "--driver-docker-base-image", test_env.docker_image_name, "--source-dir", str(repo_dir), From 65774c8967aca0d601d3d1d06963d2b64000f81e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 15 Feb 2026 12:52:03 +0100 Subject: [PATCH 11/21] refactor: rename debmagic pack to debmagic build --- .github/workflows/ci.yml | 4 ++-- packages/debmagic/src/{pack.rs => build.rs} | 4 ++-- packages/debmagic/src/{pack => build}/common.rs | 0 packages/debmagic/src/{pack => build}/config.rs | 4 ++-- packages/debmagic/src/{pack => build}/driver_bare.rs | 2 +- packages/debmagic/src/{pack => build}/driver_docker.rs | 2 +- packages/debmagic/src/cli.rs | 6 +++--- packages/debmagic/src/config.rs | 2 +- packages/debmagic/src/main.rs | 6 +++--- tests/integration/test_packages.py | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) rename packages/debmagic/src/{pack.rs => build.rs} (99%) rename packages/debmagic/src/{pack => build}/common.rs (100%) rename packages/debmagic/src/{pack => build}/config.rs (67%) rename packages/debmagic/src/{pack => build}/driver_bare.rs (99%) rename packages/debmagic/src/{pack => build}/driver_docker.rs (99%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3ddd21..103c7b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,8 +157,8 @@ jobs: run: uv sync --locked --all-extras --dev - name: Install debmagic (cli) run: uv pip install packages/debmagic - - name: Run Debmagic pack on ourself - run: uv run debmagic pack --driver=docker + - name: Run Debmagic build on ourself + run: uv run debmagic build --driver=docker # TODO: integration tests currently don't work in the CI since they require running apt source on debian trixie -> CI runs on ubuntu # integration-tests: diff --git a/packages/debmagic/src/pack.rs b/packages/debmagic/src/build.rs similarity index 99% rename from packages/debmagic/src/pack.rs rename to packages/debmagic/src/build.rs index 795fbb1..5f5ebd7 100644 --- a/packages/debmagic/src/pack.rs +++ b/packages/debmagic/src/build.rs @@ -8,13 +8,13 @@ use std::{ }; use crate::{ - config::Config, - pack::{ + build::{ common::{BuildConfig, BuildDriver, BuildDriverType, BuildMetadata}, config::DriverConfig, driver_bare::DriverBare, driver_docker::DriverDocker, }, + config::Config, package::PackageDescription, }; use anyhow::{Context, anyhow}; diff --git a/packages/debmagic/src/pack/common.rs b/packages/debmagic/src/build/common.rs similarity index 100% rename from packages/debmagic/src/pack/common.rs rename to packages/debmagic/src/build/common.rs diff --git a/packages/debmagic/src/pack/config.rs b/packages/debmagic/src/build/config.rs similarity index 67% rename from packages/debmagic/src/pack/config.rs rename to packages/debmagic/src/build/config.rs index b2b0154..46392b0 100644 --- a/packages/debmagic/src/pack/config.rs +++ b/packages/debmagic/src/build/config.rs @@ -1,7 +1,7 @@ use serde::Deserialize; -use crate::pack::driver_bare::DriverBareConfig; -use crate::pack::driver_docker::DriverDockerConfig; +use crate::build::driver_bare::DriverBareConfig; +use crate::build::driver_docker::DriverDockerConfig; #[derive(Deserialize, Debug, Clone, Default)] #[serde(default)] diff --git a/packages/debmagic/src/pack/driver_bare.rs b/packages/debmagic/src/build/driver_bare.rs similarity index 99% rename from packages/debmagic/src/pack/driver_bare.rs rename to packages/debmagic/src/build/driver_bare.rs index 6b0fced..bfd31ec 100644 --- a/packages/debmagic/src/pack/driver_bare.rs +++ b/packages/debmagic/src/build/driver_bare.rs @@ -2,7 +2,7 @@ use std::{path::Path, process::Command}; use serde::{Deserialize, Serialize}; -use crate::pack::{ +use crate::build::{ common::{ BuildConfig, BuildDriver, BuildDriverType, BuildMetadata, DriverSpecificBuildMetadata, }, diff --git a/packages/debmagic/src/pack/driver_docker.rs b/packages/debmagic/src/build/driver_docker.rs similarity index 99% rename from packages/debmagic/src/pack/driver_docker.rs rename to packages/debmagic/src/build/driver_docker.rs index 44cc329..e934847 100644 --- a/packages/debmagic/src/pack/driver_docker.rs +++ b/packages/debmagic/src/build/driver_docker.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::anyhow; use serde::{Deserialize, Serialize}; -use crate::pack::{ +use crate::build::{ common::{ BuildConfig, BuildDriver, BuildDriverType, BuildMetadata, DriverSpecificBuildMetadata, }, diff --git a/packages/debmagic/src/cli.rs b/packages/debmagic/src/cli.rs index fa560ec..b152a15 100644 --- a/packages/debmagic/src/cli.rs +++ b/packages/debmagic/src/cli.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::pack::common::BuildDriverType; +use crate::build::common::BuildDriverType; use clap::{Args, Parser, Subcommand}; #[derive(Parser, Debug)] @@ -15,7 +15,7 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Commands { - Pack(PackSubcommandArgs), + Build(BuildSubcommandArgs), Shell(ShellSubcommandArgs), Test(TestSubcommandArgs), Check(CheckSubcommandArgs), @@ -35,7 +35,7 @@ pub struct DockerArgs { } #[derive(Args, Debug)] -pub struct PackSubcommandArgs { +pub struct BuildSubcommandArgs { #[arg(short, long)] pub driver: BuildDriverType, diff --git a/packages/debmagic/src/config.rs b/packages/debmagic/src/config.rs index c527547..1dc5352 100644 --- a/packages/debmagic/src/config.rs +++ b/packages/debmagic/src/config.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::pack::config::DriverConfig; +use crate::build::config::DriverConfig; use anyhow::{Context, anyhow}; use config::{Config as ConfigBuilder, File}; use serde::Deserialize; diff --git a/packages/debmagic/src/main.rs b/packages/debmagic/src/main.rs index b17bcda..fbbdaec 100644 --- a/packages/debmagic/src/main.rs +++ b/packages/debmagic/src/main.rs @@ -7,15 +7,15 @@ use anyhow::Context; use clap::{CommandFactory, Parser}; use crate::{ + build::{build_package, get_shell_in_build}, cli::{Cli, Commands}, config::Config, - pack::{build_package, get_shell_in_build}, package::PackageDescription, }; +pub mod build; pub mod cli; pub mod config; -pub mod pack; pub mod package; /// Precedence of config files is: @@ -50,7 +50,7 @@ fn main() -> anyhow::Result<()> { let current_dir = env::current_dir()?; match &cli.command { - Commands::Pack(args) => { + Commands::Build(args) => { let source_dir = args.common.source_dir.as_deref().unwrap_or(¤t_dir); let mut config = get_config(&cli, &Some(source_dir.to_path_buf()))?; diff --git a/tests/integration/test_packages.py b/tests/integration/test_packages.py index a4d2ce9..ea133c8 100644 --- a/tests/integration/test_packages.py +++ b/tests/integration/test_packages.py @@ -95,7 +95,7 @@ def test_build_package(test_env: Environment, package: str, version: str): "--package", "debmagic", "--", - "pack", + "build", "--driver", "docker", "--driver-docker-base-image", From 44bbaa095009db65d43559ba670a3ea318ceea66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 15 Feb 2026 13:07:28 +0100 Subject: [PATCH 12/21] refactor: avoid duplicate subprocess invocations includes other small changes - proper error handling during version parsing - avoid lossy conversions in subcommand arg passing --- .../debmagic-common/src/debian/version.rs | 9 +- packages/debmagic/src/build/driver_docker.rs | 89 +++++++++++-------- packages/debmagic/src/cli.rs | 2 +- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/packages/debmagic-common/src/debian/version.rs b/packages/debmagic-common/src/debian/version.rs index 36573f7..7ba55e2 100644 --- a/packages/debmagic-common/src/debian/version.rs +++ b/packages/debmagic-common/src/debian/version.rs @@ -40,7 +40,9 @@ impl PackageVersion { /// distro epoch plus upstream version pub fn epoch_upstream(&self) -> String { - if let Some(epoch) = self.epoch { + if let Some(epoch) = self.epoch + && epoch != 0 + { return format!("{}:{}", epoch, self.upstream); } self.upstream.clone() @@ -78,7 +80,10 @@ impl FromStr for PackageVersion { Some(0) } else { let re_epoch = Regex::new(r"^([0-9]+):.*$").map_err(|_| VersionParseError)?; - re_epoch.replace(version, "$1").to_string().parse().ok() + re_epoch.replace(version, "$1").to_string(); + let epoch_str = re_epoch.replace(version, "$1").to_string(); + let parsed_epoch = epoch_str.parse::().map_err(|_| VersionParseError)?; + Some(parsed_epoch) }; let re_upstream = Regex::new(r"^([0-9]*:)?(.*?)$").map_err(|_| VersionParseError)?; diff --git a/packages/debmagic/src/build/driver_docker.rs b/packages/debmagic/src/build/driver_docker.rs index e934847..bc64519 100644 --- a/packages/debmagic/src/build/driver_docker.rs +++ b/packages/debmagic/src/build/driver_docker.rs @@ -1,7 +1,7 @@ use std::{ - fs, + fs, io, path::{Path, PathBuf}, - process::Command, + process::{Command, Output}, }; use anyhow::anyhow; @@ -52,10 +52,11 @@ pub struct DriverDocker { container_name: String, } -fn error_from_command(cmd: &mut Command, message: &str) -> anyhow::Error { +fn error_from_command(cmd_output: &io::Result, message: &str) -> anyhow::Error { anyhow!( "{message}:\n{}", - cmd.output() + cmd_output + .as_ref() .map(|o| String::from_utf8_lossy(&o.stderr).to_string()) .unwrap_or("".to_string()) ) @@ -104,17 +105,21 @@ fn build_build_image(config: &BuildConfig, driver_config: &DriverConfig) -> anyh } let mut build_cmd = Command::new("docker"); - build_cmd.args(["build"]).args(&build_args).args([ - "--tag", - &docker_image_name, - "-f", - &dockerfile_path.to_string_lossy(), - &config.build_temp_dir().to_string_lossy(), - ]); - - if !build_cmd.status().map(|s| s.success()).unwrap_or(false) { + build_cmd + .args(["build"]) + .args(&build_args) + .args(["--tag", &docker_image_name, "-f"]) + .arg(dockerfile_path) + .arg(config.build_temp_dir()); + + let build_output = build_cmd.output(); + if !build_output + .as_ref() + .map(|o| o.status.success()) + .unwrap_or(false) + { return Err(error_from_command( - &mut build_cmd, + &build_output, "Error creating docker image", )); } @@ -153,9 +158,14 @@ impl DriverDocker { if driver_config.persistent && container_exists { let mut start_cmd = Command::new("docker"); start_cmd.args(["start", &container_name]); - if !start_cmd.status().map(|s| s.success()).unwrap_or(false) { + let start_output = start_cmd.output(); + if !start_output + .as_ref() + .map(|o| o.status.success()) + .unwrap_or(false) + { return Err(error_from_command( - &mut start_cmd, + &start_output, "Error starting docker container", )); } @@ -163,9 +173,14 @@ impl DriverDocker { if container_exists { let mut rm_cmd = Command::new("docker"); rm_cmd.args(["rm", "-f", &container_name]); - if !rm_cmd.status().map(|s| s.success()).unwrap_or(false) { + let rm_output = rm_cmd.output(); + if !rm_output + .as_ref() + .map(|o| o.status.success()) + .unwrap_or(false) + { return Err(error_from_command( - &mut rm_cmd, + &rm_output, "Error removing existing docker container", )); } @@ -187,9 +202,14 @@ impl DriverDocker { &docker_image_name, ]); - if !run_cmd.status().map(|s| s.success()).unwrap_or(false) { + let run_output = run_cmd.output(); + if !run_output + .as_ref() + .map(|o| o.status.success()) + .unwrap_or(false) + { return Err(error_from_command( - &mut run_cmd, + &run_output, "Error starting docker container", )); } @@ -243,23 +263,22 @@ impl BuildDriver for DriverDocker { } fn run_command(&self, cmd: &[&str], cwd: &Path, requires_root: bool) -> std::io::Result<()> { - let mut exec_args = vec!["exec".to_string()]; - let container_path = self .translate_path_in_container(cwd) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?; - exec_args.push("--workdir".to_string()); - exec_args.push(container_path.to_string_lossy().to_string()); + + let mut exec_cmd = Command::new("docker"); + exec_cmd.args(["exec", "--workdir"]); + exec_cmd.arg(container_path); if requires_root { - exec_args.push("--user".to_string()); - exec_args.push("root".to_string()); + exec_cmd.args(["--user", "root"]); } - exec_args.push(self.container_name.clone()); - exec_args.extend(cmd.iter().map(|s| s.to_string())); + exec_cmd.arg(&self.container_name); + exec_cmd.args(cmd); - let status = Command::new("docker").args(exec_args).status()?; + let status = exec_cmd.status()?; if !status.success() { return Err(std::io::Error::other("Docker exec failed")); } @@ -281,15 +300,9 @@ impl BuildDriver for DriverDocker { fn interactive_shell(&self, cwd: &Path) -> std::io::Result<()> { let workdir = self.translate_path_in_container(cwd)?; let _ = Command::new("docker") - .args([ - "exec", - "-it", - "--workdir", - &workdir.to_string_lossy(), - &self.container_name, - "/usr/bin/env", - "bash", - ]) + .args(["exec", "-it", "--workdir"]) + .arg(&workdir) + .args([&self.container_name, "/usr/bin/env", "bash"]) .status()?; Ok(()) diff --git a/packages/debmagic/src/cli.rs b/packages/debmagic/src/cli.rs index b152a15..31022e0 100644 --- a/packages/debmagic/src/cli.rs +++ b/packages/debmagic/src/cli.rs @@ -30,7 +30,7 @@ pub struct CommonCli { #[derive(Args, Debug)] pub struct DockerArgs { - #[arg(long("driver-docker-base-image"))] + #[arg(long = "driver-docker-base-image")] pub base_image: Option, } From a8b49e336e1ac4174110f81a563f5938d0da8985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 15 Feb 2026 19:02:07 +0100 Subject: [PATCH 13/21] test(cli): add unittest for PackageDescription parsing from debian changelog --- packages/debmagic/src/package.rs | 21 +++++++++++++++++++ .../assets/test_package/debian/changelog | 12 +++++++++++ 2 files changed, 33 insertions(+) create mode 100644 packages/debmagic/tests/assets/test_package/debian/changelog diff --git a/packages/debmagic/src/package.rs b/packages/debmagic/src/package.rs index d3515aa..6bee36d 100644 --- a/packages/debmagic/src/package.rs +++ b/packages/debmagic/src/package.rs @@ -37,3 +37,24 @@ impl PackageDescription { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_package_description_from_changelog() -> Result<(), anyhow::Error> { + let test_asset_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("assets") + .join("test_package"); + + let package = PackageDescription::from_dir(&test_asset_dir)?; + + assert_eq!(package.name, "test-package"); + assert_eq!(package.version.version(), "1.2.4-1"); + assert_eq!(package.source_dir, test_asset_dir); + + Ok(()) + } +} diff --git a/packages/debmagic/tests/assets/test_package/debian/changelog b/packages/debmagic/tests/assets/test_package/debian/changelog new file mode 100644 index 0000000..f4b1cd4 --- /dev/null +++ b/packages/debmagic/tests/assets/test_package/debian/changelog @@ -0,0 +1,12 @@ +test-package (1.2.4-1) stable; urgency=medium + + * bugfix + + -- Test Maintainer Mon, 15 Feb 2026 12:00:00 +0000 + +test-package (1.2.3-4) stable; urgency=medium + + * Initial test release + * Some changes made + + -- Test Maintainer Mon, 15 Feb 2026 10:00:00 +0000 From 37c84229d8c54e3f981c5f10b78e61e0aeab32c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 15 Feb 2026 19:18:30 +0100 Subject: [PATCH 14/21] feat(cli): automatically infer distro version from changelog --- packages/debmagic/src/build.rs | 139 +++++++++++++++++- packages/debmagic/src/cli.rs | 3 + packages/debmagic/src/main.rs | 1 + packages/debmagic/src/package.rs | 24 +++ .../debian/changelog | 5 + 5 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 packages/debmagic/tests/assets/test_package_multi_distro/debian/changelog diff --git a/packages/debmagic/src/build.rs b/packages/debmagic/src/build.rs index 5f5ebd7..bb35d54 100644 --- a/packages/debmagic/src/build.rs +++ b/packages/debmagic/src/build.rs @@ -288,17 +288,62 @@ fn get_build_root_and_identifier( (package_identifier, build_root) } +/// Determine which distro version to use for the build. +/// +/// If only one distro version is specified in the changelog, it's used automatically. +/// If multiple distro versions are specified, an explicit --distro-version is required. +/// If --distro-version is provided, it's validated against the changelog versions. +fn resolve_distro_version( + changelog_distros: &[String], + explicit_distro: Option<&str>, +) -> anyhow::Result { + match (changelog_distros.len(), explicit_distro) { + (0, _) => Err(anyhow!("changelog contains no distributions")), + (1, None) => Ok(changelog_distros[0].clone()), + (1, Some(explicit)) => { + if explicit == changelog_distros[0] { + Ok(explicit.to_string()) + } else { + Err(anyhow!( + "explicit distro version '{}' conflicts with distribution specified in changelog '{}'", + explicit, + changelog_distros[0] + )) + } + } + (_, None) => Err(anyhow!( + "changelog contains multiple distributions ({}), please specify which one to build for with --distro-version", + changelog_distros.join(", ") + )), + (_, Some(explicit)) => { + if changelog_distros.contains(&explicit.to_string()) { + Ok(explicit.to_string()) + } else { + Err(anyhow!( + "explicit distro version '{}' not found in changelog distributions: {}", + explicit, + changelog_distros.join(", ") + )) + } + } + } +} + fn prepare_build_env( config: &Config, package: &PackageDescription, driver_type: BuildDriverType, output_dir: &Path, + explicit_distro_version: Option<&str>, ) -> anyhow::Result { let (package_identifier, build_root) = get_build_root_and_identifier(config, package); if build_root.exists() { fs::remove_dir_all(&build_root)?; } + let distro_version = resolve_distro_version(&package.distro_versions, explicit_distro_version) + .context("failed to determine distro version")?; + let build_config = BuildConfig { driver: driver_type, package_identifier, @@ -306,7 +351,7 @@ fn prepare_build_env( output_dir: output_dir.to_path_buf(), build_root_dir: build_root, distro: "debian".to_string(), - distro_version: "forky".to_string(), + distro_version, sign_package: false, }; @@ -340,9 +385,16 @@ pub fn build_package( package: &PackageDescription, driver_type: BuildDriverType, output_dir: &Path, + explicit_distro_version: Option<&str>, ) -> anyhow::Result<()> { - let build = prepare_build_env(config, package, driver_type, output_dir) - .context("failed to prepare build environment")?; + let build = prepare_build_env( + config, + package, + driver_type, + output_dir, + explicit_distro_version, + ) + .context("failed to prepare build environment")?; build .write_metadata() .context("failed to write build metadata")?; @@ -402,3 +454,84 @@ pub fn build_package( build.driver.cleanup(); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_resolve_distro_version_single_distro_no_explicit() { + let distros = vec!["stable".to_string()]; + let result = resolve_distro_version(&distros, None); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "stable"); + } + + #[test] + fn test_resolve_distro_version_single_distro_matching_explicit() { + let distros = vec!["stable".to_string()]; + let result = resolve_distro_version(&distros, Some("stable")); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "stable"); + } + + #[test] + fn test_resolve_distro_version_single_distro_conflicting_explicit() { + let distros = vec!["stable".to_string()]; + let result = resolve_distro_version(&distros, Some("unstable")); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("conflicts with distribution specified in changelog") + ); + } + + #[test] + fn test_resolve_distro_version_multiple_distros_no_explicit() { + let distros = vec!["unstable".to_string(), "testing".to_string()]; + let result = resolve_distro_version(&distros, None); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("multiple distributions") + ); + } + + #[test] + fn test_resolve_distro_version_multiple_distros_explicit_valid() { + let distros = vec!["unstable".to_string(), "testing".to_string()]; + let result = resolve_distro_version(&distros, Some("testing")); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "testing"); + } + + #[test] + fn test_resolve_distro_version_multiple_distros_explicit_invalid() { + let distros = vec!["unstable".to_string(), "testing".to_string()]; + let result = resolve_distro_version(&distros, Some("stable")); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("not found in changelog distributions") + ); + } + + #[test] + fn test_resolve_distro_version_empty_distros() { + let distros: Vec = vec![]; + let result = resolve_distro_version(&distros, None); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("changelog contains no distributions") + ); + } +} diff --git a/packages/debmagic/src/cli.rs b/packages/debmagic/src/cli.rs index 31022e0..e2ed030 100644 --- a/packages/debmagic/src/cli.rs +++ b/packages/debmagic/src/cli.rs @@ -48,6 +48,9 @@ pub struct BuildSubcommandArgs { #[arg(short, long, action = clap::ArgAction::SetTrue)] pub incremental: Option, + #[arg(long)] + pub distro_version: Option, + #[command(flatten)] pub common: CommonCli, diff --git a/packages/debmagic/src/main.rs b/packages/debmagic/src/main.rs index fbbdaec..b9c0009 100644 --- a/packages/debmagic/src/main.rs +++ b/packages/debmagic/src/main.rs @@ -82,6 +82,7 @@ fn main() -> anyhow::Result<()> { &package, args.driver, &path::absolute(output_dir).context("resolving output dir failed")?, + args.distro_version.as_deref(), ) .context("Building the package failed")?; } diff --git a/packages/debmagic/src/package.rs b/packages/debmagic/src/package.rs index 6bee36d..66e3d5f 100644 --- a/packages/debmagic/src/package.rs +++ b/packages/debmagic/src/package.rs @@ -9,6 +9,7 @@ pub struct PackageDescription { pub name: String, pub version: PackageVersion, pub source_dir: PathBuf, + pub distro_versions: Vec, } impl PackageDescription { @@ -30,10 +31,15 @@ impl PackageDescription { .ok_or(anyhow!("no package version in changelog entry")) .map(|v| PackageVersion::new(v.epoch, v.upstream_version, v.debian_revision))?; + let distro_versions = first_entry + .distributions() + .ok_or(anyhow!("no distribution specified in changelog entry"))?; + Ok(Self { name, version, source_dir: dir.to_path_buf(), + distro_versions, }) } } @@ -53,6 +59,24 @@ mod tests { assert_eq!(package.name, "test-package"); assert_eq!(package.version.version(), "1.2.4-1"); + assert_eq!(package.distro_versions, vec!["stable"]); + assert_eq!(package.source_dir, test_asset_dir); + + Ok(()) + } + + #[test] + fn test_package_description_with_multiple_distros() -> Result<(), anyhow::Error> { + let test_asset_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("assets") + .join("test_package_multi_distro"); + + let package = PackageDescription::from_dir(&test_asset_dir)?; + + assert_eq!(package.name, "test-package"); + assert_eq!(package.version.version(), "1.2.4-1"); + assert_eq!(package.distro_versions, vec!["unstable", "testing"]); assert_eq!(package.source_dir, test_asset_dir); Ok(()) diff --git a/packages/debmagic/tests/assets/test_package_multi_distro/debian/changelog b/packages/debmagic/tests/assets/test_package_multi_distro/debian/changelog new file mode 100644 index 0000000..da56ef0 --- /dev/null +++ b/packages/debmagic/tests/assets/test_package_multi_distro/debian/changelog @@ -0,0 +1,5 @@ +test-package (1.2.4-1) unstable testing; urgency=medium + + * bugfix for multiple distros + + -- Test Maintainer Mon, 15 Feb 2026 12:00:00 +0000 From 8d7f300e0fa2e084bd5b2450b93155cf4d84a0b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 15 Feb 2026 19:30:55 +0100 Subject: [PATCH 15/21] fix(cli): properly forward subprocess output to our standard stdout again --- packages/debmagic/src/build/driver_docker.rs | 83 +++++++------------- 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/packages/debmagic/src/build/driver_docker.rs b/packages/debmagic/src/build/driver_docker.rs index bc64519..1eaa29b 100644 --- a/packages/debmagic/src/build/driver_docker.rs +++ b/packages/debmagic/src/build/driver_docker.rs @@ -1,7 +1,7 @@ use std::{ - fs, io, + fs, path::{Path, PathBuf}, - process::{Command, Output}, + process::{Command, Stdio}, }; use anyhow::anyhow; @@ -52,16 +52,6 @@ pub struct DriverDocker { container_name: String, } -fn error_from_command(cmd_output: &io::Result, message: &str) -> anyhow::Error { - anyhow!( - "{message}:\n{}", - cmd_output - .as_ref() - .map(|o| String::from_utf8_lossy(&o.stderr).to_string()) - .unwrap_or("".to_string()) - ) -} - fn build_build_image(config: &BuildConfig, driver_config: &DriverConfig) -> anyhow::Result { let base_image = driver_config .docker @@ -112,16 +102,11 @@ fn build_build_image(config: &BuildConfig, driver_config: &DriverConfig) -> anyh .arg(dockerfile_path) .arg(config.build_temp_dir()); - let build_output = build_cmd.output(); - if !build_output - .as_ref() - .map(|o| o.status.success()) - .unwrap_or(false) - { - return Err(error_from_command( - &build_output, - "Error creating docker image", - )); + let status = build_cmd + .status() + .map_err(|e| anyhow!("Error running docker build: {}", e))?; + if !status.success() { + return Err(anyhow!("Error creating docker image")); } Ok(docker_image_name) @@ -130,14 +115,15 @@ fn build_build_image(config: &BuildConfig, driver_config: &DriverConfig) -> anyh fn does_container_exist(container_name: &str) -> anyhow::Result { let mut ps_cmd = Command::new("docker"); ps_cmd.args(["ps", "--all", "--format", "json"]); - ps_cmd.stdout(std::process::Stdio::piped()); - if !ps_cmd.status().map(|s| s.success()).unwrap_or(false) { - return Err(anyhow!("failed to query running docker containers")); - } + ps_cmd.stdout(Stdio::piped()); let output = ps_cmd .output() .map_err(|_| anyhow!("Failed to read docker ps output"))?; + + if !output.status.success() { + return Err(anyhow!("failed to query running docker containers")); + } let stdout = String::from_utf8_lossy(&output.stdout); for line in stdout.lines() { if let Ok(container) = serde_json::from_str::(line) @@ -158,31 +144,21 @@ impl DriverDocker { if driver_config.persistent && container_exists { let mut start_cmd = Command::new("docker"); start_cmd.args(["start", &container_name]); - let start_output = start_cmd.output(); - if !start_output - .as_ref() - .map(|o| o.status.success()) - .unwrap_or(false) - { - return Err(error_from_command( - &start_output, - "Error starting docker container", - )); + let status = start_cmd + .status() + .map_err(|e| anyhow!("Error running docker start: {}", e))?; + if !status.success() { + return Err(anyhow!("Error starting docker container")); } } else { if container_exists { let mut rm_cmd = Command::new("docker"); rm_cmd.args(["rm", "-f", &container_name]); - let rm_output = rm_cmd.output(); - if !rm_output - .as_ref() - .map(|o| o.status.success()) - .unwrap_or(false) - { - return Err(error_from_command( - &rm_output, - "Error removing existing docker container", - )); + let status = rm_cmd + .status() + .map_err(|e| anyhow!("Error running docker rm: {}", e))?; + if !status.success() { + return Err(anyhow!("Error removing existing docker container")); } } @@ -202,16 +178,11 @@ impl DriverDocker { &docker_image_name, ]); - let run_output = run_cmd.output(); - if !run_output - .as_ref() - .map(|o| o.status.success()) - .unwrap_or(false) - { - return Err(error_from_command( - &run_output, - "Error starting docker container", - )); + let status = run_cmd + .status() + .map_err(|e| anyhow!("Error running docker run: {}", e))?; + if !status.success() { + return Err(anyhow!("Error starting docker container")); } } From 845f0963fff57731c8e651d8d70cff7164a0aef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 15 Feb 2026 20:31:55 +0100 Subject: [PATCH 16/21] refactor(cli): change shell attachment tracking to use a unix socket for IPC --- packages/debmagic/src/build.rs | 224 ++++++++++++-------------- packages/debmagic/src/build/common.rs | 3 - 2 files changed, 102 insertions(+), 125 deletions(-) diff --git a/packages/debmagic/src/build.rs b/packages/debmagic/src/build.rs index bb35d54..68abcd9 100644 --- a/packages/debmagic/src/build.rs +++ b/packages/debmagic/src/build.rs @@ -1,8 +1,10 @@ use core::time; +use std::net::Shutdown; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::sync::{Arc, Mutex}; use std::{ - cmp::{self}, fs, - io::{self, BufReader, BufWriter, IsTerminal, Seek, stdout}, + io::{self, BufReader, IsTerminal, Read, Write, stdout}, path::{Path, PathBuf}, thread, }; @@ -79,13 +81,10 @@ impl Build { if !build_metadata_path.is_file() { return Err(anyhow!("No build.json found")); } - - let mut file = fs::OpenOptions::new() + // read metadata from file + let file = fs::OpenOptions::new() .read(true) - .write(true) .open(&build_metadata_path)?; - file.lock()?; - let metadata = || -> anyhow::Result { let reader = BufReader::new(&file); let metadata: BuildMetadata = serde_json::from_reader(reader).with_context(|| { @@ -97,39 +96,13 @@ impl Build { Ok(metadata) }(); - let metadata = match metadata { - Err(meta_err) => { - file.unlock()?; - return Err(meta_err); - } - Ok(metadata) => metadata, - }; - - let driver = create_driver_from_metadata(driver_config, &metadata); - - let driver = match driver { - Err(driver_err) => { - file.unlock()?; - return Err(driver_err); - } - Ok(driver) => driver, - }; - - let result = || -> anyhow::Result<()> { - file.seek(io::SeekFrom::Start(0))?; - let updated_metadata = BuildMetadata { - num_processes_attached: &metadata.num_processes_attached + 1, - ..metadata.clone() - }; - let writer = BufWriter::new(&file); - serde_json::to_writer_pretty(writer, &updated_metadata) - .context("Failed to serialize build metadata")?; - Ok(()) - }(); + let metadata = metadata?; - file.unlock()?; + let driver = create_driver_from_metadata(driver_config, &metadata)?; - result?; + // Try to signal the main debmagic build process that a shell attached + send_socket_command(build_root, "attach") + .context("No debmagic build is currently running for this source directory")?; Ok(Self { config: metadata.config.clone(), @@ -137,91 +110,16 @@ impl Build { }) } - fn build_metadata_path(&self) -> anyhow::Result { - let build_metadata_path = self.config.build_root_dir.join("build.json"); - if !build_metadata_path.is_file() { - return Err(anyhow!("No build.json found")); - } - Ok(build_metadata_path) - } - pub fn detach(&self) -> anyhow::Result<()> { - // TODO: refactor the whole file locking / read + write metadata thing to not be as duplicated and error prone - let build_metadata_path = self.build_metadata_path()?; - - let mut file = fs::OpenOptions::new() - .read(true) - .write(true) - .open(&build_metadata_path)?; - file.lock()?; - - let metadata = || -> anyhow::Result { - let reader = BufReader::new(&file); - let metadata: BuildMetadata = serde_json::from_reader(reader).with_context(|| { - format!( - "Failed to read build metadata from {} - invalid json", - build_metadata_path.display() - ) - })?; - Ok(metadata) - }(); - - let metadata = match metadata { - Err(meta_err) => { - file.unlock()?; - return Err(meta_err); - } - Ok(metadata) => metadata, - }; - - let result = || -> anyhow::Result<()> { - file.seek(io::SeekFrom::Start(0))?; - let updated_metadata = BuildMetadata { - num_processes_attached: cmp::max(0, &metadata.num_processes_attached - 1), - ..metadata.clone() - }; - let writer = BufWriter::new(&file); - serde_json::to_writer_pretty(writer, &updated_metadata) - .context("Failed to serialize build metadata")?; - Ok(()) - }(); - - file.unlock()?; - result - } - - pub fn get_number_of_attached_processes(&self) -> anyhow::Result { - // TODO: refactor the whole file locking / read + write metadata thing to not be as duplicated and error prone - let build_metadata_path = self.build_metadata_path()?; - - let file = fs::OpenOptions::new() - .read(true) - .open(&build_metadata_path)?; - file.lock()?; - - let metadata = || -> anyhow::Result { - let reader = BufReader::new(&file); - let metadata: BuildMetadata = serde_json::from_reader(reader).with_context(|| { - format!( - "Failed to read build metadata from {} - invalid json", - build_metadata_path.display() - ) - })?; - Ok(metadata) - }(); - file.unlock()?; - - match metadata { - Err(meta_err) => Err(meta_err), - Ok(metadata) => Ok(metadata.num_processes_attached), - } + let build_root = &self.config.build_root_dir; + send_socket_command(build_root, "detach")?; + Ok(()) } pub fn write_metadata(&self) -> anyhow::Result<()> { let metadata = BuildMetadata { config: self.config.clone(), driver_metadata: self.driver.get_build_metadata(), - num_processes_attached: 0, }; let path = self.config.build_root_dir.join("build.json"); let json = serde_json::to_string_pretty(&metadata) @@ -246,6 +144,80 @@ fn copy_glob(src_dir: &Path, pattern: &str, dest_dir: &Path) -> anyhow::Result<( Ok(()) } +fn socket_path_for_build(build_root: &Path) -> PathBuf { + build_root.join("build.sock") +} + +fn start_socket_server( + build_root: &Path, + should_exit: Arc>, +) -> anyhow::Result> { + let sock = socket_path_for_build(build_root); + if sock.exists() { + // try to remove stale socket file + let _ = fs::remove_file(&sock); + } + + let listener = UnixListener::bind(&sock) + .with_context(|| format!("failed to bind unix socket {}", sock.display()))?; + + // Set non-blocking mode so we can check the exit flag + listener + .set_nonblocking(true) + .context("failed to set socket non-blocking")?; + + let handle = thread::spawn(move || { + let mut num_attached = 0u64; + loop { + // Check if we should exit + let exit_requested = *should_exit.lock().unwrap(); + if exit_requested && num_attached == 0 { + break; + } + + match listener.accept() { + Ok((mut s, _)) => { + let mut buf = String::new(); + if s.read_to_string(&mut buf).is_err() { + let _ = s.shutdown(Shutdown::Both); + continue; + } + let cmd = buf.trim(); + match cmd { + "attach" => { + num_attached += 1; + } + "detach" => { + num_attached = std::cmp::max(0, num_attached - 1); + } + _ => {} + } + let _ = s.shutdown(Shutdown::Both); + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + // No connection available, sleep briefly to avoid busy-waiting + thread::sleep(time::Duration::from_millis(10)); + } + Err(_) => break, + } + } + let _ = fs::remove_file(&sock); + }); + + Ok(handle) +} + +fn send_socket_command(build_root: &Path, cmd: &str) -> anyhow::Result<()> { + let sock = socket_path_for_build(build_root); + let mut stream = UnixStream::connect(&sock) + .with_context(|| format!("failed to connect to socket {}", sock.display()))?; + stream + .write_all(cmd.as_bytes()) + .context("failed to send socket command")?; + stream.shutdown(Shutdown::Write).ok(); + Ok(()) +} + fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> anyhow::Result<()> { fs::create_dir_all(&dst)?; @@ -399,6 +371,10 @@ pub fn build_package( .write_metadata() .context("failed to write build metadata")?; + let should_exit = Arc::new(Mutex::new(false)); + let socket_server_handle = + start_socket_server(&build.config.build_root_dir, should_exit.clone())?; + let result = (|| -> anyhow::Result<()> { build.driver.run_command( &["apt-get", "-y", "build-dep", "."], @@ -440,16 +416,20 @@ pub fn build_package( eprintln!("Build failed: {e}"); } build.driver.cleanup(); + *should_exit.lock().unwrap() = true; + if !socket_server_handle.is_finished() { + println!("Waiting for all attached shells to exit..."); + } + socket_server_handle.join().ok(); return Err(e); } - // busy waiting until no pro - while let Ok(attached_processes) = build.get_number_of_attached_processes() - && attached_processes > 0 - { - println!("Waiting for last shell to detach ..."); - thread::sleep(time::Duration::from_millis(10)); + // Signal the socket server to exit and wait for it to complete + *should_exit.lock().unwrap() = true; + if !socket_server_handle.is_finished() { + println!("Waiting for all attached shells to exit..."); } + socket_server_handle.join().ok(); build.driver.cleanup(); Ok(()) diff --git a/packages/debmagic/src/build/common.rs b/packages/debmagic/src/build/common.rs index 2b63a4d..7899523 100644 --- a/packages/debmagic/src/build/common.rs +++ b/packages/debmagic/src/build/common.rs @@ -21,9 +21,6 @@ pub type DriverSpecificBuildMetadata = HashMap; pub struct BuildMetadata { pub config: BuildConfig, pub driver_metadata: DriverSpecificBuildMetadata, - // number of parallel debmagic processes working on this instance of a build - // used to determine when a BuildDriver can be fully stopped and cleaned up - pub num_processes_attached: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] From 30769770e1f67ed9acfa9f279f9050849cc9c4d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 15 Feb 2026 21:07:12 +0100 Subject: [PATCH 17/21] feat(cli): add help information for most subcommands and args --- packages/debmagic/src/build.rs | 1 - packages/debmagic/src/cli.rs | 31 +++++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/debmagic/src/build.rs b/packages/debmagic/src/build.rs index 68abcd9..af20622 100644 --- a/packages/debmagic/src/build.rs +++ b/packages/debmagic/src/build.rs @@ -345,7 +345,6 @@ pub fn get_shell_in_build(config: &Config, package: &PackageDescription) -> anyh .driver .interactive_shell(&build.config.build_source_dir()); - // TODO: detach - decrement num_attached_processes build.detach()?; result?; diff --git a/packages/debmagic/src/cli.rs b/packages/debmagic/src/cli.rs index e2ed030..1064f36 100644 --- a/packages/debmagic/src/cli.rs +++ b/packages/debmagic/src/cli.rs @@ -6,7 +6,7 @@ use clap::{Args, Parser, Subcommand}; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] pub struct Cli { - #[arg(short, long)] + #[arg(short, long, help = "Path to config file")] pub config: Option, #[command(subcommand)] @@ -15,46 +15,61 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Commands { + #[command(about = "Build a a debian package")] Build(BuildSubcommandArgs), + #[command(about = "Open an interactive shell to the currently active build environment")] Shell(ShellSubcommandArgs), + #[command(about = "Run tests")] Test(TestSubcommandArgs), + #[command(about = "Check the project")] Check(CheckSubcommandArgs), + #[command(about = "Show version information")] Version {}, } #[derive(Args, Debug)] pub struct CommonCli { - #[arg(short, long)] + #[arg( + short, + long, + help = "Path to the parent directory of the debian package. If not specified defaults to the current working directory" + )] pub source_dir: Option, } #[derive(Args, Debug)] pub struct DockerArgs { - #[arg(long = "driver-docker-base-image")] + #[arg( + long = "driver-docker-base-image", + help = "If passed will override the base image for the current build" + )] pub base_image: Option, } #[derive(Args, Debug)] pub struct BuildSubcommandArgs { - #[arg(short, long)] + #[arg(short, long, help = "Build driver type")] pub driver: BuildDriverType, - #[arg(long, action = clap::ArgAction::SetTrue)] + #[arg(long, action = clap::ArgAction::SetTrue, help = "Persist the build environment after the build finished")] pub persist_driver: Option, #[command(flatten)] pub docker: DockerArgs, - #[arg(short, long, action = clap::ArgAction::SetTrue)] + #[arg(short, long, action = clap::ArgAction::SetTrue, help = "Enable incremental builds. This implies --persist-driver")] pub incremental: Option, - #[arg(long)] + #[arg( + long, + help = "Select the target distribution version, only required in the debian changelog specifies multiple versions" + )] pub distro_version: Option, #[command(flatten)] pub common: CommonCli, - #[arg(short, long)] + #[arg(short, long, help = "Output directory for the package artifacts")] pub output_dir: Option, } From 43164b391937ac00bcbcc9ce56bc357cf1250096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 15 Feb 2026 21:23:27 +0100 Subject: [PATCH 18/21] feat(cli): implement proper image overriding in docker driver Allows to place a base image override per distro version in the debmagic config. --- packages/debmagic/src/build.rs | 27 +++++++++++--- packages/debmagic/src/build/config.rs | 10 ++++- packages/debmagic/src/build/driver_bare.rs | 9 ++++- packages/debmagic/src/build/driver_docker.rs | 39 ++++++++++++++++---- packages/debmagic/src/config.rs | 5 +++ packages/debmagic/src/main.rs | 15 ++++++-- packages/debmagic/tests/assets/config1.toml | 3 ++ 7 files changed, 88 insertions(+), 20 deletions(-) diff --git a/packages/debmagic/src/build.rs b/packages/debmagic/src/build.rs index af20622..1269d95 100644 --- a/packages/debmagic/src/build.rs +++ b/packages/debmagic/src/build.rs @@ -9,6 +9,7 @@ use std::{ thread, }; +use crate::build::config::DriverOverrides; use crate::{ build::{ common::{BuildConfig, BuildDriver, BuildDriverType, BuildMetadata}, @@ -35,10 +36,19 @@ struct Build { fn get_build_driver( config: &BuildConfig, driver_config: &DriverConfig, + driver_overrides: &DriverOverrides, ) -> anyhow::Result> { match config.driver { - BuildDriverType::Docker => Ok(Box::new(DriverDocker::create(config, driver_config)?)), - BuildDriverType::Bare => Ok(Box::new(DriverBare::create(config, driver_config))), + BuildDriverType::Docker => Ok(Box::new(DriverDocker::create( + config, + driver_config, + &driver_overrides.docker, + )?)), + BuildDriverType::Bare => Ok(Box::new(DriverBare::create( + config, + driver_config, + &driver_overrides.bare, + ))), // BuildDriverType::Lxd => ... } } @@ -64,8 +74,12 @@ fn create_driver_from_metadata( } impl Build { - pub fn create(config: &BuildConfig, driver_config: &DriverConfig) -> anyhow::Result { - let driver = get_build_driver(config, driver_config) + pub fn create( + config: &BuildConfig, + driver_config: &DriverConfig, + driver_overrides: &DriverOverrides, + ) -> anyhow::Result { + let driver = get_build_driver(config, driver_config, driver_overrides) .context(format!("failed to create {:?} build driver", config.driver))?; Ok(Self { config: config.clone(), @@ -303,6 +317,7 @@ fn resolve_distro_version( fn prepare_build_env( config: &Config, + driver_overrides: &DriverOverrides, package: &PackageDescription, driver_type: BuildDriverType, output_dir: &Path, @@ -334,7 +349,7 @@ fn prepare_build_env( copy_dir_all(&build_config.source_dir, build_config.build_source_dir()) .context("failed to copy source tree to build directory")?; - let build = Build::create(&build_config, &config.driver)?; + let build = Build::create(&build_config, &config.driver, driver_overrides)?; Ok(build) } @@ -355,11 +370,13 @@ pub fn build_package( config: &Config, package: &PackageDescription, driver_type: BuildDriverType, + driver_overrides: &DriverOverrides, output_dir: &Path, explicit_distro_version: Option<&str>, ) -> anyhow::Result<()> { let build = prepare_build_env( config, + driver_overrides, package, driver_type, output_dir, diff --git a/packages/debmagic/src/build/config.rs b/packages/debmagic/src/build/config.rs index 46392b0..193d454 100644 --- a/packages/debmagic/src/build/config.rs +++ b/packages/debmagic/src/build/config.rs @@ -1,7 +1,7 @@ use serde::Deserialize; -use crate::build::driver_bare::DriverBareConfig; -use crate::build::driver_docker::DriverDockerConfig; +use crate::build::driver_bare::{DriverBareConfig, DriverBareConfigOverrides}; +use crate::build::driver_docker::{DriverDockerConfig, DriverDockerConfigOverrides}; #[derive(Deserialize, Debug, Clone, Default)] #[serde(default)] @@ -10,3 +10,9 @@ pub struct DriverConfig { pub docker: DriverDockerConfig, pub bare: DriverBareConfig, } + +#[derive(Deserialize, Debug, Clone, Default)] +pub struct DriverOverrides { + pub docker: DriverDockerConfigOverrides, + pub bare: DriverBareConfigOverrides, +} diff --git a/packages/debmagic/src/build/driver_bare.rs b/packages/debmagic/src/build/driver_bare.rs index bfd31ec..647df21 100644 --- a/packages/debmagic/src/build/driver_bare.rs +++ b/packages/debmagic/src/build/driver_bare.rs @@ -13,13 +13,20 @@ use crate::build::{ #[serde(default)] pub struct DriverBareConfig {} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DriverBareConfigOverrides {} + pub struct DriverBare { config: BuildConfig, _driver_config: DriverConfig, } impl DriverBare { - pub fn create(config: &BuildConfig, driver_config: &DriverConfig) -> Self { + pub fn create( + config: &BuildConfig, + driver_config: &DriverConfig, + _overrides: &DriverBareConfigOverrides, + ) -> Self { Self { config: config.clone(), _driver_config: driver_config.clone(), diff --git a/packages/debmagic/src/build/driver_docker.rs b/packages/debmagic/src/build/driver_docker.rs index 1eaa29b..cae0abe 100644 --- a/packages/debmagic/src/build/driver_docker.rs +++ b/packages/debmagic/src/build/driver_docker.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, fs, path::{Path, PathBuf}, process::{Command, Stdio}, @@ -17,6 +18,20 @@ use crate::build::{ #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default)] pub struct DriverDockerConfig { + pub base_images: HashMap, +} + +impl DriverDockerConfig { + pub fn base_image_for_distro(&self, distro: &str, version: &str) -> String { + self.base_images + .get(&format!("{}:{}", distro, version)) + .cloned() + .unwrap_or_else(|| format!("docker.io/{}:{}", distro, version)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DriverDockerConfigOverrides { pub base_image: Option, } @@ -52,12 +67,16 @@ pub struct DriverDocker { container_name: String, } -fn build_build_image(config: &BuildConfig, driver_config: &DriverConfig) -> anyhow::Result { - let base_image = driver_config - .docker - .base_image - .clone() - .unwrap_or_else(|| format!("docker.io/{}:{}", config.distro, config.distro_version)); +fn build_build_image( + config: &BuildConfig, + driver_config: &DriverConfig, + overrides: &DriverDockerConfigOverrides, +) -> anyhow::Result { + let base_image = overrides.base_image.clone().unwrap_or_else(|| { + driver_config + .docker + .base_image_for_distro(&config.distro, &config.distro_version) + }); let debian_control_file_path = config.build_source_dir().join("debian").join("control"); @@ -137,7 +156,11 @@ fn does_container_exist(container_name: &str) -> anyhow::Result { } impl DriverDocker { - pub fn create(config: &BuildConfig, driver_config: &DriverConfig) -> anyhow::Result { + pub fn create( + config: &BuildConfig, + driver_config: &DriverConfig, + overrides: &DriverDockerConfigOverrides, + ) -> anyhow::Result { let container_name = format!("debmagic-{}", config.build_identifier()); let container_exists = does_container_exist(&container_name)?; @@ -162,7 +185,7 @@ impl DriverDocker { } } - let docker_image_name = build_build_image(config, driver_config)?; + let docker_image_name = build_build_image(config, driver_config, overrides)?; let mut run_cmd = Command::new("docker"); run_cmd.args([ "run", diff --git a/packages/debmagic/src/config.rs b/packages/debmagic/src/config.rs index 1dc5352..9ab1084 100644 --- a/packages/debmagic/src/config.rs +++ b/packages/debmagic/src/config.rs @@ -57,6 +57,11 @@ mod tests { let cfg = Config::new(&vec![test_asset_dir.join("config1.toml")])?; assert!(cfg.driver.persistent); + assert!( + cfg.driver.docker.base_images.get("debian:trixie") + == Some(&"some-debian-trixie-image:latest".to_string()) + ); + Ok(()) } } diff --git a/packages/debmagic/src/main.rs b/packages/debmagic/src/main.rs index b9c0009..bee8607 100644 --- a/packages/debmagic/src/main.rs +++ b/packages/debmagic/src/main.rs @@ -7,7 +7,10 @@ use anyhow::Context; use clap::{CommandFactory, Parser}; use crate::{ - build::{build_package, get_shell_in_build}, + build::{ + build_package, config::DriverOverrides, driver_bare::DriverBareConfigOverrides, + driver_docker::DriverDockerConfigOverrides, get_shell_in_build, + }, cli::{Cli, Commands}, config::Config, package::PackageDescription, @@ -69,9 +72,12 @@ fn main() -> anyhow::Result<()> { // TODO: investigate if this is actually needed config.driver.persistent = true; } - if let Some(docker_base_image) = &args.docker.base_image { - config.driver.docker.base_image = Some(docker_base_image.clone()); - } + let driver_overrides = DriverOverrides { + docker: DriverDockerConfigOverrides { + base_image: args.docker.base_image.clone(), + }, + bare: DriverBareConfigOverrides {}, + }; let package = PackageDescription::from_dir( &path::absolute(source_dir).context("resolving source dir failed")?, @@ -81,6 +87,7 @@ fn main() -> anyhow::Result<()> { &config, &package, args.driver, + &driver_overrides, &path::absolute(output_dir).context("resolving output dir failed")?, args.distro_version.as_deref(), ) diff --git a/packages/debmagic/tests/assets/config1.toml b/packages/debmagic/tests/assets/config1.toml index 41c650b..5a1496f 100644 --- a/packages/debmagic/tests/assets/config1.toml +++ b/packages/debmagic/tests/assets/config1.toml @@ -1,2 +1,5 @@ [driver] persistent = true + +[driver.docker] +base_images = { "debian:trixie" = "some-debian-trixie-image:latest" } From c68572985963b5b2437e6fd879687cea942580df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 15 Feb 2026 21:29:23 +0100 Subject: [PATCH 19/21] refactor(cli): rename --distro-version to just --distro --- packages/debmagic/src/cli.rs | 2 +- packages/debmagic/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/debmagic/src/cli.rs b/packages/debmagic/src/cli.rs index 1064f36..94071fd 100644 --- a/packages/debmagic/src/cli.rs +++ b/packages/debmagic/src/cli.rs @@ -64,7 +64,7 @@ pub struct BuildSubcommandArgs { long, help = "Select the target distribution version, only required in the debian changelog specifies multiple versions" )] - pub distro_version: Option, + pub distro: Option, #[command(flatten)] pub common: CommonCli, diff --git a/packages/debmagic/src/main.rs b/packages/debmagic/src/main.rs index bee8607..81f8781 100644 --- a/packages/debmagic/src/main.rs +++ b/packages/debmagic/src/main.rs @@ -89,7 +89,7 @@ fn main() -> anyhow::Result<()> { args.driver, &driver_overrides, &path::absolute(output_dir).context("resolving output dir failed")?, - args.distro_version.as_deref(), + args.distro.as_deref(), ) .context("Building the package failed")?; } From 1a5899759c96b7b1fc6149124d92f557ec610eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Sun, 15 Feb 2026 22:10:38 +0100 Subject: [PATCH 20/21] feat(cli): fully infer distro from codename alone --- Cargo.lock | 1 + packages/debmagic-common/Cargo.toml | 1 + packages/debmagic-common/src/distro.rs | 195 +++++++++++++++++++ packages/debmagic-common/src/lib.rs | 1 + packages/debmagic/src/build.rs | 47 +++-- packages/debmagic/src/build/common.rs | 9 +- packages/debmagic/src/build/driver_docker.rs | 16 +- 7 files changed, 238 insertions(+), 32 deletions(-) create mode 100644 packages/debmagic-common/src/distro.rs diff --git a/Cargo.lock b/Cargo.lock index e412c3c..8e6000b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,6 +358,7 @@ version = "0.0.1-alpha1" dependencies = [ "chrono", "regex", + "serde", "test-case", ] diff --git a/packages/debmagic-common/Cargo.toml b/packages/debmagic-common/Cargo.toml index 1aaf7c9..51b1e8c 100644 --- a/packages/debmagic-common/Cargo.toml +++ b/packages/debmagic-common/Cargo.toml @@ -10,6 +10,7 @@ rust-version.workspace = true [dependencies] chrono = { workspace = true } regex = { workspace = true } +serde = { workspace = true, features = ["derive"] } [dev-dependencies] test-case = { workspace = true } diff --git a/packages/debmagic-common/src/distro.rs b/packages/debmagic-common/src/distro.rs new file mode 100644 index 0000000..a66279c --- /dev/null +++ b/packages/debmagic-common/src/distro.rs @@ -0,0 +1,195 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::LazyLock; + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] +pub enum Distro { + Debian, + Ubuntu, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct DistroVersion { + pub distro: Distro, + pub codename: String, + /// numeric or semver version, e.g. "24.04" for ubuntu or "12" for debian + pub version: String, +} + +impl Distro { + pub fn as_str(&self) -> &'static str { + match self { + Distro::Debian => "debian", + Distro::Ubuntu => "ubuntu", + } + } +} + +impl std::fmt::Display for Distro { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +static DISTRO_INFO_MAP: LazyLock> = LazyLock::new(|| { + HashMap::from([ + // debian + ( + "experimental", + DistroVersion { + distro: Distro::Debian, + codename: "experimental".to_string(), + version: "".to_string(), + }, + ), + ( + "unstable", + DistroVersion { + distro: Distro::Debian, + codename: "unstable".to_string(), + version: "".to_string(), + }, + ), + ( + "sid", + DistroVersion { + distro: Distro::Debian, + codename: "sid".to_string(), + version: "".to_string(), + }, + ), + ( + "testing", + DistroVersion { + distro: Distro::Debian, + codename: "testing".to_string(), + version: "".to_string(), + }, + ), + ( + "duke", + DistroVersion { + distro: Distro::Debian, + codename: "duke".to_string(), + version: "15".to_string(), + }, + ), + ( + "forky", + DistroVersion { + distro: Distro::Debian, + codename: "forky".to_string(), + version: "14".to_string(), + }, + ), + ( + "trixie", + DistroVersion { + distro: Distro::Debian, + codename: "trixie".to_string(), + version: "13".to_string(), + }, + ), + ( + "bookworm", + DistroVersion { + distro: Distro::Debian, + codename: "bookworm".to_string(), + version: "12".to_string(), + }, + ), + ( + "bullseye", + DistroVersion { + distro: Distro::Debian, + codename: "bullseye".to_string(), + version: "11".to_string(), + }, + ), + ( + "buster", + DistroVersion { + distro: Distro::Debian, + codename: "buster".to_string(), + version: "10".to_string(), + }, + ), + ( + "stretch", + DistroVersion { + distro: Distro::Debian, + codename: "stretch".to_string(), + version: "9".to_string(), + }, + ), + // ubuntu + ( + "resolute", + DistroVersion { + distro: Distro::Ubuntu, + codename: "resolute".to_string(), + version: "26.04".to_string(), + }, + ), + ( + "questing", + DistroVersion { + distro: Distro::Ubuntu, + codename: "questing".to_string(), + version: "25.10".to_string(), + }, + ), + ( + "noble", + DistroVersion { + distro: Distro::Ubuntu, + codename: "noble".to_string(), + version: "24.04".to_string(), + }, + ), + ( + "jammy", + DistroVersion { + distro: Distro::Ubuntu, + codename: "jammy".to_string(), + version: "22.04".to_string(), + }, + ), + ( + "focal", + DistroVersion { + distro: Distro::Ubuntu, + codename: "focal".to_string(), + version: "20.04".to_string(), + }, + ), + ( + "bionic", + DistroVersion { + distro: Distro::Ubuntu, + codename: "bionic".to_string(), + version: "18.04".to_string(), + }, + ), + ( + "xenial", + DistroVersion { + distro: Distro::Ubuntu, + codename: "xenial".to_string(), + version: "16.04".to_string(), + }, + ), + ( + "trusty", + DistroVersion { + distro: Distro::Ubuntu, + codename: "trusty".to_string(), + version: "14.04".to_string(), + }, + ), + ]) +}); + +pub fn get_distro_version(codename: &str) -> Option { + DISTRO_INFO_MAP.get(codename).cloned() +} diff --git a/packages/debmagic-common/src/lib.rs b/packages/debmagic-common/src/lib.rs index 49a5553..e4de127 100644 --- a/packages/debmagic-common/src/lib.rs +++ b/packages/debmagic-common/src/lib.rs @@ -1 +1,2 @@ pub mod debian; +pub mod distro; diff --git a/packages/debmagic/src/build.rs b/packages/debmagic/src/build.rs index 1269d95..fb16cd5 100644 --- a/packages/debmagic/src/build.rs +++ b/packages/debmagic/src/build.rs @@ -21,6 +21,7 @@ use crate::{ package::PackageDescription, }; use anyhow::{Context, anyhow}; +use debmagic_common::distro::DistroVersion; use glob::glob; pub mod common; @@ -282,8 +283,8 @@ fn get_build_root_and_identifier( fn resolve_distro_version( changelog_distros: &[String], explicit_distro: Option<&str>, -) -> anyhow::Result { - match (changelog_distros.len(), explicit_distro) { +) -> anyhow::Result { + let resolved_codename = match (changelog_distros.len(), explicit_distro) { (0, _) => Err(anyhow!("changelog contains no distributions")), (1, None) => Ok(changelog_distros[0].clone()), (1, Some(explicit)) => { @@ -312,7 +313,10 @@ fn resolve_distro_version( )) } } - } + }?; + let resolved = debmagic_common::distro::get_distro_version(&resolved_codename) + .ok_or_else(|| anyhow!("unknown distro codename '{}'", resolved_codename))?; + Ok(resolved) } fn prepare_build_env( @@ -337,8 +341,7 @@ fn prepare_build_env( source_dir: package.source_dir.clone(), output_dir: output_dir.to_path_buf(), build_root_dir: build_root, - distro: "debian".to_string(), - distro_version, + distro: distro_version.clone(), sign_package: false, }; @@ -453,28 +456,34 @@ pub fn build_package( #[cfg(test)] mod tests { + use debmagic_common::distro::Distro; + use super::*; #[test] fn test_resolve_distro_version_single_distro_no_explicit() { - let distros = vec!["stable".to_string()]; + let distros = vec!["forky".to_string()]; let result = resolve_distro_version(&distros, None); assert!(result.is_ok()); - assert_eq!(result.unwrap(), "stable"); + let distro_version = result.unwrap(); + assert_eq!(distro_version.codename, "forky"); + assert_eq!(distro_version.distro, Distro::Debian); } #[test] fn test_resolve_distro_version_single_distro_matching_explicit() { - let distros = vec!["stable".to_string()]; - let result = resolve_distro_version(&distros, Some("stable")); + let distros = vec!["forky".to_string()]; + let result = resolve_distro_version(&distros, Some("forky")); assert!(result.is_ok()); - assert_eq!(result.unwrap(), "stable"); + let distro_version = result.unwrap(); + assert_eq!(distro_version.codename, "forky"); + assert_eq!(distro_version.distro, Distro::Debian); } #[test] fn test_resolve_distro_version_single_distro_conflicting_explicit() { - let distros = vec!["stable".to_string()]; - let result = resolve_distro_version(&distros, Some("unstable")); + let distros = vec!["forky".to_string()]; + let result = resolve_distro_version(&distros, Some("duke")); assert!(result.is_err()); assert!( result @@ -486,7 +495,7 @@ mod tests { #[test] fn test_resolve_distro_version_multiple_distros_no_explicit() { - let distros = vec!["unstable".to_string(), "testing".to_string()]; + let distros = vec!["forky".to_string(), "duke".to_string()]; let result = resolve_distro_version(&distros, None); assert!(result.is_err()); assert!( @@ -499,16 +508,18 @@ mod tests { #[test] fn test_resolve_distro_version_multiple_distros_explicit_valid() { - let distros = vec!["unstable".to_string(), "testing".to_string()]; - let result = resolve_distro_version(&distros, Some("testing")); + let distros = vec!["forky".to_string(), "duke".to_string()]; + let result = resolve_distro_version(&distros, Some("duke")); assert!(result.is_ok()); - assert_eq!(result.unwrap(), "testing"); + let distro_version = result.unwrap(); + assert_eq!(distro_version.codename, "duke"); + assert_eq!(distro_version.distro, Distro::Debian); } #[test] fn test_resolve_distro_version_multiple_distros_explicit_invalid() { - let distros = vec!["unstable".to_string(), "testing".to_string()]; - let result = resolve_distro_version(&distros, Some("stable")); + let distros = vec!["forky".to_string(), "duke".to_string()]; + let result = resolve_distro_version(&distros, Some("trixie")); assert!(result.is_err()); assert!( result diff --git a/packages/debmagic/src/build/common.rs b/packages/debmagic/src/build/common.rs index 7899523..f324257 100644 --- a/packages/debmagic/src/build/common.rs +++ b/packages/debmagic/src/build/common.rs @@ -6,6 +6,7 @@ use std::{ }; use clap::ValueEnum; +use debmagic_common::distro::DistroVersion; use serde::{Deserialize, Serialize}; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize)] @@ -31,17 +32,13 @@ pub struct BuildConfig { pub build_root_dir: PathBuf, pub source_dir: PathBuf, pub output_dir: PathBuf, - pub distro_version: String, - pub distro: String, + pub distro: DistroVersion, pub sign_package: bool, } impl BuildConfig { pub fn build_identifier(&self) -> String { - format!( - "{}-{}-{}", - self.package_identifier, self.distro, self.distro_version - ) + format!("{}-{}", self.package_identifier, self.distro.codename) } pub fn build_work_dir(&self) -> PathBuf { diff --git a/packages/debmagic/src/build/driver_docker.rs b/packages/debmagic/src/build/driver_docker.rs index cae0abe..1210064 100644 --- a/packages/debmagic/src/build/driver_docker.rs +++ b/packages/debmagic/src/build/driver_docker.rs @@ -6,6 +6,7 @@ use std::{ }; use anyhow::anyhow; +use debmagic_common::distro::DistroVersion; use serde::{Deserialize, Serialize}; use crate::build::{ @@ -22,11 +23,11 @@ pub struct DriverDockerConfig { } impl DriverDockerConfig { - pub fn base_image_for_distro(&self, distro: &str, version: &str) -> String { + pub fn base_image_for_distro(&self, distro: &DistroVersion) -> String { self.base_images - .get(&format!("{}:{}", distro, version)) + .get(&format!("{}:{}", distro.distro, distro.codename)) .cloned() - .unwrap_or_else(|| format!("docker.io/{}:{}", distro, version)) + .unwrap_or_else(|| format!("docker.io/{}:{}", distro.distro, distro.codename)) } } @@ -72,11 +73,10 @@ fn build_build_image( driver_config: &DriverConfig, overrides: &DriverDockerConfigOverrides, ) -> anyhow::Result { - let base_image = overrides.base_image.clone().unwrap_or_else(|| { - driver_config - .docker - .base_image_for_distro(&config.distro, &config.distro_version) - }); + let base_image = overrides + .base_image + .clone() + .unwrap_or_else(|| driver_config.docker.base_image_for_distro(&config.distro)); let debian_control_file_path = config.build_source_dir().join("debian").join("control"); From 28b41a6c32f1b601503069b1a12c7581d672fb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Loipf=C3=BChrer?= Date: Fri, 20 Feb 2026 21:44:19 +0100 Subject: [PATCH 21/21] refactor(cli): code cleanups and comment clarifications --- packages/debmagic-common/src/debian/version.rs | 3 +-- packages/debmagic/src/build.rs | 8 ++++---- packages/debmagic/src/build/common.rs | 5 ++++- packages/debmagic/src/cli.rs | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/debmagic-common/src/debian/version.rs b/packages/debmagic-common/src/debian/version.rs index 7ba55e2..b8c8b26 100644 --- a/packages/debmagic-common/src/debian/version.rs +++ b/packages/debmagic-common/src/debian/version.rs @@ -48,7 +48,7 @@ impl PackageVersion { self.upstream.clone() } - /// distro epoch plus upstream version + /// upstream version plus packaging revision pub fn upstream_revision(&self) -> String { if let Some(revision) = &self.revision { return format!("{}-{}", self.upstream, revision); @@ -80,7 +80,6 @@ impl FromStr for PackageVersion { Some(0) } else { let re_epoch = Regex::new(r"^([0-9]+):.*$").map_err(|_| VersionParseError)?; - re_epoch.replace(version, "$1").to_string(); let epoch_str = re_epoch.replace(version, "$1").to_string(); let parsed_epoch = epoch_str.parse::().map_err(|_| VersionParseError)?; Some(parsed_epoch) diff --git a/packages/debmagic/src/build.rs b/packages/debmagic/src/build.rs index fb16cd5..7bd28d9 100644 --- a/packages/debmagic/src/build.rs +++ b/packages/debmagic/src/build.rs @@ -203,7 +203,7 @@ fn start_socket_server( num_attached += 1; } "detach" => { - num_attached = std::cmp::max(0, num_attached - 1); + num_attached = num_attached.saturating_sub(1); } _ => {} } @@ -278,8 +278,8 @@ fn get_build_root_and_identifier( /// Determine which distro version to use for the build. /// /// If only one distro version is specified in the changelog, it's used automatically. -/// If multiple distro versions are specified, an explicit --distro-version is required. -/// If --distro-version is provided, it's validated against the changelog versions. +/// If multiple distro versions are specified, an explicit --distro is required. +/// If --distro is provided, it's validated against the changelog versions. fn resolve_distro_version( changelog_distros: &[String], explicit_distro: Option<&str>, @@ -299,7 +299,7 @@ fn resolve_distro_version( } } (_, None) => Err(anyhow!( - "changelog contains multiple distributions ({}), please specify which one to build for with --distro-version", + "changelog contains multiple distributions ({}), please specify which one to build for with --distro", changelog_distros.join(", ") )), (_, Some(explicit)) => { diff --git a/packages/debmagic/src/build/common.rs b/packages/debmagic/src/build/common.rs index f324257..b194f30 100644 --- a/packages/debmagic/src/build/common.rs +++ b/packages/debmagic/src/build/common.rs @@ -38,7 +38,10 @@ pub struct BuildConfig { impl BuildConfig { pub fn build_identifier(&self) -> String { - format!("{}-{}", self.package_identifier, self.distro.codename) + format!( + "{}-{}-{}", + self.package_identifier, self.distro.distro, self.distro.codename + ) } pub fn build_work_dir(&self) -> PathBuf { diff --git a/packages/debmagic/src/cli.rs b/packages/debmagic/src/cli.rs index 94071fd..22eac71 100644 --- a/packages/debmagic/src/cli.rs +++ b/packages/debmagic/src/cli.rs @@ -15,7 +15,7 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Commands { - #[command(about = "Build a a debian package")] + #[command(about = "Build a debian package")] Build(BuildSubcommandArgs), #[command(about = "Open an interactive shell to the currently active build environment")] Shell(ShellSubcommandArgs),