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