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(
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#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
172#[serde(default, deny_unknown_fields)]
173pub struct AppImageConfig {
174 pub id: Option<String>,
176 pub ids: Option<Vec<String>>,
179 pub filename: Option<String>,
182 pub name: Option<String>,
185 pub desktop: Option<String>,
188 pub icon: Option<String>,
190 pub appdir_extra: Option<Vec<AppImageExtra>>,
194 pub update_information: Option<String>,
199 pub runtime_harvest: Option<RuntimeHarvest>,
205 pub extra_args: Option<Vec<String>>,
207 pub os: Option<Vec<String>>,
209 pub arch: Option<Vec<String>>,
212 #[serde(
214 alias = "disable",
215 deserialize_with = "deserialize_string_or_bool_opt",
216 default
217 )]
218 pub skip: Option<StringOrBool>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
224#[serde(default, deny_unknown_fields)]
225pub struct AppImageExtra {
226 #[serde(alias = "source")]
229 pub src: String,
230 #[serde(alias = "destination")]
233 pub dst: String,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
241#[serde(default, deny_unknown_fields)]
242pub struct RuntimeHarvest {
243 pub command: String,
248 pub dir: String,
252}
253
254pub(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#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
313#[serde(default, deny_unknown_fields)]
314pub struct SrpmConfig {
315 pub enabled: Option<bool>,
317 pub package_name: Option<String>,
319 pub file_name_template: Option<String>,
321 pub spec_file: Option<String>,
323 pub epoch: Option<String>,
325 pub section: Option<String>,
327 pub maintainer: Option<String>,
329 pub vendor: Option<String>,
331 pub summary: Option<String>,
333 pub group: Option<String>,
335 pub description: Option<String>,
337 pub license: Option<String>,
339 pub license_file_name: Option<String>,
341 pub url: Option<String>,
343 pub packager: Option<String>,
345 pub compression: Option<String>,
347 pub docs: Option<Vec<String>>,
349 pub contents: Option<Vec<NfpmContent>>,
353 pub signature: Option<NfpmSignatureConfig>,
356 pub bins: Option<BTreeMap<String, String>>,
366 pub prefixes: Option<Vec<String>>,
370 pub build_host: Option<String>,
373 pub pretrans: Option<String>,
376 pub posttrans: Option<String>,
379 pub prerelease: Option<String>,
382 pub version_metadata: Option<String>,
385 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
387 pub skip: Option<StringOrBool>,
388}
389
390#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[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 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}