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