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 diff --git a/Cargo.lock b/Cargo.lock index 023bad8..8e6000b 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" @@ -253,6 +263,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" @@ -262,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" @@ -278,6 +319,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,8 +341,11 @@ dependencies = [ "anyhow", "clap", "config", + "debian-changelog", + "debmagic-common", "dirs", "glob", + "ignore", "libc", "serde", "serde_json", @@ -299,9 +358,21 @@ version = "0.0.1-alpha1" dependencies = [ "chrono", "regex", + "serde", "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" @@ -413,6 +484,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" @@ -467,6 +551,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" @@ -500,6 +600,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 +637,7 @@ checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" dependencies = [ "bitflags", "libc", + "redox_syscall", ] [[package]] @@ -528,6 +652,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 +781,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 +844,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,12 +866,27 @@ 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" 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" @@ -789,6 +968,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 +1024,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 +1125,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" @@ -952,6 +1166,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" @@ -967,6 +1191,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 +1242,25 @@ 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 = "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 aa675db..0a10d23 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,8 @@ 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" } +ignore = { version = ">=0.4.25" } # dev dependencies test-case = { version = ">=3.3.1" } diff --git a/debian/control b/debian/control index a6c2c79..827b876 100644 --- a/debian/control +++ b/debian/control @@ -24,7 +24,9 @@ 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), + librust-ignore-dev (>=0.4.25) Rules-Requires-Root: no X-Style: black Standards-Version: 4.7.2 @@ -48,7 +50,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/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/debian/version.rs b/packages/debmagic-common/src/debian/version.rs index ae4a425..b8c8b26 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,63 @@ 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 + && epoch != 0 + { + return format!("{}:{}", epoch, self.upstream); } self.upstream.clone() } - /// distro epoch plus upstream version + /// upstream version plus packaging revision 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 +77,12 @@ 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() + 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)?; @@ -75,7 +96,11 @@ impl FromStr for PackageVersion { Ok(Self { epoch, upstream, - revision, + revision: if revision.is_empty() { + None + } else { + Some(revision) + }, }) } } @@ -88,19 +113,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-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-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 diff --git a/packages/debmagic/Cargo.toml b/packages/debmagic/Cargo.toml index 4df138b..65e61b0 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,5 @@ libc = { workspace = true } 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 a407b4b..7bd28d9 100644 --- a/packages/debmagic/src/build.rs +++ b/packages/debmagic/src/build.rs @@ -1,22 +1,27 @@ 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, }; +use crate::build::config::DriverOverrides; 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 debmagic_common::distro::DistroVersion; use glob::glob; pub mod common; @@ -32,13 +37,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.docker, + driver_config, + &driver_overrides.docker, )?)), - BuildDriverType::Bare => Ok(Box::new(DriverBare::create(config, &driver_config.bare))), + BuildDriverType::Bare => Ok(Box::new(DriverBare::create( + config, + driver_config, + &driver_overrides.bare, + ))), // BuildDriverType::Lxd => ... } } @@ -50,12 +61,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 => ... @@ -64,8 +75,13 @@ 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(), driver, @@ -80,13 +96,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(|| { @@ -98,39 +111,13 @@ impl Build { Ok(metadata) }(); - let metadata = match metadata { - Err(meta_err) => { - file.unlock()?; - return Err(meta_err); - } - Ok(metadata) => metadata, - }; + let metadata = metadata?; - let driver = create_driver_from_metadata(driver_config, &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(()) - }(); - - file.unlock()?; - - 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(), @@ -138,91 +125,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) @@ -247,18 +159,107 @@ 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 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 = num_attached.saturating_sub(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)?; - 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 } @@ -274,43 +275,94 @@ 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 is required. +/// If --distro is provided, it's validated against the changelog versions. +fn resolve_distro_version( + changelog_distros: &[String], + explicit_distro: Option<&str>, +) -> 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)) => { + 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", + 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(", ") + )) + } + } + }?; + 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( config: &Config, + driver_overrides: &DriverOverrides, 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, source_dir: package.source_dir.clone(), output_dir: output_dir.to_path_buf(), build_root_dir: build_root, - distro: "debian".to_string(), - distro_version: "forky".to_string(), - dry_run: config.dry_run, + distro: distro_version.clone(), 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)?; + let build = Build::create(&build_config, &config.driver, driver_overrides)?; Ok(build) } 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()?; result?; @@ -321,10 +373,26 @@ 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, package, driver_type, output_dir)?; - build.write_metadata()?; + let build = prepare_build_env( + config, + driver_overrides, + package, + driver_type, + output_dir, + explicit_distro_version, + ) + .context("failed to prepare build environment")?; + build + .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( @@ -357,7 +425,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}"); } @@ -365,17 +435,110 @@ 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(()) } + +#[cfg(test)] +mod tests { + use debmagic_common::distro::Distro; + + use super::*; + + #[test] + fn test_resolve_distro_version_single_distro_no_explicit() { + let distros = vec!["forky".to_string()]; + let result = resolve_distro_version(&distros, None); + assert!(result.is_ok()); + 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!["forky".to_string()]; + let result = resolve_distro_version(&distros, Some("forky")); + assert!(result.is_ok()); + 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!["forky".to_string()]; + let result = resolve_distro_version(&distros, Some("duke")); + 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!["forky".to_string(), "duke".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!["forky".to_string(), "duke".to_string()]; + let result = resolve_distro_version(&distros, Some("duke")); + assert!(result.is_ok()); + 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!["forky".to_string(), "duke".to_string()]; + let result = resolve_distro_version(&distros, Some("trixie")); + 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/build/common.rs b/packages/debmagic/src/build/common.rs index ddf075d..b194f30 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)] @@ -15,22 +16,12 @@ 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)] 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)] @@ -41,9 +32,7 @@ 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 distro: DistroVersion, pub sign_package: bool, } @@ -51,7 +40,7 @@ impl BuildConfig { pub fn build_identifier(&self) -> String { format!( "{}-{}-{}", - self.package_identifier, self.distro, self.distro_version + self.package_identifier, self.distro.distro, self.distro.codename ) } @@ -82,7 +71,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/build/config.rs b/packages/debmagic/src/build/config.rs index 7e874af..193d454 100644 --- a/packages/debmagic/src/build/config.rs +++ b/packages/debmagic/src/build/config.rs @@ -1,10 +1,18 @@ 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, Default, Clone)] +#[derive(Deserialize, Debug, Clone, Default)] +#[serde(default)] pub struct DriverConfig { + pub persistent: bool, 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 d8e5014..647df21 100644 --- a/packages/debmagic/src/build/driver_bare.rs +++ b/packages/debmagic/src/build/driver_bare.rs @@ -1,21 +1,32 @@ -use std::{env, path::Path, process::Command}; +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 {} +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DriverBareConfigOverrides {} + 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, + _overrides: &DriverBareConfigOverrides, + ) -> Self { Self { config: config.clone(), _driver_config: driver_config.clone(), @@ -24,7 +35,7 @@ impl DriverBare { pub fn from_build_metadata( config: &BuildConfig, - driver_config: &DriverBareConfig, + driver_config: &DriverConfig, _build_metadata: &BuildMetadata, ) -> Self { Self { @@ -49,11 +60,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..]); @@ -75,12 +81,11 @@ impl BuildDriver for DriverBare { // No-op for bare driver } - 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()?; + fn interactive_shell(&self, _cwd: &Path) -> std::io::Result<()> { + println!( + "source directory of current package build in {}", + self.config.build_source_dir().display() + ); Ok(()) } diff --git a/packages/debmagic/src/build/driver_docker.rs b/packages/debmagic/src/build/driver_docker.rs index 8e14ff1..1210064 100644 --- a/packages/debmagic/src/build/driver_docker.rs +++ b/packages/debmagic/src/build/driver_docker.rs @@ -1,19 +1,38 @@ use std::{ + collections::HashMap, fs, path::{Path, PathBuf}, - process::Command, + process::{Command, Stdio}, }; use anyhow::anyhow; +use debmagic_common::distro::DistroVersion; 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_images: HashMap, +} + +impl DriverDockerConfig { + pub fn base_image_for_distro(&self, distro: &DistroVersion) -> String { + self.base_images + .get(&format!("{}:{}", distro.distro, distro.codename)) + .cloned() + .unwrap_or_else(|| format!("docker.io/{}:{}", distro.distro, distro.codename)) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DriverDockerConfigOverrides { pub base_image: Option, } @@ -45,97 +64,161 @@ pub struct DockerDriverBuildMetadata { pub struct DriverDocker { config: BuildConfig, - _driver_config: DriverDockerConfig, + driver_config: DriverConfig, container_name: String, } +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)); + + 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"]) + .arg(dockerfile_path) + .arg(config.build_temp_dir()); + + 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) +} + +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(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) + && let Some(names) = container.get("Names") + && names == container_name + { + return Ok(true); + } + } + Ok(false) +} + impl DriverDocker { pub fn create( config: &BuildConfig, - driver_config: &DriverDockerConfig, + driver_config: &DriverConfig, + overrides: &DriverDockerConfigOverrides, ) -> 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}")]); - } + let container_name = format!("debmagic-{}", config.build_identifier()); + let container_exists = does_container_exist(&container_name)?; - 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")); - } + if driver_config.persistent && container_exists { + let mut start_cmd = Command::new("docker"); + start_cmd.args(["start", &container_name]); + 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 status = rm_cmd + .status() + .map_err(|e| anyhow!("Error running docker rm: {}", e))?; + if !status.success() { + return Err(anyhow!("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, overrides)?; + 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, + ]); + + let status = run_cmd + .status() + .map_err(|e| anyhow!("Error running docker run: {}", e))?; + if !status.success() { + return Err(anyhow!("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 +229,7 @@ impl DriverDocker { Self { config: config.clone(), - _driver_config: driver_config.clone(), + driver_config: driver_config.clone(), container_name, } } @@ -174,28 +257,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())); - - if self.config.dry_run { - println!("[dry-run] docker {exec_args:?}"); - return Ok(()); - } + 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")); } @@ -203,27 +280,23 @@ impl BuildDriver for DriverDocker { } fn cleanup(&self) { - 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(()); + 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(); } + } - 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", - "-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 7185c95..22eac71 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,29 +15,78 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Commands { + #[command(about = "Build a debian package")] Build(BuildSubcommandArgs), + #[command(about = "Open an interactive shell to the currently active build environment")] Shell(ShellSubcommandArgs), - Test {}, - Check {}, + #[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, + 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", + 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)] - pub driver_docker_build_image: Option, + #[arg(long, action = clap::ArgAction::SetTrue, help = "Persist the build environment after the build finished")] + pub persist_driver: Option, - #[arg(short, long)] - pub source_dir: Option, - #[arg(short, long)] + #[command(flatten)] + pub docker: DockerArgs, + + #[arg(short, long, action = clap::ArgAction::SetTrue, help = "Enable incremental builds. This implies --persist-driver")] + pub incremental: Option, + + #[arg( + long, + help = "Select the target distribution version, only required in the debian changelog specifies multiple versions" + )] + pub distro: Option, + + #[command(flatten)] + pub common: CommonCli, + + #[arg(short, long, help = "Output directory for the package artifacts")] 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 2fd6728..9ab1084 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,7 @@ use serde::Deserialize; pub struct Config { pub driver: DriverConfig, pub temp_build_dir: PathBuf, - pub dry_run: bool, + pub incremental: bool, } impl Default for Config { @@ -18,13 +18,13 @@ impl Default for Config { Self { driver: DriverConfig::default(), temp_build_dir: PathBuf::from("/tmp/debmagic"), - dry_run: false, + incremental: 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 +40,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 +54,13 @@ 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); + + 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 a663c93..81f8781 100644 --- a/packages/debmagic/src/main.rs +++ b/packages/debmagic/src/main.rs @@ -7,15 +7,26 @@ use anyhow::Context; use clap::{CommandFactory, Parser}; use crate::{ - build::{build_package, common::PackageDescription, 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, }; pub mod build; pub mod cli; pub mod config; +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")); @@ -26,15 +37,14 @@ 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 { 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) } @@ -44,36 +54,57 @@ fn main() -> anyhow::Result<()> { let current_dir = env::current_dir()?; 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 package = PackageDescription { - name: "debmagic".to_string(), - version: "0.1.0".to_string(), - source_dir: path::absolute(source_dir).context("resolving source dir failed")?, + 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(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; + } + 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")?, + )?; let output_dir = args.output_dir.as_deref().unwrap_or(¤t_dir); build_package( &config, &package, args.driver, + &driver_overrides, &path::absolute(output_dir).context("resolving output dir failed")?, + args.distro.as_deref(), ) .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 { - 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 {} => { + Commands::Test(_args) => { println!("Test subcommand! - not implemented"); } - Commands::Check {} => { + Commands::Check(_args) => { println!("Check subcommand! - not implemented"); } Commands::Version {} => { diff --git a/packages/debmagic/src/package.rs b/packages/debmagic/src/package.rs new file mode 100644 index 0000000..66e3d5f --- /dev/null +++ b/packages/debmagic/src/package.rs @@ -0,0 +1,84 @@ +use std::path::{Path, PathBuf}; + +use anyhow::anyhow; + +use debmagic_common::debian::version::PackageVersion; + +#[derive(Debug, Clone)] +pub struct PackageDescription { + pub name: String, + pub version: PackageVersion, + pub source_dir: PathBuf, + pub distro_versions: Vec, +} + +impl PackageDescription { + 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()?; + + 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 = first_entry + .version() + .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, + }) + } +} + +#[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.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/config1.toml b/packages/debmagic/tests/assets/config1.toml index 3978981..5a1496f 100644 --- a/packages/debmagic/tests/assets/config1.toml +++ b/packages/debmagic/tests/assets/config1.toml @@ -1 +1,5 @@ -dry_run = true +[driver] +persistent = true + +[driver.docker] +base_images = { "debian:trixie" = "some-debian-trixie-image:latest" } 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 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 diff --git a/tests/integration/test_packages.py b/tests/integration/test_packages.py index 9e2a793..ea133c8 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( @@ -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", + "--", "build", "--driver", "docker", - "--driver-config.docker.base-image", + "--driver-docker-base-image", test_env.docker_image_name, "--source-dir", str(repo_dir),