forked from axodotdev/cargo-dist
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathlib.rs
More file actions
1223 lines (1101 loc) · 42.6 KB
/
Copy pathlib.rs
File metadata and controls
1223 lines (1101 loc) · 42.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#![deny(missing_docs)]
//! # cargo-dist-schema
//!
//! This crate exists to serialize and deserialize the dist-manifest.json produced
//! by dist. Ideally it should be reasonably forward and backward compatible
//! with different versions of this format.
//!
//! The root type of the schema is [`DistManifest`][].
pub mod macros;
pub use target_lexicon;
use std::{collections::BTreeMap, str::FromStr};
use schemars::JsonSchema;
use semver::Version;
use serde::{Deserialize, Serialize};
use target_lexicon::Triple;
declare_strongly_typed_string! {
/// A rustc-like target triple/tuple (e.g. "x86_64-pc-windows-msvc")
pub struct TripleName => &TripleNameRef;
}
impl TripleNameRef {
/// Parse as a [`Triple`]
pub fn parse(&self) -> Result<Triple, <Triple as FromStr>::Err> {
Triple::from_str(self.as_str())
}
/// Returns true if this target triple contains the word "musl"
pub fn is_musl(&self) -> bool {
self.0.contains("musl")
}
/// Returns true if this target triple contains the word "linux"
pub fn is_linux(&self) -> bool {
self.0.contains("linux")
}
/// Returns true if this target triple contains the word "apple"
pub fn is_apple(&self) -> bool {
self.0.contains("apple")
}
/// Returns true if this target triple contains the word "darwin"
pub fn is_darwin(&self) -> bool {
self.0.contains("darwin")
}
/// Returns true if this target triple contains the word "windows"
pub fn is_windows(&self) -> bool {
self.0.contains("windows")
}
/// Returns true if this target triple contains the word "x86_64"
pub fn is_x86_64(&self) -> bool {
self.0.contains("x86_64")
}
/// Returns true if this target triple contains the word "aarch64"
pub fn is_aarch64(&self) -> bool {
self.0.contains("aarch64")
}
//---------------------------
// common combinations
/// Returns true if this target triple contains the string "linux-musl"
pub fn is_linux_musl(&self) -> bool {
self.0.contains("linux-musl")
}
/// Returns true if this target triple contains the string "windows-msvc"
pub fn is_windows_msvc(&self) -> bool {
self.0.contains("windows-msvc")
}
}
declare_strongly_typed_string! {
/// The name of a Github Actions Runner, like `ubuntu-20.04` or `macos-13`
pub struct GithubRunner => &GithubRunnerRef;
/// A container image, like `quay.io/pypa/manylinux_2_28_x86_64`
pub struct ContainerImage => &ContainerImageRef;
}
/// Github runners configuration (which github image/container should be used
/// to build which target).
pub type GithubRunners = BTreeMap<TripleName, GithubRunnerConfig>;
impl GithubRunnerRef {
/// Does the runner name contain the word "buildjet"?
pub fn is_buildjet(&self) -> bool {
self.as_str().contains("buildjet")
}
}
/// A value or just a string
///
/// This allows us to have a simple string-based version of a config while still
/// allowing for a more advanced version to exist.
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(untagged)]
pub enum StringLikeOr<S, T> {
/// They gave the simple string-like value (see `declare_strongly_typed_string!`)
StringLike(S),
/// They gave a more interesting value
Val(T),
}
impl<S, T> StringLikeOr<S, T> {
/// Constructs a new `StringLikeOr` from the string-like value `s`
pub fn from_s(s: S) -> Self {
Self::StringLike(s)
}
/// Constructs a new `StringLikeOr` from the more interesting value `t`
pub fn from_t(t: T) -> Self {
Self::Val(t)
}
}
/// A local system path on the machine dist was run.
///
/// This is a String because when deserializing this may be a path format from a different OS!
pub type LocalPath = String;
/// A relative path inside an artifact
///
/// This is a String because when deserializing this may be a path format from a different OS!
///
/// (Should we normalize this one?)
pub type RelPath = String;
declare_strongly_typed_string! {
/// The unique ID of an Artifact
pub struct ArtifactId => &ArtifactIdRef;
}
/// The unique ID of a System
pub type SystemId = String;
/// The unique ID of an Asset
pub type AssetId = String;
/// A sorted set of values
pub type SortedSet<T> = std::collections::BTreeSet<T>;
/// A report of the releases and artifacts that dist generated
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DistManifest {
/// The version of dist that generated this
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub dist_version: Option<String>,
/// The (git) tag associated with this announcement
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub announcement_tag: Option<String>,
/// True if --tag wasn't explicitly passed to dist. This usually indicates
/// some kind of dry-run state like pr-run-mode=upload. Some third-party tools
/// may use this as a proxy for "is dry run"
#[serde(default)]
pub announcement_tag_is_implicit: bool,
/// Whether this announcement appears to be a prerelease
#[serde(default)]
pub announcement_is_prerelease: bool,
/// A title for the announcement
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub announcement_title: Option<String>,
/// A changelog for the announcement
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub announcement_changelog: Option<String>,
/// A Github Releases body for the announcement
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub announcement_github_body: Option<String>,
/// Info about the toolchain used to build this announcement
///
/// DEPRECATED: never appears anymore
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub system_info: Option<SystemInfo>,
/// App releases we're distributing
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub releases: Vec<Release>,
/// The artifacts included in this Announcement, referenced by releases.
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub artifacts: BTreeMap<ArtifactId, Artifact>,
/// The systems that artifacts were built on
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub systems: BTreeMap<SystemId, SystemInfo>,
/// The assets contained within artifacts (binaries)
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub assets: BTreeMap<AssetId, AssetInfo>,
/// Whether to publish prereleases to package managers
#[serde(default)]
pub publish_prereleases: bool,
/// Where possible, announce/publish a release as "latest" regardless of semver version
#[serde(default)]
pub force_latest: bool,
/// ci backend info
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ci: Option<CiInfo>,
/// Data about dynamic linkage in the built libraries
#[serde(default)]
// FIXME: turn on this skip_serializing_if at some point.
// old dist-manifest consumers don't think this field can
// be missing, so it's unsafe to stop emitting it, but
// we want to deprecate it at some point.
// #[serde(skip_serializing_if = "Vec::is_empty")]
pub linkage: Vec<Linkage>,
/// Files to upload
#[serde(default)]
// We need to make sure we always serialize this when it's empty,
// because we index into this array unconditionally during upload.
pub upload_files: Vec<String>,
/// Whether Artifact Attestations should be found in the GitHub Release
///
/// <https://github.blog/2024-05-02-introducing-artifact-attestations-now-in-public-beta/>
#[serde(default)]
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub github_attestations: bool,
}
/// Information about the build environment on this system
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum BuildEnvironment {
/// Linux-specific information
#[serde(rename = "linux")]
Linux {
/// The builder's glibc version, relevant to glibc-based
/// builds.
glibc_version: Option<GlibcVersion>,
},
/// macOS-specific information
#[serde(rename = "macos")]
MacOS {
/// The version of macOS used by the builder
os_version: String,
},
/// Windows-specific information
#[serde(rename = "windows")]
Windows,
/// Unable to determine what the host OS was - error?
#[serde(rename = "indeterminate")]
Indeterminate,
}
/// Minimum glibc version required to run software
#[derive(
Debug, Clone, Serialize, Deserialize, JsonSchema, Hash, PartialEq, Eq, PartialOrd, Ord,
)]
pub struct GlibcVersion {
/// Major version
pub major: u64,
/// Series (minor) version
pub series: u64,
}
impl Default for GlibcVersion {
fn default() -> Self {
Self {
// Values from the default Ubuntu runner
major: 2,
series: 35,
}
}
}
/// Info about an Asset (binary)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AssetInfo {
/// unique id of the Asset
pub id: AssetId,
/// filename of the Asset
pub name: String,
/// the system it was built on
pub system: SystemId,
/// rust-style target triples the Asset natively supports
///
/// * length 0: not a meaningful question, maybe some static file
/// * length 1: typical of binaries
/// * length 2+: some kind of universal binary
pub target_triples: Vec<TripleName>,
/// the linkage of this Asset
pub linkage: Option<Linkage>,
}
/// CI backend info
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CiInfo {
/// GitHub CI backend
#[serde(skip_serializing_if = "Option::is_none")]
pub github: Option<GithubCiInfo>,
}
/// Github CI backend
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GithubCiInfo {
/// Github CI Matrix for upload-artifacts
#[serde(skip_serializing_if = "Option::is_none")]
pub artifacts_matrix: Option<GithubMatrix>,
/// What kind of job to run on pull request
#[serde(skip_serializing_if = "Option::is_none")]
pub pr_run_mode: Option<PrRunMode>,
/// A specific commit to tag in an external repository
#[serde(skip_serializing_if = "Option::is_none")]
pub external_repo_commit: Option<String>,
}
/// Github CI Matrix
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GithubMatrix {
/// define each task manually rather than doing cross-product stuff
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub include: Vec<GithubLocalJobConfig>,
}
impl GithubMatrix {
/// Gets if the matrix has no entries
///
/// this is useful for checking if there should be No matrix
pub fn is_empty(&self) -> bool {
self.include.is_empty()
}
}
declare_strongly_typed_string! {
/// A bit of shell script to install brew/apt/chocolatey/etc. packages
pub struct PackageInstallScript => &PackageInstallScriptRef;
}
/// The version of `GithubRunnerConfig` that's deserialized from the config file: it
/// has optional fields that are computed later.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GithubRunnerConfigInput {
/// GHA's `runs-on` key: Github Runner image to use, see <https://github.com/actions/runner-images>
/// and <https://docs.github.com/en/actions/writing-workflows/choosing-where-your-workflow-runs/choosing-the-runner-for-a-job>
///
/// This is not necessarily a well-known runner, it could be something self-hosted, it
/// could be from BuildJet, Namespace, etc.
///
/// If not specified, `container` has to be set.
pub runner: Option<GithubRunner>,
/// Host triple of the runner (well-known, custom, or best guess).
/// If the runner is one of GitHub's official runner images, the platform
/// is hardcoded. If it's custom, then we have a `target_triple => runner` in the config
pub host: Option<TripleName>,
/// Container image to run the job in, using GitHub's builtin
/// container support, see <https://docs.github.com/en/actions/writing-workflows/choosing-where-your-workflow-runs/running-jobs-in-a-container>
///
/// This doesn't allow mounting volumes, or anything, because we're only able
/// to set the `container` key to something stringy
///
/// If not specified, `runner` has to be set.
pub container: Option<StringLikeOr<ContainerImage, ContainerConfigInput>>,
}
/// GitHub config that's common between different kinds of jobs (global, local)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord)]
pub struct GithubRunnerConfig {
/// GHA's `runs-on` key: Github Runner image to use, see <https://github.com/actions/runner-images>
/// and <https://docs.github.com/en/actions/writing-workflows/choosing-where-your-workflow-runs/choosing-the-runner-for-a-job>
///
/// This is not necessarily a well-known runner, it could be something self-hosted, it
/// could be from BuildJet, Namespace, etc.
pub runner: GithubRunner,
/// Host triple of the runner (well-known, custom, or best guess).
/// If the runner is one of GitHub's official runner images, the platform
/// is hardcoded. If it's custom, then we have a `target_triple => runner` in the config
pub host: TripleName,
/// Container image to run the job in, using GitHub's builtin
/// container support, see <https://docs.github.com/en/actions/writing-workflows/choosing-where-your-workflow-runs/running-jobs-in-a-container>
///
/// This doesn't allow mounting volumes, or anything, because we're only able
/// to set the `container` key to something stringy
#[serde(skip_serializing_if = "Option::is_none")]
pub container: Option<ContainerConfig>,
}
impl GithubRunnerConfig {
/// If the container runs through a container, that container might have a different
/// architecture than the outer VM — this returns the container's triple if any,
/// and falls back to the "machine"'s triple if not.
pub fn real_triple_name(&self) -> &TripleNameRef {
if let Some(container) = &self.container {
&container.host
} else {
&self.host
}
}
/// cf. [`Self::real_triple_name`], but parsed
pub fn real_triple(&self) -> Triple {
self.real_triple_name().parse().unwrap()
}
}
/// GitHub config that's common between different kinds of jobs (global, local)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ContainerConfigInput {
/// The container image to run, something like `ubuntu:20.04` or
/// `quay.io/pypa/manylinux_2_28_x86_64`
pub image: ContainerImage,
/// The host triple of the container, something like `x86_64-unknown-linux-gnu`
/// or `aarch64-unknown-linux-musl` or whatever.
pub host: Option<TripleName>,
/// The package manager to use within the container, like `apt`.
#[serde(rename = "package-manager")]
pub package_manager: Option<PackageManager>,
}
/// GitHub config that's common between different kinds of jobs (global, local)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord)]
pub struct ContainerConfig {
/// The container image to run, something like `ubuntu:20.04` or
/// `quay.io/pypa/manylinux_2_28_x86_64`
pub image: ContainerImage,
/// The host triple of the container, something like `x86_64-unknown-linux-gnu`
/// or `aarch64-unknown-linux-musl` or whatever.
pub host: TripleName,
/// The package manager to use within the container, like `apt`.
pub package_manager: Option<PackageManager>,
}
/// Used in `github/release.yml.j2` to template out "global" build jobs
/// (plan, global assets, announce, etc)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GithubGlobalJobConfig {
/// Where to run this job?
#[serde(flatten)]
pub runner: GithubRunnerConfig,
/// Expression to execute to install dist
pub install_dist: GhaRunStep,
/// Arguments to pass to dist
pub dist_args: String,
/// Expression to execute to install cargo-cyclonedx
#[serde(skip_serializing_if = "Option::is_none")]
pub install_cargo_cyclonedx: Option<GhaRunStep>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Expression to execute to install omnibor-cli
pub install_omnibor: Option<GhaRunStep>,
}
/// Used in `github/release.yml.j2` to template out "local" build jobs
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GithubLocalJobConfig {
/// Where to run this job?
#[serde(flatten)]
pub runner: GithubRunnerConfig,
/// Expression to execute to install dist
pub install_dist: GhaRunStep,
/// Arguments to pass to dist
pub dist_args: String,
/// Target triples to build for
#[serde(skip_serializing_if = "Option::is_none")]
pub targets: Option<Vec<TripleName>>,
/// Expression to execute to install cargo-auditable
#[serde(skip_serializing_if = "Option::is_none")]
pub install_cargo_auditable: Option<GhaRunStep>,
/// Expression to execute to install omnibor-cli
#[serde(skip_serializing_if = "Option::is_none")]
pub install_omnibor: Option<GhaRunStep>,
/// Command to run to install dependencies
#[serde(skip_serializing_if = "Option::is_none")]
pub packages_install: Option<PackageInstallScript>,
/// What cache provider to use
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_provider: Option<String>,
}
/// A GitHub Actions "run" step, either bash or powershell
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
// this mirrors GHA's structure, see
// * <https://serde.rs/enum-representations.html>
// * <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell>
#[serde(tag = "shell", content = "run")]
pub enum GhaRunStep {
/// see [`DashScript`]
#[serde(rename = "sh")]
Dash(DashScript),
/// see [`PowershellScript`]
#[serde(rename = "pwsh")]
Powershell(PowershellScript),
}
impl From<DashScript> for GhaRunStep {
fn from(bash: DashScript) -> Self {
Self::Dash(bash)
}
}
impl From<PowershellScript> for GhaRunStep {
fn from(powershell: PowershellScript) -> Self {
Self::Powershell(powershell)
}
}
declare_strongly_typed_string! {
/// A bit of shell script (that can run with `/bin/sh`), ran on CI runners. Can be multi-line.
pub struct DashScript => &DashScriptRef;
/// A bit of powershell script, ran on CI runners. Can be multi-line.
pub struct PowershellScript => &PowershellScriptRef;
}
/// Type of job to run on pull request
#[derive(
Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq, PartialOrd, Ord,
)]
pub enum PrRunMode {
/// Do not run on pull requests at all
#[serde(rename = "skip")]
Skip,
/// Only run the plan step
#[default]
#[serde(rename = "plan")]
Plan,
/// Build and upload artifacts
#[serde(rename = "upload")]
Upload,
}
impl std::fmt::Display for PrRunMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PrRunMode::Skip => write!(f, "skip"),
PrRunMode::Plan => write!(f, "plan"),
PrRunMode::Upload => write!(f, "upload"),
}
}
}
/// Info about a system used to build this announcement.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SystemInfo {
/// The unique id of the System
pub id: SystemId,
/// The version of Cargo used (first line of cargo -vV)
pub cargo_version_line: Option<String>,
/// Environment of the System
pub build_environment: BuildEnvironment,
}
/// Release-specific environment variables
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct EnvironmentVariables {
/// Environment variable to force an install location
pub install_dir_env_var: String,
/// Environment variable to force an unmanaged install location
pub unmanaged_dir_env_var: String,
/// Environment variable to disable updater features
pub disable_update_env_var: String,
/// Environment variable to disable modifying the path
pub no_modify_path_env_var: String,
/// Environment variable to set the GitHub base URL
pub github_base_url_env_var: String,
/// Environment variable to set the GitHub Enterprise base URL
pub ghe_base_url_env_var: String,
}
/// A Release of an Application
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Release {
/// The name of the app
pub app_name: String,
/// The version of the app
// FIXME: should be a Version but JsonSchema doesn't support (yet?)
pub app_version: String,
/// Environment variables which control this release's installer's behaviour
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<EnvironmentVariables>,
/// Alternative display name that can be prettier
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
/// Whether to advertise this app's installers/artifacts in announcements
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<bool>,
/// The artifacts for this release (zips, debuginfo, metadata...)
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub artifacts: Vec<ArtifactId>,
/// Hosting info
#[serde(default)]
#[serde(skip_serializing_if = "Hosting::is_empty")]
pub hosting: Hosting,
}
declare_strongly_typed_string! {
/// A lowercase descriptor for a checksum algorithm, like "sha256"
/// or "blake2b".
///
/// TODO(amos): Honestly this type should not exist, it's just what
/// `ChecksumStyle` serializes to. `ChecksumsStyle` should just
/// be serializable, that's it.
pub struct ChecksumExtension => &ChecksumExtensionRef;
/// A checksum value, usually the lower-cased hex string of the checksum
pub struct ChecksumValue => &ChecksumValueRef;
}
/// A distributable artifact that's part of a Release
///
/// i.e. a zip or installer
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Artifact {
/// The unique name of the artifact (e.g. `myapp-v1.0.0-x86_64-pc-windows-msvc.zip`)
///
/// If this is missing then that indicates the artifact is purely informative and has
/// no physical files associated with it. This may be used (in the future) to e.g.
/// indicate you can install the application with `cargo install` or `npm install`.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub name: Option<ArtifactId>,
/// The kind of artifact this is (e.g. "executable-zip")
#[serde(flatten)]
pub kind: ArtifactKind,
/// The target triple of the bundle
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub target_triples: Vec<TripleName>,
/// The location of the artifact on the local system
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub path: Option<LocalPath>,
/// Assets included in the bundle (like executables and READMEs)
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub assets: Vec<Asset>,
/// A string describing how to install this
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub install_hint: Option<String>,
/// A brief description of what this artifact is
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
/// id of an Artifact that contains the checksum for this Artifact
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub checksum: Option<ArtifactId>,
/// checksums for this artifact
///
/// keys are the name of an algorithm like "sha256" or "sha512"
/// values are the actual hex string of the checksum
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub checksums: BTreeMap<ChecksumExtension, ChecksumValue>,
}
/// An asset contained in an artifact (executable, license, etc.)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Asset {
/// A unique opaque id for an Asset
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// The high-level name of the asset
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// The path of the asset relative to the root of the artifact
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<RelPath>,
/// The kind of asset this is
#[serde(flatten)]
pub kind: AssetKind,
}
/// An artifact included in a Distributable
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind")]
#[non_exhaustive]
pub enum AssetKind {
/// An executable artifact
#[serde(rename = "executable")]
Executable(ExecutableAsset),
/// A C dynamic library
#[serde(rename = "c_dynamic_library")]
CDynamicLibrary(DynamicLibraryAsset),
/// A C static library
#[serde(rename = "c_static_library")]
CStaticLibrary(StaticLibraryAsset),
/// A README file
#[serde(rename = "readme")]
Readme,
/// A LICENSE file
#[serde(rename = "license")]
License,
/// A CHANGELOG or RELEASES file
#[serde(rename = "changelog")]
Changelog,
/// Unknown to this version of cargo-dist-schema
///
/// This is a fallback for forward/backward-compat
#[serde(other)]
#[serde(rename = "unknown")]
Unknown,
}
/// A kind of Artifact
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind")]
#[non_exhaustive]
pub enum ArtifactKind {
/// A zip or a tarball
#[serde(rename = "executable-zip")]
ExecutableZip,
/// Standalone Symbols/Debuginfo for a build
#[serde(rename = "symbols")]
Symbols,
/// Installer
#[serde(rename = "installer")]
Installer,
/// A checksum of another artifact
#[serde(rename = "checksum")]
Checksum,
/// The checksums of many artifacts
#[serde(rename = "unified-checksum")]
UnifiedChecksum,
/// A tarball containing the source code
#[serde(rename = "source-tarball")]
SourceTarball,
/// Some form of extra artifact produced by a sidecar build
#[serde(rename = "extra-artifact")]
ExtraArtifact,
/// An updater executable
#[serde(rename = "updater")]
Updater,
/// A file that already exists
#[serde(rename = "sbom")]
SBOM,
/// An OmniBOR Artifact ID
#[serde(rename = "omnibor-artifact-id")]
OmniborArtifactId,
/// Unknown to this version of cargo-dist-schema
///
/// This is a fallback for forward/backward-compat
#[serde(other)]
#[serde(rename = "unknown")]
Unknown,
}
/// An executable artifact (exe/binary)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ExecutableAsset {
/// The name of the Artifact containing symbols for this executable
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub symbols_artifact: Option<ArtifactId>,
}
/// A C dynamic library artifact (so/dylib/dll)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DynamicLibraryAsset {
/// The name of the Artifact containing symbols for this library
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub symbols_artifact: Option<ArtifactId>,
}
/// A C static library artifact (a/lib)
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct StaticLibraryAsset {
/// The name of the Artifact containing symbols for this library
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub symbols_artifact: Option<ArtifactId>,
}
/// Info about a manifest version
pub struct VersionInfo {
/// The version
pub version: Version,
/// The rough epoch of the format
pub format: Format,
}
/// The current version of cargo-dist-schema
pub const SELF_VERSION: &str = env!("CARGO_PKG_VERSION");
/// The first epoch of cargo-dist, after this version a bunch of things changed
/// and we don't support that design anymore!
pub const DIST_EPOCH_1_MAX: &str = "0.0.3-prerelease8";
/// Second epoch of cargo-dist, after this we stopped putting versions in artifact ids.
/// This changes the download URL, but everything else works the same.
pub const DIST_EPOCH_2_MAX: &str = "0.0.6-prerelease6";
/// More coarse-grained version info, indicating periods when significant changes were made
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Format {
/// THE BEFORE TIMES -- Unsupported
Epoch1,
/// First stable versions; during this epoch artifact names/ids contained their version numbers.
Epoch2,
/// Same as Epoch2, but now artifact names/ids don't include the version number,
/// making /latest/ a stable path/url you can perma-link. This only affects download URLs.
Epoch3,
/// The version is newer than this version of cargo-dist-schema, so we don't know. Most
/// likely it's compatible/readable, but maybe a breaking change was made?
Future,
}
impl Format {
/// Whether this format is too old to be supported
pub fn unsupported(&self) -> bool {
self <= &Format::Epoch1
}
/// Whether this format has version numbers in artifact names
pub fn artifact_names_contain_versions(&self) -> bool {
self <= &Format::Epoch2
}
}
impl DistManifest {
/// Create a new DistManifest
pub fn new(releases: Vec<Release>, artifacts: BTreeMap<ArtifactId, Artifact>) -> Self {
Self {
dist_version: None,
announcement_tag: None,
announcement_tag_is_implicit: false,
announcement_is_prerelease: false,
announcement_title: None,
announcement_changelog: None,
announcement_github_body: None,
github_attestations: false,
system_info: None,
releases,
artifacts,
systems: Default::default(),
assets: Default::default(),
publish_prereleases: false,
force_latest: false,
ci: None,
linkage: vec![],
upload_files: vec![],
}
}
/// Get the JSON Schema for a DistManifest
pub fn json_schema() -> schemars::Schema {
schemars::schema_for!(DistManifest)
}
/// Get the format of the manifest
///
/// If anything goes wrong we'll default to Format::Future
pub fn format(&self) -> Format {
self.dist_version
.as_ref()
.and_then(|v| v.parse().ok())
.map(|v| format_of_version(&v))
.unwrap_or(Format::Future)
}
/// Convenience for iterating artifacts
pub fn artifacts_for_release<'a>(
&'a self,
release: &'a Release,
) -> impl Iterator<Item = (&'a ArtifactIdRef, &'a Artifact)> {
release
.artifacts
.iter()
.filter_map(|k| Some((&**k, self.artifacts.get(k)?)))
}
/// Look up a release by its name
pub fn release_by_name(&self, name: &str) -> Option<&Release> {
self.releases.iter().find(|r| r.app_name == name)
}
/// Update the download url for an Axo Release (to a prettier one)
pub fn update_release_axodotdev_artifact_download_url(&mut self, name: &str, new_url: String) {
// Find the release
let release = self.releases.iter_mut().find(|r| r.app_name == name);
let Some(release) = release else {
return;
};
// Swap the new URL in
let mut old_url = None;
if let Some(host) = &mut release.hosting.axodotdev {
old_url = host.set_download_url.take();
host.set_download_url = Some(new_url.clone());
}
// If the url changed, update install_hints
if let Some(old_url) = old_url {
for artifact_name in &release.artifacts {
let artifact = self
.artifacts
.get_mut(artifact_name)
.expect("release referenced non-existent artifacts");
if let Some(hint) = &mut artifact.install_hint {
*hint = hint.replace(&old_url, &new_url);
}
}
}
}
/// Either get the release with the given name, or make a minimal one
/// with no hosting/artifacts (to be populated)
pub fn ensure_release(&mut self, name: String, version: String) -> &mut Release {
// Written slightly awkwardly to make the borrowchecker happy :/
if let Some(position) = self.releases.iter().position(|r| r.app_name == name) {
&mut self.releases[position]
} else {
let env_app_name = name.to_ascii_uppercase().replace('-', "_");
let install_dir_env_var = format!("{env_app_name}_INSTALL_DIR");
let unmanaged_dir_env_var = format!("{env_app_name}_UNMANAGED_INSTALL");
let disable_update_env_var = format!("{env_app_name}_DISABLE_UPDATE");
let no_modify_path_env_var = format!("{env_app_name}_NO_MODIFY_PATH");
let github_base_url_env_var = format!("{env_app_name}_INSTALLER_GITHUB_BASE_URL");
let ghe_base_url_env_var = format!("{env_app_name}_INSTALLER_GHE_BASE_URL");
let environment_variables = EnvironmentVariables {
install_dir_env_var,
unmanaged_dir_env_var,
disable_update_env_var,
no_modify_path_env_var,
github_base_url_env_var,
ghe_base_url_env_var,
};
self.releases.push(Release {
app_name: name,
app_version: version,
env: Some(environment_variables),
artifacts: vec![],
hosting: Hosting::default(),
display: None,
display_name: None,
});
self.releases.last_mut().unwrap()
}
}
/// Get the merged linkage for an artifact
///
/// This lets you know what system dependencies an entire archive of binaries requires
pub fn linkage_for_artifact(&self, artifact_id: &ArtifactId) -> Linkage {
let mut output = Linkage::default();
let Some(artifact) = self.artifacts.get(artifact_id) else {
return output;
};
for base_asset in &artifact.assets {
let Some(asset_id) = &base_asset.id else {
continue;
};
let Some(true_asset) = self.assets.get(asset_id) else {
continue;
};
let Some(linkage) = &true_asset.linkage else {
continue;
};
output.extend(linkage);
}
output
}
}
impl Release {
/// Get the base URL that artifacts should be downloaded from (append the artifact name to the URL)
pub fn artifact_download_url(&self) -> Option<String> {
self.hosting.artifact_download_url()
}
}