1use crate::{Git, Release, SortedSlice, Status, api, git::GITHUB_API_CLIENT};
4pub use binary::*;
5use derivative::Derivative;
6use duct::cmd;
7use flate2::read::GzDecoder;
8use regex::Regex;
9use reqwest::StatusCode;
10use std::{
11 collections::HashMap,
12 error::Error as _,
13 fs::{File, copy, metadata, read_dir, rename},
14 io::{BufRead, Seek, SeekFrom, Write},
15 os::unix::fs::PermissionsExt,
16 path::{Path, PathBuf},
17 time::Duration,
18};
19use tar::Archive;
20use tempfile::{tempdir, tempfile};
21use thiserror::Error;
22use url::Url;
23
24mod binary;
25
26#[derive(Error, Debug)]
28pub enum Error {
29 #[error("Anyhow error: {0}")]
31 AnyhowError(#[from] anyhow::Error),
32 #[error("API error: {0}")]
34 ApiError(#[from] api::Error),
35 #[error("Archive error: {0}")]
37 ArchiveError(String),
38 #[error("HTTP error: {0} caused by {:?}", reqwest::Error::source(.0))]
40 HttpError(#[from] reqwest::Error),
41 #[error("IO error: {0}")]
43 IO(#[from] std::io::Error),
44 #[error("Missing binary: {0}")]
46 MissingBinary(String),
47 #[error("ParseError error: {0}")]
49 ParseError(#[from] url::ParseError),
50}
51
52#[derive(Clone, Debug, PartialEq)]
54pub enum Source {
55 #[allow(dead_code)]
57 Archive {
58 url: String,
60 contents: Vec<String>,
62 },
63 Git {
65 url: Url,
67 reference: Option<String>,
69 manifest: Option<PathBuf>,
71 package: String,
73 artifacts: Vec<String>,
75 },
76 GitHub(GitHub),
78 #[allow(dead_code)]
80 Url {
81 url: String,
83 name: String,
85 },
86}
87
88impl Source {
89 pub(super) async fn source(
98 &self,
99 cache: &Path,
100 release: bool,
101 status: &impl Status,
102 verbose: bool,
103 ) -> Result<(), Error> {
104 use Source::*;
105 match self {
106 Archive { url, contents } => {
107 let contents: Vec<_> = contents
108 .iter()
109 .map(|name| ArchiveFileSpec::new(name.into(), Some(cache.join(name)), true))
110 .collect();
111 from_archive(url, &contents, status).await
112 },
113 Git { url, reference, manifest, package, artifacts } => {
114 let artifacts: Vec<_> = artifacts
115 .iter()
116 .map(|name| match reference {
117 Some(version) => (name.as_str(), cache.join(format!("{name}-{version}"))),
118 None => (name.as_str(), cache.join(name)),
119 })
120 .collect();
121 from_git(
122 url.as_str(),
123 reference.as_deref(),
124 manifest.as_ref(),
125 package,
126 &artifacts,
127 release,
128 status,
129 verbose,
130 )
131 .await
132 },
133 GitHub(source) => source.source(cache, release, status, verbose).await,
134 Url { url, name } => from_url(url, &cache.join(name), status).await,
135 }
136 }
137
138 pub async fn resolve(
149 self,
150 name: &str,
151 version: Option<&str>,
152 cache: &Path,
153 cache_filter: impl for<'a> FnOnce(&'a str) -> bool + Copy,
154 ) -> Self {
155 match self {
156 Source::GitHub(github) =>
157 Source::GitHub(github.resolve(name, version, cache, cache_filter).await),
158 _ => self,
159 }
160 }
161}
162
163#[derive(Clone, Debug, Derivative)]
165#[derivative(PartialEq)]
166pub enum GitHub {
167 ReleaseArchive {
169 owner: String,
171 repository: String,
173 tag: Option<String>,
175 tag_pattern: Option<TagPattern>,
178 prerelease: bool,
180 #[derivative(PartialEq = "ignore")]
182 version_comparator: for<'a> fn(&'a mut [String]) -> SortedSlice<'a, String>,
183 fallback: String,
185 archive: String,
187 contents: Vec<ArchiveFileSpec>,
189 latest: Option<String>,
191 },
192 SourceCodeArchive {
194 owner: String,
196 repository: String,
198 reference: Option<String>,
200 manifest: Option<PathBuf>,
202 package: String,
204 artifacts: Vec<String>,
206 },
207}
208
209impl GitHub {
210 async fn source(
220 &self,
221 cache: &Path,
222 release: bool,
223 status: &impl Status,
224 verbose: bool,
225 ) -> Result<(), Error> {
226 use GitHub::*;
227 match self {
228 ReleaseArchive { owner, repository, tag, tag_pattern, archive, contents, .. } => {
229 let base_url = format!("https://github.com/{owner}/{repository}/releases");
231 let url = match tag.as_ref() {
232 Some(tag) => {
233 format!("{base_url}/download/{tag}/{archive}")
234 },
235 None => format!("{base_url}/latest/download/{archive}"),
236 };
237 let contents: Vec<_> = contents
238 .iter()
239 .map(|ArchiveFileSpec { name, target, required }| match tag.as_ref() {
240 Some(tag) => ArchiveFileSpec::new(
241 name.into(),
242 Some(cache.join(format!(
243 "{}-{}",
244 target.as_ref().map_or(name.as_str(), |t| t
245 .to_str()
246 .expect("expected target file name to be valid utf-8")),
247 tag_pattern
248 .as_ref()
249 .and_then(|pattern| pattern.version(tag))
250 .unwrap_or(tag)
251 ))),
252 *required,
253 ),
254 None => ArchiveFileSpec::new(
255 name.into(),
256 Some(cache.join(target.as_ref().map_or(name.as_str(), |t| {
257 t.to_str().expect("expected target file name to be valid utf-8")
258 }))),
259 *required,
260 ),
261 })
262 .collect();
263 from_archive(&url, &contents, status).await
264 },
265 SourceCodeArchive { owner, repository, reference, manifest, package, artifacts } => {
266 let artifacts: Vec<_> = artifacts
267 .iter()
268 .map(|name| match reference {
269 Some(reference) =>
270 (name.as_str(), cache.join(format!("{name}-{reference}"))),
271 None => (name.as_str(), cache.join(name)),
272 })
273 .collect();
274 from_github_archive(
275 owner,
276 repository,
277 reference.as_ref().map(|r| r.as_str()),
278 manifest.as_ref(),
279 package,
280 &artifacts,
281 release,
282 status,
283 verbose,
284 )
285 .await
286 },
287 }
288 }
289
290 async fn resolve(
301 self,
302 name: &str,
303 version: Option<&str>,
304 cache: &Path,
305 cache_filter: impl FnOnce(&str) -> bool + Copy,
306 ) -> Self {
307 match self {
308 Self::ReleaseArchive {
309 owner,
310 repository,
311 tag: _,
312 tag_pattern,
313 prerelease,
314 version_comparator,
315 fallback,
316 archive,
317 contents,
318 latest: _,
319 } => {
320 let repo = crate::GitHub::new(owner.as_str(), repository.as_str());
322 let mut releases = repo.releases(prerelease).await.unwrap_or_else(|_e| {
323 let version = version.unwrap_or(fallback.as_str());
325 vec![Release {
326 tag_name: tag_pattern.as_ref().map_or_else(
327 || version.to_string(),
328 |pattern| pattern.resolve_tag(version),
329 ),
330 name: String::default(),
331 prerelease,
332 commit: None,
333 published_at: String::default(),
334 }]
335 });
336
337 if let Some(pattern) = tag_pattern.as_ref() {
339 releases.retain(|r| pattern.regex.is_match(&r.tag_name));
340 }
341
342 let mut binaries: HashMap<_, _> = releases
345 .into_iter()
346 .map(|r| {
347 let version = tag_pattern
348 .as_ref()
349 .and_then(|pattern| pattern.version(&r.tag_name).map(|v| v.to_string()))
350 .unwrap_or_else(|| r.tag_name.clone());
351 (version, r.tag_name)
352 })
353 .collect();
354
355 let version = version.map(|v| {
358 tag_pattern
359 .as_ref()
360 .and_then(|pattern| pattern.version(v))
361 .unwrap_or(v)
362 .to_string()
363 });
364
365 let cached_files = read_dir(cache).into_iter().flatten();
367 let cached_file_names = cached_files
368 .filter_map(|f| f.ok().and_then(|f| f.file_name().into_string().ok()));
369 for file in cached_file_names.filter(|f| cache_filter(f)) {
370 let version = file.replace(&format!("{name}-"), "");
371 let tag = tag_pattern.as_ref().map_or_else(
372 || version.to_string(),
373 |pattern| pattern.resolve_tag(&version),
374 );
375 binaries.insert(version, tag);
376 }
377
378 let mut versions: Vec<_> = binaries.keys().cloned().collect();
380 let versions = version_comparator(versions.as_mut_slice());
381
382 let tag = version.as_ref().map_or_else(
385 || {
386 let resolved_version =
388 Binary::resolve_version(name, None, &versions, cache);
389 resolved_version.and_then(|v| binaries.get(v)).cloned()
390 },
391 |v| {
392 Some(
394 tag_pattern
395 .as_ref()
396 .map_or_else(|| v.to_string(), |pattern| pattern.resolve_tag(v)),
397 )
398 },
399 );
400
401 let latest: Option<String> = version
404 .is_none()
405 .then(|| versions.first().and_then(|v| binaries.get(v.as_str()).cloned()))
406 .flatten();
407
408 Self::ReleaseArchive {
409 owner,
410 repository,
411 tag,
412 tag_pattern,
413 prerelease,
414 version_comparator,
415 fallback,
416 archive,
417 contents,
418 latest,
419 }
420 },
421 _ => self,
422 }
423 }
424}
425
426#[derive(Clone, Debug, PartialEq)]
428pub struct ArchiveFileSpec {
429 pub name: String,
431 pub target: Option<PathBuf>,
433 pub required: bool,
435}
436
437impl ArchiveFileSpec {
438 pub fn new(name: String, target: Option<PathBuf>, required: bool) -> Self {
445 Self { name, target, required }
446 }
447}
448
449#[derive(Clone, Debug)]
454pub struct TagPattern {
455 regex: Regex,
456 pattern: String,
457}
458
459impl TagPattern {
460 pub fn new(pattern: &str) -> Self {
465 Self {
466 regex: Regex::new(&format!("^{}$", pattern.replace("{version}", "(?P<version>.+)")))
467 .expect("expected valid regex"),
468 pattern: pattern.into(),
469 }
470 }
471
472 pub fn resolve_tag(&self, value: &str) -> String {
477 if self.regex.is_match(value) {
479 return value.to_string();
480 }
481
482 self.pattern.replace("{version}", value)
483 }
484
485 pub fn version<'a>(&self, value: &'a str) -> Option<&'a str> {
490 self.regex.captures(value).and_then(|c| c.name("version").map(|v| v.as_str()))
491 }
492}
493
494impl PartialEq for TagPattern {
495 fn eq(&self, other: &Self) -> bool {
496 self.regex.as_str() == other.regex.as_str() && self.pattern == other.pattern
497 }
498}
499
500impl From<&str> for TagPattern {
501 fn from(value: &str) -> Self {
502 Self::new(value)
503 }
504}
505
506async fn from_archive(
513 url: &str,
514 contents: &[ArchiveFileSpec],
515 status: &impl Status,
516) -> Result<(), Error> {
517 status.update(&format!("Downloading from {url}..."));
519 let response = reqwest::get(url).await?.error_for_status()?;
520 let mut file = tempfile()?;
521 file.write_all(&response.bytes().await?)?;
522 file.seek(SeekFrom::Start(0))?;
523 status.update("Extracting from archive...");
525 let tar = GzDecoder::new(file);
526 let mut archive = Archive::new(tar);
527 let temp_dir = tempdir()?;
528 let working_dir = temp_dir.path();
529 archive.unpack(working_dir)?;
530 for ArchiveFileSpec { name, target, required } in contents {
531 let src = working_dir.join(name);
532 if src.exists() {
533 set_executable_permission(&src)?;
534 if let Some(target) = target &&
535 let Err(_e) = rename(&src, target)
536 {
537 copy(&src, target)?;
540 std::fs::remove_file(&src)?;
541 }
542 } else if *required {
543 return Err(Error::ArchiveError(format!(
544 "Expected file '{}' in archive, but it was not found.",
545 name
546 )));
547 }
548 }
549 status.update("Sourcing complete.");
550 Ok(())
551}
552
553#[allow(clippy::too_many_arguments)]
565async fn from_git(
566 url: &str,
567 reference: Option<&str>,
568 manifest: Option<impl AsRef<Path>>,
569 package: &str,
570 artifacts: &[(&str, impl AsRef<Path>)],
571 release: bool,
572 status: &impl Status,
573 verbose: bool,
574) -> Result<(), Error> {
575 let temp_dir = tempdir()?;
577 let working_dir = temp_dir.path();
578 status.update(&format!("Cloning {url}..."));
579 Git::clone(&Url::parse(url)?, working_dir, reference)?;
580 status.update("Starting build of binary...");
582 let manifest = manifest
583 .as_ref()
584 .map_or_else(|| working_dir.join("Cargo.toml"), |m| working_dir.join(m));
585 build(manifest, package, artifacts, release, status, verbose).await?;
586 status.update("Sourcing complete.");
587 Ok(())
588}
589
590#[allow(clippy::too_many_arguments)]
603async fn from_github_archive(
604 owner: &str,
605 repository: &str,
606 reference: Option<&str>,
607 manifest: Option<impl AsRef<Path>>,
608 package: &str,
609 artifacts: &[(&str, impl AsRef<Path>)],
610 release: bool,
611 status: &impl Status,
612 verbose: bool,
613) -> Result<(), Error> {
614 let response = match reference {
616 Some(reference) => {
617 let urls = [
619 format!(
620 "https://github.com/{owner}/{repository}/archive/refs/heads/{reference}.tar.gz"
621 ),
622 format!(
623 "https://github.com/{owner}/{repository}/archive/refs/tags/{reference}.tar.gz"
624 ),
625 format!("https://github.com/{owner}/{repository}/archive/{reference}.tar.gz"),
626 ];
627 let mut response = None;
628 for url in urls {
629 status.update(&format!("Downloading from {url}..."));
630 response = Some(GITHUB_API_CLIENT.get(url).await);
631 if let Some(Err(api::Error::HttpError(e))) = &response &&
632 e.status() == Some(StatusCode::NOT_FOUND)
633 {
634 tokio::time::sleep(Duration::from_secs(1)).await;
635 continue;
636 }
637 break;
638 }
639 response.expect("value set above")?
640 },
641 None => {
642 let url = format!("https://api.github.com/repos/{owner}/{repository}/tarball");
643 status.update(&format!("Downloading from {url}..."));
644 GITHUB_API_CLIENT.get(url).await?
645 },
646 };
647 let mut file = tempfile()?;
648 file.write_all(&response)?;
649 file.seek(SeekFrom::Start(0))?;
650 status.update("Extracting from archive...");
652 let tar = GzDecoder::new(file);
653 let mut archive = Archive::new(tar);
654 let temp_dir = tempdir()?;
655 let mut working_dir = temp_dir.path().into();
656 archive.unpack(&working_dir)?;
657 let entries: Vec<_> = read_dir(&working_dir)?.take(2).filter_map(|x| x.ok()).collect();
659 match entries.len() {
660 0 => {
661 return Err(Error::ArchiveError(
662 "The downloaded archive does not contain any entries.".into(),
663 ));
664 },
665 1 => working_dir = entries[0].path(), _ => {}, }
669 status.update("Starting build of binary...");
671 let manifest = manifest
672 .as_ref()
673 .map_or_else(|| working_dir.join("Cargo.toml"), |m| working_dir.join(m));
674 build(&manifest, package, artifacts, release, status, verbose).await?;
675 status.update("Sourcing complete.");
676 Ok(())
677}
678
679pub(crate) async fn from_local_package(
688 manifest: &Path,
689 package: &str,
690 release: bool,
691 status: &impl Status,
692 verbose: bool,
693) -> Result<(), Error> {
694 status.update("Starting build of binary...");
696 const EMPTY: [(&str, PathBuf); 0] = [];
697 build(manifest, package, &EMPTY, release, status, verbose).await?;
698 status.update("Sourcing complete.");
699 Ok(())
700}
701
702async fn from_url(url: &str, path: &Path, status: &impl Status) -> Result<(), Error> {
709 status.update(&format!("Downloading from {url}..."));
711 download(url, path).await?;
712 status.update("Sourcing complete.");
713 Ok(())
714}
715
716async fn build(
726 manifest: impl AsRef<Path>,
727 package: &str,
728 artifacts: &[(&str, impl AsRef<Path>)],
729 release: bool,
730 status: &impl Status,
731 verbose: bool,
732) -> Result<(), Error> {
733 let manifest_path = manifest.as_ref().to_str().expect("expected manifest path to be valid");
735 let mut args = vec!["build", "-p", package, "--manifest-path", manifest_path];
736 if release {
737 args.push("--release")
738 }
739 let command = cmd("cargo", args);
741 match verbose {
742 false => {
743 let reader = command.stderr_to_stdout().reader()?;
744 let output = std::io::BufReader::new(reader).lines();
745 for line in output {
746 status.update(&line?);
747 }
748 },
749 true => {
750 command.run()?;
751 },
752 }
753 let target = manifest
755 .as_ref()
756 .parent()
757 .expect("expected parent directory to be valid")
758 .join(format!("target/{}", if release { "release" } else { "debug" }));
759 for (name, dest) in artifacts {
760 copy(target.join(name), dest)?;
761 }
762 Ok(())
763}
764
765async fn download(url: &str, dest: &Path) -> Result<(), Error> {
771 let response = reqwest::get(url).await?.error_for_status()?;
773 let mut file = File::create(dest)?;
774 file.write_all(&response.bytes().await?)?;
775 set_executable_permission(dest)?;
777 Ok(())
778}
779
780pub fn set_executable_permission<P: AsRef<Path>>(path: P) -> Result<(), Error> {
785 let mut perms = metadata(&path)?.permissions();
786 perms.set_mode(0o755);
787 std::fs::set_permissions(path, perms)?;
788 Ok(())
789}
790
791#[cfg(test)]
792pub(super) mod tests {
793 use super::{GitHub::*, Status, *};
794 use crate::{polkadot_sdk::parse_version, target};
795 use tempfile::tempdir;
796
797 #[tokio::test]
798 async fn sourcing_from_archive_works() -> anyhow::Result<()> {
799 let url = "https://github.com/r0gue-io/polkadot/releases/latest/download/polkadot-aarch64-apple-darwin.tar.gz".to_string();
800 let name = "polkadot".to_string();
801 let contents =
802 vec![name.clone(), "polkadot-execute-worker".into(), "polkadot-prepare-worker".into()];
803 let temp_dir = tempdir()?;
804
805 Source::Archive { url, contents: contents.clone() }
806 .source(temp_dir.path(), true, &Output, true)
807 .await?;
808 for item in contents {
809 assert!(temp_dir.path().join(item).exists());
810 }
811 Ok(())
812 }
813
814 #[tokio::test]
815 async fn resolve_from_archive_is_noop() -> anyhow::Result<()> {
816 let url = "https://github.com/r0gue-io/polkadot/releases/latest/download/polkadot-aarch64-apple-darwin.tar.gz".to_string();
817 let name = "polkadot".to_string();
818 let contents =
819 vec![name.clone(), "polkadot-execute-worker".into(), "polkadot-prepare-worker".into()];
820 let temp_dir = tempdir()?;
821
822 let source = Source::Archive { url, contents: contents.clone() };
823 assert_eq!(
824 source.clone().resolve(&name, None, temp_dir.path(), filters::polkadot).await,
825 source
826 );
827 Ok(())
828 }
829
830 #[tokio::test]
831 async fn sourcing_from_git_works() -> anyhow::Result<()> {
832 let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?;
833 let package = "hello_world".to_string();
834 let temp_dir = tempdir()?;
835
836 Source::Git {
837 url,
838 reference: None,
839 manifest: None,
840 package: package.clone(),
841 artifacts: vec![package.clone()],
842 }
843 .source(temp_dir.path(), true, &Output, true)
844 .await?;
845 assert!(temp_dir.path().join(package).exists());
846 Ok(())
847 }
848
849 #[tokio::test]
850 async fn resolve_from_git_is_noop() -> anyhow::Result<()> {
851 let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?;
852 let package = "hello_world".to_string();
853 let temp_dir = tempdir()?;
854
855 let source = Source::Git {
856 url,
857 reference: None,
858 manifest: None,
859 package: package.clone(),
860 artifacts: vec![package.clone()],
861 };
862 assert_eq!(
863 source
864 .clone()
865 .resolve(&package, None, temp_dir.path(), |f| filters::prefix(f, &package))
866 .await,
867 source
868 );
869 Ok(())
870 }
871
872 #[tokio::test]
873 async fn sourcing_from_git_ref_works() -> anyhow::Result<()> {
874 let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?;
875 let initial_commit = "436b7dbffdfaaf7ad90bf44ae8fdcb17eeee65a3".to_string();
876 let package = "hello_world".to_string();
877 let temp_dir = tempdir()?;
878
879 Source::Git {
880 url,
881 reference: Some(initial_commit.clone()),
882 manifest: None,
883 package: package.clone(),
884 artifacts: vec![package.clone()],
885 }
886 .source(temp_dir.path(), true, &Output, true)
887 .await?;
888 assert!(temp_dir.path().join(format!("{package}-{initial_commit}")).exists());
889 Ok(())
890 }
891
892 #[tokio::test]
893 async fn sourcing_from_github_release_archive_works() -> anyhow::Result<()> {
894 let owner = "r0gue-io".to_string();
895 let repository = "polkadot".to_string();
896 let version = "stable2503";
897 let tag_pattern = Some("polkadot-{version}".into());
898 let fallback = "stable2412-4".into();
899 let archive = format!("polkadot-{}.tar.gz", target()?);
900 let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
901 let temp_dir = tempdir()?;
902
903 Source::GitHub(ReleaseArchive {
904 owner,
905 repository,
906 tag: Some(format!("polkadot-{version}")),
907 tag_pattern,
908 prerelease: false,
909 version_comparator,
910 fallback,
911 archive,
912 contents: contents.map(|n| ArchiveFileSpec::new(n.into(), None, true)).to_vec(),
913 latest: None,
914 })
915 .source(temp_dir.path(), true, &Output, true)
916 .await?;
917 for item in contents {
918 assert!(temp_dir.path().join(format!("{item}-{version}")).exists());
919 }
920 Ok(())
921 }
922
923 #[tokio::test]
924 async fn resolve_from_github_release_archive_works() -> anyhow::Result<()> {
925 let owner = "r0gue-io".to_string();
926 let repository = "polkadot".to_string();
927 let version = "stable2503";
928 let tag_pattern = Some("polkadot-{version}".into());
929 let fallback = "stable2412-4".into();
930 let archive = format!("polkadot-{}.tar.gz", target()?);
931 let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
932 let temp_dir = tempdir()?;
933
934 let mut releases: Vec<_> = crate::GitHub::new(owner.as_str(), repository.as_str())
936 .releases(false)
937 .await?
938 .into_iter()
939 .map(|r| r.tag_name)
940 .collect();
941 let sorted_releases = version_comparator(releases.as_mut_slice());
942
943 let source = Source::GitHub(ReleaseArchive {
944 owner,
945 repository,
946 tag: None,
947 tag_pattern,
948 prerelease: false,
949 version_comparator,
950 fallback,
951 archive,
952 contents: contents.map(|n| ArchiveFileSpec::new(n.into(), None, true)).to_vec(),
953 latest: None,
954 });
955
956 for version in [Some(version), None] {
958 let source = source
959 .clone()
960 .resolve("polkadot", version, temp_dir.path(), filters::polkadot)
961 .await;
962 let expected_tag = version.map_or_else(
963 || sorted_releases.0.first().unwrap().into(),
964 |v| format!("polkadot-{v}"),
965 );
966 let expected_latest = version.map_or_else(|| sorted_releases.0.first(), |_| None);
967 assert!(matches!(
968 source,
969 Source::GitHub(ReleaseArchive { tag, latest, .. } )
970 if tag == Some(expected_tag) && latest.as_ref() == expected_latest
971 ));
972 }
973
974 let cached_version = "polkadot-stable2612";
976 File::create(temp_dir.path().join(cached_version))?;
977 for version in [Some(version), None] {
978 let source = source
979 .clone()
980 .resolve("polkadot", version, temp_dir.path(), filters::polkadot)
981 .await;
982 let expected_tag =
983 version.map_or_else(|| cached_version.to_string(), |v| format!("polkadot-{v}"));
984 let expected_latest =
985 version.map_or_else(|| Some(cached_version.to_string()), |_| None);
986 assert!(matches!(
987 source,
988 Source::GitHub(ReleaseArchive { tag, latest, .. } )
989 if tag == Some(expected_tag) && latest == expected_latest
990 ));
991 }
992
993 Ok(())
994 }
995
996 #[tokio::test]
997 async fn sourcing_from_github_release_archive_maps_contents() -> anyhow::Result<()> {
998 let owner = "r0gue-io".to_string();
999 let repository = "polkadot".to_string();
1000 let version = "stable2503";
1001 let tag_pattern = Some("polkadot-{version}".into());
1002 let name = "polkadot".to_string();
1003 let fallback = "stable2412-4".into();
1004 let archive = format!("{name}-{}.tar.gz", target()?);
1005 let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
1006 let temp_dir = tempdir()?;
1007 let prefix = "test";
1008
1009 Source::GitHub(ReleaseArchive {
1010 owner,
1011 repository,
1012 tag: Some(format!("polkadot-{version}")),
1013 tag_pattern,
1014 prerelease: false,
1015 version_comparator,
1016 fallback,
1017 archive,
1018 contents: contents
1019 .map(|n| ArchiveFileSpec::new(n.into(), Some(format!("{prefix}-{n}").into()), true))
1020 .to_vec(),
1021 latest: None,
1022 })
1023 .source(temp_dir.path(), true, &Output, true)
1024 .await?;
1025 for item in contents {
1026 assert!(temp_dir.path().join(format!("{prefix}-{item}-{version}")).exists());
1027 }
1028 Ok(())
1029 }
1030
1031 #[tokio::test]
1032 async fn sourcing_from_latest_github_release_archive_works() -> anyhow::Result<()> {
1033 let owner = "r0gue-io".to_string();
1034 let repository = "polkadot".to_string();
1035 let tag_pattern = Some("polkadot-{version}".into());
1036 let name = "polkadot".to_string();
1037 let fallback = "stable2412-4".into();
1038 let archive = format!("{name}-{}.tar.gz", target()?);
1039 let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
1040 let temp_dir = tempdir()?;
1041
1042 Source::GitHub(ReleaseArchive {
1043 owner,
1044 repository,
1045 tag: None,
1046 tag_pattern,
1047 prerelease: false,
1048 version_comparator,
1049 fallback,
1050 archive,
1051 contents: contents.map(|n| ArchiveFileSpec::new(n.into(), None, true)).to_vec(),
1052 latest: None,
1053 })
1054 .source(temp_dir.path(), true, &Output, true)
1055 .await?;
1056 for item in contents {
1057 assert!(temp_dir.path().join(item).exists());
1058 }
1059 Ok(())
1060 }
1061
1062 #[tokio::test]
1063 async fn sourcing_from_github_source_code_archive_works() -> anyhow::Result<()> {
1064 let owner = "paritytech".to_string();
1065 let repository = "polkadot-sdk".to_string();
1066 let package = "polkadot".to_string();
1067 let temp_dir = tempdir()?;
1068 let initial_commit = "72dba98250a6267c61772cd55f8caf193141050f";
1069 let manifest = PathBuf::from("substrate/Cargo.toml");
1070
1071 Source::GitHub(SourceCodeArchive {
1072 owner,
1073 repository,
1074 reference: Some(initial_commit.to_string()),
1075 manifest: Some(manifest),
1076 package: package.clone(),
1077 artifacts: vec![package.clone()],
1078 })
1079 .source(temp_dir.path(), true, &Output, true)
1080 .await?;
1081 assert!(temp_dir.path().join(format!("{package}-{initial_commit}")).exists());
1082 Ok(())
1083 }
1084
1085 #[tokio::test]
1086 async fn resolve_from_github_source_code_archive_is_noop() -> anyhow::Result<()> {
1087 let owner = "paritytech".to_string();
1088 let repository = "polkadot-sdk".to_string();
1089 let package = "polkadot".to_string();
1090 let temp_dir = tempdir()?;
1091 let initial_commit = "72dba98250a6267c61772cd55f8caf193141050f";
1092 let manifest = PathBuf::from("substrate/Cargo.toml");
1093
1094 let source = Source::GitHub(SourceCodeArchive {
1095 owner,
1096 repository,
1097 reference: Some(initial_commit.to_string()),
1098 manifest: Some(manifest),
1099 package: package.clone(),
1100 artifacts: vec![package.clone()],
1101 });
1102 assert_eq!(
1103 source.clone().resolve(&package, None, temp_dir.path(), filters::polkadot).await,
1104 source
1105 );
1106 Ok(())
1107 }
1108
1109 #[tokio::test]
1110 async fn sourcing_from_latest_github_source_code_archive_works() -> anyhow::Result<()> {
1111 let owner = "hpaluch".to_string();
1112 let repository = "rust-hello-world".to_string();
1113 let package = "hello_world".to_string();
1114 let temp_dir = tempdir()?;
1115
1116 Source::GitHub(SourceCodeArchive {
1117 owner,
1118 repository,
1119 reference: None,
1120 manifest: None,
1121 package: package.clone(),
1122 artifacts: vec![package.clone()],
1123 })
1124 .source(temp_dir.path(), true, &Output, true)
1125 .await?;
1126 assert!(temp_dir.path().join(package).exists());
1127 Ok(())
1128 }
1129
1130 #[tokio::test]
1131 async fn sourcing_from_url_works() -> anyhow::Result<()> {
1132 let url =
1133 "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc"
1134 .to_string();
1135 let name = "polkadot";
1136 let temp_dir = tempdir()?;
1137
1138 Source::Url { url, name: name.into() }
1139 .source(temp_dir.path(), false, &Output, true)
1140 .await?;
1141 assert!(temp_dir.path().join(name).exists());
1142 Ok(())
1143 }
1144
1145 #[tokio::test]
1146 async fn resolve_from_url_is_noop() -> anyhow::Result<()> {
1147 let url =
1148 "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc"
1149 .to_string();
1150 let name = "polkadot";
1151 let temp_dir = tempdir()?;
1152
1153 let source = Source::Url { url, name: name.into() };
1154 assert_eq!(
1155 source.clone().resolve(name, None, temp_dir.path(), filters::polkadot).await,
1156 source
1157 );
1158 Ok(())
1159 }
1160
1161 #[tokio::test]
1162 async fn from_archive_works() -> anyhow::Result<()> {
1163 let temp_dir = tempdir()?;
1164 let url = "https://github.com/r0gue-io/polkadot/releases/latest/download/polkadot-aarch64-apple-darwin.tar.gz";
1165 let contents: Vec<_> = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"]
1166 .into_iter()
1167 .map(|b| ArchiveFileSpec::new(b.into(), Some(temp_dir.path().join(b)), true))
1168 .collect();
1169
1170 from_archive(url, &contents, &Output).await?;
1171 for ArchiveFileSpec { target, .. } in contents {
1172 assert!(target.unwrap().exists());
1173 }
1174 Ok(())
1175 }
1176
1177 #[tokio::test]
1178 async fn from_git_works() -> anyhow::Result<()> {
1179 let url = "https://github.com/hpaluch/rust-hello-world";
1180 let package = "hello_world";
1181 let initial_commit = "436b7dbffdfaaf7ad90bf44ae8fdcb17eeee65a3";
1182 let temp_dir = tempdir()?;
1183 let path = temp_dir.path().join(package);
1184
1185 from_git(
1186 url,
1187 Some(initial_commit),
1188 None::<&Path>,
1189 package,
1190 &[(package, &path)],
1191 true,
1192 &Output,
1193 false,
1194 )
1195 .await?;
1196 assert!(path.exists());
1197 Ok(())
1198 }
1199
1200 #[tokio::test]
1201 async fn from_github_archive_works() -> anyhow::Result<()> {
1202 let owner = "paritytech";
1203 let repository = "polkadot-sdk";
1204 let package = "polkadot";
1205 let temp_dir = tempdir()?;
1206 let path = temp_dir.path().join(package);
1207 let initial_commit = "72dba98250a6267c61772cd55f8caf193141050f";
1208 let manifest = "substrate/Cargo.toml";
1209
1210 from_github_archive(
1211 owner,
1212 repository,
1213 Some(initial_commit),
1214 Some(manifest),
1215 package,
1216 &[(package, &path)],
1217 true,
1218 &Output,
1219 true,
1220 )
1221 .await?;
1222 assert!(path.exists());
1223 Ok(())
1224 }
1225
1226 #[tokio::test]
1227 async fn from_latest_github_archive_works() -> anyhow::Result<()> {
1228 let owner = "hpaluch";
1229 let repository = "rust-hello-world";
1230 let package = "hello_world";
1231 let temp_dir = tempdir()?;
1232 let path = temp_dir.path().join(package);
1233
1234 from_github_archive(
1235 owner,
1236 repository,
1237 None,
1238 None::<&Path>,
1239 package,
1240 &[(package, &path)],
1241 true,
1242 &Output,
1243 true,
1244 )
1245 .await?;
1246 assert!(path.exists());
1247 Ok(())
1248 }
1249
1250 #[tokio::test]
1251 async fn from_local_package_works() -> anyhow::Result<()> {
1252 let temp_dir = tempdir()?;
1253 let name = "hello_world";
1254 cmd("cargo", ["new", name, "--bin"]).dir(temp_dir.path()).run()?;
1255 let manifest = temp_dir.path().join(name).join("Cargo.toml");
1256
1257 from_local_package(&manifest, name, false, &Output, true).await?;
1258 assert!(manifest.parent().unwrap().join("target/debug").join(name).exists());
1259 Ok(())
1260 }
1261
1262 #[tokio::test]
1263 async fn from_url_works() -> anyhow::Result<()> {
1264 let url =
1265 "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc";
1266 let temp_dir = tempdir()?;
1267 let path = temp_dir.path().join("polkadot");
1268
1269 from_url(url, &path, &Output).await?;
1270 assert!(path.exists());
1271 assert_ne!(metadata(path)?.permissions().mode() & 0o755, 0);
1272 Ok(())
1273 }
1274
1275 #[test]
1276 fn tag_pattern_works() {
1277 let pattern: TagPattern = "polkadot-{version}".into();
1278 assert_eq!(pattern.regex.as_str(), "^polkadot-(?P<version>.+)$");
1279 assert_eq!(pattern.pattern, "polkadot-{version}");
1280 assert_eq!(pattern, pattern.clone());
1281
1282 for value in ["polkadot-stable2503", "stable2503"] {
1283 assert_eq!(pattern.resolve_tag(value).as_str(), "polkadot-stable2503");
1284 }
1285 assert_eq!(pattern.version("polkadot-stable2503"), Some("stable2503"));
1286 }
1287
1288 fn version_comparator<T: AsRef<str> + Ord>(versions: &'_ mut [T]) -> SortedSlice<'_, T> {
1289 SortedSlice::by(versions, |a, b| parse_version(b.as_ref()).cmp(&parse_version(a.as_ref())))
1290 }
1291
1292 pub(crate) struct Output;
1293 impl Status for Output {
1294 fn update(&self, status: &str) {
1295 println!("{status}")
1296 }
1297 }
1298}
1299
1300pub mod traits {
1302 pub trait Source {
1304 type Error;
1306
1307 fn source(&self) -> Result<super::Source, Self::Error>;
1309 }
1310
1311 pub mod enums {
1313 use strum::EnumProperty;
1314
1315 pub trait Source {
1317 fn binary(&self) -> &'static str;
1319
1320 fn fallback(&self) -> &str;
1322
1323 fn prerelease(&self) -> Option<bool>;
1325 }
1326
1327 pub trait Repository: Source {
1329 fn repository(&self) -> &str;
1331
1332 fn tag_pattern(&self) -> Option<&str>;
1335 }
1336
1337 impl<T: EnumProperty> Source for T {
1338 fn binary(&self) -> &'static str {
1339 self.get_str("Binary").expect("expected specification of `Binary` name")
1340 }
1341
1342 fn fallback(&self) -> &str {
1343 self.get_str("Fallback")
1344 .expect("expected specification of `Fallback` release tag")
1345 }
1346
1347 fn prerelease(&self) -> Option<bool> {
1348 self.get_str("Prerelease").map(|v| {
1349 v.parse().expect("expected parachain prerelease value to be true/false")
1350 })
1351 }
1352 }
1353
1354 impl<T: EnumProperty> Repository for T {
1355 fn repository(&self) -> &str {
1356 self.get_str("Repository").expect("expected specification of `Repository` url")
1357 }
1358
1359 fn tag_pattern(&self) -> Option<&str> {
1360 self.get_str("TagPattern")
1361 }
1362 }
1363 }
1364
1365 #[cfg(test)]
1366 mod tests {
1367 use super::enums::{Repository, Source};
1368 use strum_macros::{EnumProperty, VariantArray};
1369
1370 #[derive(EnumProperty, VariantArray)]
1371 pub(super) enum Chain {
1372 #[strum(props(
1373 Repository = "https://github.com/paritytech/polkadot-sdk",
1374 Binary = "polkadot",
1375 Prerelease = "false",
1376 Fallback = "v1.12.0",
1377 TagPattern = "polkadot-{version}"
1378 ))]
1379 Polkadot,
1380 #[strum(props(Repository = "https://github.com/r0gue-io/fallback", Fallback = "v1.0"))]
1381 Fallback,
1382 }
1383
1384 #[test]
1385 fn binary_works() {
1386 assert_eq!("polkadot", Chain::Polkadot.binary())
1387 }
1388
1389 #[test]
1390 fn fallback_works() {
1391 assert_eq!("v1.12.0", Chain::Polkadot.fallback())
1392 }
1393
1394 #[test]
1395 fn prerelease_works() {
1396 assert!(!Chain::Polkadot.prerelease().unwrap())
1397 }
1398
1399 #[test]
1400 fn repository_works() {
1401 assert_eq!("https://github.com/paritytech/polkadot-sdk", Chain::Polkadot.repository())
1402 }
1403
1404 #[test]
1405 fn tag_pattern_works() {
1406 assert_eq!("polkadot-{version}", Chain::Polkadot.tag_pattern().unwrap())
1407 }
1408 }
1409}
1410
1411pub mod filters {
1413 pub fn prefix(candidate: &str, prefix: &str) -> bool {
1419 candidate.starts_with(prefix) &&
1420 (prefix != "polkadot" ||
1422 !["polkadot-execute-worker", "polkadot-prepare-worker", "polkadot-parachain"]
1423 .iter()
1424 .any(|i| candidate.starts_with(i)))
1425 }
1426
1427 #[cfg(test)]
1428 pub(crate) fn polkadot(file: &str) -> bool {
1429 prefix(file, "polkadot")
1430 }
1431}