1#![deny(missing_docs)]
2
3pub mod macros;
12pub use target_lexicon;
13
14use std::{collections::BTreeMap, str::FromStr};
15
16use schemars::JsonSchema;
17use semver::Version;
18use serde::{Deserialize, Serialize};
19use target_lexicon::Triple;
20
21declare_strongly_typed_string! {
22 pub struct TripleName => &TripleNameRef;
24}
25
26impl TripleNameRef {
27 pub fn parse(&self) -> Result<Triple, <Triple as FromStr>::Err> {
29 Triple::from_str(self.as_str())
30 }
31
32 pub fn is_musl(&self) -> bool {
34 self.0.contains("musl")
35 }
36
37 pub fn is_linux(&self) -> bool {
39 self.0.contains("linux")
40 }
41
42 pub fn is_apple(&self) -> bool {
44 self.0.contains("apple")
45 }
46
47 pub fn is_darwin(&self) -> bool {
49 self.0.contains("darwin")
50 }
51
52 pub fn is_windows(&self) -> bool {
54 self.0.contains("windows")
55 }
56
57 pub fn is_x86_64(&self) -> bool {
59 self.0.contains("x86_64")
60 }
61
62 pub fn is_aarch64(&self) -> bool {
64 self.0.contains("aarch64")
65 }
66
67 pub fn is_linux_musl(&self) -> bool {
72 self.0.contains("linux-musl")
73 }
74
75 pub fn is_windows_msvc(&self) -> bool {
77 self.0.contains("windows-msvc")
78 }
79}
80declare_strongly_typed_string! {
81 pub struct GithubRunner => &GithubRunnerRef;
83
84 pub struct ContainerImage => &ContainerImageRef;
86}
87
88pub type GithubRunners = BTreeMap<TripleName, GithubRunnerConfig>;
91
92impl GithubRunnerRef {
93 pub fn is_buildjet(&self) -> bool {
95 self.as_str().contains("buildjet")
96 }
97}
98
99#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
104#[serde(untagged)]
105pub enum StringLikeOr<S, T> {
106 StringLike(S),
108 Val(T),
110}
111
112impl<S, T> StringLikeOr<S, T> {
113 pub fn from_s(s: S) -> Self {
115 Self::StringLike(s)
116 }
117
118 pub fn from_t(t: T) -> Self {
120 Self::Val(t)
121 }
122}
123
124pub type LocalPath = String;
128pub type RelPath = String;
134
135declare_strongly_typed_string! {
136 pub struct ArtifactId => &ArtifactIdRef;
138}
139
140pub type SystemId = String;
142pub type AssetId = String;
144pub type SortedSet<T> = std::collections::BTreeSet<T>;
146
147#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
149pub struct DistManifest {
150 #[serde(default)]
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub dist_version: Option<String>,
154 #[serde(default)]
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub announcement_tag: Option<String>,
158 #[serde(default)]
162 pub announcement_tag_is_implicit: bool,
163 #[serde(default)]
165 pub announcement_is_prerelease: bool,
166 #[serde(default)]
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub announcement_title: Option<String>,
170 #[serde(default)]
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub announcement_changelog: Option<String>,
174 #[serde(default)]
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub announcement_github_body: Option<String>,
178 #[serde(default)]
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub system_info: Option<SystemInfo>,
184 #[serde(default)]
186 #[serde(skip_serializing_if = "Vec::is_empty")]
187 pub releases: Vec<Release>,
188 #[serde(default)]
190 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
191 pub artifacts: BTreeMap<ArtifactId, Artifact>,
192 #[serde(default)]
194 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
195 pub systems: BTreeMap<SystemId, SystemInfo>,
196 #[serde(default)]
198 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
199 pub assets: BTreeMap<AssetId, AssetInfo>,
200 #[serde(default)]
202 pub publish_prereleases: bool,
203 #[serde(default)]
205 pub force_latest: bool,
206 #[serde(default)]
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub ci: Option<CiInfo>,
210 #[serde(default)]
212 pub linkage: Vec<Linkage>,
218 #[serde(default)]
220 pub upload_files: Vec<String>,
223 #[serde(default)]
227 #[serde(skip_serializing_if = "std::ops::Not::not")]
228 pub github_attestations: bool,
229 #[serde(default)]
231 #[serde(skip_serializing_if = "GithubAttestationsFilters::is_default")]
232 pub github_attestations_filters: GithubAttestationsFilters,
233 #[serde(default)]
237 #[serde(skip_serializing_if = "GithubAttestationsPhase::is_default")]
238 pub github_attestations_phase: GithubAttestationsPhase,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
243pub enum BuildEnvironment {
244 #[serde(rename = "linux")]
246 Linux {
247 glibc_version: Option<GlibcVersion>,
250 },
251 #[serde(rename = "macos")]
253 MacOS {
254 os_version: String,
256 },
257 #[serde(rename = "windows")]
259 Windows,
260 #[serde(rename = "indeterminate")]
262 Indeterminate,
263}
264
265#[derive(
267 Debug, Clone, Serialize, Deserialize, JsonSchema, Hash, PartialEq, Eq, PartialOrd, Ord,
268)]
269pub struct GlibcVersion {
270 pub major: u64,
272 pub series: u64,
274}
275
276impl Default for GlibcVersion {
277 fn default() -> Self {
278 Self {
279 major: 2,
281 series: 31,
282 }
283 }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
288pub struct AssetInfo {
289 pub id: AssetId,
291 pub name: String,
293 pub system: SystemId,
295 pub target_triples: Vec<TripleName>,
301 pub linkage: Option<Linkage>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
307pub struct CiInfo {
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub github: Option<GithubCiInfo>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
315pub struct GithubCiInfo {
316 #[serde(skip_serializing_if = "Option::is_none")]
318 pub artifacts_matrix: Option<GithubMatrix>,
319
320 #[serde(skip_serializing_if = "Option::is_none")]
322 pub pr_run_mode: Option<PrRunMode>,
323
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub external_repo_commit: Option<String>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
331pub struct GithubMatrix {
332 #[serde(default)]
334 #[serde(skip_serializing_if = "Vec::is_empty")]
335 pub include: Vec<GithubLocalJobConfig>,
336}
337
338impl GithubMatrix {
339 pub fn is_empty(&self) -> bool {
343 self.include.is_empty()
344 }
345}
346
347declare_strongly_typed_string! {
348 pub struct PackageInstallScript => &PackageInstallScriptRef;
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
355pub struct GithubRunnerConfigInput {
356 pub runner: Option<GithubRunner>,
364
365 pub host: Option<TripleName>,
369
370 pub container: Option<StringLikeOr<ContainerImage, ContainerConfigInput>>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord)]
382pub struct GithubRunnerConfig {
383 pub runner: GithubRunner,
389
390 pub host: TripleName,
394
395 #[serde(skip_serializing_if = "Option::is_none")]
401 pub container: Option<ContainerConfig>,
402}
403
404impl GithubRunnerConfig {
405 pub fn real_triple_name(&self) -> &TripleNameRef {
409 if let Some(container) = &self.container {
410 &container.host
411 } else {
412 &self.host
413 }
414 }
415
416 pub fn real_triple(&self) -> Triple {
418 self.real_triple_name().parse().unwrap()
419 }
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
424pub struct ContainerConfigInput {
425 pub image: ContainerImage,
428
429 pub host: Option<TripleName>,
432
433 #[serde(rename = "package-manager")]
435 pub package_manager: Option<PackageManager>,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord)]
440pub struct ContainerConfig {
441 pub image: ContainerImage,
444
445 pub host: TripleName,
448
449 pub package_manager: Option<PackageManager>,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
456pub struct GithubGlobalJobConfig {
457 #[serde(flatten)]
459 pub runner: GithubRunnerConfig,
460
461 pub install_dist: GhaRunStep,
463
464 pub dist_args: String,
466
467 #[serde(skip_serializing_if = "Option::is_none")]
469 pub install_cargo_cyclonedx: Option<GhaRunStep>,
470
471 #[serde(skip_serializing_if = "Option::is_none")]
472 pub install_omnibor: Option<GhaRunStep>,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
478pub struct GithubLocalJobConfig {
479 #[serde(flatten)]
481 pub runner: GithubRunnerConfig,
482
483 pub install_dist: GhaRunStep,
485
486 pub dist_args: String,
488
489 #[serde(skip_serializing_if = "Option::is_none")]
491 pub targets: Option<Vec<TripleName>>,
492
493 #[serde(skip_serializing_if = "Option::is_none")]
495 pub install_cargo_auditable: Option<GhaRunStep>,
496
497 #[serde(skip_serializing_if = "Option::is_none")]
499 pub install_omnibor: Option<GhaRunStep>,
500
501 #[serde(skip_serializing_if = "Option::is_none")]
503 pub packages_install: Option<PackageInstallScript>,
504
505 #[serde(skip_serializing_if = "Option::is_none")]
507 pub cache_provider: Option<String>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
512pub struct GithubAttestationsFilters(Vec<String>);
513
514impl Default for GithubAttestationsFilters {
515 fn default() -> Self {
516 Self(vec!["*".to_string()])
517 }
518}
519
520impl<'a> IntoIterator for &'a GithubAttestationsFilters {
521 type Item = &'a String;
522 type IntoIter = std::slice::Iter<'a, String>;
523
524 fn into_iter(self) -> Self::IntoIter {
525 self.0.iter()
526 }
527}
528
529impl GithubAttestationsFilters {
530 fn is_default(&self) -> bool {
531 *self == Default::default()
532 }
533}
534
535#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, Default)]
537pub enum GithubAttestationsPhase {
538 #[serde(rename = "announce")]
540 Announce,
541 #[serde(rename = "host")]
543 Host,
544 #[default]
546 #[serde(rename = "build-local-artifacts")]
547 BuildLocalArtifacts,
548}
549
550impl GithubAttestationsPhase {
551 fn is_default(&self) -> bool {
552 matches!(self, GithubAttestationsPhase::BuildLocalArtifacts)
553 }
554}
555
556impl std::fmt::Display for GithubAttestationsPhase {
557 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558 match self {
559 GithubAttestationsPhase::Announce => write!(f, "announce"),
560 GithubAttestationsPhase::Host => write!(f, "host"),
561 GithubAttestationsPhase::BuildLocalArtifacts => write!(f, "build-local-artifacts"),
562 }
563 }
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
568#[serde(tag = "shell", content = "run")]
572pub enum GhaRunStep {
573 #[serde(rename = "sh")]
575 Dash(DashScript),
576 #[serde(rename = "pwsh")]
578 Powershell(PowershellScript),
579}
580
581impl From<DashScript> for GhaRunStep {
582 fn from(bash: DashScript) -> Self {
583 Self::Dash(bash)
584 }
585}
586
587impl From<PowershellScript> for GhaRunStep {
588 fn from(powershell: PowershellScript) -> Self {
589 Self::Powershell(powershell)
590 }
591}
592
593declare_strongly_typed_string! {
594 pub struct DashScript => &DashScriptRef;
596
597 pub struct PowershellScript => &PowershellScriptRef;
599}
600
601#[derive(
603 Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq, PartialOrd, Ord,
604)]
605pub enum PrRunMode {
606 #[serde(rename = "skip")]
608 Skip,
609 #[default]
611 #[serde(rename = "plan")]
612 Plan,
613 #[serde(rename = "upload")]
615 Upload,
616}
617
618impl std::fmt::Display for PrRunMode {
619 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
620 match self {
621 PrRunMode::Skip => write!(f, "skip"),
622 PrRunMode::Plan => write!(f, "plan"),
623 PrRunMode::Upload => write!(f, "upload"),
624 }
625 }
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
630pub struct SystemInfo {
631 pub id: SystemId,
633 pub cargo_version_line: Option<String>,
635 pub build_environment: BuildEnvironment,
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
641pub struct EnvironmentVariables {
642 pub install_dir_env_var: String,
644 pub unmanaged_dir_env_var: String,
646 pub disable_update_env_var: String,
648 pub no_modify_path_env_var: String,
650 pub print_quiet_env_var: String,
652 pub print_verbose_env_var: String,
654 pub download_url_env_var: String,
658 pub github_base_url_env_var: String,
663 pub ghe_base_url_env_var: String,
668 pub github_token_env_var: String,
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
674pub struct Release {
675 pub app_name: String,
677 pub app_version: String,
680 #[serde(default)]
682 #[serde(skip_serializing_if = "Option::is_none")]
683 pub env: Option<EnvironmentVariables>,
684 #[serde(default)]
686 #[serde(skip_serializing_if = "Option::is_none")]
687 pub display_name: Option<String>,
688 #[serde(default)]
690 #[serde(skip_serializing_if = "Option::is_none")]
691 pub display: Option<bool>,
692 #[serde(default)]
694 #[serde(skip_serializing_if = "Vec::is_empty")]
695 pub artifacts: Vec<ArtifactId>,
696 #[serde(default)]
698 #[serde(skip_serializing_if = "Hosting::is_empty")]
699 pub hosting: Hosting,
700}
701
702declare_strongly_typed_string! {
703 pub struct ChecksumExtension => &ChecksumExtensionRef;
710
711 pub struct ChecksumValue => &ChecksumValueRef;
713}
714
715#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
719pub struct Artifact {
720 #[serde(skip_serializing_if = "Option::is_none")]
726 #[serde(default)]
727 pub name: Option<ArtifactId>,
728 #[serde(flatten)]
730 pub kind: ArtifactKind,
731 #[serde(skip_serializing_if = "Vec::is_empty")]
733 #[serde(default)]
734 pub target_triples: Vec<TripleName>,
735 #[serde(skip_serializing_if = "Option::is_none")]
737 #[serde(default)]
738 pub path: Option<LocalPath>,
739 #[serde(skip_serializing_if = "Vec::is_empty")]
741 #[serde(default)]
742 pub assets: Vec<Asset>,
743 #[serde(skip_serializing_if = "Option::is_none")]
745 #[serde(default)]
746 pub install_hint: Option<String>,
747 #[serde(skip_serializing_if = "Option::is_none")]
749 #[serde(default)]
750 pub description: Option<String>,
751 #[serde(skip_serializing_if = "Option::is_none")]
753 #[serde(default)]
754 pub checksum: Option<ArtifactId>,
755 #[serde(default)]
760 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
761 pub checksums: BTreeMap<ChecksumExtension, ChecksumValue>,
762}
763
764#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
766pub struct Asset {
767 #[serde(default)]
769 #[serde(skip_serializing_if = "Option::is_none")]
770 pub id: Option<String>,
771 #[serde(default)]
773 #[serde(skip_serializing_if = "Option::is_none")]
774 pub name: Option<String>,
775 #[serde(default)]
777 #[serde(skip_serializing_if = "Option::is_none")]
778 pub path: Option<RelPath>,
779 #[serde(flatten)]
781 pub kind: AssetKind,
782}
783
784#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
786#[serde(tag = "kind")]
787#[non_exhaustive]
788pub enum AssetKind {
789 #[serde(rename = "executable")]
791 Executable(ExecutableAsset),
792 #[serde(rename = "c_dynamic_library")]
794 CDynamicLibrary(DynamicLibraryAsset),
795 #[serde(rename = "c_static_library")]
797 CStaticLibrary(StaticLibraryAsset),
798 #[serde(rename = "readme")]
800 Readme,
801 #[serde(rename = "license")]
803 License,
804 #[serde(rename = "changelog")]
806 Changelog,
807 #[serde(other)]
811 #[serde(rename = "unknown")]
812 Unknown,
813}
814
815#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
817#[serde(tag = "kind")]
818#[non_exhaustive]
819pub enum ArtifactKind {
820 #[serde(rename = "executable-zip")]
822 ExecutableZip,
823 #[serde(rename = "symbols")]
825 Symbols,
826 #[serde(rename = "installer")]
828 Installer,
829 #[serde(rename = "checksum")]
831 Checksum,
832 #[serde(rename = "unified-checksum")]
834 UnifiedChecksum,
835 #[serde(rename = "source-tarball")]
837 SourceTarball,
838 #[serde(rename = "extra-artifact")]
840 ExtraArtifact,
841 #[serde(rename = "updater")]
843 Updater,
844 #[serde(rename = "sbom")]
846 SBOM,
847 #[serde(rename = "omnibor-artifact-id")]
849 OmniborArtifactId,
850 #[serde(other)]
854 #[serde(rename = "unknown")]
855 Unknown,
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
860pub struct ExecutableAsset {
861 #[serde(skip_serializing_if = "Option::is_none")]
863 #[serde(default)]
864 pub symbols_artifact: Option<ArtifactId>,
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
869pub struct DynamicLibraryAsset {
870 #[serde(skip_serializing_if = "Option::is_none")]
872 #[serde(default)]
873 pub symbols_artifact: Option<ArtifactId>,
874}
875
876#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
878pub struct StaticLibraryAsset {
879 #[serde(skip_serializing_if = "Option::is_none")]
881 #[serde(default)]
882 pub symbols_artifact: Option<ArtifactId>,
883}
884
885pub struct VersionInfo {
887 pub version: Version,
889 pub format: Format,
891}
892
893pub const SELF_VERSION: &str = env!("CARGO_PKG_VERSION");
895pub const DIST_EPOCH_1_MAX: &str = "0.0.3-prerelease8";
898pub const DIST_EPOCH_2_MAX: &str = "0.0.6-prerelease6";
901
902#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
904pub enum Format {
905 Epoch1,
907 Epoch2,
909 Epoch3,
912 Future,
915}
916
917impl Format {
918 pub fn unsupported(&self) -> bool {
920 self <= &Format::Epoch1
921 }
922 pub fn artifact_names_contain_versions(&self) -> bool {
924 self <= &Format::Epoch2
925 }
926}
927
928impl DistManifest {
929 pub fn new(releases: Vec<Release>, artifacts: BTreeMap<ArtifactId, Artifact>) -> Self {
931 Self {
932 dist_version: None,
933 announcement_tag: None,
934 announcement_tag_is_implicit: false,
935 announcement_is_prerelease: false,
936 announcement_title: None,
937 announcement_changelog: None,
938 announcement_github_body: None,
939 github_attestations: false,
940 github_attestations_filters: Default::default(),
941 github_attestations_phase: Default::default(),
942 system_info: None,
943 releases,
944 artifacts,
945 systems: Default::default(),
946 assets: Default::default(),
947 publish_prereleases: false,
948 force_latest: false,
949 ci: None,
950 linkage: vec![],
951 upload_files: vec![],
952 }
953 }
954
955 pub fn json_schema() -> schemars::Schema {
957 schemars::schema_for!(DistManifest)
958 }
959
960 pub fn format(&self) -> Format {
964 self.dist_version
965 .as_ref()
966 .and_then(|v| v.parse().ok())
967 .map(|v| format_of_version(&v))
968 .unwrap_or(Format::Future)
969 }
970
971 pub fn artifacts_for_release<'a>(
973 &'a self,
974 release: &'a Release,
975 ) -> impl Iterator<Item = (&'a ArtifactIdRef, &'a Artifact)> {
976 release
977 .artifacts
978 .iter()
979 .filter_map(|k| Some((&**k, self.artifacts.get(k)?)))
980 }
981
982 pub fn release_by_name(&self, name: &str) -> Option<&Release> {
984 self.releases.iter().find(|r| r.app_name == name)
985 }
986
987 pub fn ensure_release(&mut self, name: String, version: String) -> &mut Release {
990 if let Some(position) = self.releases.iter().position(|r| r.app_name == name) {
992 &mut self.releases[position]
993 } else {
994 let env_app_name = name.to_ascii_uppercase().replace('-', "_");
995 let install_dir_env_var = format!("{env_app_name}_INSTALL_DIR");
996 let download_url_env_var = format!("{env_app_name}_DOWNLOAD_URL");
997 let unmanaged_dir_env_var = format!("{env_app_name}_UNMANAGED_INSTALL");
998 let disable_update_env_var = format!("{env_app_name}_DISABLE_UPDATE");
999 let print_quiet_env_var = format!("{env_app_name}_PRINT_QUIET");
1000 let print_verbose_env_var = format!("{env_app_name}_PRINT_VERBOSE");
1001 let no_modify_path_env_var = format!("{env_app_name}_NO_MODIFY_PATH");
1002 let github_base_url_env_var = format!("{env_app_name}_INSTALLER_GITHUB_BASE_URL");
1003 let ghe_base_url_env_var = format!("{env_app_name}_INSTALLER_GHE_BASE_URL");
1004 let github_token_env_var = format!("{env_app_name}_GITHUB_TOKEN");
1005
1006 let environment_variables = EnvironmentVariables {
1007 install_dir_env_var,
1008 download_url_env_var,
1009 unmanaged_dir_env_var,
1010 disable_update_env_var,
1011 print_quiet_env_var,
1012 print_verbose_env_var,
1013 no_modify_path_env_var,
1014 github_base_url_env_var,
1015 ghe_base_url_env_var,
1016 github_token_env_var,
1017 };
1018
1019 self.releases.push(Release {
1020 app_name: name,
1021 app_version: version,
1022 env: Some(environment_variables),
1023 artifacts: vec![],
1024 hosting: Hosting::default(),
1025 display: None,
1026 display_name: None,
1027 });
1028 self.releases.last_mut().unwrap()
1029 }
1030 }
1031
1032 pub fn linkage_for_artifact(&self, artifact_id: &ArtifactId) -> Linkage {
1036 let mut output = Linkage::default();
1037
1038 let Some(artifact) = self.artifacts.get(artifact_id) else {
1039 return output;
1040 };
1041 for base_asset in &artifact.assets {
1042 let Some(asset_id) = &base_asset.id else {
1043 continue;
1044 };
1045 let Some(true_asset) = self.assets.get(asset_id) else {
1046 continue;
1047 };
1048 let Some(linkage) = &true_asset.linkage else {
1049 continue;
1050 };
1051 output.extend(linkage);
1052 }
1053
1054 output
1055 }
1056}
1057
1058impl Release {
1059 pub fn artifact_download_urls(&self) -> Option<Vec<String>> {
1061 self.hosting.artifact_download_urls()
1062 }
1063}
1064
1065#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
1067#[serde(rename_all = "kebab-case")]
1068pub struct Hosting {
1069 #[serde(default)]
1071 #[serde(skip_serializing_if = "Option::is_none")]
1072 pub order: Option<Vec<HostingStyle>>,
1073 #[serde(default)]
1075 #[serde(skip_serializing_if = "Option::is_none")]
1076 pub github: Option<GithubHosting>,
1077 #[serde(default)]
1079 #[serde(skip_serializing_if = "Option::is_none")]
1080 pub simple: Option<SimpleHosting>,
1081}
1082
1083#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
1085pub struct GithubHosting {
1086 pub artifact_base_url: String,
1089 pub artifact_download_path: String,
1093 pub owner: String,
1095 pub repo: String,
1097}
1098
1099#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
1101pub struct SimpleHosting {
1102 pub download_url: String,
1106}
1107
1108#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
1110#[serde(rename_all = "kebab-case")]
1111pub enum HostingStyle {
1112 Github,
1114 Simple,
1116}
1117
1118impl Hosting {
1119 pub fn artifact_download_urls(&self) -> Option<Vec<String>> {
1121 let Hosting {
1122 github,
1123 simple,
1124 order,
1125 } = &self;
1126 let mut results = vec![];
1127 let priority_matters = order.as_ref().map(|list| !list.is_empty()).unwrap_or(false);
1128
1129 if let Some(host) = &simple {
1130 let priority = order
1131 .iter()
1132 .flatten()
1133 .position(|key| *key == HostingStyle::Simple);
1134 if priority.is_some() || !priority_matters {
1135 results.push((priority, host.download_url.clone()));
1136 }
1137 }
1138
1139 if let Some(host) = &github {
1140 let priority = order
1141 .iter()
1142 .flatten()
1143 .position(|key| *key == HostingStyle::Github);
1144 if priority.is_some() || !priority_matters {
1145 results.push((
1146 priority,
1147 format!("{}{}", host.artifact_base_url, host.artifact_download_path),
1148 ));
1149 }
1150 }
1151
1152 if results.is_empty() {
1153 return None;
1154 }
1155
1156 results.sort_by_key(|(priority, _)| *priority);
1157 Some(results.into_iter().map(|(_, val)| val).collect())
1158 }
1159
1160 pub fn is_empty(&self) -> bool {
1162 let Hosting {
1163 github,
1164 simple,
1165 order: _,
1166 } = &self;
1167 github.is_none() && simple.is_none()
1168 }
1169}
1170
1171#[derive(Clone, Default, Debug, Deserialize, Serialize, JsonSchema)]
1173pub struct Linkage {
1174 #[serde(default)]
1176 #[serde(skip_serializing_if = "SortedSet::is_empty")]
1177 pub system: SortedSet<Library>,
1178 #[serde(default)]
1180 #[serde(skip_serializing_if = "SortedSet::is_empty")]
1181 pub homebrew: SortedSet<Library>,
1182 #[serde(default)]
1184 #[serde(skip_serializing_if = "SortedSet::is_empty")]
1185 pub public_unmanaged: SortedSet<Library>,
1186 #[serde(default)]
1188 #[serde(skip_serializing_if = "SortedSet::is_empty")]
1189 pub other: SortedSet<Library>,
1190 #[serde(default)]
1192 #[serde(skip_serializing_if = "SortedSet::is_empty")]
1193 pub frameworks: SortedSet<Library>,
1194}
1195
1196#[derive(
1198 Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord,
1199)]
1200#[serde(rename_all = "lowercase")]
1201pub enum PackageManager {
1202 Homebrew,
1204 Apt,
1206}
1207
1208declare_strongly_typed_string! {
1209 pub struct HomebrewPackageName => &HomebrewPackageNameRef;
1211
1212 pub struct AptPackageName => &AptPackageNameRef;
1214
1215 pub struct ChocolateyPackageName => &ChocolateyPackageNameRef;
1217
1218 pub struct PipPackageName => &PipPackageNameRef;
1220
1221 pub struct PackageVersion => &PackageVersionRef;
1223}
1224
1225#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord)]
1227pub struct Library {
1228 pub path: String,
1230 #[serde(skip_serializing_if = "Option::is_none")]
1232 pub source: Option<String>,
1233 pub package_manager: Option<PackageManager>,
1235 }
1239
1240impl Linkage {
1241 pub fn extend(&mut self, val: &Linkage) {
1243 let Linkage {
1244 system,
1245 homebrew,
1246 public_unmanaged,
1247 other,
1248 frameworks,
1249 } = val;
1250 self.system.extend(system.iter().cloned());
1251 self.homebrew.extend(homebrew.iter().cloned());
1252 self.public_unmanaged
1253 .extend(public_unmanaged.iter().cloned());
1254 self.other.extend(other.iter().cloned());
1255 self.frameworks.extend(frameworks.iter().cloned());
1256 }
1257}
1258
1259impl Library {
1260 pub fn new(path: String) -> Self {
1262 Self {
1263 path,
1264 source: None,
1265 package_manager: None,
1266 }
1267 }
1268
1269 pub fn is_glibc(&self) -> bool {
1271 if let Some(source) = &self.source {
1273 source == "libc6"
1274 } else {
1275 self.path.contains("libc.so.6") ||
1277 self.path.contains("libc-2")
1282 }
1283 }
1284}
1285
1286impl std::fmt::Display for Library {
1287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1288 if let Some(package) = &self.source {
1289 write!(f, "{} ({package})", self.path)
1290 } else {
1291 write!(f, "{}", self.path)
1292 }
1293 }
1294}
1295
1296fn dist_version(input: &str) -> Option<Version> {
1298 #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1299 struct PartialDistManifest {
1300 #[serde(default)]
1302 #[serde(skip_serializing_if = "Option::is_none")]
1303 pub dist_version: Option<String>,
1304 }
1305
1306 let manifest: PartialDistManifest = serde_json::from_str(input).ok()?;
1307 let version: Version = manifest.dist_version?.parse().ok()?;
1308 Some(version)
1309}
1310
1311pub fn check_version(input: &str) -> Option<VersionInfo> {
1313 let version = dist_version(input)?;
1314 let format = format_of_version(&version);
1315 Some(VersionInfo { version, format })
1316}
1317
1318pub fn format_of_version(version: &Version) -> Format {
1320 let epoch1 = Version::parse(DIST_EPOCH_1_MAX).unwrap();
1321 let epoch2 = Version::parse(DIST_EPOCH_2_MAX).unwrap();
1322 let self_ver = Version::parse(SELF_VERSION).unwrap();
1323 if version > &self_ver {
1324 Format::Future
1325 } else if version > &epoch2 {
1326 Format::Epoch3
1327 } else if version > &epoch1 {
1328 Format::Epoch2
1329 } else {
1330 Format::Epoch1
1331 }
1332}
1333
1334#[test]
1335fn emit() {
1336 let schema = DistManifest::json_schema();
1337 let json_schema = serde_json::to_string_pretty(&schema).unwrap();
1338 insta::assert_snapshot!(json_schema);
1339}