Skip to main content

anodizer_core/
packagers.rs

1//! Packaging-axis config types lifted out of the monolithic
2//! `crate::config` module.
3//!
4//! Currently houses [`MakeselfConfig`] + helpers and [`SrpmConfig`].
5//! The remaining packaging types (`NfpmConfig`, `SnapcraftConfig`,
6//! `FlatpakConfig`, `AppBundleConfig`, `DmgConfig`, `PkgConfig`,
7//! `MsiConfig`, `NsisConfig`) still live in `config.rs` and will move
8//! here in subsequent extraction passes — each with their dedicated
9//! deserializer / `*_schema()` helper alongside.
10//!
11//! Public API path is preserved by re-exports in `config.rs` so consumers
12//! can keep importing from `anodizer_core::config::*`.
13
14use crate::config::{
15    NfpmContent, NfpmSignatureConfig, StringOrBool, deserialize_string_or_bool_opt,
16};
17use schemars::JsonSchema;
18use serde::{Deserialize, Deserializer, Serialize};
19use std::collections::BTreeMap;
20
21// ---------------------------------------------------------------------------
22// MakeselfConfig
23// ---------------------------------------------------------------------------
24
25#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
26#[serde(default, deny_unknown_fields)]
27pub struct MakeselfConfig {
28    /// Unique identifier for this makeself config (default: "default").
29    pub id: Option<String>,
30    /// Build IDs filter: only include artifacts whose `id` is in this list.
31    pub ids: Option<Vec<String>>,
32    /// Output filename template (default includes project, version, os, arch).
33    pub filename: Option<String>,
34    /// Display name embedded in the self-extracting archive.
35    pub name: Option<String>,
36    /// Startup script to run when the archive is extracted and executed.
37    /// Required — the archive will not be created without this.
38    pub script: Option<String>,
39    /// Description for LSM metadata.
40    pub description: Option<String>,
41    /// Maintainer for LSM metadata.
42    pub maintainer: Option<String>,
43    /// Keywords for LSM metadata.
44    pub keywords: Option<Vec<String>>,
45    /// Homepage URL for LSM metadata.
46    pub homepage: Option<String>,
47    /// License for LSM metadata.
48    pub license: Option<String>,
49    /// Compression algorithm: gzip, bzip2, xz, lzo, compress, or none.
50    pub compression: Option<String>,
51    /// Extra arguments passed to the makeself command.
52    pub extra_args: Option<Vec<String>>,
53    /// Additional files to include in the archive.
54    pub files: Option<Vec<MakeselfFile>>,
55    /// Target OS filter (default: ["linux", "darwin"]).
56    pub os: Option<Vec<String>>,
57    /// Target architecture filter.
58    pub arch: Option<Vec<String>>,
59    /// Skip this config. Accepts bool or template string.
60    /// Accepts the legacy `disable:` spelling via serde alias for back-compat
61    /// with imported configs.
62    #[serde(
63        alias = "disable",
64        deserialize_with = "deserialize_string_or_bool_opt",
65        default
66    )]
67    pub skip: Option<StringOrBool>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
71#[serde(default, deny_unknown_fields)]
72pub struct MakeselfFile {
73    /// Source file path (relative to project root).
74    /// Accepts the `src:` spelling via serde alias for back-compat
75    /// with imported configs.
76    #[serde(alias = "src")]
77    pub source: String,
78    /// Destination path inside the archive.
79    /// Accepts the `dst:` spelling via serde alias for back-compat
80    /// with imported configs.
81    #[serde(alias = "dst")]
82    pub destination: Option<String>,
83    /// Strip the parent directory from the source path.
84    pub strip_parent: Option<bool>,
85}
86
87/// Deserialize makeselfs: single object → vec of one, array → vec of many.
88pub(crate) fn deserialize_makeselfs<'de, D>(
89    deserializer: D,
90) -> Result<Vec<MakeselfConfig>, D::Error>
91where
92    D: Deserializer<'de>,
93{
94    use serde::de::{self, Visitor};
95
96    struct MakeselfVisitor;
97
98    impl<'de> Visitor<'de> for MakeselfVisitor {
99        type Value = Vec<MakeselfConfig>;
100
101        fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102            f.write_str("a makeself config object or an array of makeself config objects")
103        }
104
105        fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
106            let mut configs = Vec::new();
107            while let Some(item) = seq.next_element::<MakeselfConfig>()? {
108                configs.push(item);
109            }
110            Ok(configs)
111        }
112
113        fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
114            let config = MakeselfConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
115            Ok(vec![config])
116        }
117
118        fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
119            Ok(Vec::new())
120        }
121
122        fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
123            Ok(Vec::new())
124        }
125    }
126
127    deserializer.deserialize_any(MakeselfVisitor)
128}
129
130pub(crate) fn makeselfs_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
131    let mut schema = generator.subschema_for::<Vec<MakeselfConfig>>();
132    schema.ensure_object().insert(
133        "description".to_owned(),
134        "Makeself self-extracting archive configurations. Accepts a single object or array.".into(),
135    );
136    schema
137}
138
139// ---------------------------------------------------------------------------
140// AppImageConfig
141// ---------------------------------------------------------------------------
142
143/// AppImage packaging configuration.
144///
145/// Drives the [AppImage](https://appimage.org/) stage, which bundles a built
146/// Linux binary plus its desktop integration (a `.desktop` entry + icon) into
147/// a single self-contained, runnable `.AppImage` file via
148/// [`linuxdeploy`](https://github.com/linuxdeploy/linuxdeploy)'s `appimage`
149/// output plugin. One `.AppImage` is produced per matching Linux target so a
150/// multi-arch build yields distinct, non-colliding outputs.
151///
152/// YAML:
153/// ```yaml
154/// appimages:
155///   - id: helix
156///     ids: [helix-bin]
157///     desktop: contrib/Helix.desktop
158///     icon: contrib/helix.png
159///     appdir_extra:
160///       - src: runtime/
161///         dst: usr/lib/helix/runtime
162///     update_information: "gh-releases-zsync|helix-editor|helix|latest|helix-*.AppImage.zsync"
163///     runtime_harvest:
164///       command: "{{ ArtifactPath }} --populate-runtime {{ HarvestDir }}"
165///       dir: runtime/
166/// ```
167#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
168#[serde(default, deny_unknown_fields)]
169pub struct AppImageConfig {
170    /// Unique identifier for this AppImage config (default: "default").
171    pub id: Option<String>,
172    /// Build IDs filter: only bundle binaries whose `id` is in this list.
173    /// When omitted, every Linux binary in the build matrix is eligible.
174    pub ids: Option<Vec<String>>,
175    /// Output filename template (default includes project, version, os, arch).
176    /// The `.AppImage` extension is appended automatically when absent.
177    pub filename: Option<String>,
178    /// Application name passed to linuxdeploy via the `APP` env var and used
179    /// as the AppDir basename. Defaults to the project name.
180    pub name: Option<String>,
181    /// Path to the `.desktop` entry file (template). Required — linuxdeploy
182    /// will not assemble an AppImage without a desktop file.
183    pub desktop: Option<String>,
184    /// Path to the application icon (template). Required.
185    pub icon: Option<String>,
186    /// Extra files / directories copied into the AppDir before linuxdeploy
187    /// runs (e.g. a harvested `runtime/` tree). Each entry's `dst` is
188    /// interpreted relative to the AppDir root.
189    pub appdir_extra: Option<Vec<AppImageExtra>>,
190    /// zsync delta-update metadata embedded in the AppImage, passed to
191    /// linuxdeploy via the `UPDATE_INFORMATION` env var. When omitted, the
192    /// AppImage carries no update information and `UPDATE_INFORMATION` is
193    /// left unset (matching linuxdeploy's default).
194    pub update_information: Option<String>,
195    /// Runtime-asset harvest hook: run the freshly-built binary ONCE on the
196    /// host to populate a directory, then bundle that directory into the
197    /// AppDir. The harvested data is architecture-independent (grammars,
198    /// themes, queries), so it is produced once on the host-native binary and
199    /// reused for every target's AppImage.
200    pub runtime_harvest: Option<RuntimeHarvest>,
201    /// Extra arguments appended to the linuxdeploy command line.
202    pub extra_args: Option<Vec<String>>,
203    /// Target OS filter (default: ["linux"]). AppImage is a Linux-only format.
204    pub os: Option<Vec<String>>,
205    /// Target architecture filter. When omitted, every architecture in the
206    /// build matrix produces its own `.AppImage`.
207    pub arch: Option<Vec<String>>,
208    /// Skip this config. Accepts bool or template string.
209    #[serde(
210        alias = "disable",
211        deserialize_with = "deserialize_string_or_bool_opt",
212        default
213    )]
214    pub skip: Option<StringOrBool>,
215}
216
217/// A file or directory copied into the AppDir before linuxdeploy assembles
218/// the AppImage. Mirrors [`MakeselfFile`]'s `src` / `dst` shape.
219#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
220#[serde(default, deny_unknown_fields)]
221pub struct AppImageExtra {
222    /// Source path (file or directory, relative to project root). A trailing
223    /// `/` is not required; directories are copied recursively.
224    #[serde(alias = "source")]
225    pub src: String,
226    /// Destination path inside the AppDir (relative to the AppDir root, e.g.
227    /// `usr/lib/helix/runtime`).
228    #[serde(alias = "destination")]
229    pub dst: String,
230}
231
232/// Runtime-asset harvest hook for an AppImage config. The `command` template
233/// runs the freshly-built host-native binary to populate `dir`; the resulting
234/// directory is then bundled into the AppDir (and staged at a stable dist
235/// path so an archive `extra_files` glob can reuse it).
236#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
237#[serde(default, deny_unknown_fields)]
238pub struct RuntimeHarvest {
239    /// Command template run once on the host to populate the harvest dir.
240    /// `{{ .ArtifactPath }}` resolves to the host-native binary's path and
241    /// `{{ .HarvestDir }}` to the absolute harvest output directory. Run via
242    /// `sh -c`.
243    pub command: String,
244    /// Directory (relative to the AppDir root) the harvested assets are
245    /// bundled into. Also the AppDir-relative destination for the staged
246    /// host-harvested tree.
247    pub dir: String,
248}
249
250/// Deserialize appimages: single object → vec of one, array → vec of many.
251pub(crate) fn deserialize_appimages<'de, D>(
252    deserializer: D,
253) -> Result<Vec<AppImageConfig>, D::Error>
254where
255    D: Deserializer<'de>,
256{
257    use serde::de::{self, Visitor};
258
259    struct AppImageVisitor;
260
261    impl<'de> Visitor<'de> for AppImageVisitor {
262        type Value = Vec<AppImageConfig>;
263
264        fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265            f.write_str("an appimage config object or an array of appimage config objects")
266        }
267
268        fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
269            let mut configs = Vec::new();
270            while let Some(item) = seq.next_element::<AppImageConfig>()? {
271                configs.push(item);
272            }
273            Ok(configs)
274        }
275
276        fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
277            let config = AppImageConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
278            Ok(vec![config])
279        }
280
281        fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
282            Ok(Vec::new())
283        }
284
285        fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
286            Ok(Vec::new())
287        }
288    }
289
290    deserializer.deserialize_any(AppImageVisitor)
291}
292
293pub(crate) fn appimages_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
294    let mut schema = generator.subschema_for::<Vec<AppImageConfig>>();
295    schema.ensure_object().insert(
296        "description".to_owned(),
297        "AppImage packaging configurations. Accepts a single object or array.".into(),
298    );
299    schema
300}
301
302// ---------------------------------------------------------------------------
303// SrpmConfig
304// ---------------------------------------------------------------------------
305
306#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
307#[serde(default, deny_unknown_fields)]
308pub struct SrpmConfig {
309    /// Enable source RPM generation. Default: false.
310    pub enabled: Option<bool>,
311    /// Package name (default: project_name).
312    pub package_name: Option<String>,
313    /// Output filename template.
314    pub file_name_template: Option<String>,
315    /// Path to the RPM spec file template.
316    pub spec_file: Option<String>,
317    /// RPM epoch.
318    pub epoch: Option<String>,
319    /// RPM section.
320    pub section: Option<String>,
321    /// Package maintainer.
322    pub maintainer: Option<String>,
323    /// Package vendor.
324    pub vendor: Option<String>,
325    /// Summary line.
326    pub summary: Option<String>,
327    /// RPM group.
328    pub group: Option<String>,
329    /// Package description.
330    pub description: Option<String>,
331    /// License identifier.
332    pub license: Option<String>,
333    /// License file name to include.
334    pub license_file_name: Option<String>,
335    /// Homepage URL.
336    pub url: Option<String>,
337    /// RPM packager field.
338    pub packager: Option<String>,
339    /// Compression algorithm (gzip, xz, zstd, none).
340    pub compression: Option<String>,
341    /// Documentation files to include.
342    pub docs: Option<Vec<String>>,
343    /// Additional contents to include in the source RPM. Shares the unified
344    /// [`NfpmContent`] type with nFPM contents; SRPM-style `source:` /
345    /// `destination:` / `type:` keys are accepted via serde aliases.
346    pub contents: Option<Vec<NfpmContent>>,
347    /// RPM signature configuration. Shares the unified
348    /// [`NfpmSignatureConfig`] type with nFPM.
349    pub signature: Option<NfpmSignatureConfig>,
350    /// Map of binary name → install path declared in the spec's `%files`
351    /// section. Each entry tells the generated
352    /// `.spec` which installed file the package owns. When omitted, each
353    /// binary produced by the build for this crate defaults to
354    /// `%{_bindir}/<name>` (i.e. `/usr/bin/<name>`, the RPM-idiomatic
355    /// location for a built binary). Provide this only to override the
356    /// install path or to declare extra owned paths. Stored as a
357    /// `BTreeMap` so the emitted `%files` section iterates in
358    /// deterministic key order.
359    pub bins: Option<BTreeMap<String, String>>,
360    /// Filesystem prefixes the package may install to (RPM `Prefix:` tag).
361    /// Each entry becomes one `Prefix:` directive — relocatable RPMs need
362    /// at least one prefix declared.
363    pub prefixes: Option<Vec<String>>,
364    /// Override the build host recorded in the RPM header. Useful for
365    /// reproducible builds where the actual hostname leaks build-env detail.
366    pub build_host: Option<String>,
367    /// `%pretrans` scriptlet — executed on the package transaction *before*
368    /// any package in the transaction is installed. Path to a script file.
369    pub pretrans: Option<String>,
370    /// `%posttrans` scriptlet — executed *after* all packages in the
371    /// transaction have been installed. Path to a script file.
372    pub posttrans: Option<String>,
373    /// Prerelease suffix appended to the version (e.g. `rc1`, `beta2`).
374    /// Prerelease component of the package version.
375    pub prerelease: Option<String>,
376    /// Build metadata appended to the version (e.g. git commit hash).
377    /// Version-metadata component of the package version.
378    pub version_metadata: Option<String>,
379    /// Skip this config. Accepts bool or template string.
380    #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
381    pub skip: Option<StringOrBool>,
382}
383
384// SRPM signatures share [`NfpmSignatureConfig`]; the SRPM-style
385// `passphrase:` key is accepted as a serde alias for `key_passphrase:`.
386//
387// SRPM contents share [`NfpmContent`]; both the canonical `src` / `dst`
388// keys and the SRPM-style `source` / `destination` aliases parse.
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    /// Wrapper exercising the `deserialize_makeselfs` custom visitor through
395    /// the same `deserialize_with` attribute the real config uses.
396    #[derive(Deserialize)]
397    struct MakeselfWrap {
398        #[serde(deserialize_with = "deserialize_makeselfs", default)]
399        makeselfs: Vec<MakeselfConfig>,
400    }
401
402    #[derive(Deserialize)]
403    struct AppImageWrap {
404        #[serde(deserialize_with = "deserialize_appimages", default)]
405        appimages: Vec<AppImageConfig>,
406    }
407
408    fn makeselfs(yaml: &str) -> Vec<MakeselfConfig> {
409        serde_yaml_ng::from_str::<MakeselfWrap>(yaml)
410            .expect("valid makeselfs YAML")
411            .makeselfs
412    }
413
414    fn appimages(yaml: &str) -> Vec<AppImageConfig> {
415        serde_yaml_ng::from_str::<AppImageWrap>(yaml)
416            .expect("valid appimages YAML")
417            .appimages
418    }
419
420    #[test]
421    fn makeself_single_object_becomes_one_element_vec() {
422        let v = makeselfs("makeselfs:\n  id: solo\n  script: run.sh\n");
423        assert_eq!(v.len(), 1);
424        assert_eq!(v[0].id.as_deref(), Some("solo"));
425        assert_eq!(v[0].script.as_deref(), Some("run.sh"));
426    }
427
428    #[test]
429    fn makeself_array_preserves_order() {
430        let v = makeselfs("makeselfs:\n  - id: a\n  - id: b\n");
431        assert_eq!(v.len(), 2);
432        assert_eq!(v[0].id.as_deref(), Some("a"));
433        assert_eq!(v[1].id.as_deref(), Some("b"));
434    }
435
436    #[test]
437    fn makeself_null_is_empty_vec() {
438        assert!(makeselfs("makeselfs: null").is_empty());
439    }
440
441    #[test]
442    fn makeself_missing_is_empty_vec() {
443        assert!(makeselfs("other: 1").is_empty());
444    }
445
446    #[test]
447    fn makeself_skip_accepts_bool_and_template_via_string_or_bool() {
448        let v = makeselfs("makeselfs:\n  id: x\n  skip: true\n");
449        assert!(v[0].skip.as_ref().unwrap().as_bool());
450
451        let t = makeselfs("makeselfs:\n  id: x\n  skip: \"{{ .IsSnapshot }}\"\n");
452        assert!(t[0].skip.as_ref().unwrap().is_template());
453    }
454
455    #[test]
456    fn makeself_disable_alias_folds_into_skip() {
457        let v = makeselfs("makeselfs:\n  id: x\n  disable: true\n");
458        assert!(v[0].skip.as_ref().unwrap().as_bool());
459    }
460
461    #[test]
462    fn makeself_file_src_dst_aliases_map_to_source_destination() {
463        let v = makeselfs("makeselfs:\n  id: x\n  files:\n    - src: in.txt\n      dst: out.txt\n");
464        let f = &v[0].files.as_ref().unwrap()[0];
465        assert_eq!(f.source, "in.txt");
466        assert_eq!(f.destination.as_deref(), Some("out.txt"));
467    }
468
469    #[test]
470    fn makeself_file_canonical_source_destination_keys_also_parse() {
471        let v = makeselfs("makeselfs:\n  id: x\n  files:\n    - source: a\n      destination: b\n");
472        let f = &v[0].files.as_ref().unwrap()[0];
473        assert_eq!(f.source, "a");
474        assert_eq!(f.destination.as_deref(), Some("b"));
475    }
476
477    #[test]
478    fn makeself_rejects_unknown_field() {
479        let err = serde_yaml_ng::from_str::<MakeselfWrap>("makeselfs:\n  id: x\n  bogus: 1\n");
480        assert!(err.is_err(), "deny_unknown_fields must reject typos");
481    }
482
483    #[test]
484    fn appimage_single_object_becomes_one_element_vec() {
485        let v = appimages("appimages:\n  id: hx\n  desktop: a.desktop\n  icon: a.png\n");
486        assert_eq!(v.len(), 1);
487        assert_eq!(v[0].id.as_deref(), Some("hx"));
488        assert_eq!(v[0].desktop.as_deref(), Some("a.desktop"));
489    }
490
491    #[test]
492    fn appimage_array_and_null_paths() {
493        assert_eq!(appimages("appimages:\n  - id: a\n  - id: b").len(), 2);
494        assert!(appimages("appimages: null").is_empty());
495    }
496
497    #[test]
498    fn appimage_extra_source_destination_aliases_map_to_src_dst() {
499        let v = appimages(
500            "appimages:\n  id: x\n  appdir_extra:\n    - source: runtime/\n      destination: usr/lib/runtime\n",
501        );
502        let extra = &v[0].appdir_extra.as_ref().unwrap()[0];
503        assert_eq!(extra.src, "runtime/");
504        assert_eq!(extra.dst, "usr/lib/runtime");
505    }
506
507    #[test]
508    fn appimage_runtime_harvest_parses_command_and_dir() {
509        let v = appimages(
510            "appimages:\n  id: x\n  runtime_harvest:\n    command: \"{{ .ArtifactPath }} --populate {{ .HarvestDir }}\"\n    dir: runtime/\n",
511        );
512        let rh = v[0].runtime_harvest.as_ref().unwrap();
513        assert!(rh.command.contains("--populate"));
514        assert_eq!(rh.dir, "runtime/");
515    }
516
517    #[test]
518    fn appimage_disable_alias_folds_into_skip() {
519        let v = appimages("appimages:\n  id: x\n  disable: true\n");
520        assert!(v[0].skip.as_ref().unwrap().as_bool());
521    }
522
523    #[test]
524    fn srpm_enabled_defaults_to_none_and_parses_when_set() {
525        let off: SrpmConfig = serde_yaml_ng::from_str("package_name: foo").unwrap();
526        assert!(off.enabled.is_none());
527        let on: SrpmConfig = serde_yaml_ng::from_str("enabled: true").unwrap();
528        assert_eq!(on.enabled, Some(true));
529    }
530
531    #[test]
532    fn srpm_bins_stored_in_deterministic_btreemap_order() {
533        let cfg: SrpmConfig =
534            serde_yaml_ng::from_str("bins:\n  zeta: /usr/bin/zeta\n  alpha: /usr/bin/alpha\n")
535                .unwrap();
536        let keys: Vec<&String> = cfg.bins.as_ref().unwrap().keys().collect();
537        assert_eq!(keys, vec!["alpha", "zeta"], "BTreeMap iterates sorted");
538    }
539
540    #[test]
541    fn srpm_skip_template_string_parses() {
542        let cfg: SrpmConfig = serde_yaml_ng::from_str("skip: \"{{ .Env.SKIP }}\"").unwrap();
543        assert!(cfg.skip.as_ref().unwrap().is_template());
544    }
545
546    #[test]
547    fn srpm_rejects_unknown_field() {
548        assert!(serde_yaml_ng::from_str::<SrpmConfig>("nope: 1").is_err());
549    }
550
551    #[test]
552    fn makeself_empty_array_is_empty_vec() {
553        // visit_seq with zero elements is a distinct branch from visit_unit.
554        assert!(makeselfs("makeselfs: []").is_empty());
555    }
556
557    #[test]
558    fn makeself_full_metadata_block_parses() {
559        let v = makeselfs(
560            "makeselfs:\n  id: x\n  script: run.sh\n  compression: xz\n  os: [linux, darwin]\n  arch: [amd64]\n  extra_args: [--nox11]\n  keywords: [cli, tool]\n",
561        );
562        let m = &v[0];
563        assert_eq!(m.compression.as_deref(), Some("xz"));
564        assert_eq!(
565            m.os.as_deref(),
566            Some(&["linux".to_string(), "darwin".to_string()][..])
567        );
568        assert_eq!(m.arch.as_deref(), Some(&["amd64".to_string()][..]));
569        assert_eq!(m.extra_args.as_deref(), Some(&["--nox11".to_string()][..]));
570        assert_eq!(
571            m.keywords.as_deref(),
572            Some(&["cli".to_string(), "tool".to_string()][..])
573        );
574    }
575
576    #[test]
577    fn makeself_file_strip_parent_round_trips() {
578        let v = makeselfs(
579            "makeselfs:\n  id: x\n  files:\n    - source: a/b\n      strip_parent: true\n",
580        );
581        assert_eq!(v[0].files.as_ref().unwrap()[0].strip_parent, Some(true));
582    }
583
584    #[test]
585    fn makeself_file_rejects_unknown_field() {
586        let err = serde_yaml_ng::from_str::<MakeselfWrap>(
587            "makeselfs:\n  id: x\n  files:\n    - source: a\n      bogus: 1\n",
588        );
589        assert!(err.is_err(), "MakeselfFile must deny unknown fields");
590    }
591
592    #[test]
593    fn appimage_empty_array_is_empty_vec() {
594        assert!(appimages("appimages: []").is_empty());
595    }
596
597    #[test]
598    fn appimage_update_information_and_os_arch_filters_parse() {
599        let v = appimages(
600            "appimages:\n  id: x\n  update_information: \"gh-releases-zsync|me|app|latest|app-*.AppImage.zsync\"\n  os: [linux]\n  arch: [amd64, arm64]\n",
601        );
602        let a = &v[0];
603        assert!(
604            a.update_information
605                .as_deref()
606                .unwrap()
607                .starts_with("gh-releases-zsync")
608        );
609        assert_eq!(a.os.as_deref(), Some(&["linux".to_string()][..]));
610        assert_eq!(
611            a.arch.as_deref(),
612            Some(&["amd64".to_string(), "arm64".to_string()][..])
613        );
614    }
615
616    #[test]
617    fn appimage_extra_rejects_unknown_field() {
618        let err = serde_yaml_ng::from_str::<AppImageWrap>(
619            "appimages:\n  id: x\n  appdir_extra:\n    - src: a\n      dst: b\n      bogus: 1\n",
620        );
621        assert!(err.is_err(), "AppImageExtra must deny unknown fields");
622    }
623
624    #[test]
625    fn appimage_runtime_harvest_rejects_unknown_field() {
626        let err = serde_yaml_ng::from_str::<AppImageWrap>(
627            "appimages:\n  id: x\n  runtime_harvest:\n    command: c\n    dir: d\n    bogus: 1\n",
628        );
629        assert!(err.is_err(), "RuntimeHarvest must deny unknown fields");
630    }
631
632    #[test]
633    fn appimage_skip_template_marks_template_not_bool() {
634        let v = appimages("appimages:\n  id: x\n  skip: \"{{ .IsSnapshot }}\"\n");
635        let skip = v[0].skip.as_ref().unwrap();
636        assert!(skip.is_template());
637        assert!(
638            !skip.as_bool(),
639            "an unrendered template is not a literal true"
640        );
641    }
642
643    #[test]
644    fn srpm_contents_and_prefixes_and_scriptlets_parse() {
645        let cfg: SrpmConfig = serde_yaml_ng::from_str(
646            "enabled: true\nprefixes: [/usr, /etc]\npretrans: scripts/pre.sh\nposttrans: scripts/post.sh\ncontents:\n  - src: a.conf\n    dst: /etc/a.conf\n    type: config\n",
647        )
648        .unwrap();
649        assert_eq!(
650            cfg.prefixes.as_deref(),
651            Some(&["/usr".to_string(), "/etc".to_string()][..])
652        );
653        assert_eq!(cfg.pretrans.as_deref(), Some("scripts/pre.sh"));
654        assert_eq!(cfg.posttrans.as_deref(), Some("scripts/post.sh"));
655        let contents = cfg.contents.as_ref().unwrap();
656        assert_eq!(contents[0].src, "a.conf");
657        assert_eq!(contents[0].dst, "/etc/a.conf");
658        assert_eq!(contents[0].content_type.as_deref(), Some("config"));
659    }
660
661    #[test]
662    fn srpm_version_components_parse() {
663        let cfg: SrpmConfig =
664            serde_yaml_ng::from_str("prerelease: rc1\nversion_metadata: gitabc123\n").unwrap();
665        assert_eq!(cfg.prerelease.as_deref(), Some("rc1"));
666        assert_eq!(cfg.version_metadata.as_deref(), Some("gitabc123"));
667    }
668}