pop_common/sourcing/
binary.rs

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