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 crate::command_mock::CommandMock::default()
833 .execute(async || {
834 let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?;
835 let package = "hello_world".to_string();
836 let temp_dir = tempdir()?;
837
838 Source::Git {
839 url,
840 reference: None,
841 manifest: None,
842 package: package.clone(),
843 artifacts: vec![package.clone()],
844 }
845 .source(temp_dir.path(), true, &Output, true)
846 .await?;
847 assert!(temp_dir.path().join(package).exists());
848 Ok(())
849 })
850 .await
851 }
852
853 #[tokio::test]
854 async fn resolve_from_git_is_noop() -> anyhow::Result<()> {
855 let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?;
856 let package = "hello_world".to_string();
857 let temp_dir = tempdir()?;
858
859 let source = Source::Git {
860 url,
861 reference: None,
862 manifest: None,
863 package: package.clone(),
864 artifacts: vec![package.clone()],
865 };
866 assert_eq!(
867 source
868 .clone()
869 .resolve(&package, None, temp_dir.path(), |f| filters::prefix(f, &package))
870 .await,
871 source
872 );
873 Ok(())
874 }
875
876 #[tokio::test]
877 async fn sourcing_from_git_ref_works() -> anyhow::Result<()> {
878 crate::command_mock::CommandMock::default()
879 .execute(async || {
880 let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?;
881 let initial_commit = "436b7dbffdfaaf7ad90bf44ae8fdcb17eeee65a3".to_string();
882 let package = "hello_world".to_string();
883 let temp_dir = tempdir()?;
884
885 Source::Git {
886 url,
887 reference: Some(initial_commit.clone()),
888 manifest: None,
889 package: package.clone(),
890 artifacts: vec![package.clone()],
891 }
892 .source(temp_dir.path(), true, &Output, true)
893 .await?;
894 assert!(temp_dir.path().join(format!("{package}-{initial_commit}")).exists());
895 Ok(())
896 })
897 .await
898 }
899
900 #[tokio::test]
901 async fn sourcing_from_github_release_archive_works() -> anyhow::Result<()> {
902 let owner = "r0gue-io".to_string();
903 let repository = "polkadot".to_string();
904 let version = "stable2512";
905 let tag_pattern = Some("polkadot-{version}".into());
906 let fallback = "stable2512".into();
907 let archive = format!("polkadot-{}.tar.gz", target()?);
908 let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
909 let temp_dir = tempdir()?;
910
911 Source::GitHub(ReleaseArchive {
912 owner,
913 repository,
914 tag: Some(format!("polkadot-{version}")),
915 tag_pattern,
916 prerelease: false,
917 version_comparator,
918 fallback,
919 archive,
920 contents: contents.map(|n| ArchiveFileSpec::new(n.into(), None, true)).to_vec(),
921 latest: None,
922 })
923 .source(temp_dir.path(), true, &Output, true)
924 .await?;
925 for item in contents {
926 assert!(temp_dir.path().join(format!("{item}-{version}")).exists());
927 }
928 Ok(())
929 }
930
931 #[tokio::test]
932 async fn resolve_from_github_release_archive_works() -> anyhow::Result<()> {
933 crate::command_mock::CommandMock::default()
934 .execute(async || {
935 let owner = "r0gue-io".to_string();
936 let repository = "polkadot".to_string();
937 let version = "stable2512";
938 let tag_pattern = Some("polkadot-{version}".into());
939 let fallback = "stable2512".into();
940 let archive = format!("polkadot-{}.tar.gz", target()?);
941 let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
942 let temp_dir = tempdir()?;
943
944 let mut releases: Vec<_> = crate::GitHub::new(owner.as_str(), repository.as_str())
946 .releases(false)
947 .await?
948 .into_iter()
949 .map(|r| r.tag_name)
950 .collect();
951 let sorted_releases = version_comparator(releases.as_mut_slice());
952
953 let source = Source::GitHub(ReleaseArchive {
954 owner,
955 repository,
956 tag: None,
957 tag_pattern,
958 prerelease: false,
959 version_comparator,
960 fallback,
961 archive,
962 contents: contents.map(|n| ArchiveFileSpec::new(n.into(), None, true)).to_vec(),
963 latest: None,
964 });
965
966 for version in [Some(version), None] {
968 let source = source
969 .clone()
970 .resolve("polkadot", version, temp_dir.path(), filters::polkadot)
971 .await;
972 let expected_tag = version.map_or_else(
973 || sorted_releases.0.first().unwrap().into(),
974 |v| format!("polkadot-{v}"),
975 );
976 let expected_latest =
977 version.map_or_else(|| sorted_releases.0.first(), |_| None);
978 assert!(matches!(
979 source,
980 Source::GitHub(ReleaseArchive { tag, latest, .. } )
981 if tag == Some(expected_tag) && latest.as_ref() == expected_latest
982 ));
983 }
984
985 let cached_version = "polkadot-stable2612";
987 File::create(temp_dir.path().join(cached_version))?;
988 for version in [Some(version), None] {
989 let source = source
990 .clone()
991 .resolve("polkadot", version, temp_dir.path(), filters::polkadot)
992 .await;
993 let expected_tag = version
994 .map_or_else(|| cached_version.to_string(), |v| format!("polkadot-{v}"));
995 let expected_latest =
996 version.map_or_else(|| Some(cached_version.to_string()), |_| None);
997 assert!(matches!(
998 source,
999 Source::GitHub(ReleaseArchive { tag, latest, .. } )
1000 if tag == Some(expected_tag) && latest == expected_latest
1001 ));
1002 }
1003
1004 Ok(())
1005 })
1006 .await
1007 }
1008
1009 #[tokio::test]
1010 async fn sourcing_from_github_release_archive_maps_contents() -> anyhow::Result<()> {
1011 let owner = "r0gue-io".to_string();
1012 let repository = "polkadot".to_string();
1013 let version = "stable2512";
1014 let tag_pattern = Some("polkadot-{version}".into());
1015 let name = "polkadot".to_string();
1016 let fallback = "stable2512".into();
1017 let archive = format!("{name}-{}.tar.gz", target()?);
1018 let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
1019 let temp_dir = tempdir()?;
1020 let prefix = "test";
1021
1022 Source::GitHub(ReleaseArchive {
1023 owner,
1024 repository,
1025 tag: Some(format!("polkadot-{version}")),
1026 tag_pattern,
1027 prerelease: false,
1028 version_comparator,
1029 fallback,
1030 archive,
1031 contents: contents
1032 .map(|n| ArchiveFileSpec::new(n.into(), Some(format!("{prefix}-{n}").into()), true))
1033 .to_vec(),
1034 latest: None,
1035 })
1036 .source(temp_dir.path(), true, &Output, true)
1037 .await?;
1038 for item in contents {
1039 assert!(temp_dir.path().join(format!("{prefix}-{item}-{version}")).exists());
1040 }
1041 Ok(())
1042 }
1043
1044 #[tokio::test]
1045 async fn sourcing_from_latest_github_release_archive_works() -> anyhow::Result<()> {
1046 let owner = "r0gue-io".to_string();
1047 let repository = "polkadot".to_string();
1048 let tag_pattern = Some("polkadot-{version}".into());
1049 let name = "polkadot".to_string();
1050 let fallback = "stable2512".into();
1051 let archive = format!("{name}-{}.tar.gz", target()?);
1052 let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
1053 let temp_dir = tempdir()?;
1054
1055 Source::GitHub(ReleaseArchive {
1056 owner,
1057 repository,
1058 tag: None,
1059 tag_pattern,
1060 prerelease: false,
1061 version_comparator,
1062 fallback,
1063 archive,
1064 contents: contents.map(|n| ArchiveFileSpec::new(n.into(), None, true)).to_vec(),
1065 latest: None,
1066 })
1067 .source(temp_dir.path(), true, &Output, true)
1068 .await?;
1069 for item in contents {
1070 assert!(temp_dir.path().join(item).exists());
1071 }
1072 Ok(())
1073 }
1074
1075 #[tokio::test]
1076 async fn sourcing_from_github_source_code_archive_works() -> anyhow::Result<()> {
1077 crate::command_mock::CommandMock::default()
1078 .execute(async || {
1079 let owner = "paritytech".to_string();
1080 let repository = "polkadot-sdk".to_string();
1081 let package = "polkadot".to_string();
1082 let temp_dir = tempdir()?;
1083 let initial_commit = "72dba98250a6267c61772cd55f8caf193141050f";
1084 let manifest = PathBuf::from("substrate/Cargo.toml");
1085
1086 Source::GitHub(SourceCodeArchive {
1087 owner,
1088 repository,
1089 reference: Some(initial_commit.to_string()),
1090 manifest: Some(manifest),
1091 package: package.clone(),
1092 artifacts: vec![package.clone()],
1093 })
1094 .source(temp_dir.path(), true, &Output, true)
1095 .await?;
1096 assert!(temp_dir.path().join(format!("{package}-{initial_commit}")).exists());
1097 Ok(())
1098 })
1099 .await
1100 }
1101
1102 #[tokio::test]
1103 async fn resolve_from_github_source_code_archive_is_noop() -> anyhow::Result<()> {
1104 let owner = "paritytech".to_string();
1105 let repository = "polkadot-sdk".to_string();
1106 let package = "polkadot".to_string();
1107 let temp_dir = tempdir()?;
1108 let initial_commit = "72dba98250a6267c61772cd55f8caf193141050f";
1109 let manifest = PathBuf::from("substrate/Cargo.toml");
1110
1111 let source = Source::GitHub(SourceCodeArchive {
1112 owner,
1113 repository,
1114 reference: Some(initial_commit.to_string()),
1115 manifest: Some(manifest),
1116 package: package.clone(),
1117 artifacts: vec![package.clone()],
1118 });
1119 assert_eq!(
1120 source.clone().resolve(&package, None, temp_dir.path(), filters::polkadot).await,
1121 source
1122 );
1123 Ok(())
1124 }
1125
1126 #[tokio::test]
1127 async fn sourcing_from_latest_github_source_code_archive_works() -> anyhow::Result<()> {
1128 crate::command_mock::CommandMock::default()
1129 .execute(async || {
1130 let owner = "hpaluch".to_string();
1131 let repository = "rust-hello-world".to_string();
1132 let package = "hello_world".to_string();
1133 let temp_dir = tempdir()?;
1134
1135 Source::GitHub(SourceCodeArchive {
1136 owner,
1137 repository,
1138 reference: None,
1139 manifest: None,
1140 package: package.clone(),
1141 artifacts: vec![package.clone()],
1142 })
1143 .source(temp_dir.path(), true, &Output, true)
1144 .await?;
1145 assert!(temp_dir.path().join(package).exists());
1146 Ok(())
1147 })
1148 .await
1149 }
1150
1151 #[tokio::test]
1152 async fn sourcing_from_url_works() -> anyhow::Result<()> {
1153 let url =
1154 "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc"
1155 .to_string();
1156 let name = "polkadot";
1157 let temp_dir = tempdir()?;
1158
1159 Source::Url { url, name: name.into() }
1160 .source(temp_dir.path(), false, &Output, true)
1161 .await?;
1162 assert!(temp_dir.path().join(name).exists());
1163 Ok(())
1164 }
1165
1166 #[tokio::test]
1167 async fn resolve_from_url_is_noop() -> anyhow::Result<()> {
1168 let url =
1169 "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc"
1170 .to_string();
1171 let name = "polkadot";
1172 let temp_dir = tempdir()?;
1173
1174 let source = Source::Url { url, name: name.into() };
1175 assert_eq!(
1176 source.clone().resolve(name, None, temp_dir.path(), filters::polkadot).await,
1177 source
1178 );
1179 Ok(())
1180 }
1181
1182 #[tokio::test]
1183 async fn from_archive_works() -> anyhow::Result<()> {
1184 let temp_dir = tempdir()?;
1185 let url = "https://github.com/r0gue-io/polkadot/releases/latest/download/polkadot-aarch64-apple-darwin.tar.gz";
1186 let contents: Vec<_> = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"]
1187 .into_iter()
1188 .map(|b| ArchiveFileSpec::new(b.into(), Some(temp_dir.path().join(b)), true))
1189 .collect();
1190
1191 from_archive(url, &contents, &Output).await?;
1192 for ArchiveFileSpec { target, .. } in contents {
1193 assert!(target.unwrap().exists());
1194 }
1195 Ok(())
1196 }
1197
1198 #[tokio::test]
1199 async fn from_git_works() -> anyhow::Result<()> {
1200 let url = "https://github.com/hpaluch/rust-hello-world";
1201 let package = "hello_world";
1202 let initial_commit = "436b7dbffdfaaf7ad90bf44ae8fdcb17eeee65a3";
1203 let temp_dir = tempdir()?;
1204 let path = temp_dir.path().join(package);
1205
1206 from_git(
1207 url,
1208 Some(initial_commit),
1209 None::<&Path>,
1210 package,
1211 &[(package, &path)],
1212 true,
1213 &Output,
1214 false,
1215 )
1216 .await?;
1217 assert!(path.exists());
1218 Ok(())
1219 }
1220
1221 #[tokio::test]
1222 async fn from_github_archive_works() -> anyhow::Result<()> {
1223 crate::command_mock::CommandMock::default()
1224 .execute(async || {
1225 let owner = "paritytech";
1226 let repository = "polkadot-sdk";
1227 let package = "polkadot";
1228 let temp_dir = tempdir()?;
1229 let path = temp_dir.path().join(package);
1230 let initial_commit = "72dba98250a6267c61772cd55f8caf193141050f";
1231 let manifest = "substrate/Cargo.toml";
1232
1233 from_github_archive(
1234 owner,
1235 repository,
1236 Some(initial_commit),
1237 Some(manifest),
1238 package,
1239 &[(package, &path)],
1240 true,
1241 &Output,
1242 true,
1243 )
1244 .await?;
1245 assert!(path.exists());
1246 Ok(())
1247 })
1248 .await
1249 }
1250
1251 #[tokio::test]
1252 async fn from_latest_github_archive_works() -> anyhow::Result<()> {
1253 crate::command_mock::CommandMock::default()
1254 .execute(async || {
1255 let owner = "hpaluch";
1256 let repository = "rust-hello-world";
1257 let package = "hello_world";
1258 let temp_dir = tempdir()?;
1259 let path = temp_dir.path().join(package);
1260
1261 from_github_archive(
1262 owner,
1263 repository,
1264 None,
1265 None::<&Path>,
1266 package,
1267 &[(package, &path)],
1268 true,
1269 &Output,
1270 true,
1271 )
1272 .await?;
1273 assert!(path.exists());
1274 Ok(())
1275 })
1276 .await
1277 }
1278
1279 #[tokio::test]
1280 async fn from_local_package_works() -> anyhow::Result<()> {
1281 crate::command_mock::CommandMock::default()
1282 .execute(async || {
1283 let temp_dir = tempdir()?;
1284 let name = "hello_world";
1285 cmd("cargo", ["new", name, "--bin"]).dir(temp_dir.path()).run()?;
1286 let manifest = temp_dir.path().join(name).join("Cargo.toml");
1287
1288 from_local_package(&manifest, name, false, &Output, true).await?;
1289 assert!(manifest.parent().unwrap().join("target/debug").join(name).exists());
1290 Ok(())
1291 })
1292 .await
1293 }
1294
1295 #[tokio::test]
1296 async fn from_url_works() -> anyhow::Result<()> {
1297 let url =
1298 "https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc";
1299 let temp_dir = tempdir()?;
1300 let path = temp_dir.path().join("polkadot");
1301
1302 from_url(url, &path, &Output).await?;
1303 assert!(path.exists());
1304 assert_ne!(metadata(path)?.permissions().mode() & 0o755, 0);
1305 Ok(())
1306 }
1307
1308 #[test]
1309 fn tag_pattern_works() {
1310 let pattern: TagPattern = "polkadot-{version}".into();
1311 assert_eq!(pattern.regex.as_str(), "^polkadot-(?P<version>.+)$");
1312 assert_eq!(pattern.pattern, "polkadot-{version}");
1313 assert_eq!(pattern, pattern.clone());
1314
1315 for value in ["polkadot-stable2512", "stable2512"] {
1316 assert_eq!(pattern.resolve_tag(value).as_str(), "polkadot-stable2512");
1317 }
1318 assert_eq!(pattern.version("polkadot-stable2512"), Some("stable2512"));
1319 }
1320
1321 fn version_comparator<T: AsRef<str> + Ord>(versions: &'_ mut [T]) -> SortedSlice<'_, T> {
1322 SortedSlice::by(versions, |a, b| parse_version(b.as_ref()).cmp(&parse_version(a.as_ref())))
1323 }
1324
1325 pub(crate) struct Output;
1326 impl Status for Output {
1327 fn update(&self, status: &str) {
1328 println!("{status}")
1329 }
1330 }
1331}
1332
1333pub mod traits {
1335 pub trait Source {
1337 type Error;
1339
1340 fn source(&self) -> Result<super::Source, Self::Error>;
1342 }
1343
1344 pub mod enums {
1346 use strum::EnumProperty;
1347
1348 pub trait Source {
1350 fn binary(&self) -> &'static str;
1352
1353 fn fallback(&self) -> &str;
1355
1356 fn prerelease(&self) -> Option<bool>;
1358 }
1359
1360 pub trait Repository: Source {
1362 fn repository(&self) -> &str;
1364
1365 fn tag_pattern(&self) -> Option<&str>;
1368 }
1369
1370 impl<T: EnumProperty> Source for T {
1371 fn binary(&self) -> &'static str {
1372 self.get_str("Binary").expect("expected specification of `Binary` name")
1373 }
1374
1375 fn fallback(&self) -> &str {
1376 self.get_str("Fallback")
1377 .expect("expected specification of `Fallback` release tag")
1378 }
1379
1380 fn prerelease(&self) -> Option<bool> {
1381 self.get_str("Prerelease").map(|v| {
1382 v.parse().expect("expected parachain prerelease value to be true/false")
1383 })
1384 }
1385 }
1386
1387 impl<T: EnumProperty> Repository for T {
1388 fn repository(&self) -> &str {
1389 self.get_str("Repository").expect("expected specification of `Repository` url")
1390 }
1391
1392 fn tag_pattern(&self) -> Option<&str> {
1393 self.get_str("TagPattern")
1394 }
1395 }
1396 }
1397
1398 #[cfg(test)]
1399 mod tests {
1400 use super::enums::{Repository, Source};
1401 use strum_macros::{EnumProperty, VariantArray};
1402
1403 #[derive(EnumProperty, VariantArray)]
1404 pub(super) enum Chain {
1405 #[strum(props(
1406 Repository = "https://github.com/paritytech/polkadot-sdk",
1407 Binary = "polkadot",
1408 Prerelease = "false",
1409 Fallback = "v1.12.0",
1410 TagPattern = "polkadot-{version}"
1411 ))]
1412 Polkadot,
1413 #[strum(props(Repository = "https://github.com/r0gue-io/fallback", Fallback = "v1.0"))]
1414 Fallback,
1415 }
1416
1417 #[test]
1418 fn binary_works() {
1419 assert_eq!("polkadot", Chain::Polkadot.binary())
1420 }
1421
1422 #[test]
1423 fn fallback_works() {
1424 assert_eq!("v1.12.0", Chain::Polkadot.fallback())
1425 }
1426
1427 #[test]
1428 fn prerelease_works() {
1429 assert!(!Chain::Polkadot.prerelease().unwrap())
1430 }
1431
1432 #[test]
1433 fn repository_works() {
1434 assert_eq!("https://github.com/paritytech/polkadot-sdk", Chain::Polkadot.repository())
1435 }
1436
1437 #[test]
1438 fn tag_pattern_works() {
1439 assert_eq!("polkadot-{version}", Chain::Polkadot.tag_pattern().unwrap())
1440 }
1441 }
1442}
1443
1444pub mod filters {
1446 pub fn prefix(candidate: &str, prefix: &str) -> bool {
1452 candidate.starts_with(prefix) &&
1453 (prefix != "polkadot" ||
1455 !["polkadot-execute-worker", "polkadot-prepare-worker", "polkadot-parachain"]
1456 .iter()
1457 .any(|i| candidate.starts_with(i)))
1458 }
1459
1460 #[cfg(test)]
1461 pub(crate) fn polkadot(file: &str) -> bool {
1462 prefix(file, "polkadot")
1463 }
1464}