1use 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#[derive(Debug, PartialEq)]
16pub enum Binary {
17 Local {
19 name: String,
21 path: PathBuf,
23 manifest: Option<PathBuf>,
25 },
26 Source {
28 name: String,
30 #[allow(private_interfaces)]
32 source: Box<Source>,
33 cache: PathBuf,
35 },
36}
37
38impl Binary {
39 pub fn exists(&self) -> bool {
41 self.path().exists()
42 }
43
44 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 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 pub fn local(&self) -> bool {
66 matches!(self, Self::Local { .. })
67 }
68
69 pub fn name(&self) -> &str {
71 match self {
72 Self::Local { name, .. } => name,
73 Self::Source { name, .. } => name,
74 }
75 }
76
77 pub fn path(&self) -> PathBuf {
79 match self {
80 Self::Local { path, .. } => path.to_path_buf(),
81 Self::Source { name, cache, .. } => {
82 self.version()
84 .map_or_else(|| cache.join(name), |v| cache.join(format!("{name}-{v}")))
85 },
86 }
87 }
88
89 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 .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 .unwrap_or_else(|| available.first().map(|version| version.as_ref())),
117 }
118 }
119
120 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 pub fn stale(&self) -> bool {
148 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 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 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 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 let specified = Some("v1.12.0");
251 assert_eq!(
252 Binary::resolve_version(name, specified, &available, temp_dir.path()),
253 specified
254 );
255 assert_eq!(
257 Binary::resolve_version(name, None, &available, temp_dir.path()).unwrap(),
258 "stable2409"
259 );
260 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}