pop_common/sourcing/
binary.rs

1// SPDX-License-Identifier: GPL-3.0
2
3use crate::{
4	sourcing::{
5		from_local_package, Error,
6		GitHub::{ReleaseArchive, SourceCodeArchive},
7		Source::{self, Archive, Git, GitHub},
8	},
9	SortedSlice, Status,
10};
11use std::path::{Path, PathBuf};
12
13/// A binary used to launch a node.
14#[derive(Debug, PartialEq)]
15pub enum Binary {
16	/// A local binary.
17	Local {
18		/// The name of the binary.
19		name: String,
20		/// The path of the binary.
21		path: PathBuf,
22		/// If applicable, the path to a manifest used to build the binary if missing.
23		manifest: Option<PathBuf>,
24	},
25	/// A binary which needs to be sourced.
26	Source {
27		/// The name of the binary.
28		name: String,
29		/// The source of the binary.
30		#[allow(private_interfaces)]
31		source: Box<Source>,
32		/// The cache to be used to store the binary.
33		cache: PathBuf,
34	},
35}
36
37impl Binary {
38	/// Whether the binary exists.
39	pub fn exists(&self) -> bool {
40		self.path().exists()
41	}
42
43	/// If applicable, the latest version available.
44	pub fn latest(&self) -> Option<&str> {
45		match self {
46			Self::Local { .. } => None,
47			Self::Source { source, .. } => {
48				if let GitHub(ReleaseArchive { latest, tag_pattern, .. }) = source.as_ref() {
49					{
50						// Extract the version from `latest`, provided it is a tag and that a tag
51						// pattern exists
52						latest.as_deref().and_then(|tag| {
53							tag_pattern.as_ref().map_or(Some(tag), |pattern| pattern.version(tag))
54						})
55					}
56				} else {
57					None
58				}
59			},
60		}
61	}
62
63	/// Whether the binary is defined locally.
64	pub fn local(&self) -> bool {
65		matches!(self, Self::Local { .. })
66	}
67
68	/// The name of the binary.
69	pub fn name(&self) -> &str {
70		match self {
71			Self::Local { name, .. } => name,
72			Self::Source { name, .. } => name,
73		}
74	}
75
76	/// The path of the binary.
77	pub fn path(&self) -> PathBuf {
78		match self {
79			Self::Local { path, .. } => path.to_path_buf(),
80			Self::Source { name, cache, .. } => {
81				// Determine whether a specific version is specified
82				self.version()
83					.map_or_else(|| cache.join(name), |v| cache.join(format!("{name}-{v}")))
84			},
85		}
86	}
87
88	/// Attempts to resolve a version of a binary based on whether one is specified, an existing
89	/// version can be found cached locally, or uses the latest version.
90	///
91	/// # Arguments
92	/// * `name` - The name of the binary.
93	/// * `specified` - If available, a version explicitly specified.
94	/// * `available` - The available versions, which are used to check for existing matches already
95	///   cached locally or the latest otherwise.
96	/// * `cache` - The location used for caching binaries.
97	pub(super) fn resolve_version<'a>(
98		name: &str,
99		specified: Option<&'a str>,
100		available: &'a SortedSlice<impl AsRef<str>>,
101		cache: &Path,
102	) -> Option<&'a str> {
103		match specified {
104			Some(version) => Some(version),
105			None => available
106				.iter()
107				// Default to latest version available locally
108				.filter_map(|version| {
109					let version = version.as_ref();
110					let path = cache.join(format!("{name}-{version}"));
111					path.exists().then_some(Some(version))
112				})
113				.nth(0)
114				// Default to latest version
115				.unwrap_or_else(|| available.first().map(|version| version.as_ref())),
116		}
117	}
118
119	/// Sources the binary.
120	///
121	/// # Arguments
122	/// * `release` - Whether any binaries needing to be built should be done so using the release
123	///   profile.
124	/// * `status` - Used to observe status updates.
125	/// * `verbose` - Whether verbose output is required.
126	pub async fn source(
127		&self,
128		release: bool,
129		status: &impl Status,
130		verbose: bool,
131	) -> Result<(), Error> {
132		match self {
133			Self::Local { name, path, manifest, .. } => match manifest {
134				None => Err(Error::MissingBinary(format!(
135					"The {path:?} binary cannot be sourced automatically."
136				))),
137				Some(manifest) =>
138					from_local_package(manifest, name, release, status, verbose).await,
139			},
140			Self::Source { source, cache, .. } =>
141				source.source(cache, release, status, verbose).await,
142		}
143	}
144
145	/// Whether any locally cached version can be replaced with a newer version.
146	pub fn stale(&self) -> bool {
147		// Only binaries sourced from GitHub release archives can currently be determined as stale
148		let Self::Source { source, .. } = self else {
149			return false;
150		};
151		let GitHub(ReleaseArchive { tag, latest, .. }) = source.as_ref() else {
152			return false;
153		};
154		latest.as_ref().is_some_and(|l| tag.as_ref() != Some(l))
155	}
156
157	/// Specifies that the latest available versions are to be used (where possible).
158	pub fn use_latest(&mut self) {
159		let Self::Source { source, .. } = self else {
160			return;
161		};
162		if let GitHub(ReleaseArchive { tag, latest: Some(latest), .. }) = source.as_mut() {
163			*tag = Some(latest.clone())
164		};
165	}
166
167	/// If applicable, the version of the binary.
168	pub fn version(&self) -> Option<&str> {
169		match self {
170			Self::Local { .. } => None,
171			Self::Source { source, .. } => match source.as_ref() {
172				Git { reference, .. } => reference.as_ref().map(|r| r.as_str()),
173				GitHub(source) => match source {
174					ReleaseArchive { tag, tag_pattern, .. } => tag.as_ref().map(|tag| {
175						// Use any tag pattern defined to extract a version, otherwise use the tag.
176						tag_pattern.as_ref().and_then(|pattern| pattern.version(tag)).unwrap_or(tag)
177					}),
178					SourceCodeArchive { reference, .. } => reference.as_ref().map(|r| r.as_str()),
179				},
180				Archive { .. } | Source::Url { .. } => None,
181			},
182		}
183	}
184}
185
186#[cfg(test)]
187mod tests {
188	use super::*;
189	use crate::{
190		polkadot_sdk::{sort_by_latest_semantic_version, sort_by_latest_version},
191		sourcing::{tests::Output, ArchiveFileSpec},
192		target,
193	};
194	use anyhow::Result;
195	use duct::cmd;
196	use std::fs::{create_dir_all, File};
197	use tempfile::tempdir;
198	use url::Url;
199
200	#[test]
201	fn local_binary_works() -> Result<()> {
202		let name = "polkadot";
203		let temp_dir = tempdir()?;
204		let path = temp_dir.path().join(name);
205		File::create(&path)?;
206
207		let binary = Binary::Local { name: name.to_string(), path: path.clone(), manifest: None };
208
209		assert!(binary.exists());
210		assert_eq!(binary.latest(), None);
211		assert!(binary.local());
212		assert_eq!(binary.name(), name);
213		assert_eq!(binary.path(), path);
214		assert!(!binary.stale());
215		assert_eq!(binary.version(), None);
216		Ok(())
217	}
218
219	#[test]
220	fn local_package_works() -> Result<()> {
221		let name = "polkadot";
222		let temp_dir = tempdir()?;
223		let path = temp_dir.path().join("target/release").join(name);
224		create_dir_all(path.parent().unwrap())?;
225		File::create(&path)?;
226		let manifest = Some(temp_dir.path().join("Cargo.toml"));
227
228		let binary = Binary::Local { name: name.to_string(), path: path.clone(), manifest };
229
230		assert!(binary.exists());
231		assert_eq!(binary.latest(), None);
232		assert!(binary.local());
233		assert_eq!(binary.name(), name);
234		assert_eq!(binary.path(), path);
235		assert!(!binary.stale());
236		assert_eq!(binary.version(), None);
237		Ok(())
238	}
239
240	#[test]
241	fn resolve_version_works() -> Result<()> {
242		let name = "polkadot";
243		let temp_dir = tempdir()?;
244
245		let mut available = vec!["v1.13.0", "v1.12.0", "v1.11.0", "stable2409"];
246		let available = sort_by_latest_version(available.as_mut_slice());
247
248		// Specified
249		let specified = Some("v1.12.0");
250		assert_eq!(
251			Binary::resolve_version(name, specified, &available, temp_dir.path()),
252			specified
253		);
254		// Latest
255		assert_eq!(
256			Binary::resolve_version(name, None, &available, temp_dir.path()).unwrap(),
257			"stable2409"
258		);
259		// Cached
260		File::create(temp_dir.path().join(format!("{name}-{}", available[1])))?;
261		assert_eq!(
262			Binary::resolve_version(name, None, &available, temp_dir.path()).unwrap(),
263			available[1]
264		);
265		Ok(())
266	}
267
268	#[test]
269	fn sourced_from_archive_works() -> Result<()> {
270		let name = "polkadot";
271		let url = "https://github.com/r0gue-io/polkadot/releases/latest/download/polkadot-aarch64-apple-darwin.tar.gz".to_string();
272		let contents = vec![
273			name.to_string(),
274			"polkadot-execute-worker".into(),
275			"polkadot-prepare-worker".into(),
276		];
277		let temp_dir = tempdir()?;
278		let path = temp_dir.path().join(name);
279		File::create(&path)?;
280
281		let mut binary = Binary::Source {
282			name: name.to_string(),
283			source: Archive { url: url.to_string(), contents }.into(),
284			cache: temp_dir.path().to_path_buf(),
285		};
286
287		assert!(binary.exists());
288		assert_eq!(binary.latest(), None);
289		assert!(!binary.local());
290		assert_eq!(binary.name(), name);
291		assert_eq!(binary.path(), path);
292		assert!(!binary.stale());
293		assert_eq!(binary.version(), None);
294		binary.use_latest();
295		assert_eq!(binary.version(), None);
296		Ok(())
297	}
298
299	#[test]
300	fn sourced_from_git_works() -> Result<()> {
301		let package = "hello_world";
302		let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?;
303		let temp_dir = tempdir()?;
304		for reference in [None, Some("436b7dbffdfaaf7ad90bf44ae8fdcb17eeee65a3".to_string())] {
305			let path = temp_dir.path().join(
306				reference
307					.as_ref()
308					.map_or(package.into(), |reference| format!("{package}-{reference}")),
309			);
310			File::create(&path)?;
311
312			let mut binary = Binary::Source {
313				name: package.to_string(),
314				source: Git {
315					url: url.clone(),
316					reference: reference.clone(),
317					manifest: None,
318					package: package.to_string(),
319					artifacts: vec![package.to_string()],
320				}
321				.into(),
322				cache: temp_dir.path().to_path_buf(),
323			};
324
325			assert!(binary.exists());
326			assert_eq!(binary.latest(), None);
327			assert!(!binary.local());
328			assert_eq!(binary.name(), package);
329			assert_eq!(binary.path(), path);
330			assert!(!binary.stale());
331			assert_eq!(binary.version(), reference.as_deref());
332			binary.use_latest();
333			assert_eq!(binary.version(), reference.as_deref());
334		}
335
336		Ok(())
337	}
338
339	#[test]
340	fn sourced_from_github_release_archive_works() -> Result<()> {
341		let owner = "r0gue-io";
342		let repository = "polkadot";
343		let tag_pattern = "polkadot-{version}";
344		let name = "polkadot";
345		let archive = format!("{name}-{}.tar.gz", target()?);
346		let fallback = "stable2412-4".to_string();
347		let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
348		let temp_dir = tempdir()?;
349		for tag in [None, Some("stable2412".to_string())] {
350			let path = temp_dir
351				.path()
352				.join(tag.as_ref().map_or(name.to_string(), |t| format!("{name}-{t}")));
353			File::create(&path)?;
354			for latest in [None, Some("polkadot-stable2503".to_string())] {
355				let mut binary = Binary::Source {
356					name: name.to_string(),
357					source: GitHub(ReleaseArchive {
358						owner: owner.into(),
359						repository: repository.into(),
360						tag: tag.clone(),
361						tag_pattern: Some(tag_pattern.into()),
362						prerelease: false,
363						version_comparator: sort_by_latest_semantic_version,
364						fallback: fallback.clone(),
365						archive: archive.clone(),
366						contents: contents
367							.into_iter()
368							.map(|b| ArchiveFileSpec::new(b.into(), None, true))
369							.collect(),
370						latest: latest.clone(),
371					})
372					.into(),
373					cache: temp_dir.path().to_path_buf(),
374				};
375
376				let latest = latest.as_ref().map(|l| l.replace("polkadot-", ""));
377
378				assert!(binary.exists());
379				assert_eq!(binary.latest(), latest.as_deref());
380				assert!(!binary.local());
381				assert_eq!(binary.name(), name);
382				assert_eq!(binary.path(), path);
383				assert_eq!(binary.stale(), latest.is_some());
384				assert_eq!(binary.version(), tag.as_deref());
385				binary.use_latest();
386				if latest.is_some() {
387					assert_eq!(binary.version(), latest.as_deref());
388				}
389			}
390		}
391		Ok(())
392	}
393
394	#[test]
395	fn sourced_from_github_source_code_archive_works() -> Result<()> {
396		let owner = "paritytech";
397		let repository = "polkadot-sdk";
398		let package = "polkadot";
399		let manifest = "substrate/Cargo.toml";
400		let temp_dir = tempdir()?;
401		for reference in [None, Some("72dba98250a6267c61772cd55f8caf193141050f".to_string())] {
402			let path = temp_dir
403				.path()
404				.join(reference.as_ref().map_or(package.to_string(), |t| format!("{package}-{t}")));
405			File::create(&path)?;
406			let mut binary = Binary::Source {
407				name: package.to_string(),
408				source: GitHub(SourceCodeArchive {
409					owner: owner.to_string(),
410					repository: repository.to_string(),
411					reference: reference.clone(),
412					manifest: Some(PathBuf::from(manifest)),
413					package: package.to_string(),
414					artifacts: vec![package.to_string()],
415				})
416				.into(),
417				cache: temp_dir.path().to_path_buf(),
418			};
419
420			assert!(binary.exists());
421			assert_eq!(binary.latest(), None);
422			assert!(!binary.local());
423			assert_eq!(binary.name(), package);
424			assert_eq!(binary.path(), path);
425			assert!(!binary.stale());
426			assert_eq!(binary.version(), reference.as_deref());
427			binary.use_latest();
428			assert_eq!(binary.version(), reference.as_deref());
429		}
430		Ok(())
431	}
432
433	#[test]
434	fn sourced_from_url_works() -> Result<()> {
435		let name = "polkadot";
436		let url =
437			"https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc";
438		let temp_dir = tempdir()?;
439		let path = temp_dir.path().join(name);
440		File::create(&path)?;
441
442		let mut binary = Binary::Source {
443			name: name.to_string(),
444			source: Source::Url { url: url.to_string(), name: name.to_string() }.into(),
445			cache: temp_dir.path().to_path_buf(),
446		};
447
448		assert!(binary.exists());
449		assert_eq!(binary.latest(), None);
450		assert!(!binary.local());
451		assert_eq!(binary.name(), name);
452		assert_eq!(binary.path(), path);
453		assert!(!binary.stale());
454		assert_eq!(binary.version(), None);
455		binary.use_latest();
456		assert_eq!(binary.version(), None);
457		Ok(())
458	}
459
460	#[tokio::test]
461	async fn sourcing_from_local_binary_not_supported() -> Result<()> {
462		let name = "polkadot".to_string();
463		let temp_dir = tempdir()?;
464		let path = temp_dir.path().join(&name);
465		assert!(matches!(
466			Binary::Local { name, path: path.clone(), manifest: None }.source(true, &Output, true).await,
467			Err(Error::MissingBinary(error)) if error == format!("The {path:?} binary cannot be sourced automatically.")
468		));
469		Ok(())
470	}
471
472	#[tokio::test]
473	async fn sourcing_from_local_package_works() -> Result<()> {
474		let temp_dir = tempdir()?;
475		let name = "hello_world";
476		cmd("cargo", ["new", name, "--bin"]).dir(temp_dir.path()).run()?;
477		let path = temp_dir.path().join(name);
478		let manifest = Some(path.join("Cargo.toml"));
479		let path = path.join("target/release").join(name);
480		Binary::Local { name: name.to_string(), path: path.clone(), manifest }
481			.source(true, &Output, true)
482			.await?;
483		assert!(path.exists());
484		Ok(())
485	}
486
487	#[tokio::test]
488	async fn sourcing_from_url_works() -> Result<()> {
489		let name = "polkadot";
490		let url =
491			"https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc";
492		let temp_dir = tempdir()?;
493		let path = temp_dir.path().join(name);
494
495		Binary::Source {
496			name: name.to_string(),
497			source: Source::Url { url: url.to_string(), name: name.to_string() }.into(),
498			cache: temp_dir.path().to_path_buf(),
499		}
500		.source(true, &Output, true)
501		.await?;
502		assert!(path.exists());
503		Ok(())
504	}
505}