anodizer_core/config/nfpm.rs
1use std::collections::HashMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use super::{FileInfo, StringOrU32};
7
8// ---------------------------------------------------------------------------
9// NfpmConfig
10// ---------------------------------------------------------------------------
11
12#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
13#[serde(default, deny_unknown_fields)]
14pub struct NfpmConfig {
15 /// Unique identifier for cross-referencing this nFPM config.
16 pub id: Option<String>,
17 /// Package name (defaults to crate name).
18 pub package_name: Option<String>,
19 /// Package formats to produce: deb, rpm, apk, archlinux (at least one required).
20 pub formats: Vec<String>,
21 /// Package vendor name — the distributing entity recorded in the
22 /// rpm/deb Vendor field. When unset, derived from the crate's first
23 /// `Cargo.toml [package].authors` entry with any `<email>` suffix
24 /// stripped (e.g. `"Ada Lovelace <ada@x>"` → `"Ada Lovelace"`).
25 pub vendor: Option<String>,
26 /// Project homepage URL.
27 pub homepage: Option<String>,
28 /// Package maintainer in "Name <email>" format.
29 pub maintainer: Option<String>,
30 /// Package description (multiline supported).
31 pub description: Option<String>,
32 /// SPDX license identifier (e.g., "MIT", "Apache-2.0").
33 pub license: Option<String>,
34 /// Installation directory for binaries (default: /usr/bin).
35 pub bindir: Option<String>,
36 /// Rename the installed binary inside the package only.
37 ///
38 /// When set, the auto-emitted binary content entry is installed under this
39 /// name (in `bindir`) instead of the built file's name; the archive/build
40 /// output is untouched. Use this to resolve Debian/RPM name clashes — e.g.
41 /// `fd` ships its binary as `fdfind` in the Debian package while the tarball
42 /// keeps `fd`. Templated.
43 pub bin_alias: Option<String>,
44 /// Files to include in the package beyond the main binary.
45 pub contents: Option<Vec<NfpmContent>>,
46 /// Runtime package dependencies keyed by format (e.g., {"deb": ["libc6"], "rpm": ["glibc"]}).
47 pub dependencies: Option<HashMap<String, Vec<String>>>,
48 /// Per-format setting overrides (e.g., {"deb": {compression: "xz"}}).
49 pub overrides: Option<HashMap<String, serde_json::Value>>,
50 /// Package filename template (supports templates).
51 pub file_name_template: Option<String>,
52 /// Package lifecycle scripts (preinstall, postinstall, preremove, postremove).
53 pub scripts: Option<NfpmScripts>,
54 /// Packages recommended (soft dependency) by this package.
55 pub recommends: Option<Vec<String>>,
56 /// Packages suggested (weaker than recommends) by this package.
57 pub suggests: Option<Vec<String>>,
58 /// Packages this package conflicts with.
59 pub conflicts: Option<Vec<String>>,
60 /// Packages this package replaces (for upgrade paths from old package names).
61 pub replaces: Option<Vec<String>>,
62 /// Virtual packages provided by this package.
63 pub provides: Option<Vec<String>>,
64 /// Build IDs filter: only include artifacts from builds whose `id` is in this list.
65 /// Accepts the deprecated `builds:` spelling via serde alias for
66 /// back-compat with imported configs (the legacy `builds` key
67 /// marked `deprecated`, aliasing `ids`).
68 #[serde(alias = "builds")]
69 pub ids: Option<Vec<String>>,
70 /// amd64 microarchitecture variant filter (`["v1"]`, `["v2", "v3"]`, etc.),
71 /// set via the `amd64_variant:` key. When set, only amd64 binaries with
72 /// `amd64_variant` matching one of the listed values are included. The
73 /// legacy `goamd64:` spelling is accepted via serde alias for back-compat
74 /// with imported configs. When unset, all amd64 variants are included (no
75 /// filtering).
76 #[serde(alias = "goamd64")]
77 pub amd64_variant: Option<Vec<String>>,
78 /// Package epoch for versioning (integer as string).
79 pub epoch: Option<String>,
80 /// Package release number.
81 pub release: Option<String>,
82 /// Prerelease version suffix.
83 pub prerelease: Option<String>,
84 /// Version metadata (e.g. git commit hash).
85 pub version_metadata: Option<String>,
86 /// Package section (e.g. "utils", "devel").
87 pub section: Option<String>,
88 /// Package priority (e.g. "optional", "required").
89 pub priority: Option<String>,
90 /// Whether this is a meta-package (no files, only dependencies).
91 pub meta: Option<bool>,
92 /// File permission umask. Accepts a YAML int (`18`), an octal-prefixed
93 /// string (`"0o022"`), or a leading-zero octal string (`"022"`).
94 pub umask: Option<StringOrU32>,
95 /// Default modification time for files in the package.
96 pub mtime: Option<String>,
97 /// RPM-specific configuration.
98 pub rpm: Option<NfpmRpmConfig>,
99 /// Deb-specific configuration.
100 pub deb: Option<NfpmDebConfig>,
101 /// APK-specific configuration.
102 pub apk: Option<NfpmApkConfig>,
103 /// Archlinux-specific configuration.
104 pub archlinux: Option<NfpmArchlinuxConfig>,
105 /// IPK-specific configuration (OpenWrt packages).
106 pub ipk: Option<NfpmIpkConfig>,
107 /// CGo library installation directories (header, carchive, cshared).
108 pub libdirs: Option<NfpmLibdirs>,
109 /// Path to a YAML-format changelog file for deb/rpm packages.
110 pub changelog: Option<String>,
111 /// Template-conditional: skip this nfpm config if rendered result is "false" or empty.
112 /// Conditional-skip gate.
113 #[serde(rename = "if")]
114 pub if_condition: Option<String>,
115 /// Extra file contents whose source files are Tera-rendered before packaging.
116 /// Each entry mirrors `contents`; the difference is that at stage time the file at `src` is
117 /// read, rendered through the template engine, written to a temp file, and then included
118 /// in the package at `dst` using the temp file as the real source. Useful for shipping
119 /// config files with templated values (version, commit, maintainer, etc.).
120 pub templated_contents: Option<Vec<NfpmContent>>,
121 /// Lifecycle scripts whose script-file bodies are Tera-rendered before packaging
122 /// Each path is read, rendered through the template engine, written to
123 /// a temp file, and used as the real script. If a field is set on both `scripts` and
124 /// `templated_scripts`, the templated version wins.
125 pub templated_scripts: Option<NfpmScripts>,
126}
127
128/// Installation directories for CGo library outputs.
129///
130/// Controls where header files, static archives, and shared libraries
131/// are installed in the package.
132#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
133#[serde(default, deny_unknown_fields)]
134pub struct NfpmLibdirs {
135 /// Installation directory for C header files.
136 pub header: Option<String>,
137 /// Installation directory for carchive (.a) static libraries.
138 pub carchive: Option<String>,
139 /// Installation directory for cshared (.so / .dylib) shared libraries.
140 pub cshared: Option<String>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
144#[serde(default, deny_unknown_fields)]
145pub struct NfpmScripts {
146 /// Path to script run before package installation.
147 pub preinstall: Option<String>,
148 /// Path to script run after package installation.
149 pub postinstall: Option<String>,
150 /// Path to script run before package removal.
151 pub preremove: Option<String>,
152 /// Path to script run after package removal.
153 pub postremove: Option<String>,
154}
155
156/// Backward-compatible alias — nFPM contents share the same `FileInfo` struct.
157pub type NfpmFileInfo = FileInfo;
158
159/// A single file/directory entry in an nFPM (or SRPM) package's `contents`
160/// list. Merged the formerly-separate `NfpmContentConfig`
161/// (used for SRPM) into this struct — `source` / `destination` / `type` are
162/// accepted as aliases for `src` / `dst` / the renamed `type` so srpm-style
163/// keys still parse.
164///
165/// `Default` is intentionally **not** derived because `src` and `dst` are
166/// required fields with no meaningful defaults — forcing callers to provide
167/// them explicitly prevents accidentally packaging empty paths.
168#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
169#[serde(deny_unknown_fields)]
170pub struct NfpmContent {
171 /// Source path on the build machine (supports glob patterns and templates).
172 ///
173 /// Paths are resolved relative to the project root. `..` segments are
174 /// NOT stripped, so a templated value resolving to `../../etc/passwd`
175 /// will reach outside the project tree — avoid splicing untrusted
176 /// template inputs (e.g. arbitrary `{{ Env.X }}` values) into `src`.
177 pub src: String,
178 /// Destination path inside the package (absolute path, supports templates).
179 ///
180 /// Same caveat as `src`: `..` segments are passed through to nfpm
181 /// verbatim. Templated values from untrusted sources should be
182 /// canonicalised by the caller before use.
183 pub dst: String,
184 /// Content entry type: "config", "config|noreplace", "doc", "dir", "symlink", "ghost", or empty for regular file.
185 #[serde(rename = "type")]
186 pub content_type: Option<String>,
187 /// File ownership and permission metadata.
188 pub file_info: Option<NfpmFileInfo>,
189 /// Per-packager filter: only include this content entry for the specified packager
190 /// (e.g. "deb", "rpm", "apk").
191 pub packager: Option<String>,
192 /// When true, expand template variables in the `src` and `dst` paths.
193 pub expand: Option<bool>,
194}
195
196// ---------------------------------------------------------------------------
197// nFPM format-specific configs
198// ---------------------------------------------------------------------------
199
200#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
201#[serde(default, deny_unknown_fields)]
202pub struct NfpmRpmConfig {
203 /// One-line package summary (RPM Summary tag).
204 pub summary: Option<String>,
205 /// RPM compression algorithm (e.g. "lzma", "gzip", "xz", "zstd").
206 pub compression: Option<String>,
207 /// RPM group classification (e.g. "System/Tools").
208 pub group: Option<String>,
209 /// RPM packager identity (e.g. "Build Team <build@example.com>").
210 pub packager: Option<String>,
211 /// Relocatable RPM prefix paths (e.g. ["/usr", "/etc"]).
212 pub prefixes: Option<Vec<String>>,
213 /// RPM signing configuration.
214 pub signature: Option<NfpmSignatureConfig>,
215 /// RPM-specific lifecycle scripts (pretrans/posttrans).
216 pub scripts: Option<NfpmRpmScripts>,
217 /// RPM BuildHost tag value.
218 pub build_host: Option<String>,
219}
220
221/// RPM-specific transaction scripts that run outside the normal install/remove lifecycle.
222#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
223#[serde(default, deny_unknown_fields)]
224pub struct NfpmRpmScripts {
225 /// Script to run before the RPM transaction begins.
226 pub pretrans: Option<String>,
227 /// Script to run after the RPM transaction completes.
228 pub posttrans: Option<String>,
229}
230
231impl NfpmRpmConfig {
232 /// Returns `true` when every field is `None` — the YAML section would be
233 /// empty and should be omitted.
234 pub fn is_empty(&self) -> bool {
235 self.summary.is_none()
236 && self.compression.is_none()
237 && self.group.is_none()
238 && self.packager.is_none()
239 && self.prefixes.is_none()
240 && self.signature.is_none()
241 && self.scripts.is_none()
242 && self.build_host.is_none()
243 }
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
247#[serde(default, deny_unknown_fields)]
248pub struct NfpmDebConfig {
249 /// Deb compression algorithm (e.g. "gzip", "xz", "zstd", "none").
250 pub compression: Option<String>,
251 /// Pre-dependency packages (stronger than Depends).
252 pub predepends: Option<Vec<String>>,
253 /// Deb trigger definitions.
254 pub triggers: Option<NfpmDebTriggers>,
255 /// Packages this package breaks (Breaks relationship).
256 pub breaks: Option<Vec<String>>,
257 /// Lintian overrides to embed in the package.
258 pub lintian_overrides: Option<Vec<String>>,
259 /// Deb signing configuration.
260 pub signature: Option<NfpmSignatureConfig>,
261 /// Additional control fields (e.g. Bugs, Built-Using).
262 pub fields: Option<HashMap<String, String>>,
263 /// Deb-specific maintainer scripts (rules, templates, config).
264 pub scripts: Option<NfpmDebScripts>,
265 /// Target architecture variant in deb nomenclature (e.g. `amd64v3`).
266 ///
267 /// Auto-derived from the built binary's `amd64_variant` (`v1`..`v4`) GOAMD64
268 /// microarchitecture metadata when unset, so an amd64 deb is tagged with the
269 /// microarch it was compiled for. Maps to nfpm's `deb.arch_variant`.
270 pub arch_variant: Option<String>,
271}
272
273/// Deb-specific maintainer scripts for package configuration and rules.
274#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
275#[serde(default, deny_unknown_fields)]
276pub struct NfpmDebScripts {
277 /// Path to debian/rules file.
278 pub rules: Option<String>,
279 /// Path to debian/templates file (debconf templates).
280 pub templates: Option<String>,
281 /// Path to debian/config script (debconf configuration).
282 pub config: Option<String>,
283}
284
285impl NfpmDebConfig {
286 /// Returns `true` when every field is `None` — the YAML section would be
287 /// empty and should be omitted.
288 pub fn is_empty(&self) -> bool {
289 self.compression.is_none()
290 && self.predepends.is_none()
291 && self.triggers.is_none()
292 && self.breaks.is_none()
293 && self.lintian_overrides.is_none()
294 && self.signature.is_none()
295 && self.fields.is_none()
296 && self.scripts.is_none()
297 // `arch_variant` keeps the deb block alive when set alone: a config
298 // carrying only `arch_variant: v3` must not be dropped as "empty",
299 // which would silently lose the microarch tag (`amd64v3` → plain
300 // `amd64`).
301 && self.arch_variant.is_none()
302 }
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
306#[serde(default, deny_unknown_fields)]
307pub struct NfpmDebTriggers {
308 /// Deb interest triggers: package waits for these triggers to complete.
309 pub interest: Option<Vec<String>>,
310 /// Deb interest-await triggers: package waits with synchronous trigger processing.
311 pub interest_await: Option<Vec<String>>,
312 /// Deb interest-noawait triggers: package registers interest without waiting.
313 pub interest_noawait: Option<Vec<String>>,
314 /// Deb activate triggers: package activates these triggers after install.
315 pub activate: Option<Vec<String>>,
316 /// Deb activate-await triggers: activate and wait for synchronous trigger processing.
317 pub activate_await: Option<Vec<String>>,
318 /// Deb activate-noawait triggers: activate without waiting.
319 pub activate_noawait: Option<Vec<String>>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
323#[serde(default, deny_unknown_fields)]
324pub struct NfpmApkConfig {
325 /// APK signing configuration.
326 pub signature: Option<NfpmSignatureConfig>,
327 /// APK-specific lifecycle scripts (preupgrade/postupgrade).
328 pub scripts: Option<NfpmApkScripts>,
329}
330
331/// APK-specific upgrade lifecycle scripts.
332#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
333#[serde(default, deny_unknown_fields)]
334pub struct NfpmApkScripts {
335 /// Script to run before upgrading an existing package.
336 pub preupgrade: Option<String>,
337 /// Script to run after upgrading an existing package.
338 pub postupgrade: Option<String>,
339}
340
341impl NfpmApkConfig {
342 /// Returns `true` when every field is `None` — the YAML section would be
343 /// empty and should be omitted.
344 pub fn is_empty(&self) -> bool {
345 self.signature.is_none() && self.scripts.is_none()
346 }
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
350#[serde(default, deny_unknown_fields)]
351pub struct NfpmArchlinuxConfig {
352 /// Base package name for split packages.
353 pub pkgbase: Option<String>,
354 /// Packager identity (e.g. "Build Team <build@example.com>").
355 pub packager: Option<String>,
356 /// Archlinux-specific lifecycle scripts.
357 pub scripts: Option<NfpmArchlinuxScripts>,
358}
359
360impl NfpmArchlinuxConfig {
361 /// Returns `true` when every field is `None` — the YAML section would be
362 /// empty and should be omitted.
363 pub fn is_empty(&self) -> bool {
364 self.pkgbase.is_none() && self.packager.is_none() && self.scripts.is_none()
365 }
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
369#[serde(default, deny_unknown_fields)]
370pub struct NfpmArchlinuxScripts {
371 /// Script to run before upgrading an existing package.
372 pub preupgrade: Option<String>,
373 /// Script to run after upgrading an existing package.
374 pub postupgrade: Option<String>,
375}
376
377/// IPK (OpenWrt) package-specific configuration.
378#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
379#[serde(default, deny_unknown_fields)]
380pub struct NfpmIpkConfig {
381 /// ABI version string for the package.
382 pub abi_version: Option<String>,
383 /// Alternative file links managed by the update-alternatives system.
384 pub alternatives: Option<Vec<NfpmIpkAlternative>>,
385 /// Whether the package was automatically installed as a dependency.
386 pub auto_installed: Option<bool>,
387 /// Whether the package is essential for the system.
388 pub essential: Option<bool>,
389 /// Strong pre-dependencies that must be fully installed before this package.
390 pub predepends: Option<Vec<String>>,
391 /// Tags for categorizing the package.
392 pub tags: Option<Vec<String>>,
393 /// Additional control fields as key-value pairs.
394 pub fields: Option<HashMap<String, String>>,
395}
396
397impl NfpmIpkConfig {
398 /// Returns `true` when every field is `None` — the YAML section would be
399 /// empty and should be omitted.
400 pub fn is_empty(&self) -> bool {
401 self.abi_version.is_none()
402 && self.alternatives.is_none()
403 && self.auto_installed.is_none()
404 && self.essential.is_none()
405 && self.predepends.is_none()
406 && self.tags.is_none()
407 && self.fields.is_none()
408 }
409}
410
411/// An alternative file link for IPK's update-alternatives system.
412#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
413#[serde(default, deny_unknown_fields)]
414pub struct NfpmIpkAlternative {
415 /// Priority for alternative selection (higher wins).
416 pub priority: Option<i32>,
417 /// Target file path that the alternative points to.
418 pub target: Option<String>,
419 /// Symlink name in the alternatives directory.
420 pub link_name: Option<String>,
421}
422
423#[cfg(test)]
424mod is_empty_tests {
425 use super::*;
426
427 /// `arch_variant` is the load-bearing single field that, when set in
428 /// isolation, must keep the deb block alive — otherwise the "drop empty
429 /// blocks" path silently dropped microarch tagging (`amd64v3` collapsing to
430 /// plain `amd64`).
431 #[test]
432 fn deb_arch_variant_alone_is_not_empty() {
433 let cfg = NfpmDebConfig {
434 arch_variant: Some("v3".to_string()),
435 ..Default::default()
436 };
437 assert!(
438 !cfg.is_empty(),
439 "deb block with only arch_variant must NOT be dropped"
440 );
441 }
442
443 /// Sanity: a fully empty deb block IS empty.
444 #[test]
445 fn deb_default_is_empty() {
446 assert!(NfpmDebConfig::default().is_empty());
447 }
448
449 /// The legacy `goamd64:` spelling folds into `amd64_variant` so imported
450 /// configs keep parsing under `deny_unknown_fields`.
451 #[test]
452 fn nfpm_goamd64_alias_parses_into_amd64_variant() {
453 let nfpm: NfpmConfig =
454 serde_yaml_ng::from_str("formats: [deb]\ngoamd64: [v2, v3]").unwrap();
455 assert_eq!(
456 nfpm.amd64_variant.as_deref(),
457 Some(["v2".to_string(), "v3".to_string()].as_slice())
458 );
459 }
460
461 #[test]
462 fn nfpm_canonical_amd64_variant_still_parses() {
463 let nfpm: NfpmConfig =
464 serde_yaml_ng::from_str("formats: [deb]\namd64_variant: [v1]").unwrap();
465 assert_eq!(
466 nfpm.amd64_variant.as_deref(),
467 Some(["v1".to_string()].as_slice())
468 );
469 }
470}
471
472/// Unified signature configuration shared by nFPM (deb/rpm/apk) and SRPM
473/// packages — SRPM's surface is a strict subset, so a single struct covers
474/// both. The legacy SRPM `passphrase:` key is accepted as a serde alias
475/// for `key_passphrase:` so both spellings parse.
476///
477/// There are three distinct signature types (`NFPMRPMSignature`,
478/// `NFPMDebSignature`, `NFPMAPKSignature`) with overlapping but slightly
479/// different fields. Anodizer's union here avoids the 3-struct cascade
480/// when 90% of fields overlap.
481#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
482#[serde(default, deny_unknown_fields)]
483pub struct NfpmSignatureConfig {
484 /// Path to the signing key file.
485 pub key_file: Option<String>,
486 /// Key ID to use for signing.
487 pub key_id: Option<String>,
488 /// Passphrase for the signing key. Falls back to `NFPM_PASSPHRASE` /
489 /// `SRPM_PASSPHRASE` env vars in their respective stages.
490 pub key_passphrase: Option<String>,
491 /// Public key name for APK signatures (defaults to `<maintainer email>.rsa.pub`).
492 pub key_name: Option<String>,
493 /// Signature type for deb packages: "origin", "maint", or "archive" (default: "origin").
494 #[serde(rename = "type")]
495 pub type_: Option<String>,
496}