Skip to main content

anodizer_core/config/
nfpm.rs

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