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