1use crate::download::{
2 create_client, download_binary, download_dist_manfiest, download_json, read_dist_manfiest,
3};
4use crate::manfiest::{self, Artifact, Asset, DistManifest};
5use crate::tool::{display_output, get_bin_name, get_meta};
6use crate::{artifact::Artifacts, download::download_files, env::get_install_dir};
7use binstalk_downloader::download::{Download, ExtractedFilesEntry, PkgFmt};
8use binstalk_registry::Registry;
9use detect_targets::detect_targets;
10use regex::Regex;
11use semver::VersionReq;
12use std::collections::HashMap;
13use std::path::PathBuf;
14use std::str::FromStr;
15use std::{collections::VecDeque, fmt::Display, path::Path};
16use tempfile::tempdir;
17use tracing::trace;
18#[derive(Debug, Clone, PartialEq, PartialOrd, Default)]
19pub struct OutputFile {
20 pub install_path: String,
21 pub mode: u32,
22 pub size: u32,
23 pub origin_path: String,
24 pub is_dir: bool,
25}
26#[derive(Debug, Clone, PartialEq, Default)]
27pub struct OutputItem {
28 pub install_dir: String,
29 pub bin_dir: String,
30 pub files: Vec<OutputFile>,
31}
32
33pub type Output = HashMap<String, OutputItem>;
34
35pub fn atomic_install(src: &Path, dst: &Path) -> std::io::Result<u64> {
36 std::fs::copy(src, dst)
37}
38
39pub async fn install(url: &str, dir: Option<String>) -> Output {
40 trace!("install {}", url);
41 if is_dist_manfiest(url) {
42 return install_from_manfiest(url, dir).await;
43 }
44 if is_url(url) {
45 if is_archive_file(url) {
46 return install_from_artifact_url(url, None, dir).await;
47 }
48
49 if is_exe_file(url) {
50 return install_from_single_file(url, None, dir).await;
51 }
52 }
53
54 if let Ok(repo) = Repo::try_from(url) {
55 return install_from_github(&repo, dir).await;
56 }
57
58 install_from_crate_name(url, dir).await
59}
60
61async fn install_from_crate_name(crate_name: &str, dir: Option<String>) -> Output {
62 trace!("install_from_crate_name {}", crate_name);
63 let client = create_client().await;
64 let version_req = &VersionReq::STAR;
65 let sparse_registry: Registry = Registry::crates_io_sparse_registry();
66 let manifest_from_sparse = sparse_registry
67 .fetch_crate_matched(client, crate_name, version_req)
68 .await
69 .unwrap();
70 let mut v = Output::new();
71 if let Some(pkg) = manifest_from_sparse.package {
72 if let Some(repository) = pkg.repository() {
73 if let Ok(repo) = Repo::try_from(repository) {
74 v.extend(install_from_github(&repo, dir).await);
75 }
76 }
77 }
78 v
79}
80async fn get_artifact_download_url(art_url: &str) -> Vec<String> {
81 if !art_url.contains("*") {
82 return vec![art_url.to_string()];
83 }
84
85 if let Ok(repo) = Repo::try_from(art_url) {
86 return repo.match_artifact_url(art_url).await;
87 }
88 vec![]
89}
90
91fn path_to_str(p: &Path) -> String {
92 p.to_str().unwrap().replace("\\", "/")
93}
94
95async fn install_from_single_file(
96 url: &str,
97 manfiest: Option<DistManifest>,
98 dir: Option<String>,
99) -> Output {
100 let mut install_dir = get_install_dir();
102 let mut output = Output::new();
103 if let Some(target_dir) = dir {
104 if target_dir.contains("/") || target_dir.contains("\\") {
105 install_dir = target_dir.into();
106 } else {
107 install_dir.push(target_dir);
108 }
109 }
110
111 if let Some(bin) = download_binary(url).await {
112 let artifact = manfiest.and_then(|i| i.get_artifact_by_key(url));
113
114 let art_name = url
115 .split("/")
116 .last()
117 .map(|i| i.to_string())
118 .expect("can't get artifact name");
119 let name = artifact.and_then(|i| i.name).unwrap_or(art_name);
120 let mut install_path = install_dir.clone();
121 install_path.push(get_bin_name(&name));
122
123 if let Some(dir) = install_path.parent() {
124 std::fs::create_dir_all(dir).expect("Failed to create_dir dir");
125 }
126 std::fs::write(&install_path, &bin).expect("write file failed");
127 let (mode, size, is_dir) = get_meta(&install_path);
128 let install_path = install_path.to_str().unwrap().replace("\\", "/");
129 println!("Installation Successful");
130 let origin_path = url.split("/").last().unwrap_or(name.as_str()).to_string();
131
132 let files = vec![OutputFile {
133 mode,
134 size,
135 origin_path,
136 is_dir,
137 install_path,
138 }];
139
140 let bin_dir_str = path_to_str(&install_dir);
141 let item = OutputItem {
142 install_dir: bin_dir_str.clone(),
143 bin_dir: bin_dir_str.clone(),
144 files,
145 };
146
147 output.insert(url.to_string(), item);
148 println!("{}", display_output(&output));
149 } else {
150 println!("not found/download artifact for {url}")
151 }
152 output
153}
154
155async fn install_from_artifact_url(
156 art_url: &str,
157 manfiest: Option<DistManifest>,
158 dir: Option<String>,
159) -> Output {
160 trace!("install_from_artifact_url {}", art_url);
161 let urls = get_artifact_download_url(art_url).await;
162 let mut v = Output::new();
163 if urls.is_empty() {
164 println!("not found download_url for {art_url}");
165 return v;
166 }
167 if urls.len() == 1 && !is_archive_file(&urls[0]) {
168 println!("download {}", urls[0]);
169 let output = install_from_single_file(&urls[0], manfiest.clone(), dir.clone()).await;
170 return output;
171 }
172 for url in urls {
173 println!("download {}", url);
174 let files = download_files(&url).await;
175 let fmt = PkgFmt::guess_pkg_format(art_url).unwrap();
176 let output =
177 install_from_download_file(&url, fmt, files, manfiest.clone(), dir.clone()).await;
178 v.extend(output);
180 }
181 v
182}
183
184fn replace_filename(base_url: &str, name: &str) -> String {
185 if let Some(pos) = base_url.rfind('/') {
186 format!("{}{}", &base_url[..pos + 1], name)
187 } else {
188 name.to_string()
189 }
190}
191
192async fn get_artifact_url_from_manfiest(url: &str, manfiest: &DistManifest) -> Vec<String> {
193 let targets = detect_targets().await;
194 let mut v = vec![];
195 for (name, art) in manfiest.artifacts.iter() {
196 if art.match_targets(&targets)
197 && art.kind.clone().unwrap_or("executable-zip".to_owned()) == "executable-zip"
199 {
200 if !is_url(name) {
201 v.push(replace_filename(url, name));
202 } else {
203 v.push(name.clone());
204 }
205 }
206 }
207 v
208}
209
210async fn install_from_manfiest(url: &str, dir: Option<String>) -> Output {
211 trace!("install_from_manfiest {}", url);
212 let manfiest = if is_url(url) {
213 download_dist_manfiest(url).await
214 } else {
215 read_dist_manfiest(url)
216 };
217
218 let mut v = Output::new();
219 if let Some(manfiest) = manfiest {
220 let art_url_list = get_artifact_url_from_manfiest(url, &manfiest).await;
221 if art_url_list.is_empty() {
222 println!("install_from_manfiest {} failed", url);
223 return v;
224 }
225 for art_url in art_url_list {
226 trace!("install_from_manfiest art_url {}", art_url);
227 v.extend(
228 install_from_artifact_url(&art_url, Some(manfiest.clone()), dir.clone()).await,
229 );
230 }
231 }
232 v
233}
234
235fn remove_postfix(s: &str) -> String {
236 use PkgFmt::*;
237 for i in [Tar, Tbz2, Tgz, Txz, Tzstd, Zip, Bin] {
238 for ext in i.extensions(IS_WINDOWS) {
239 if !ext.is_empty() && s.ends_with(ext) {
240 return s[0..s.len() - ext.len()].to_string();
241 }
242 }
243 }
244 s.to_string()
245}
246
247impl Artifact {
248 fn has_file(&self, p: &str) -> bool {
249 let mut p = p.to_string().replace("\\", "/");
250 if let Some(name) = &(self.name) {
253 let prefix = remove_postfix(name) + "/";
254 if p.starts_with(&prefix) {
255 p = p[prefix.len()..].to_string();
256 }
257 }
258
259 for i in &self.assets {
260 let name = PathBuf::from_str(&p).unwrap().to_str().unwrap().to_string();
261 if i.path.clone().unwrap_or_default() == "*" {
262 return true;
263 }
264 if Some(name.as_str()) == i.path.as_deref() {
265 return match &i.kind {
266 manfiest::AssetKind::Executable(_) => true,
267 manfiest::AssetKind::ExecutableDir(_) => false,
268 manfiest::AssetKind::CDynamicLibrary(_) => true,
269 manfiest::AssetKind::CStaticLibrary(_) => true,
270 manfiest::AssetKind::Readme => false,
271 manfiest::AssetKind::License => false,
272 manfiest::AssetKind::Changelog => false,
273 manfiest::AssetKind::Unknown => false,
274 };
275 }
276 }
277 false
278 }
279
280 fn match_targets(&self, targets: &Vec<String>) -> bool {
281 for i in targets {
282 if self.target_triples.contains(i) {
283 return true;
284 }
285 }
286 false
287 }
288
289 fn get_assets_executable_dir(&self) -> Option<Asset> {
290 for i in self.assets.clone() {
291 if let manfiest::AssetKind::ExecutableDir(_) = i.kind {
292 return Some(i);
293 }
294 }
295 None
296 }
297
298 fn get_asset(&self, path: &str) -> Option<Asset> {
299 self.assets.clone().into_iter().find_map(|i| {
300 if i.path == Some(path.to_owned()) {
301 return Some(i);
302 }
303 None
304 })
305 }
306}
307
308impl DistManifest {
309 fn get_artifact(&self, targets: &Vec<String>) -> Option<Artifact> {
310 self.artifacts.clone().into_iter().find_map(|(_, art)| {
311 if art.match_targets(targets)
312 && art.kind.clone().unwrap_or("executable-zip".to_owned()) == "executable-zip"
314 {
315 return Some(art);
316 }
317 None
318 })
319 }
320
321 fn get_artifact_by_key(&self, key: &str) -> Option<Artifact> {
322 self.artifacts.get(key).cloned()
323 }
324}
325
326#[cfg(unix)]
327pub(crate) fn add_execute_permission(file_path: &str) -> std::io::Result<()> {
328 use std::os::unix::fs::PermissionsExt;
329 let metadata = std::fs::metadata(file_path)?;
330 if metadata.is_dir() {
331 return Ok(());
332 }
333
334 let mut permissions = metadata.permissions();
335 let current_mode = permissions.mode();
336
337 let new_mode = current_mode | 0o111;
338 permissions.set_mode(new_mode);
339
340 std::fs::set_permissions(file_path, permissions)?;
341
342 Ok(())
343}
344
345async fn install_from_download_file(
346 url: &str,
347 fmt: PkgFmt,
348 download: Download<'static>,
349 manfiest: Option<DistManifest>,
350 dir: Option<String>,
351) -> Output {
352 trace!("install_from_download_file");
353 let out_dir = tempdir().unwrap();
354 let mut install_dir = get_install_dir();
355 let src_dir = out_dir.path().to_path_buf();
356 let mut v: OutputItem = Default::default();
357 let mut files: Vec<OutputFile> = vec![];
358 let mut q = VecDeque::new();
359 let targets = detect_targets().await;
360 let artifact = manfiest.and_then(|i| i.get_artifact(&targets));
361 let mut output = Output::new();
362 if let Some(asset) = artifact.clone().and_then(|a| a.get_assets_executable_dir()) {
363 if let Some(target_dir) = dir.or(asset.name) {
364 if target_dir.contains("/") || target_dir.contains("\\") {
365 install_dir = target_dir.into();
366 } else {
367 install_dir.push(target_dir);
368 }
369
370 let prefix = asset.path.unwrap_or(".".to_string());
371
372 let install_dir_str = path_to_str(&install_dir);
373
374 let mut bin_dir = install_dir.clone();
375 if let Some(ref dir) = asset.executable_dir {
376 bin_dir.push(dir);
377 }
378 let bin_dir_str = path_to_str(&bin_dir);
379 v.bin_dir = bin_dir_str;
380 v.install_dir = install_dir_str;
381
382 q.push_back(prefix.clone());
383 if let Ok(download_files) = download.and_extract(fmt, &out_dir).await {
384 while let Some(top) = q.pop_front() {
385 let p = Path::new(&top);
386 let entry = download_files.get_entry(p);
387 match entry {
388 Some(ExtractedFilesEntry::Dir(dir)) => {
389 for i in dir.iter() {
390 let p = p.join(i.to_str().unwrap());
391 let next = path_clean::clean(p.to_str().unwrap())
392 .to_str()
393 .unwrap()
394 .to_string()
395 .replace("\\", "/");
396 q.push_back(next);
397 }
398 }
399 Some(ExtractedFilesEntry::File) => {
400 let mut src = src_dir.clone();
401 let mut dst = install_dir.clone();
402 src.push(&top);
403 dst.push(top.replace(&(prefix.clone() + "/"), ""));
404
405 if let Some(dst_dir) = dst.parent() {
406 if dst_dir.exists() && dst_dir.is_file() {
407 std::fs::remove_file(dst_dir).unwrap_or_else(|_| {
408 panic!("failed to remove file : {:?}", dst_dir)
409 });
410 println!("remove {:?}", dst_dir);
411 }
412 if !dst_dir.exists() {
413 std::fs::create_dir_all(dst_dir)
414 .expect("Failed to create_dir install_dir");
415 }
416 }
417
418 atomic_install(&src, dst.as_path()).unwrap_or_else(|_| {
419 panic!("failed to atomic_install from {:?} to {:?}", src, dst)
420 });
421
422 let (mode, size, is_dir) = get_meta(&dst);
423
424 files.push(OutputFile {
425 install_path: dst.to_string_lossy().to_string().replace("\\", "/"),
426 mode,
427 size,
428 origin_path: top,
429 is_dir,
430 });
431 }
432 None => {}
433 }
434 }
435 v.files = files;
436 if v.files.is_empty() {
437 println!("No files installed");
438 } else {
439 println!("Installation Successful");
440 output.insert(url.to_string(), v);
441 println!("{}", display_output(&output));
442 }
443 }
444 } else {
445 println!("Maybe you should use -d to set the folder");
446 }
447 } else {
448 if let Some(target_dir) = dir {
449 if target_dir.contains("/") || target_dir.contains("\\") {
450 install_dir = target_dir.into();
451 } else {
452 install_dir.push(target_dir);
453 }
454 }
455 let install_dir_str = install_dir.to_string_lossy().to_string().replace("\\", "/");
456
457 v.bin_dir = install_dir_str.clone();
458 v.install_dir = install_dir_str;
459
460 q.push_back(".".to_string());
461 let allow = |p: &str| -> bool {
462 match artifact.clone() {
463 None => true,
464 Some(art) => art.has_file(p),
465 }
466 };
467 if let Ok(download_files) = download.and_extract(fmt, &out_dir).await {
468 while let Some(top) = q.pop_front() {
469 let p = Path::new(&top);
470 let entry = download_files.get_entry(p);
471 match entry {
472 Some(ExtractedFilesEntry::Dir(dir)) => {
473 for i in dir.iter() {
474 let p = p.join(i.to_str().unwrap());
475 let next = path_clean::clean(p.to_str().unwrap())
476 .to_str()
477 .unwrap()
478 .to_string()
479 .replace("\\", "/");
480 q.push_back(next);
481 }
482 }
483 Some(ExtractedFilesEntry::File) => {
484 if !allow(&top) {
485 continue;
486 }
487 let mut src = src_dir.clone();
488 let mut dst = install_dir.clone();
489
490 let file_name = p.file_name().unwrap().to_str().unwrap().to_string();
491 let name = artifact
492 .clone()
493 .and_then(|a| {
494 a.get_asset(p.to_str().unwrap())
495 .and_then(|i| i.executable_name)
496 })
497 .unwrap_or(file_name.clone());
498
499 src.push(&top);
500 dst.push(get_bin_name(&name));
501 atomic_install(&src, dst.as_path()).unwrap();
502 let (mode, size, is_dir) = get_meta(&dst);
503 files.push(OutputFile {
504 install_path: dst.to_string_lossy().to_string().replace("\\", "/"),
505 mode,
506 size,
507 origin_path: top,
508 is_dir,
509 });
510 }
511 None => {}
512 }
513 }
514 v.files = files;
515 if v.files.is_empty() {
516 println!("No files installed");
517 } else {
518 println!("Installation Successful");
519 output.insert(url.to_string(), v);
520 println!("{}", display_output(&output));
521 }
522 }
523 }
524
525 output
526}
527
528#[derive(Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
529struct Repo {
530 pub owner: String,
531 pub name: String,
532 pub tag: Option<String>,
533}
534
535impl TryFrom<&str> for Repo {
536 type Error = ();
537
538 fn try_from(value: &str) -> Result<Self, Self::Error> {
539 trace!("get_artifact_api {}", value);
540 let re_gh_tag = Regex::new(
541 r"https?://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/releases/tag/(?P<tag>[^/]+)",
542 )
543 .unwrap();
544
545 let re_gh_download_tag = Regex::new(r"https?://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/releases/download/(?P<tag>[^/]+)/(?P<filename>.+)").unwrap();
546
547 let re_gh_releases =
548 Regex::new(r"http?s://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)").unwrap();
549
550 if let Some(captures) = re_gh_tag.captures(value) {
551 if let (Some(owner), Some(name), Some(tag)) = (
552 captures.name("owner"),
553 captures.name("repo"),
554 captures.name("tag"),
555 ) {
556 return Ok(Repo {
557 owner: owner.as_str().to_string(),
558 name: name.as_str().to_string(),
559 tag: Some(tag.as_str().to_string()),
560 });
561 }
562 }
563
564 if let Some(captures) = re_gh_download_tag.captures(value) {
565 if let (Some(owner), Some(name), Some(tag)) = (
566 captures.name("owner"),
567 captures.name("repo"),
568 captures.name("tag"),
569 ) {
570 return Ok(Repo {
571 owner: owner.as_str().to_string(),
572 name: name.as_str().to_string(),
573 tag: Some(tag.as_str().to_string()),
574 });
575 }
576 }
577
578 if let Some(captures) = re_gh_releases.captures(value) {
579 if let (Some(owner), Some(name)) = (captures.name("owner"), captures.name("repo")) {
580 return Ok(Repo {
581 owner: owner.as_str().to_string(),
582 name: name.as_str().to_string(),
583 tag: None,
584 });
585 }
586 }
587 Err(())
588 }
589}
590
591impl Repo {
592 fn get_gh_url(&self) -> String {
593 format!("https://github.com/{}/{}", self.owner, self.name)
594 }
595
596 fn get_artifact_api(&self) -> String {
597 trace!("get_artifact_api {}/{}", self.owner, self.name);
598 if let Some(tag) = &self.tag {
599 return format!(
600 "https://api.github.com/repos/{}/{}/releases/tags/{}",
601 self.owner, self.name, tag
602 );
603 }
604
605 format!(
606 "https://api.github.com/repos/{}/{}/releases/latest",
607 self.owner, self.name,
608 )
609 }
610
611 fn get_manfiest_url(&self) -> String {
612 match &self.tag {
613 Some(t) => format!(
614 "https://github.com/{}/{}/releases/download/{}/dist-manifest.json",
615 self.owner, self.name, t
616 ),
617 None => format!(
618 "https://github.com/{}/{}/releases/latest/download/dist-manifest.json",
619 self.owner, self.name
620 ),
621 }
622 }
623
624 async fn get_manfiest(&self) -> Option<DistManifest> {
625 download_dist_manfiest(&self.get_manfiest_url()).await
626 }
627
628 async fn get_artifact_url(&self) -> Vec<String> {
629 trace!("get_artifact_url {}/{}", self.owner, self.name);
630 let api = self.get_artifact_api();
631 trace!("get_artifact_url api {}", api);
632 let mut v = vec![];
633 if let Some(artifacts) = download_json::<Artifacts>(&api).await {
634 let targets = detect_targets().await;
635 let mut filter = vec![];
636 for i in artifacts.assets {
637 for pat in &targets {
638 let remove_target = i.name.replace(pat, "");
639 if i.name.contains(pat)
640 && is_archive_file(&i.name)
641 && !filter.contains(&remove_target)
642 {
643 v.push(i.browser_download_url.clone());
644 filter.push(remove_target)
645 }
646 }
647 }
648 }
649
650 v
651 }
652
653 async fn match_artifact_url(&self, pattern: &str) -> Vec<String> {
654 trace!("get_artifact_url {}/{}", self.owner, self.name);
655 let api = self.get_artifact_api();
656 trace!("get_artifact_url api {}", api);
657
658 let mut v = vec![];
659 let re = Regex::new(pattern).unwrap();
660 let pattern_name = pattern.split("/").last();
661 let name_re = pattern_name.map(|i| Regex::new(i).unwrap());
662 if let Some(artifacts) = download_json::<Artifacts>(&api).await {
663 for art in artifacts.assets {
664 if !is_hash_file(&art.browser_download_url)
665 && !is_msi_file(&art.browser_download_url)
666 && (re.is_match(&art.browser_download_url)
667 || name_re.clone().map(|r| r.is_match(&art.name)) == Some(true))
668 {
669 v.push(art.browser_download_url);
670 }
671 }
672 }
673 v
674 }
675}
676
677impl Display for Repo {
678 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
679 match &self.tag {
680 Some(t) => f.write_str(&format!("{}/{}@{}", self.owner, self.name, t)),
681 None => f.write_str(&format!("{}/{}", self.owner, self.name)),
682 }
683 }
684}
685
686async fn install_from_github(repo: &Repo, dir: Option<String>) -> Output {
687 trace!("install_from_git {}", repo);
688 let artifact_url = repo.get_artifact_url().await;
689 let mut v = Output::new();
690 if !artifact_url.is_empty() {
691 for i in artifact_url {
692 trace!("install_from_git artifact_url {}", i);
693 let manfiest = repo.get_manfiest().await;
694 v.extend(install_from_artifact_url(&i, manfiest, dir.clone()).await);
695 }
696 } else {
697 println!(
698 "not found asset for {} on {}",
699 detect_targets().await.join(","),
700 repo.get_gh_url()
701 );
702 }
703 v
704}
705
706const IS_WINDOWS: bool = cfg!(target_os = "windows");
707
708fn is_archive_file(s: &str) -> bool {
709 use PkgFmt::*;
710
711 for i in [
712 Tar, Tbz2, Tgz, Txz, Tzstd, Zip,
713 ] {
715 for ext in i.extensions(IS_WINDOWS) {
716 if !ext.is_empty() && s.ends_with(ext) {
717 return true;
718 }
719 }
720 }
721
722 false
723}
724
725pub fn is_exe_file(s: &str) -> bool {
726 if s.ends_with(".exe") {
727 return true;
728 }
729 let re_latest =
730 Regex::new(r"^https://github\.com/([^/]+)/([^/]+)/releases/latest/download/([^/]+)$")
731 .expect("failed to build github latest release regex");
732 let re_tag =
733 Regex::new(r"^https://github\.com/([^/]+)/([^/]+)/releases/download/([^/]+)/([^/]+)$")
734 .expect("failed to build github release regex");
735
736 for (re, n) in [(re_latest, 3), (re_tag, 4)] {
737 if let Some(cap) = re.captures(s) {
738 if let Some(name) = cap.get(n) {
739 if is_archive_file(name.as_str()) {
740 return false;
741 }
742 if !name.as_str().contains(".") {
743 return true;
744 }
745 }
746 }
747 }
748
749 false
750}
751
752fn is_url(s: &str) -> bool {
753 s.starts_with("http://") || s.starts_with("https://")
754}
755
756fn is_dist_manfiest(s: &str) -> bool {
757 s.ends_with(".json")
758}
759
760fn is_hash_file(s: &str) -> bool {
761 s.ends_with(".sha256")
762}
763
764fn is_msi_file(s: &str) -> bool {
765 s.ends_with(".msi")
766}
767
768#[cfg(test)]
769mod test {
770 use std::path::Path;
771
772 use binstalk_downloader::download::PkgFmt;
773 use tempfile::tempdir;
774
775 use crate::{
776 download::{download_dist_manfiest, download_files, read_dist_manfiest},
777 env::IS_WINDOWS,
778 install::{
779 get_artifact_download_url, get_artifact_url_from_manfiest, is_archive_file,
780 is_exe_file, is_url, Repo,
781 },
782 };
783
784 #[test]
785 fn test_is_file() {
786 assert!(!is_archive_file("https://github.com/ahaoboy/ansi2"));
787
788 assert!(!is_archive_file(
789 "https://api.github.com/repos/ahaoboy/ansi2/releases/latest"
790 ));
791 assert!(!is_archive_file(
792 "https://github.com/ahaoboy/ansi2/releases/tag/v0.2.11"
793 ));
794 assert!(is_archive_file("https://github.com/ahaoboy/ansi2/releases/download/v0.2.11/ansi2-x86_64-unknown-linux-musl.tar.gz"));
795 assert!(is_archive_file("https://github.com/ahaoboy/ansi2/releases/download/v0.2.11/ansi2-x86_64-pc-windows-msvc.zip"));
796 }
797
798 #[test]
799 fn test_is_github() {
800 let repo = Repo {
801 owner: "ahaoboy".to_string(),
802 name: "ansi2".to_string(),
803 tag: None,
804 };
805 assert_eq!(
806 Repo::try_from("https://github.com/ahaoboy/ansi2").unwrap(),
807 repo
808 );
809
810 assert!(
811 Repo::try_from("https://api.github.com/repos/ahaoboy/ansi2/releases/latest").is_err()
812 );
813
814 let repo = Repo {
815 owner: "ahaoboy".to_string(),
816 name: "ansi2".to_string(),
817 tag: Some("v0.2.11".to_string()),
818 };
819
820 assert_eq!(
821 Repo::try_from("https://github.com/ahaoboy/ansi2/releases/tag/v0.2.11").unwrap(),
822 repo
823 );
824
825 assert_eq!(
826 Repo::try_from("https://github.com/ahaoboy/ansi2/releases/download/v0.2.11/ansi2-x86_64-unknown-linux-musl.tar.gz").unwrap(),
827 repo
828 );
829
830 assert_eq!(
831 Repo::try_from("https://github.com/ahaoboy/ansi2/releases/download/v0.2.11/ansi2-x86_64-pc-windows-msvc.zip").unwrap(),
832 repo
833 );
834
835 let repo = Repo {
836 owner: "Ryubing".to_string(),
837 name: "Ryujinx".to_string(),
838 tag: Some("1.2.78".to_string()),
839 };
840 assert_eq!(
841 Repo::try_from("https://github.com/Ryubing/Ryujinx/releases/download/1.2.78/ryujinx-*.*.*-win_x64.zip").unwrap(),
842 repo
843 );
844 }
845
846 #[test]
847 fn test_is_url() {
848 assert!(is_url("https://github.com/ahaoboy/ansi2"));
849 assert!(!is_url("ansi2"));
850 }
851
852 #[tokio::test]
853 async fn test_get_artifact_url() {
854 let repo = Repo::try_from("https://github.com/ahaoboy/mujs-build").unwrap();
855 let url = repo.get_artifact_url().await[0].clone();
856 let fmt = PkgFmt::guess_pkg_format(&url).unwrap();
857 let files = download_files(&url).await;
858 let out_dir = tempdir().unwrap();
859 let files = files.and_extract(fmt, out_dir.path()).await.unwrap();
860 assert!(files.has_file(Path::new(if IS_WINDOWS { "mujs.exe" } else { "mujs" })));
861 }
862
863 #[tokio::test]
864 async fn test_get_artifact_api() {
865 let repo = Repo::try_from("https://github.com/axodotdev/cargo-dist").unwrap();
866 let url = repo.get_artifact_api();
867 assert_eq!(
868 url,
869 "https://api.github.com/repos/axodotdev/cargo-dist/releases/latest"
870 )
871 }
872 #[tokio::test]
873 async fn test_get_manfiest() {
874 let repo = Repo::try_from("https://github.com/axodotdev/cargo-dist/releases").unwrap();
875 let url = repo.get_manfiest_url();
876 assert_eq!(
877 url,
878 "https://github.com/axodotdev/cargo-dist/releases/latest/download/dist-manifest.json"
879 );
880 assert!(repo.get_manfiest().await.is_some());
881
882 let repo =
883 Repo::try_from("https://github.com/axodotdev/cargo-dist/releases/tag/v0.25.1").unwrap();
884 let url = repo.get_manfiest_url();
885 assert_eq!(
886 url,
887 "https://github.com/axodotdev/cargo-dist/releases/download/v0.25.1/dist-manifest.json"
888 );
889
890 let manfiest = repo.get_manfiest().await.unwrap();
891 assert!(!manfiest.artifacts.is_empty());
892
893 let repo =
894 Repo::try_from("https://github.com/ahaoboy/mujs-build/releases/tag/v0.0.2").unwrap();
895 let url = repo.get_manfiest_url();
896 assert_eq!(
897 url,
898 "https://github.com/ahaoboy/mujs-build/releases/download/v0.0.2/dist-manifest.json"
899 );
900
901 let manfiest = repo.get_manfiest().await.unwrap();
902 assert!(!manfiest.artifacts.is_empty())
903 }
904
905 #[tokio::test]
906 async fn test_manifest_jsc() {
907 let repo = Repo {
908 owner: "ahaoboy".to_string(),
909 name: "jsc-build".to_string(),
910 tag: None,
911 };
912
913 let manifest = repo.get_manfiest().await.unwrap();
914 let art = manifest
915 .get_artifact(&vec!["x86_64-unknown-linux-gnu".to_string()])
916 .unwrap();
917
918 assert!(art.has_file("bin/jsc"));
919 assert!(art.has_file("lib/libJavaScriptCore.a"));
920 assert!(!art.has_file("lib/jsc"));
921 }
922
923 #[tokio::test]
924 async fn test_manifest_mujs() {
925 let repo = Repo {
926 owner: "ahaoboy".to_string(),
927 name: "mujs-build".to_string(),
928 tag: None,
929 };
930
931 let manifest = repo.get_manfiest().await.unwrap();
932 let art = manifest
933 .get_artifact(&vec!["x86_64-unknown-linux-gnu".to_string()])
934 .unwrap();
935
936 assert!(art.has_file("mujs"));
937 assert!(!art.has_file("mujs.exe"));
938
939 let manifest = repo.get_manfiest().await.unwrap();
940 let art = manifest
941 .get_artifact(&vec!["x86_64-pc-windows-gnu".to_string()])
942 .unwrap();
943
944 assert!(!art.has_file("mujs"));
945 assert!(art.has_file("mujs.exe"));
946 }
947
948 #[tokio::test]
949 async fn test_install_from_manfiest() {
950 let url =
951 "https://github.com/ahaoboy/mujs-build/releases/latest/download/dist-manifest.json";
952 let manfiest = download_dist_manfiest(url).await.unwrap();
953 let art_url = get_artifact_url_from_manfiest(url, &manfiest).await;
954 assert!(!art_url.is_empty())
955 }
956
957 #[tokio::test]
958 async fn test_cargo_dist() {
959 let url =
960 "https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-rc.1/dist-manifest.json";
961 let manfiest = download_dist_manfiest(url).await.unwrap();
962 let art_url = get_artifact_url_from_manfiest(url, &manfiest).await;
963 assert!(!art_url.is_empty())
964 }
965
966 #[tokio::test]
967 async fn test_deno() {
968 let url = "https://github.com/denoland/deno";
969 let repo = Repo::try_from(url).unwrap();
970 let artifact_url = repo.get_artifact_url().await;
971 assert_eq!(artifact_url.len(), 2);
972 }
973
974 #[tokio::test]
975 async fn test_get_artifact_download_url() {
976 for url in [
977 "https://github.com/Ryubing/Ryujinx/releases/latest/download/^ryujinx-*.*.*-win_x64.zip",
978 "https://github.com/Ryubing/Ryujinx/releases/download/1.2.80/ryujinx-*.*.*-win_x64.zip",
979 "https://github.com/Ryubing/Ryujinx/releases/download/1.2.78/ryujinx-*.*.*-win_x64.zip",
980 "https://github.com/shinchiro/mpv-winbuild-cmake/releases/latest/download/^mpv-x86_64-v3-.*?-git-.*?",
981 "https://github.com/NickeManarin/ScreenToGif/releases/latest/download/ScreenToGif.[0-9]*.[0-9]*.[0-9]*.Portable.x64.zip",
982 "https://github.com/ip7z/7zip/releases/latest/download/7z.*?-linux-x64.tar.xz",
983 "https://github.com/mpv-easy/mpv-winbuild/releases/latest/download/mpv-x86_64-v3-.*?-git-.*?.zip",
984 ]{
985 let art_url = get_artifact_download_url(url).await;
986 assert_eq!(art_url.len(), 1);
987 }
988 }
989
990 #[tokio::test]
991 async fn test_starship() {
992 let repo = Repo::try_from("https://github.com/starship/starship").unwrap();
993 let artifact_url = repo.get_artifact_url().await;
994 assert_eq!(artifact_url.len(), 1);
995 }
996
997 #[tokio::test]
998 async fn test_quickjs_ng() {
999 let json = "./dist-manifest/quickjs-ng.json";
1000 let manifest = read_dist_manfiest(json).unwrap();
1001 let urls = get_artifact_url_from_manfiest(json, &manifest).await;
1002 assert_eq!(urls.len(), 2);
1003
1004 for i in urls {
1005 let download_urls = get_artifact_download_url(&i).await;
1006 assert_eq!(download_urls.len(), 1);
1007 }
1008 }
1009
1010 #[tokio::test]
1011 async fn test_graaljs() {
1012 let json = "./dist-manifest/graaljs.json";
1013 let manifest = read_dist_manfiest(json).unwrap();
1014 let urls = get_artifact_url_from_manfiest(json, &manifest).await;
1015 assert_eq!(urls.len(), 1);
1016
1017 for i in urls {
1018 let download_urls = get_artifact_download_url(&i).await;
1019 assert_eq!(download_urls.len(), 1);
1020 }
1021 }
1022
1023 #[test]
1024 fn test_is_exe_file() {
1025 for (a,b) in [
1026 ("https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe", true),
1027 ("https://github.com/pnpm/pnpm/releases/latest/download/pnpm-win-x64.exe", true),
1028 ("https://github.com/pnpm/pnpm/releases/latest/download/pnpm-win-x64", true),
1029 ("https://github.com/easy-install/easy-install/releases/download/v0.1.5/ei-x86_64-apple-darwin.tar.gz", false),
1030 ("https://github.com/easy-install/easy-install", false),
1031 ("https://github.com/easy-install/easy-install/releases/tag/v0.1.5", false)
1032 ]{
1033 assert_eq!(is_exe_file(a),b);
1034 }
1035 }
1036}