1use 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#[derive(Debug, PartialEq)]
15pub enum Binary {
16 Local {
18 name: String,
20 path: PathBuf,
22 manifest: Option<PathBuf>,
24 },
25 Source {
27 name: String,
29 #[allow(private_interfaces)]
31 source: Box<Source>,
32 cache: PathBuf,
34 },
35}
36
37impl Binary {
38 pub fn exists(&self) -> bool {
40 self.path().exists()
41 }
42
43 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 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 pub fn local(&self) -> bool {
65 matches!(self, Self::Local { .. })
66 }
67
68 pub fn name(&self) -> &str {
70 match self {
71 Self::Local { name, .. } => name,
72 Self::Source { name, .. } => name,
73 }
74 }
75
76 pub fn path(&self) -> PathBuf {
78 match self {
79 Self::Local { path, .. } => path.to_path_buf(),
80 Self::Source { name, cache, .. } => {
81 self.version()
83 .map_or_else(|| cache.join(name), |v| cache.join(format!("{name}-{v}")))
84 },
85 }
86 }
87
88 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 .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 .unwrap_or_else(|| available.first().map(|version| version.as_ref())),
116 }
117 }
118
119 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 pub fn stale(&self) -> bool {
147 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 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 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 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 let specified = Some("v1.12.0");
250 assert_eq!(
251 Binary::resolve_version(name, specified, &available, temp_dir.path()),
252 specified
253 );
254 assert_eq!(
256 Binary::resolve_version(name, None, &available, temp_dir.path()).unwrap(),
257 "stable2409"
258 );
259 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}