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