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)]
168#[serde(default, deny_unknown_fields)]
169pub struct AppImageConfig {
170 pub id: Option<String>,
172 pub ids: Option<Vec<String>>,
175 pub filename: Option<String>,
178 pub name: Option<String>,
181 pub desktop: Option<String>,
184 pub icon: Option<String>,
186 pub appdir_extra: Option<Vec<AppImageExtra>>,
190 pub update_information: Option<String>,
195 pub runtime_harvest: Option<RuntimeHarvest>,
201 pub extra_args: Option<Vec<String>>,
203 pub os: Option<Vec<String>>,
205 pub arch: Option<Vec<String>>,
208 #[serde(
210 alias = "disable",
211 deserialize_with = "deserialize_string_or_bool_opt",
212 default
213 )]
214 pub skip: Option<StringOrBool>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
220#[serde(default, deny_unknown_fields)]
221pub struct AppImageExtra {
222 #[serde(alias = "source")]
225 pub src: String,
226 #[serde(alias = "destination")]
229 pub dst: String,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
237#[serde(default, deny_unknown_fields)]
238pub struct RuntimeHarvest {
239 pub command: String,
244 pub dir: String,
248}
249
250pub(crate) fn deserialize_appimages<'de, D>(
252 deserializer: D,
253) -> Result<Vec<AppImageConfig>, D::Error>
254where
255 D: Deserializer<'de>,
256{
257 use serde::de::{self, Visitor};
258
259 struct AppImageVisitor;
260
261 impl<'de> Visitor<'de> for AppImageVisitor {
262 type Value = Vec<AppImageConfig>;
263
264 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 f.write_str("an appimage config object or an array of appimage config objects")
266 }
267
268 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
269 let mut configs = Vec::new();
270 while let Some(item) = seq.next_element::<AppImageConfig>()? {
271 configs.push(item);
272 }
273 Ok(configs)
274 }
275
276 fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
277 let config = AppImageConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
278 Ok(vec![config])
279 }
280
281 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
282 Ok(Vec::new())
283 }
284
285 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
286 Ok(Vec::new())
287 }
288 }
289
290 deserializer.deserialize_any(AppImageVisitor)
291}
292
293pub(crate) fn appimages_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
294 let mut schema = generator.subschema_for::<Vec<AppImageConfig>>();
295 schema.ensure_object().insert(
296 "description".to_owned(),
297 "AppImage packaging configurations. Accepts a single object or array.".into(),
298 );
299 schema
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
307#[serde(default, deny_unknown_fields)]
308pub struct SrpmConfig {
309 pub enabled: Option<bool>,
311 pub package_name: Option<String>,
313 pub file_name_template: Option<String>,
315 pub spec_file: Option<String>,
317 pub epoch: Option<String>,
319 pub section: Option<String>,
321 pub maintainer: Option<String>,
323 pub vendor: Option<String>,
325 pub summary: Option<String>,
327 pub group: Option<String>,
329 pub description: Option<String>,
331 pub license: Option<String>,
333 pub license_file_name: Option<String>,
335 pub url: Option<String>,
337 pub packager: Option<String>,
339 pub compression: Option<String>,
341 pub docs: Option<Vec<String>>,
343 pub contents: Option<Vec<NfpmContent>>,
347 pub signature: Option<NfpmSignatureConfig>,
350 pub bins: Option<BTreeMap<String, String>>,
360 pub prefixes: Option<Vec<String>>,
364 pub build_host: Option<String>,
367 pub pretrans: Option<String>,
370 pub posttrans: Option<String>,
373 pub prerelease: Option<String>,
376 pub version_metadata: Option<String>,
379 #[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
381 pub skip: Option<StringOrBool>,
382}
383
384#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[derive(Deserialize)]
397 struct MakeselfWrap {
398 #[serde(deserialize_with = "deserialize_makeselfs", default)]
399 makeselfs: Vec<MakeselfConfig>,
400 }
401
402 #[derive(Deserialize)]
403 struct AppImageWrap {
404 #[serde(deserialize_with = "deserialize_appimages", default)]
405 appimages: Vec<AppImageConfig>,
406 }
407
408 fn makeselfs(yaml: &str) -> Vec<MakeselfConfig> {
409 serde_yaml_ng::from_str::<MakeselfWrap>(yaml)
410 .expect("valid makeselfs YAML")
411 .makeselfs
412 }
413
414 fn appimages(yaml: &str) -> Vec<AppImageConfig> {
415 serde_yaml_ng::from_str::<AppImageWrap>(yaml)
416 .expect("valid appimages YAML")
417 .appimages
418 }
419
420 #[test]
421 fn makeself_single_object_becomes_one_element_vec() {
422 let v = makeselfs("makeselfs:\n id: solo\n script: run.sh\n");
423 assert_eq!(v.len(), 1);
424 assert_eq!(v[0].id.as_deref(), Some("solo"));
425 assert_eq!(v[0].script.as_deref(), Some("run.sh"));
426 }
427
428 #[test]
429 fn makeself_array_preserves_order() {
430 let v = makeselfs("makeselfs:\n - id: a\n - id: b\n");
431 assert_eq!(v.len(), 2);
432 assert_eq!(v[0].id.as_deref(), Some("a"));
433 assert_eq!(v[1].id.as_deref(), Some("b"));
434 }
435
436 #[test]
437 fn makeself_null_is_empty_vec() {
438 assert!(makeselfs("makeselfs: null").is_empty());
439 }
440
441 #[test]
442 fn makeself_missing_is_empty_vec() {
443 assert!(makeselfs("other: 1").is_empty());
444 }
445
446 #[test]
447 fn makeself_skip_accepts_bool_and_template_via_string_or_bool() {
448 let v = makeselfs("makeselfs:\n id: x\n skip: true\n");
449 assert!(v[0].skip.as_ref().unwrap().as_bool());
450
451 let t = makeselfs("makeselfs:\n id: x\n skip: \"{{ .IsSnapshot }}\"\n");
452 assert!(t[0].skip.as_ref().unwrap().is_template());
453 }
454
455 #[test]
456 fn makeself_disable_alias_folds_into_skip() {
457 let v = makeselfs("makeselfs:\n id: x\n disable: true\n");
458 assert!(v[0].skip.as_ref().unwrap().as_bool());
459 }
460
461 #[test]
462 fn makeself_file_src_dst_aliases_map_to_source_destination() {
463 let v = makeselfs("makeselfs:\n id: x\n files:\n - src: in.txt\n dst: out.txt\n");
464 let f = &v[0].files.as_ref().unwrap()[0];
465 assert_eq!(f.source, "in.txt");
466 assert_eq!(f.destination.as_deref(), Some("out.txt"));
467 }
468
469 #[test]
470 fn makeself_file_canonical_source_destination_keys_also_parse() {
471 let v = makeselfs("makeselfs:\n id: x\n files:\n - source: a\n destination: b\n");
472 let f = &v[0].files.as_ref().unwrap()[0];
473 assert_eq!(f.source, "a");
474 assert_eq!(f.destination.as_deref(), Some("b"));
475 }
476
477 #[test]
478 fn makeself_rejects_unknown_field() {
479 let err = serde_yaml_ng::from_str::<MakeselfWrap>("makeselfs:\n id: x\n bogus: 1\n");
480 assert!(err.is_err(), "deny_unknown_fields must reject typos");
481 }
482
483 #[test]
484 fn appimage_single_object_becomes_one_element_vec() {
485 let v = appimages("appimages:\n id: hx\n desktop: a.desktop\n icon: a.png\n");
486 assert_eq!(v.len(), 1);
487 assert_eq!(v[0].id.as_deref(), Some("hx"));
488 assert_eq!(v[0].desktop.as_deref(), Some("a.desktop"));
489 }
490
491 #[test]
492 fn appimage_array_and_null_paths() {
493 assert_eq!(appimages("appimages:\n - id: a\n - id: b").len(), 2);
494 assert!(appimages("appimages: null").is_empty());
495 }
496
497 #[test]
498 fn appimage_extra_source_destination_aliases_map_to_src_dst() {
499 let v = appimages(
500 "appimages:\n id: x\n appdir_extra:\n - source: runtime/\n destination: usr/lib/runtime\n",
501 );
502 let extra = &v[0].appdir_extra.as_ref().unwrap()[0];
503 assert_eq!(extra.src, "runtime/");
504 assert_eq!(extra.dst, "usr/lib/runtime");
505 }
506
507 #[test]
508 fn appimage_runtime_harvest_parses_command_and_dir() {
509 let v = appimages(
510 "appimages:\n id: x\n runtime_harvest:\n command: \"{{ .ArtifactPath }} --populate {{ .HarvestDir }}\"\n dir: runtime/\n",
511 );
512 let rh = v[0].runtime_harvest.as_ref().unwrap();
513 assert!(rh.command.contains("--populate"));
514 assert_eq!(rh.dir, "runtime/");
515 }
516
517 #[test]
518 fn appimage_disable_alias_folds_into_skip() {
519 let v = appimages("appimages:\n id: x\n disable: true\n");
520 assert!(v[0].skip.as_ref().unwrap().as_bool());
521 }
522
523 #[test]
524 fn srpm_enabled_defaults_to_none_and_parses_when_set() {
525 let off: SrpmConfig = serde_yaml_ng::from_str("package_name: foo").unwrap();
526 assert!(off.enabled.is_none());
527 let on: SrpmConfig = serde_yaml_ng::from_str("enabled: true").unwrap();
528 assert_eq!(on.enabled, Some(true));
529 }
530
531 #[test]
532 fn srpm_bins_stored_in_deterministic_btreemap_order() {
533 let cfg: SrpmConfig =
534 serde_yaml_ng::from_str("bins:\n zeta: /usr/bin/zeta\n alpha: /usr/bin/alpha\n")
535 .unwrap();
536 let keys: Vec<&String> = cfg.bins.as_ref().unwrap().keys().collect();
537 assert_eq!(keys, vec!["alpha", "zeta"], "BTreeMap iterates sorted");
538 }
539
540 #[test]
541 fn srpm_skip_template_string_parses() {
542 let cfg: SrpmConfig = serde_yaml_ng::from_str("skip: \"{{ .Env.SKIP }}\"").unwrap();
543 assert!(cfg.skip.as_ref().unwrap().is_template());
544 }
545
546 #[test]
547 fn srpm_rejects_unknown_field() {
548 assert!(serde_yaml_ng::from_str::<SrpmConfig>("nope: 1").is_err());
549 }
550
551 #[test]
552 fn makeself_empty_array_is_empty_vec() {
553 assert!(makeselfs("makeselfs: []").is_empty());
555 }
556
557 #[test]
558 fn makeself_full_metadata_block_parses() {
559 let v = makeselfs(
560 "makeselfs:\n id: x\n script: run.sh\n compression: xz\n os: [linux, darwin]\n arch: [amd64]\n extra_args: [--nox11]\n keywords: [cli, tool]\n",
561 );
562 let m = &v[0];
563 assert_eq!(m.compression.as_deref(), Some("xz"));
564 assert_eq!(
565 m.os.as_deref(),
566 Some(&["linux".to_string(), "darwin".to_string()][..])
567 );
568 assert_eq!(m.arch.as_deref(), Some(&["amd64".to_string()][..]));
569 assert_eq!(m.extra_args.as_deref(), Some(&["--nox11".to_string()][..]));
570 assert_eq!(
571 m.keywords.as_deref(),
572 Some(&["cli".to_string(), "tool".to_string()][..])
573 );
574 }
575
576 #[test]
577 fn makeself_file_strip_parent_round_trips() {
578 let v = makeselfs(
579 "makeselfs:\n id: x\n files:\n - source: a/b\n strip_parent: true\n",
580 );
581 assert_eq!(v[0].files.as_ref().unwrap()[0].strip_parent, Some(true));
582 }
583
584 #[test]
585 fn makeself_file_rejects_unknown_field() {
586 let err = serde_yaml_ng::from_str::<MakeselfWrap>(
587 "makeselfs:\n id: x\n files:\n - source: a\n bogus: 1\n",
588 );
589 assert!(err.is_err(), "MakeselfFile must deny unknown fields");
590 }
591
592 #[test]
593 fn appimage_empty_array_is_empty_vec() {
594 assert!(appimages("appimages: []").is_empty());
595 }
596
597 #[test]
598 fn appimage_update_information_and_os_arch_filters_parse() {
599 let v = appimages(
600 "appimages:\n id: x\n update_information: \"gh-releases-zsync|me|app|latest|app-*.AppImage.zsync\"\n os: [linux]\n arch: [amd64, arm64]\n",
601 );
602 let a = &v[0];
603 assert!(
604 a.update_information
605 .as_deref()
606 .unwrap()
607 .starts_with("gh-releases-zsync")
608 );
609 assert_eq!(a.os.as_deref(), Some(&["linux".to_string()][..]));
610 assert_eq!(
611 a.arch.as_deref(),
612 Some(&["amd64".to_string(), "arm64".to_string()][..])
613 );
614 }
615
616 #[test]
617 fn appimage_extra_rejects_unknown_field() {
618 let err = serde_yaml_ng::from_str::<AppImageWrap>(
619 "appimages:\n id: x\n appdir_extra:\n - src: a\n dst: b\n bogus: 1\n",
620 );
621 assert!(err.is_err(), "AppImageExtra must deny unknown fields");
622 }
623
624 #[test]
625 fn appimage_runtime_harvest_rejects_unknown_field() {
626 let err = serde_yaml_ng::from_str::<AppImageWrap>(
627 "appimages:\n id: x\n runtime_harvest:\n command: c\n dir: d\n bogus: 1\n",
628 );
629 assert!(err.is_err(), "RuntimeHarvest must deny unknown fields");
630 }
631
632 #[test]
633 fn appimage_skip_template_marks_template_not_bool() {
634 let v = appimages("appimages:\n id: x\n skip: \"{{ .IsSnapshot }}\"\n");
635 let skip = v[0].skip.as_ref().unwrap();
636 assert!(skip.is_template());
637 assert!(
638 !skip.as_bool(),
639 "an unrendered template is not a literal true"
640 );
641 }
642
643 #[test]
644 fn srpm_contents_and_prefixes_and_scriptlets_parse() {
645 let cfg: SrpmConfig = serde_yaml_ng::from_str(
646 "enabled: true\nprefixes: [/usr, /etc]\npretrans: scripts/pre.sh\nposttrans: scripts/post.sh\ncontents:\n - src: a.conf\n dst: /etc/a.conf\n type: config\n",
647 )
648 .unwrap();
649 assert_eq!(
650 cfg.prefixes.as_deref(),
651 Some(&["/usr".to_string(), "/etc".to_string()][..])
652 );
653 assert_eq!(cfg.pretrans.as_deref(), Some("scripts/pre.sh"));
654 assert_eq!(cfg.posttrans.as_deref(), Some("scripts/post.sh"));
655 let contents = cfg.contents.as_ref().unwrap();
656 assert_eq!(contents[0].src, "a.conf");
657 assert_eq!(contents[0].dst, "/etc/a.conf");
658 assert_eq!(contents[0].content_type.as_deref(), Some("config"));
659 }
660
661 #[test]
662 fn srpm_version_components_parse() {
663 let cfg: SrpmConfig =
664 serde_yaml_ng::from_str("prerelease: rc1\nversion_metadata: gitabc123\n").unwrap();
665 assert_eq!(cfg.prerelease.as_deref(), Some("rc1"));
666 assert_eq!(cfg.version_metadata.as_deref(), Some("gitabc123"));
667 }
668}