Skip to main content

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	#[tokio::test]
270	async 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 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		Ok(())
296	}
297
298	#[tokio::test]
299	async fn sourced_from_git_works() -> Result<()> {
300		let package = "hello_world";
301		let url = Url::parse("https://github.com/hpaluch/rust-hello-world")?;
302		let temp_dir = tempdir()?;
303		for reference in [None, Some("436b7dbffdfaaf7ad90bf44ae8fdcb17eeee65a3".to_string())] {
304			let path = temp_dir.path().join(
305				reference
306					.as_ref()
307					.map_or(package.into(), |reference| format!("{package}-{reference}")),
308			);
309			File::create(&path)?;
310
311			let mut binary = Binary::Source {
312				name: package.to_string(),
313				source: Git {
314					url: url.clone(),
315					reference: reference.clone(),
316					manifest: None,
317					package: package.to_string(),
318					artifacts: vec![package.to_string()],
319				}
320				.into(),
321				cache: temp_dir.path().to_path_buf(),
322			};
323
324			assert!(binary.exists());
325			assert_eq!(binary.latest(), None);
326			assert!(!binary.local());
327			assert_eq!(binary.name(), package);
328			assert_eq!(binary.path(), path);
329			assert!(!binary.stale());
330			assert_eq!(binary.version(), reference.as_deref());
331			binary.use_latest();
332			assert_eq!(binary.version(), reference.as_deref());
333		}
334
335		Ok(())
336	}
337
338	#[tokio::test]
339	async fn sourced_from_github_release_archive_works() -> Result<()> {
340		let owner = "r0gue-io";
341		let repository = "polkadot";
342		let tag_pattern = "polkadot-{version}";
343		let name = "polkadot";
344		let archive = format!("{name}-{}.tar.gz", target()?);
345		let fallback = "stable2512".to_string();
346		let contents = ["polkadot", "polkadot-execute-worker", "polkadot-prepare-worker"];
347		let temp_dir = tempdir()?;
348		for tag in [None, Some("stable2512".to_string())] {
349			let path = temp_dir
350				.path()
351				.join(tag.as_ref().map_or(name.to_string(), |t| format!("{name}-{t}")));
352			File::create(&path)?;
353			for latest in [None, Some("polkadot-stable2512".to_string())] {
354				let mut binary = Binary::Source {
355					name: name.to_string(),
356					source: GitHub(ReleaseArchive {
357						owner: owner.into(),
358						repository: repository.into(),
359						tag: tag.clone(),
360						tag_pattern: Some(tag_pattern.into()),
361						prerelease: false,
362						version_comparator: sort_by_latest_semantic_version,
363						fallback: fallback.clone(),
364						archive: archive.clone(),
365						contents: contents
366							.into_iter()
367							.map(|b| ArchiveFileSpec::new(b.into(), None, true))
368							.collect(),
369						latest: latest.clone(),
370					})
371					.into(),
372					cache: temp_dir.path().to_path_buf(),
373				};
374
375				let latest = latest.as_ref().map(|l| l.replace("polkadot-", ""));
376
377				assert!(binary.exists());
378				assert_eq!(binary.latest(), latest.as_deref());
379				assert!(!binary.local());
380				assert_eq!(binary.name(), name);
381				assert_eq!(binary.path(), path);
382				assert_eq!(binary.stale(), latest.is_some());
383				assert_eq!(binary.version(), tag.as_deref());
384				binary.use_latest();
385				if latest.is_some() {
386					assert_eq!(binary.version(), latest.as_deref());
387				}
388			}
389		}
390		Ok(())
391	}
392
393	#[tokio::test]
394	async fn sourced_from_github_source_code_archive_works() -> Result<()> {
395		let owner = "paritytech";
396		let repository = "polkadot-sdk";
397		let package = "polkadot";
398		let manifest = "substrate/Cargo.toml";
399		let temp_dir = tempdir()?;
400		for reference in [None, Some("72dba98250a6267c61772cd55f8caf193141050f".to_string())] {
401			let path = temp_dir
402				.path()
403				.join(reference.as_ref().map_or(package.to_string(), |t| format!("{package}-{t}")));
404			File::create(&path)?;
405			let mut binary = Binary::Source {
406				name: package.to_string(),
407				source: GitHub(SourceCodeArchive {
408					owner: owner.to_string(),
409					repository: repository.to_string(),
410					reference: reference.clone(),
411					manifest: Some(PathBuf::from(manifest)),
412					package: package.to_string(),
413					artifacts: vec![package.to_string()],
414				})
415				.into(),
416				cache: temp_dir.path().to_path_buf(),
417			};
418
419			assert!(binary.exists());
420			assert_eq!(binary.latest(), None);
421			assert!(!binary.local());
422			assert_eq!(binary.name(), package);
423			assert_eq!(binary.path(), path);
424			assert!(!binary.stale());
425			assert_eq!(binary.version(), reference.as_deref());
426			binary.use_latest();
427			assert_eq!(binary.version(), reference.as_deref());
428		}
429		Ok(())
430	}
431
432	#[tokio::test]
433	async fn sourced_from_url_works() -> Result<()> {
434		let name = "polkadot";
435		let url =
436			"https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc";
437		let temp_dir = tempdir()?;
438		let path = temp_dir.path().join(name);
439		File::create(&path)?;
440
441		let mut binary = Binary::Source {
442			name: name.to_string(),
443			source: Source::Url { url: url.to_string(), name: name.to_string() }.into(),
444			cache: temp_dir.path().to_path_buf(),
445		};
446
447		assert!(binary.exists());
448		assert_eq!(binary.latest(), None);
449		assert!(!binary.local());
450		assert_eq!(binary.name(), name);
451		assert_eq!(binary.path(), path);
452		assert!(!binary.stale());
453		assert_eq!(binary.version(), None);
454		binary.use_latest();
455		assert_eq!(binary.version(), None);
456		Ok(())
457	}
458
459	#[tokio::test]
460	async fn sourcing_from_local_binary_not_supported() -> Result<()> {
461		let name = "polkadot".to_string();
462		let temp_dir = tempdir()?;
463		let path = temp_dir.path().join(&name);
464		assert!(matches!(
465			Binary::Local { name, path: path.clone(), manifest: None }.source(true, &Output, true).await,
466			Err(Error::MissingBinary(error)) if error == format!("The {path:?} binary cannot be sourced automatically.")
467		));
468		Ok(())
469	}
470
471	#[tokio::test]
472	async fn sourcing_from_local_package_works() -> Result<()> {
473		crate::command_mock::CommandMock::default()
474			.execute(async || {
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			.await
488	}
489
490	#[tokio::test]
491	async fn sourcing_from_url_works() -> Result<()> {
492		let name = "polkadot";
493		let url =
494			"https://github.com/paritytech/polkadot-sdk/releases/latest/download/polkadot.asc";
495		let temp_dir = tempdir()?;
496		let path = temp_dir.path().join(name);
497
498		Binary::Source {
499			name: name.to_string(),
500			source: Source::Url { url: url.to_string(), name: name.to_string() }.into(),
501			cache: temp_dir.path().to_path_buf(),
502		}
503		.source(true, &Output, true)
504		.await?;
505		assert!(path.exists());
506		Ok(())
507	}
508}