Skip to main content

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