Skip to main content

pop_common/sourcing/
mod.rs

1// SPDX-License-Identifier: GPL-3.0
2
3use 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/// An error relating to the sourcing of binaries.
27#[derive(Error, Debug)]
28pub enum Error {
29	/// An error occurred.
30	#[error("Anyhow error: {0}")]
31	AnyhowError(#[from] anyhow::Error),
32	/// An API error occurred.
33	#[error("API error: {0}")]
34	ApiError(#[from] api::Error),
35	/// An error occurred sourcing a binary from an archive.
36	#[error("Archive error: {0}")]
37	ArchiveError(String),
38	/// A HTTP error occurred.
39	#[error("HTTP error: {0} caused by {:?}", reqwest::Error::source(.0))]
40	HttpError(#[from] reqwest::Error),
41	/// An IO error occurred.
42	#[error("IO error: {0}")]
43	IO(#[from] std::io::Error),
44	/// A binary cannot be sourced.
45	#[error("Missing binary: {0}")]
46	MissingBinary(String),
47	/// An error occurred during parsing.
48	#[error("ParseError error: {0}")]
49	ParseError(#[from] url::ParseError),
50}
51
52/// The source of a binary.
53#[derive(Clone, Debug, PartialEq)]
54pub enum Source {
55	/// An archive for download.
56	#[allow(dead_code)]
57	Archive {
58		/// The url of the archive.
59		url: String,
60		/// The archive contents required, including the binary name.
61		contents: Vec<String>,
62	},
63	/// A git repository.
64	Git {
65		/// The url of the repository.
66		url: Url,
67		/// If applicable, the branch, tag or commit.
68		reference: Option<String>,
69		/// If applicable, a specification of the path to the manifest.
70		manifest: Option<PathBuf>,
71		/// The name of the package to be built.
72		package: String,
73		/// Any additional build artifacts that are required.
74		artifacts: Vec<String>,
75	},
76	/// A GitHub repository.
77	GitHub(GitHub),
78	/// A URL for download.
79	#[allow(dead_code)]
80	Url {
81		/// The URL for download.
82		url: String,
83		/// The name of the binary.
84		name: String,
85	},
86}
87
88impl Source {
89	/// Sources the binary.
90	///
91	/// # Arguments
92	/// * `cache` - the cache to be used.
93	/// * `release` - whether any binaries needing to be built should be done so using the release
94	///   profile.
95	/// * `status` - used to observe status updates.
96	/// * `verbose` - whether verbose output is required.
97	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	/// Performs any additional processing required to resolve the binary from a source.
139	///
140	/// Determines whether the binary already exists locally, using the latest version available,
141	/// and whether there are any newer versions available
142	///
143	/// # Arguments
144	/// * `name` - the name of the binary.
145	/// * `version` - a specific version of the binary required.
146	/// * `cache` - the cache being used.
147	/// * `cache_filter` - a filter to be used to determine whether a cached binary is eligible.
148	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/// A binary sourced from GitHub.
164#[derive(Clone, Debug, Derivative)]
165#[derivative(PartialEq)]
166pub enum GitHub {
167	/// An archive for download from a GitHub release.
168	ReleaseArchive {
169		/// The owner of the repository - i.e. <https://github.com/{owner}/repository>.
170		owner: String,
171		/// The name of the repository - i.e. <https://github.com/owner/{repository}>.
172		repository: String,
173		/// The release tag to be used, where `None` is latest.
174		tag: Option<String>,
175		/// If applicable, a pattern to be used to determine applicable releases along with
176		/// determining subcomponents from a release tag - e.g. `polkadot-{version}`.
177		tag_pattern: Option<TagPattern>,
178		/// Whether pre-releases are to be used.
179		prerelease: bool,
180		/// A function that orders candidates for selection when multiple versions are available.
181		#[derivative(PartialEq = "ignore")]
182		version_comparator: for<'a> fn(&'a mut [String]) -> SortedSlice<'a, String>,
183		/// The version to use if an appropriate version cannot be resolved.
184		fallback: String,
185		/// The name of the archive (asset) to download.
186		archive: String,
187		/// The archive contents required.
188		contents: Vec<ArchiveFileSpec>,
189		/// If applicable, the latest release tag available.
190		latest: Option<String>,
191	},
192	/// A source code archive for download from GitHub.
193	SourceCodeArchive {
194		/// The owner of the repository - i.e. <https://github.com/{owner}/repository>.
195		owner: String,
196		/// The name of the repository - i.e. <https://github.com/owner/{repository}>.
197		repository: String,
198		/// If applicable, the branch, tag or commit.
199		reference: Option<String>,
200		/// If applicable, a specification of the path to the manifest.
201		manifest: Option<PathBuf>,
202		/// The name of the package to be built.
203		package: String,
204		/// Any additional artifacts that are required.
205		artifacts: Vec<String>,
206	},
207}
208
209impl GitHub {
210	/// Sources the binary.
211	///
212	/// # Arguments
213	///
214	/// * `cache` - the cache to be used.
215	/// * `release` - whether any binaries needing to be built should be done so using the release
216	///   profile.
217	/// * `status` - used to observe status updates.
218	/// * `verbose` - whether verbose output is required.
219	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				// Complete url and contents based on the tag
230				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	/// Performs any additional processing required to resolve the binary from a source.
291	///
292	/// Determines whether the binary already exists locally, using the latest version available,
293	/// and whether there are any newer versions available
294	///
295	/// # Arguments
296	/// * `name` - the name of the binary.
297	/// * `version` - a specific version of the binary required.
298	/// * `cache` - the cache being used.
299	/// * `cache_filter` - a filter to be used to determine whether a cached binary is eligible.
300	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				// Get releases, defaulting to the specified fallback version if there's an error.
321				let repo = crate::GitHub::new(owner.as_str(), repository.as_str());
322				let mut releases = repo.releases(prerelease).await.unwrap_or_else(|_e| {
323					// Use any specified version or fall back to the last known version.
324					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				// Filter releases if a tag pattern specified
338				if let Some(pattern) = tag_pattern.as_ref() {
339					releases.retain(|r| pattern.regex.is_match(&r.tag_name));
340				}
341
342				// Select versions from release tags, used for resolving the candidate versions and
343				// local binary versioning.
344				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				// Resolve any specified version - i.e., the version could be provided as a concrete
356				// version or just a tag.
357				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				// Extract versions from any cached binaries - e.g., offline or rate-limited.
366				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				// Prepare for version resolution by sorting by configured version comparator.
379				let mut versions: Vec<_> = binaries.keys().cloned().collect();
380				let versions = version_comparator(versions.as_mut_slice());
381
382				// Define the tag to be used as either a specified version or the latest available
383				// locally.
384				let tag = version.as_ref().map_or_else(
385					|| {
386						// Resolve the version to be used.
387						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						// Ensure any specified version is a tag.
393						Some(
394							tag_pattern
395								.as_ref()
396								.map_or_else(|| v.to_string(), |pattern| pattern.resolve_tag(v)),
397						)
398					},
399				);
400
401				// // Default to the latest version when no specific version is provided by the
402				// caller.
403				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/// A specification of a file within an archive.
427#[derive(Clone, Debug, PartialEq)]
428pub struct ArchiveFileSpec {
429	/// The name of the file within the archive.
430	pub name: String,
431	/// An optional file name to be used for the file once extracted.
432	pub target: Option<PathBuf>,
433	/// Whether the file is required.
434	pub required: bool,
435}
436
437impl ArchiveFileSpec {
438	/// A specification of a file within an archive.
439	///
440	/// # Arguments
441	/// * `name` - The name of the file within the archive.
442	/// * `target` - An optional file name to be used for the file once extracted.
443	/// * `required` - Whether the file is required.
444	pub fn new(name: String, target: Option<PathBuf>, required: bool) -> Self {
445		Self { name, target, required }
446	}
447}
448
449/// A pattern used to determine captures from a release tag.
450///
451/// Only `{version}` is currently supported, used to determine a version from a release tag.
452/// Examples: `polkadot-{version}`, `node-{version}`.
453#[derive(Clone, Debug)]
454pub struct TagPattern {
455	regex: Regex,
456	pattern: String,
457}
458
459impl TagPattern {
460	/// A new pattern used to determine captures from a release tag.
461	///
462	/// # Arguments
463	/// * `pattern` - the pattern to be used.
464	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	/// Resolves a tag for the specified value.
473	///
474	/// # Arguments
475	/// * `value` - the value to resolve into a tag using the inner tag pattern.
476	pub fn resolve_tag(&self, value: &str) -> String {
477		// If input already in expected tag format, return as-is.
478		if self.regex.is_match(value) {
479			return value.to_string();
480		}
481
482		self.pattern.replace("{version}", value)
483	}
484
485	/// Extracts a version from the specified value.
486	///
487	/// # Arguments
488	/// * `value` - the value to parse.
489	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
506/// Source binary by downloading and extracting from an archive.
507///
508/// # Arguments
509/// * `url` - The url of the archive.
510/// * `contents` - The contents within the archive which are required.
511/// * `status` - Used to observe status updates.
512async fn from_archive(
513	url: &str,
514	contents: &[ArchiveFileSpec],
515	status: &impl Status,
516) -> Result<(), Error> {
517	// Download archive
518	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	// Extract contents
524	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				// If rename fails (e.g., due to cross-device linking), fallback to copy and
538				// remove
539				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/// Source binary by cloning a git repository and then building.
554///
555/// # Arguments
556/// * `url` - The url of the repository.
557/// * `reference` - If applicable, the branch, tag or commit.
558/// * `manifest` - If applicable, a specification of the path to the manifest.
559/// * `package` - The name of the package to be built.
560/// * `artifacts` - Any additional artifacts that are required.
561/// * `release` - Whether to build optimized artifacts using the release profile.
562/// * `status` - Used to observe status updates.
563/// * `verbose` - Whether verbose output is required.
564#[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	// Clone repository into working directory
576	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	// Build binaries
581	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/// Source binary by downloading from a source code archive and then building.
591///
592/// # Arguments
593/// * `owner` - The owner of the repository.
594/// * `repository` - The name of the repository.
595/// * `reference` - If applicable, the branch, tag or commit.
596/// * `manifest` - If applicable, a specification of the path to the manifest.
597/// * `package` - The name of the package to be built.
598/// * `artifacts` - Any additional artifacts that are required.
599/// * `release` - Whether to build optimized artifacts using the release profile.
600/// * `status` - Used to observe status updates.
601/// * `verbose` - Whether verbose output is required.
602#[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	// User agent required when using GitHub API
615	let response = match reference {
616		Some(reference) => {
617			// Various potential urls to try based on not knowing the type of ref
618			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	// Extract contents
651	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	// Prepare archive contents for build
658	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(), // Automatically switch to top level directory
666		_ => {},                              /* Assume that downloaded archive does not have a
667		                                        * top level directory */
668	}
669	// Build binaries
670	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
679/// Source binary by building a local package.
680///
681/// # Arguments
682/// * `manifest` - The path to the local package manifest.
683/// * `package` - The name of the package to be built.
684/// * `release` - Whether to build optimized artifacts using the release profile.
685/// * `status` - Used to observe status updates.
686/// * `verbose` - Whether verbose output is required.
687pub(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	// Build binaries
695	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
702/// Source binary by downloading from a URL.
703///
704/// # Arguments
705/// * `url` - The url of the binary.
706/// * `path` - The (local) destination path.
707/// * `status` - Used to observe status updates.
708async fn from_url(url: &str, path: &Path, status: &impl Status) -> Result<(), Error> {
709	// Download the binary
710	status.update(&format!("Downloading from {url}..."));
711	download(url, path).await?;
712	status.update("Sourcing complete.");
713	Ok(())
714}
715
716/// Builds a package.
717///
718/// # Arguments
719/// * `manifest` - The path to the manifest.
720/// * `package` - The name of the package to be built.
721/// * `artifacts` - Any additional artifacts that are required.
722/// * `release` - Whether to build optimized artifacts using the release profile.
723/// * `status` - Used to observe status updates.
724/// * `verbose` - Whether verbose output is required.
725async 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	// Define arguments
734	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	// Build binaries
740	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	// Copy required artifacts to the destination path
754	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
765/// Downloads a file from a URL.
766///
767/// # Arguments
768/// * `url` - The url of the file.
769/// * `path` - The (local) destination path.
770async fn download(url: &str, dest: &Path) -> Result<(), Error> {
771	// Download to the destination path
772	let response = reqwest::get(url).await?.error_for_status()?;
773	let mut file = File::create(dest)?;
774	file.write_all(&response.bytes().await?)?;
775	// Make executable
776	set_executable_permission(dest)?;
777	Ok(())
778}
779
780/// Sets the executable permission for a given file.
781///
782/// # Arguments
783/// * `path` - The file path to which permissions should be granted.
784pub 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				// Determine release for comparison
945				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				// Check results for a specified/unspecified version
967				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				// Create a later version as a cached binary
986				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
1333/// Traits for the sourcing of a binary.
1334pub mod traits {
1335	/// The source of a binary.
1336	pub trait Source {
1337		/// The type returned in the event of an error.
1338		type Error;
1339
1340		/// Defines the source of a binary.
1341		fn source(&self) -> Result<super::Source, Self::Error>;
1342	}
1343
1344	/// Traits for the sourcing of a binary using [strum]-based configuration.
1345	pub mod enums {
1346		use strum::EnumProperty;
1347
1348		/// The source of a binary.
1349		pub trait Source {
1350			/// The name of the binary.
1351			fn binary(&self) -> &'static str;
1352
1353			/// The fallback version to be used when the latest version cannot be determined.
1354			fn fallback(&self) -> &str;
1355
1356			/// Whether pre-releases are to be used.
1357			fn prerelease(&self) -> Option<bool>;
1358		}
1359
1360		/// The source of a binary.
1361		pub trait Repository: Source {
1362			/// The repository to be used.
1363			fn repository(&self) -> &str;
1364
1365			/// If applicable, a pattern to be used to determine applicable releases along with
1366			/// subcomponents from a release tag - e.g. `polkadot-{version}`.
1367			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
1444/// Filters which can be used when resolving a binary.
1445pub mod filters {
1446	/// A filter which ensures a candidate file name starts with a prefix.
1447	///
1448	/// # Arguments
1449	/// * `candidate` - the candidate to be evaluated.
1450	/// * `prefix` - the specified prefix.
1451	pub fn prefix(candidate: &str, prefix: &str) -> bool {
1452		candidate.starts_with(prefix) &&
1453			// Ignore any known related `polkadot`-prefixed binaries when `polkadot` only.
1454			(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}