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, termux.deb, ipk,
20    /// msix (at least one required).
21    pub formats: Vec<String>,
22    /// Package vendor name — the distributing entity recorded in the
23    /// rpm/deb Vendor field. When unset, derived from the crate's first
24    /// `Cargo.toml [package].authors` entry with any `<email>` suffix
25    /// stripped (e.g. `"Ada Lovelace <ada@x>"` → `"Ada Lovelace"`).
26    pub vendor: Option<String>,
27    /// Project homepage URL.
28    pub homepage: Option<String>,
29    /// Package maintainer in `Name <email>` format.
30    pub maintainer: Option<String>,
31    /// Package description (multiline supported).
32    pub description: Option<String>,
33    /// SPDX license identifier (e.g., "MIT", "Apache-2.0").
34    pub license: Option<String>,
35    /// Installation directory for binaries (default: /usr/bin).
36    pub bindir: Option<String>,
37    /// Rename the installed binary inside the package only.
38    ///
39    /// When set, the auto-emitted binary content entry is installed under this
40    /// name (in `bindir`) instead of the built file's name; the archive/build
41    /// output is untouched. Use this to resolve Debian/RPM name clashes — e.g.
42    /// `fd` ships its binary as `fdfind` in the Debian package while the tarball
43    /// keeps `fd`. Templated.
44    pub bin_alias: Option<String>,
45    /// Files to include in the package beyond the main binary.
46    pub contents: Option<Vec<NfpmContent>>,
47    /// Runtime package dependencies keyed by format (e.g., {"deb": ["libc6"], "rpm": ["glibc"]}).
48    pub dependencies: Option<HashMap<String, Vec<String>>>,
49    /// Per-format setting overrides (e.g., {"deb": {compression: "xz"}}).
50    pub overrides: Option<HashMap<String, serde_json::Value>>,
51    /// Package filename template (supports templates).
52    pub file_name_template: Option<String>,
53    /// Package lifecycle scripts (preinstall, postinstall, preremove, postremove).
54    pub scripts: Option<NfpmScripts>,
55    /// Packages recommended (soft dependency) by this package.
56    pub recommends: Option<Vec<String>>,
57    /// Packages suggested (weaker than recommends) by this package.
58    pub suggests: Option<Vec<String>>,
59    /// Packages this package conflicts with.
60    pub conflicts: Option<Vec<String>>,
61    /// Packages this package replaces (for upgrade paths from old package names).
62    pub replaces: Option<Vec<String>>,
63    /// Virtual packages provided by this package.
64    pub provides: Option<Vec<String>>,
65    /// Build IDs filter: only include artifacts from builds whose `id` is in this list.
66    /// Accepts the deprecated `builds:` spelling via serde alias for
67    /// back-compat with imported configs (the legacy `builds` key
68    /// marked `deprecated`, aliasing `ids`).
69    #[serde(alias = "builds")]
70    pub ids: Option<Vec<String>>,
71    /// amd64 microarchitecture variant filter (`["v1"]`, `["v2", "v3"]`, etc.),
72    /// set via the `amd64_variant:` key. When set, only amd64 binaries with
73    /// `amd64_variant` matching one of the listed values are included. The
74    /// legacy `goamd64:` spelling is accepted via serde alias for back-compat
75    /// with imported configs. When unset, all amd64 variants are included (no
76    /// filtering).
77    /// Each entry is typed as [`Amd64Variant`], so any value outside
78    /// `v1`..`v4` is rejected when the config is parsed.
79    #[serde(alias = "goamd64")]
80    pub amd64_variant: Option<Vec<Amd64Variant>>,
81    /// Package epoch for versioning (integer as string).
82    pub epoch: Option<String>,
83    /// Package release number.
84    pub release: Option<String>,
85    /// Prerelease version suffix.
86    pub prerelease: Option<String>,
87    /// Version metadata (e.g. git commit hash).
88    pub version_metadata: Option<String>,
89    /// Package section (e.g. "utils", "devel").
90    pub section: Option<String>,
91    /// Package priority (e.g. "optional", "required").
92    pub priority: Option<String>,
93    /// Whether this is a meta-package (no files, only dependencies).
94    pub meta: Option<bool>,
95    /// File permission umask. Accepts a YAML int (`18`), an octal-prefixed
96    /// string (`"0o022"`), or a leading-zero octal string (`"022"`).
97    pub umask: Option<StringOrU32>,
98    /// Default modification time for files in the package.
99    pub mtime: Option<String>,
100    /// RPM-specific configuration.
101    pub rpm: Option<NfpmRpmConfig>,
102    /// Deb-specific configuration.
103    pub deb: Option<NfpmDebConfig>,
104    /// APK-specific configuration.
105    pub apk: Option<NfpmApkConfig>,
106    /// Archlinux-specific configuration.
107    pub archlinux: Option<NfpmArchlinuxConfig>,
108    /// IPK-specific configuration (OpenWrt packages).
109    pub ipk: Option<NfpmIpkConfig>,
110    /// MSIX-specific configuration (Windows app packages).
111    ///
112    /// Only consumed when `formats` includes `msix`. nfpm requires
113    /// `publisher`, `properties.logo`, and at least one `applications` entry;
114    /// everything else has derived defaults.
115    ///
116    /// ```yaml
117    /// msix:
118    ///   publisher: "CN=My Company, O=My Company, C=US"
119    ///   properties:
120    ///     logo: assets/logo.png
121    ///   applications:
122    ///     - id: MyApp
123    ///       executable: myapp.exe
124    /// ```
125    pub msix: Option<NfpmMsixConfig>,
126    /// CGo library installation directories (header, carchive, cshared).
127    pub libdirs: Option<NfpmLibdirs>,
128    /// Path to a YAML-format changelog file for deb/rpm packages.
129    pub changelog: Option<String>,
130    /// Template-conditional: skip this nfpm config if rendered result is "false" or empty.
131    /// Conditional-skip gate.
132    #[serde(rename = "if")]
133    pub if_condition: Option<String>,
134    /// Extra file contents whose source files are Tera-rendered before packaging.
135    /// Each entry mirrors `contents`; the difference is that at stage time the file at `src` is
136    /// read, rendered through the template engine, written to a temp file, and then included
137    /// in the package at `dst` using the temp file as the real source. Useful for shipping
138    /// config files with templated values (version, commit, maintainer, etc.).
139    pub templated_contents: Option<Vec<NfpmContent>>,
140    /// Lifecycle scripts whose script-file bodies are Tera-rendered before packaging
141    /// Each path is read, rendered through the template engine, written to
142    /// a temp file, and used as the real script. If a field is set on both `scripts` and
143    /// `templated_scripts`, the templated version wins.
144    pub templated_scripts: Option<NfpmScripts>,
145}
146
147/// Installation directories for CGo library outputs.
148///
149/// Controls where header files, static archives, and shared libraries
150/// are installed in the package.
151#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
152#[serde(default, deny_unknown_fields)]
153pub struct NfpmLibdirs {
154    /// Installation directory for C header files.
155    pub header: Option<String>,
156    /// Installation directory for carchive (.a) static libraries.
157    pub carchive: Option<String>,
158    /// Installation directory for cshared (.so / .dylib) shared libraries.
159    pub cshared: Option<String>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
163#[serde(default, deny_unknown_fields)]
164pub struct NfpmScripts {
165    /// Path to script run before package installation.
166    pub preinstall: Option<String>,
167    /// Path to script run after package installation.
168    pub postinstall: Option<String>,
169    /// Path to script run before package removal.
170    pub preremove: Option<String>,
171    /// Path to script run after package removal.
172    pub postremove: Option<String>,
173}
174
175/// Backward-compatible alias — nFPM contents share the same `FileInfo` struct.
176pub type NfpmFileInfo = FileInfo;
177
178/// A single file/directory entry in an nFPM (or SRPM) package's `contents`
179/// list. Merged the formerly-separate `NfpmContentConfig`
180/// (used for SRPM) into this struct — `source` / `destination` / `type` are
181/// accepted as aliases for `src` / `dst` / the renamed `type` so srpm-style
182/// keys still parse.
183///
184/// `Default` is intentionally **not** derived because `src` and `dst` are
185/// required fields with no meaningful defaults — forcing callers to provide
186/// them explicitly prevents accidentally packaging empty paths.
187#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
188#[serde(deny_unknown_fields)]
189pub struct NfpmContent {
190    /// Source path on the build machine (supports glob patterns and templates).
191    ///
192    /// Paths are resolved relative to the project root. `..` segments are
193    /// NOT stripped, so a templated value resolving to `../../etc/passwd`
194    /// will reach outside the project tree — avoid splicing untrusted
195    /// template inputs (e.g. arbitrary `{{ Env.X }}` values) into `src`.
196    pub src: String,
197    /// Destination path inside the package (absolute path, supports templates).
198    ///
199    /// Same caveat as `src`: `..` segments are passed through to nfpm
200    /// verbatim. Templated values from untrusted sources should be
201    /// canonicalised by the caller before use.
202    pub dst: String,
203    /// Content entry type: "config", "config|noreplace", "doc", "dir", "symlink", "ghost", or empty for regular file.
204    #[serde(rename = "type")]
205    pub content_type: Option<String>,
206    /// File ownership and permission metadata.
207    pub file_info: Option<NfpmFileInfo>,
208    /// Per-packager filter: only include this content entry for the specified packager
209    /// (e.g. "deb", "rpm", "apk").
210    pub packager: Option<String>,
211    /// When true, expand template variables in the `src` and `dst` paths.
212    pub expand: Option<bool>,
213}
214
215// ---------------------------------------------------------------------------
216// nFPM format-specific configs
217// ---------------------------------------------------------------------------
218
219#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
220#[serde(default, deny_unknown_fields)]
221pub struct NfpmRpmConfig {
222    /// One-line package summary (RPM Summary tag).
223    pub summary: Option<String>,
224    /// RPM compression algorithm (e.g. "lzma", "gzip", "xz", "zstd").
225    pub compression: Option<String>,
226    /// RPM group classification (e.g. "System/Tools").
227    pub group: Option<String>,
228    /// RPM packager identity (e.g. "Build Team <build@example.com>").
229    pub packager: Option<String>,
230    /// Relocatable RPM prefix paths (e.g. ["/usr", "/etc"]).
231    pub prefixes: Option<Vec<String>>,
232    /// RPM signing configuration.
233    pub signature: Option<NfpmSignatureConfig>,
234    /// RPM-specific lifecycle scripts (pretrans/posttrans).
235    pub scripts: Option<NfpmRpmScripts>,
236    /// RPM BuildHost tag value.
237    pub build_host: Option<String>,
238}
239
240/// RPM-specific transaction scripts that run outside the normal install/remove lifecycle.
241#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
242#[serde(default, deny_unknown_fields)]
243pub struct NfpmRpmScripts {
244    /// Script to run before the RPM transaction begins.
245    pub pretrans: Option<String>,
246    /// Script to run after the RPM transaction completes.
247    pub posttrans: Option<String>,
248}
249
250impl NfpmRpmConfig {
251    /// Returns `true` when every field is `None` — the YAML section would be
252    /// empty and should be omitted.
253    pub fn is_empty(&self) -> bool {
254        self.summary.is_none()
255            && self.compression.is_none()
256            && self.group.is_none()
257            && self.packager.is_none()
258            && self.prefixes.is_none()
259            && self.signature.is_none()
260            && self.scripts.is_none()
261            && self.build_host.is_none()
262    }
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
266#[serde(default, deny_unknown_fields)]
267pub struct NfpmDebConfig {
268    /// Deb compression algorithm (e.g. "gzip", "xz", "zstd", "none").
269    pub compression: Option<String>,
270    /// Pre-dependency packages (stronger than Depends).
271    pub predepends: Option<Vec<String>>,
272    /// Deb trigger definitions.
273    pub triggers: Option<NfpmDebTriggers>,
274    /// Packages this package breaks (Breaks relationship).
275    pub breaks: Option<Vec<String>>,
276    /// Lintian overrides to embed in the package.
277    pub lintian_overrides: Option<Vec<String>>,
278    /// Deb signing configuration.
279    pub signature: Option<NfpmSignatureConfig>,
280    /// Additional control fields (e.g. Bugs, Built-Using).
281    pub fields: Option<HashMap<String, String>>,
282    /// Deb-specific maintainer scripts (rules, templates, config).
283    pub scripts: Option<NfpmDebScripts>,
284    /// Target architecture variant in deb nomenclature (e.g. `amd64v3`).
285    ///
286    /// Auto-derived from the built binary's `amd64_variant` (`v1`..`v4`) GOAMD64
287    /// microarchitecture metadata when unset, so an amd64 deb is tagged with the
288    /// microarch it was compiled for. Maps to nfpm's `deb.arch_variant`.
289    pub arch_variant: Option<String>,
290}
291
292/// Deb-specific maintainer scripts for package configuration and rules.
293#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
294#[serde(default, deny_unknown_fields)]
295pub struct NfpmDebScripts {
296    /// Path to debian/rules file.
297    pub rules: Option<String>,
298    /// Path to debian/templates file (debconf templates).
299    pub templates: Option<String>,
300    /// Path to debian/config script (debconf configuration).
301    pub config: Option<String>,
302}
303
304impl NfpmDebConfig {
305    /// Returns `true` when every field is `None` — the YAML section would be
306    /// empty and should be omitted.
307    pub fn is_empty(&self) -> bool {
308        self.compression.is_none()
309            && self.predepends.is_none()
310            && self.triggers.is_none()
311            && self.breaks.is_none()
312            && self.lintian_overrides.is_none()
313            && self.signature.is_none()
314            && self.fields.is_none()
315            && self.scripts.is_none()
316            // `arch_variant` keeps the deb block alive when set alone: a config
317            // carrying only `arch_variant: v3` must not be dropped as "empty",
318            // which would silently lose the microarch tag (`amd64v3` → plain
319            // `amd64`).
320            && self.arch_variant.is_none()
321    }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
325#[serde(default, deny_unknown_fields)]
326pub struct NfpmDebTriggers {
327    /// Deb interest triggers: package waits for these triggers to complete.
328    pub interest: Option<Vec<String>>,
329    /// Deb interest-await triggers: package waits with synchronous trigger processing.
330    pub interest_await: Option<Vec<String>>,
331    /// Deb interest-noawait triggers: package registers interest without waiting.
332    pub interest_noawait: Option<Vec<String>>,
333    /// Deb activate triggers: package activates these triggers after install.
334    pub activate: Option<Vec<String>>,
335    /// Deb activate-await triggers: activate and wait for synchronous trigger processing.
336    pub activate_await: Option<Vec<String>>,
337    /// Deb activate-noawait triggers: activate without waiting.
338    pub activate_noawait: Option<Vec<String>>,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
342#[serde(default, deny_unknown_fields)]
343pub struct NfpmApkConfig {
344    /// APK signing configuration.
345    pub signature: Option<NfpmSignatureConfig>,
346    /// APK-specific lifecycle scripts (preupgrade/postupgrade).
347    pub scripts: Option<NfpmApkScripts>,
348}
349
350/// APK-specific upgrade lifecycle scripts.
351#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
352#[serde(default, deny_unknown_fields)]
353pub struct NfpmApkScripts {
354    /// Script to run before upgrading an existing package.
355    pub preupgrade: Option<String>,
356    /// Script to run after upgrading an existing package.
357    pub postupgrade: Option<String>,
358}
359
360impl NfpmApkConfig {
361    /// Returns `true` when every field is `None` — the YAML section would be
362    /// empty and should be omitted.
363    pub fn is_empty(&self) -> bool {
364        self.signature.is_none() && self.scripts.is_none()
365    }
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
369#[serde(default, deny_unknown_fields)]
370pub struct NfpmArchlinuxConfig {
371    /// Base package name for split packages.
372    pub pkgbase: Option<String>,
373    /// Packager identity (e.g. "Build Team <build@example.com>").
374    pub packager: Option<String>,
375    /// Archlinux-specific lifecycle scripts.
376    pub scripts: Option<NfpmArchlinuxScripts>,
377}
378
379impl NfpmArchlinuxConfig {
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.pkgbase.is_none() && self.packager.is_none() && self.scripts.is_none()
384    }
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
388#[serde(default, deny_unknown_fields)]
389pub struct NfpmArchlinuxScripts {
390    /// Script to run before upgrading an existing package.
391    pub preupgrade: Option<String>,
392    /// Script to run after upgrading an existing package.
393    pub postupgrade: Option<String>,
394}
395
396/// IPK (OpenWrt) package-specific configuration.
397#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
398#[serde(default, deny_unknown_fields)]
399pub struct NfpmIpkConfig {
400    /// ABI version string for the package.
401    pub abi_version: Option<String>,
402    /// Alternative file links managed by the update-alternatives system.
403    pub alternatives: Option<Vec<NfpmIpkAlternative>>,
404    /// Whether the package was automatically installed as a dependency.
405    pub auto_installed: Option<bool>,
406    /// Whether the package is essential for the system.
407    pub essential: Option<bool>,
408    /// Strong pre-dependencies that must be fully installed before this package.
409    pub predepends: Option<Vec<String>>,
410    /// Tags for categorizing the package.
411    pub tags: Option<Vec<String>>,
412    /// Additional control fields as key-value pairs.
413    pub fields: Option<HashMap<String, String>>,
414}
415
416impl NfpmIpkConfig {
417    /// Returns `true` when every field is `None` — the YAML section would be
418    /// empty and should be omitted.
419    pub fn is_empty(&self) -> bool {
420        self.abi_version.is_none()
421            && self.alternatives.is_none()
422            && self.auto_installed.is_none()
423            && self.essential.is_none()
424            && self.predepends.is_none()
425            && self.tags.is_none()
426            && self.fields.is_none()
427    }
428}
429
430/// MSIX (Windows app package) specific configuration.
431///
432/// Field names mirror nfpm's `msix:` YAML block. nfpm validates that
433/// `publisher`, `properties.logo`, and at least one application (with `id`
434/// and `executable`) are set; the remaining fields have derived defaults
435/// (e.g. `entry_point` defaults to `Windows.FullTrustApplication`, display
436/// names default to the package name, and `dependencies` defaults to a
437/// `Windows.Desktop` 10.0.17763.0–10.0.22621.0 target device family).
438#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
439#[serde(default, deny_unknown_fields)]
440pub struct NfpmMsixConfig {
441    /// Target architecture override in MSIX nomenclature (`x64`, `x86`,
442    /// `arm64`, `arm`, `neutral`). When unset, derived from the build target
443    /// (e.g. `x86_64` → `x64`).
444    pub arch: Option<String>,
445    /// Publisher identity, matching the signing certificate subject
446    /// (e.g. `"CN=My Company, O=My Company, C=US"`). Required by nfpm.
447    /// Templated.
448    pub publisher: Option<String>,
449    /// Package identity fields.
450    pub identity: Option<NfpmMsixIdentity>,
451    /// Package display properties.
452    pub properties: Option<NfpmMsixProperties>,
453    /// Applications contained in the package (nfpm requires at least one,
454    /// each with `id` and `executable`). When omitted, anodizer derives one
455    /// application per packaged binary — `executable` is the binary's file
456    /// name and `id` its sanitized file stem — so this only needs setting to
457    /// override entry points or visual elements.
458    pub applications: Option<Vec<NfpmMsixApplication>>,
459    /// Target device family dependencies. Defaults to
460    /// `Windows.Desktop` min `10.0.17763.0` / max tested `10.0.22621.0`.
461    pub dependencies: Option<NfpmMsixDependencies>,
462    /// Capability declarations for the package.
463    pub capabilities: Option<NfpmMsixCapabilities>,
464    /// MSIX signing configuration.
465    pub signature: Option<NfpmMsixSignature>,
466}
467
468impl NfpmMsixConfig {
469    /// Returns `true` when every field is `None` — the YAML section would be
470    /// empty and should be omitted.
471    pub fn is_empty(&self) -> bool {
472        self.arch.is_none()
473            && self.publisher.is_none()
474            && self.identity.is_none()
475            && self.properties.is_none()
476            && self.applications.is_none()
477            && self.dependencies.is_none()
478            && self.capabilities.is_none()
479            && self.signature.is_none()
480    }
481}
482
483/// Identity fields for MSIX packages.
484#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
485#[serde(default, deny_unknown_fields)]
486pub struct NfpmMsixIdentity {
487    /// Resource identifier for the package identity (e.g. `"en-us"`).
488    pub resource_id: Option<String>,
489}
490
491/// Display properties for MSIX packages.
492#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
493#[serde(default, deny_unknown_fields)]
494pub struct NfpmMsixProperties {
495    /// Package display name (defaults to the package name). Templated.
496    pub display_name: Option<String>,
497    /// Publisher display name (defaults to the package name). Templated.
498    pub publisher_display_name: Option<String>,
499    /// Path to the package logo image (e.g. `assets/logo.png`). Required by
500    /// nfpm. Templated.
501    pub logo: Option<String>,
502}
503
504/// An application entry in an MSIX package.
505#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
506#[serde(default, deny_unknown_fields)]
507pub struct NfpmMsixApplication {
508    /// Application identifier (e.g. `"MyApp"`). Required by nfpm.
509    pub id: Option<String>,
510    /// Executable path inside the package (e.g. `myapp.exe`). Required by nfpm.
511    pub executable: Option<String>,
512    /// Application entry point (default: `Windows.FullTrustApplication`).
513    pub entry_point: Option<String>,
514    /// Visual presentation settings for this application.
515    pub visual_elements: Option<NfpmMsixVisualElements>,
516}
517
518/// Visual presentation settings for an MSIX application.
519#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
520#[serde(default, deny_unknown_fields)]
521pub struct NfpmMsixVisualElements {
522    /// Application display name (defaults to the package name).
523    pub display_name: Option<String>,
524    /// Application description (defaults to the package description).
525    pub description: Option<String>,
526    /// Tile background color (default: `transparent`).
527    pub background_color: Option<String>,
528    /// Path to the 150x150 tile logo (defaults to `properties.logo`).
529    pub square150x150_logo: Option<String>,
530    /// Path to the 44x44 tile logo (defaults to `properties.logo`).
531    pub square44x44_logo: Option<String>,
532}
533
534/// Dependency information for MSIX packages.
535#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
536#[serde(default, deny_unknown_fields)]
537pub struct NfpmMsixDependencies {
538    /// Target device families the package supports.
539    pub target_device_families: Option<Vec<NfpmMsixTargetDeviceFamily>>,
540}
541
542/// A target device family for an MSIX package.
543#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
544#[serde(default, deny_unknown_fields)]
545pub struct NfpmMsixTargetDeviceFamily {
546    /// Device family name (e.g. `"Windows.Desktop"`).
547    pub name: Option<String>,
548    /// Minimum OS version (e.g. `"10.0.17763.0"`).
549    pub min_version: Option<String>,
550    /// Maximum tested OS version (e.g. `"10.0.22621.0"`).
551    pub max_version_tested: Option<String>,
552}
553
554/// Capability declarations for MSIX packages.
555#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
556#[serde(default, deny_unknown_fields)]
557pub struct NfpmMsixCapabilities {
558    /// General capabilities (e.g. `["internetClient"]`).
559    pub capabilities: Option<Vec<String>>,
560    /// Device capabilities (e.g. `["microphone"]`).
561    pub device_capabilities: Option<Vec<String>>,
562    /// Restricted capabilities (e.g. `["runFullTrust"]` — added
563    /// automatically when an application uses the full-trust entry point).
564    pub restricted: Option<Vec<String>>,
565}
566
567/// Signing configuration for MSIX packages.
568///
569/// The passphrase is NOT a config field: nfpm reads it from the
570/// `NFPM_MSIX_PASSPHRASE` environment variable, which anodizer resolves via
571/// the same `NFPM_{ID}_MSIX_PASSPHRASE` → `NFPM_{ID}_PASSPHRASE` →
572/// `NFPM_PASSPHRASE` fallback the other signature blocks use and forwards to
573/// the nfpm subprocess.
574#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
575#[serde(default, deny_unknown_fields)]
576pub struct NfpmMsixSignature {
577    /// Path to the PFX certificate file used to sign the package. Templated.
578    pub pfx_file: Option<String>,
579}
580
581/// An alternative file link for IPK's update-alternatives system.
582#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
583#[serde(default, deny_unknown_fields)]
584pub struct NfpmIpkAlternative {
585    /// Priority for alternative selection (higher wins).
586    pub priority: Option<i32>,
587    /// Target file path that the alternative points to.
588    pub target: Option<String>,
589    /// Symlink name in the alternatives directory.
590    pub link_name: Option<String>,
591}
592
593#[cfg(test)]
594mod is_empty_tests {
595    use super::*;
596
597    /// `arch_variant` is the load-bearing single field that, when set in
598    /// isolation, must keep the deb block alive — otherwise the "drop empty
599    /// blocks" path silently dropped microarch tagging (`amd64v3` collapsing to
600    /// plain `amd64`).
601    #[test]
602    fn deb_arch_variant_alone_is_not_empty() {
603        let cfg = NfpmDebConfig {
604            arch_variant: Some("v3".to_string()),
605            ..Default::default()
606        };
607        assert!(
608            !cfg.is_empty(),
609            "deb block with only arch_variant must NOT be dropped"
610        );
611    }
612
613    /// Sanity: a fully empty deb block IS empty.
614    #[test]
615    fn deb_default_is_empty() {
616        assert!(NfpmDebConfig::default().is_empty());
617    }
618
619    /// A default MSIX block is empty (every field None) so the "drop empty
620    /// blocks" path omits the section; a single populated field keeps it alive.
621    #[test]
622    fn msix_default_is_empty_but_one_field_keeps_it() {
623        assert!(NfpmMsixConfig::default().is_empty());
624        let with_arch = NfpmMsixConfig {
625            arch: Some("x64".to_string()),
626            ..Default::default()
627        };
628        assert!(
629            !with_arch.is_empty(),
630            "an MSIX block with a set field must NOT be dropped"
631        );
632        let with_publisher = NfpmMsixConfig {
633            publisher: Some("CN=Acme".to_string()),
634            ..Default::default()
635        };
636        assert!(!with_publisher.is_empty());
637    }
638
639    /// The legacy `goamd64:` spelling folds into `amd64_variant` so imported
640    /// configs keep parsing under `deny_unknown_fields`.
641    #[test]
642    fn nfpm_goamd64_alias_parses_into_amd64_variant() {
643        let nfpm: NfpmConfig =
644            serde_yaml_ng::from_str("formats: [deb]\ngoamd64: [v2, v3]").unwrap();
645        assert_eq!(
646            nfpm.amd64_variant.as_deref(),
647            Some([Amd64Variant::V2, Amd64Variant::V3].as_slice())
648        );
649    }
650
651    #[test]
652    fn nfpm_canonical_amd64_variant_still_parses() {
653        let nfpm: NfpmConfig =
654            serde_yaml_ng::from_str("formats: [deb]\namd64_variant: [v1]").unwrap();
655        assert_eq!(
656            nfpm.amd64_variant.as_deref(),
657            Some([Amd64Variant::V1].as_slice())
658        );
659    }
660
661    /// A typo'd level in the list form must fail at parse — the field is a
662    /// closed enum, not free text, so `x86-64-v3` cannot silently disable
663    /// the variant filter.
664    #[test]
665    fn nfpm_amd64_variant_list_rejects_garbage_at_parse() {
666        let err =
667            serde_yaml_ng::from_str::<NfpmConfig>("formats: [deb]\namd64_variant: [v2, x86-64-v3]")
668                .unwrap_err()
669                .to_string();
670        assert!(
671            err.contains("unknown variant `x86-64-v3`")
672                && err.contains("expected one of `v1`, `v2`, `v3`, `v4`"),
673            "parse error must name the bad value and the valid set: {err}"
674        );
675    }
676}
677
678/// Unified signature configuration shared by nFPM (deb/rpm/apk) and SRPM
679/// packages — SRPM's surface is a strict subset, so a single struct covers
680/// both. The legacy SRPM `passphrase:` key is accepted as a serde alias
681/// for `key_passphrase:` so both spellings parse.
682///
683/// There are three distinct signature types (`NFPMRPMSignature`,
684/// `NFPMDebSignature`, `NFPMAPKSignature`) with overlapping but slightly
685/// different fields. Anodizer's union here avoids the 3-struct cascade
686/// when 90% of fields overlap.
687#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
688#[serde(default, deny_unknown_fields)]
689pub struct NfpmSignatureConfig {
690    /// Path to the signing key file.
691    pub key_file: Option<String>,
692    /// Key ID to use for signing.
693    pub key_id: Option<String>,
694    /// Passphrase for the signing key. Falls back to `NFPM_PASSPHRASE` /
695    /// `SRPM_PASSPHRASE` env vars in their respective stages.
696    pub key_passphrase: Option<String>,
697    /// Public key name for APK signatures (defaults to `<maintainer email>.rsa.pub`).
698    pub key_name: Option<String>,
699    /// Signature type for deb packages: "origin", "maint", or "archive" (default: "origin").
700    #[serde(rename = "type")]
701    pub type_: Option<String>,
702}