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}