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 #[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}