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}