1use 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#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
26#[serde(default, deny_unknown_fields)]
27pub struct MakeselfConfig {
28 pub id: Option<String>,
30 pub ids: Option<Vec<String>>,
32 pub filename: Option<String>,
34 pub name: Option<String>,
36 pub script: Option<String>,
39 pub description: Option<String>,
41 pub maintainer: Option<String>,
43 pub keywords: Option<Vec<String>>,
45 pub homepage: Option<String>,
47 pub license: Option<String>,
49 pub compression: Option<String>,
51 pub extra_args: Option<Vec<String>>,
53 pub files: Option<Vec<MakeselfFile>>,
55 pub os: Option<Vec<String>>,
57 pub arch: Option<Vec<String>>,
59 #[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 #[serde(alias = "src")]
77 pub source: String,
78 #[serde(alias = "dst")]
82 pub destination: Option<String>,
83 pub strip_parent: Option<bool>,
85}
86
87pub(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#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
160#[serde(default, deny_unknown_fields)]
161pub struct InstallScriptConfig {
162 pub id: Option<String>,
164 pub filename: Option<String>,
166 pub binaries: Option<Vec<String>>,
170 pub repo: Option<String>,
173 pub base_url: Option<String>,
179 pub verify_checksum: Option<bool>,
183 pub install_dir: Option<String>,
187 pub name: Option<String>,
190 pub description: Option<String>,
192 pub homepage: Option<String>,
194 #[serde(
198 alias = "disable",
199 deserialize_with = "deserialize_string_or_bool_opt",
200 default
201 )]
202 pub skip: Option<StringOrBool>,
203}
204
205pub(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#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
291#[serde(default, deny_unknown_fields)]
292pub struct AppImageConfig {
293 pub id: Option<String>,
295 pub ids: Option<Vec<String>>,
298 pub filename: Option<String>,
301 pub name: Option<String>,
304 pub desktop: Option<String>,
307 pub icon: Option<String>,
309 pub appdir_extra: Option<Vec<AppImageExtra>>,
313 pub update_information: Option<String>,
318 pub runtime_harvest: Option<RuntimeHarvest>,
324 pub extra_args: Option<Vec<String>>,
326 pub os: Option<Vec<String>>,
328 pub arch: Option<Vec<String>>,
331 #[serde(
333 alias = "disable",
334 deserialize_with = "deserialize_string_or_bool_opt",
335 default
336 )]
337 pub skip: Option<StringOrBool>,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
343#[serde(default, deny_unknown_fields)]
344pub struct AppImageExtra {
345 #[serde(alias = "source")]
348 pub src: String,
349 #[serde(alias = "destination")]
352 pub dst: String,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
360#[serde(default, deny_unknown_fields)]
361pub struct RuntimeHarvest {
362 pub command: String,
367 pub dir: String,
371}
372
373pub(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#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
430#[serde(default, deny_unknown_fields)]
431pub struct SrpmConfig {
432 pub enabled: Option<bool>,
434 pub package_name: Option<String>,
436 pub file_name_template: Option<String>,
438 pub spec_file: Option<String>,
440 pub epoch: Option<String>,
442 pub section: Option<String>,
444 pub maintainer: Option<String>,
446 pub vendor: Option<String>,
448 pub summary: Option<String>,
450 pub group: Option<String>,
452 pub description: Option<String>,
454 pub license: Option<String>,
456 pub license_file_name: Option<String>,
458 pub url: Option<String>,
460 pub packager: Option<String>,
462 pub compression: Option<String>,
464 pub docs: Option<Vec<String>>,
466 pub contents: Option<Vec<NfpmContent>>,
470 pub signature: Option<NfpmSignatureConfig>,
473 pub bins: Option<BTreeMap<String, String>>,
483 pub prefixes: Option<Vec<String>>,
487 pub build_host: Option<String>,
490 pub pretrans: Option<String>,
493 pub posttrans: Option<String>,
496 pub prerelease: Option<String>,
499 pub version_metadata: Option<String>,
502 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
504 pub skip: Option<StringOrBool>,
505}
506
507#[cfg(test)]
514mod tests {
515 use super::*;
516
517 #[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 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}