1use crate::InstallConfig;
2use crate::artifact::GhArtifacts;
3use crate::env::add_to_path;
4use crate::manfiest::DistManifest;
5use crate::ty::{Output, OutputFile};
6use anyhow::{Context, Result};
7use easy_archive::{Fmt, IntoEnumIterator, clean};
8use easy_archive::{human_size, mode_to_string};
9use guess_target::{Abi, Arch, Os, get_local_target, guess_target};
10use regex::Regex;
11use std::collections::HashSet;
12#[cfg(unix)]
13use std::os::unix::prelude::PermissionsExt;
14use std::path::Path;
15use std::str::FromStr;
16
17pub(crate) const DEEP: usize = 3;
18pub(crate) const WINDOWS_EXE_EXTS: [&str; 6] = [".exe", ".ps1", ".bat", ".cmd", ".com", ".vbs"];
19pub(crate) const INSTALLER_EXTS: [&str; 11] = [
20 ".msi",
21 ".msix",
22 ".appx",
23 ".deb",
24 ".rpm",
25 ".dmg",
26 ".pkg",
27 ".app",
28 ".apk",
29 ".ipa",
30 ".appimage",
31];
32pub(crate) const TEXT_FILE_EXTS: [&str; 11] = [
33 ".txt", ".md", ".json", ".xml", ".csv", ".log", ".ini", ".cfg", ".conf", ".yaml", ".yml",
34];
35pub(crate) const MAYBE_EXECUTABLE_EXTS: [&str; 13] = [
36 ".out", ".sh", ".bash", ".zsh", ".py", ".pl", ".js", ".ts", ".jsx", ".tsx", ".wasm", ".fish",
37 ".nu",
38];
39
40pub(crate) const SKIP_FMT_LIST: [&str; 18] = [
41 ".sha256sum",
42 ".sha256",
43 ".sha1",
44 ".md5",
45 ".sum",
46 ".msi",
47 ".msix",
48 ".appx",
49 ".app",
50 ".appimage",
51 ".json",
52 ".txt",
53 ".md",
54 ".log",
55 ".sig",
56 ".asc",
57 ".intoto.jsonl",
58 ".jsonl",
59];
60
61pub(crate) fn is_known_format(s: &str) -> bool {
62 let all: &[&[&str]] = &[
63 &WINDOWS_EXE_EXTS[..],
64 &INSTALLER_EXTS[..],
65 &TEXT_FILE_EXTS[..],
66 &MAYBE_EXECUTABLE_EXTS[..],
67 &SKIP_FMT_LIST[..],
68 ];
69
70 for i in all {
71 for ext in i.iter() {
72 if s.ends_with(ext) {
73 return true;
74 }
75 }
76 }
77
78 false
79}
80
81pub(crate) fn is_skip(s: &str) -> bool {
82 s.rsplit('/').next().unwrap_or_default().starts_with('.')
83 || INSTALLER_EXTS
84 .iter()
85 .chain(TEXT_FILE_EXTS.iter())
86 .chain(SKIP_FMT_LIST.iter())
87 .any(|&ext| s.to_ascii_lowercase().ends_with(&ext.to_ascii_lowercase()))
88}
89
90pub(crate) fn get_bin_name(s: &str) -> String {
91 if cfg!(windows) && !WINDOWS_EXE_EXTS.iter().any(|i| s.ends_with(i)) && !s.contains(".") {
92 return s.to_string() + ".exe";
93 }
94 s.to_string()
95}
96
97const MAX_FILE_COUNT: usize = 16;
98pub(crate) fn display_output(output: &Output) -> String {
99 let mut v = vec![];
100 for i in output.values() {
101 if i.files.len() > MAX_FILE_COUNT {
102 let sum_size = i.files.iter().fold(0, |pre, cur| pre + cur.size);
103 v.push(
104 [
105 human_size(sum_size as usize).as_str(),
106 format!("(total {})", i.files.len()).as_str(),
107 i.install_dir.as_str(),
108 ]
109 .join(" "),
110 );
111 } else {
112 let max_size_len = i
113 .files
114 .iter()
115 .fold(0, |pre, cur| pre.max(human_size(cur.size as usize).len()));
116
117 for k in &i.files {
118 let s = human_size(k.size as usize);
119 v.push(
120 [
121 mode_to_string(k.mode.unwrap_or(0), k.is_dir),
122 " ".repeat(max_size_len - s.len()) + &s,
123 [k.origin_path.as_str(), k.install_path.as_str()].join(" -> "),
124 ]
125 .join(" "),
126 );
127 }
128 }
129 }
130 v.join("\n")
131}
132
133fn dirname(s: &str) -> String {
134 let i = s.rfind('/').map_or(s.len(), |i| i + 1);
135 s[0..i].to_string()
136}
137
138pub(crate) fn add_output_to_path(output: &Output) {
139 let mut maybe_exe = HashSet::new();
140 for v in output.values() {
141 for f in &v.files {
142 if !is_skip(&f.install_path) {
143 maybe_exe.insert(f.install_path.clone());
144 }
145 let deep = f.origin_path.split("/").count();
146 if deep <= DEEP
147 && let Some(p) = check(f)
148 {
149 let msg = if p != f.install_path {
150 format!("Warning: file exists at {p}")
151 } else {
152 format!("Warning: file updated at {p}")
153 };
154 println!("{msg}");
155 }
156 }
157 }
158
159 let mut filter = HashSet::new();
160 for v in output.values() {
161 add_to_path(&v.install_dir);
162
163 for f in &v.files {
164 let deep = f.origin_path.split("/").count();
165 let is_exe = (maybe_exe.len() == 1 && maybe_exe.contains(&f.install_path))
166 || ends_with_exe(&f.origin_path)
167 || (f.mode.unwrap_or(0) & EXEC_MASK != 0);
168 let dir = dirname(&f.install_path);
169 if deep <= DEEP && is_exe && !filter.contains(&dir) {
170 add_to_path(&dir);
171 filter.insert(dir);
172 }
173 }
174 }
175}
176
177pub(crate) fn get_filename(s: &str) -> String {
178 let s = s.replace("\\\\", "/");
179 let s = s.replace("\\", "/");
180 let i = s.rfind("/").map_or(0, |i| i + 1);
181 s[i..].to_string()
182}
183
184#[cfg(windows)]
185pub(crate) fn which(name: &str) -> Option<String> {
186 let cmd = std::process::Command::new("powershell")
187 .args(["-c", &format!("(get-command {name}).Source")])
188 .output()
189 .ok()?;
190 String::from_utf8(cmd.stdout)
191 .ok()
192 .map(|i| i.trim().replace("\\", "/").replace("//", "/"))
193}
194
195#[cfg(unix)]
196pub(crate) fn which(name: &str) -> Option<String> {
197 let cmd = std::process::Command::new("which")
198 .arg(name)
199 .output()
200 .ok()?;
201 String::from_utf8(cmd.stdout)
202 .ok()
203 .map(|i| i.trim().to_string().replace("\\", "/").replace("//", "/"))
204}
205
206const EXEC_MASK: u32 = 0o111;
207pub(crate) fn executable(name: &str, mode: &Option<u32>) -> bool {
208 ends_with_exe(name) || (!name.contains(".") && mode.unwrap_or(0) & EXEC_MASK != 0)
209}
210
211pub(crate) fn check(file: &OutputFile) -> Option<String> {
212 let file_path = &file.install_path;
213 let name = get_filename(file_path);
214 if !executable(&name, &file.mode) {
215 return None;
216 }
217 if let Some(p) = which(&name)
218 && !p.is_empty()
219 && file_path != &p
220 {
221 return Some(p);
222 }
223 None
224}
225
226pub(crate) fn write_to_file(src: &str, buffer: &[u8], mode: &Option<u32>) -> Result<()> {
227 let d = std::path::PathBuf::from_str(src).context("invalid path for write_to_file")?;
228 if let Some(p) = d.parent()
229 && !std::fs::exists(p).unwrap_or(false)
230 {
231 std::fs::create_dir_all(p).context("failed to create_dir_all")?;
232 }
233
234 if std::fs::exists(src).unwrap_or(false)
235 && let Ok(meta) = std::fs::metadata(src)
236 {
237 if meta.is_file() {
238 std::fs::remove_file(src).context("failed to remove file")?;
239 } else {
240 std::fs::remove_dir_all(src).context("failed to remove dir")?;
241 }
242 }
243
244 std::fs::write(src, buffer).context("failed to write file")?;
245
246 #[cfg(unix)]
247 if let Some(mode) = mode
248 && *mode > 0
249 {
250 std::fs::set_permissions(src, PermissionsExt::from_mode(*mode))
251 .context("failed to set_permissions")?;
252 }
253
254 #[cfg(windows)]
255 {
256 _ = mode;
257 }
258 Ok(())
259}
260
261fn has_common_elements(arr1: &[String], arr2: &[String]) -> bool {
262 arr1.iter().any(|x| arr2.contains(x))
263}
264
265pub(crate) fn get_artifact_url_from_manfiest(
266 url: &str,
267 manfiest: &DistManifest,
268) -> Vec<(String, String)> {
269 let mut v = vec![];
270 let local_target = get_local_target();
272
273 for (key, art) in manfiest.artifacts.iter() {
274 let filename = get_filename(key);
275 if is_skip(&filename) {
276 continue;
277 }
278
279 if ends_with_exe(key) && local_target.iter().any(|t| t.os() != Os::Windows) {
280 continue;
281 }
282
283 if has_common_elements(
299 &art.target_triples,
300 local_target
301 .iter()
302 .map(|i| i.to_str().to_string())
303 .collect::<Vec<_>>()
304 .as_slice(),
305 ) {
306 if let Some(kind) = &art.kind
307 && !["executable-zip"].contains(&kind.as_str())
308 {
309 continue;
310 }
311 let name = name_no_ext(&filename);
312 let name = guess_target(&name).pop().map_or(name, |i| i.name);
313 if !is_url(key) {
314 v.push((name, replace_filename(url, key)));
315 } else {
316 v.push((name, key.to_string()));
317 }
318 continue;
319 }
320 }
321 v
332}
333
334pub(crate) fn get_common_prefix_len(list: &[&str]) -> usize {
335 if list.is_empty() {
336 return 0;
337 }
338
339 if list.len() == 1 {
340 match list[0].rfind('/') {
341 Some(i) => return i + 1,
342 None => return 0,
343 }
344 }
345
346 let parts: Vec<Vec<&str>> = list.iter().map(|i| i.split('/').collect()).collect();
347 let max_len = parts.iter().map(|p| p.len()).max().unwrap_or(0);
348
349 let mut p = 0;
350 while p < max_len {
351 let head: Vec<_> = parts.iter().map(|k| k.get(p).unwrap_or(&"")).collect();
352 let first = head[0];
353 if head.iter().any(|&i| i != first) {
354 break;
355 }
356 p += 1;
357 }
358
359 if p == 0 {
360 return 0;
361 }
362 parts[0][..p].join("/").len() + 1
363}
364
365pub(crate) fn is_executable(mode: u32) -> bool {
366 const S_IXUSR: u32 = 0o100; const S_IXGRP: u32 = 0o010; const S_IXOTH: u32 = 0o001; mode & (S_IXUSR | S_IXGRP | S_IXOTH) != 0
371}
372
373pub(crate) fn maybe_executable(name: &str) -> bool {
374 MAYBE_EXECUTABLE_EXTS.iter().any(|i| name.ends_with(i))
375}
376
377pub(crate) fn guess_executable(files: &mut [OutputFile]) {
379 let exe_files: Vec<_> = files
380 .iter()
381 .filter(|i| is_executable(i.mode.unwrap_or(0)))
382 .collect();
383
384 if !exe_files.is_empty() {
385 return;
386 }
387
388 let mut no_ext_files: Vec<_> = files
389 .iter_mut()
390 .filter(|i| !get_filename(&i.origin_path).contains("."))
391 .collect();
392
393 if let &mut [first] = &mut no_ext_files.as_mut_slice() {
394 first.mode = Some(0o755);
395 return;
396 }
397
398 let mut maybe_executable: Vec<_> = files
399 .iter_mut()
400 .filter(|i| maybe_executable(&i.origin_path))
401 .collect();
402 if let &mut [first] = &mut maybe_executable.as_mut_slice() {
403 first.mode = Some(0o755);
404 }
405}
406
407fn rename_alias(files: &mut [OutputFile], alias: &str) {
408 let file = if files.len() == 1 {
409 Some(&mut files[0])
410 } else {
411 let mut iter = files.iter_mut().filter(|i| {
412 let name = get_filename(&i.origin_path);
413 executable(&name, &i.mode)
414 });
415
416 let first = iter.next();
417 if first.is_some() && iter.next().is_none() {
418 first
419 } else {
420 None
421 }
422 };
423
424 let Some(first) = file else { return };
425
426 let filename = get_filename(&first.install_path);
427 let bin = name_no_ext(&filename);
428 let alias_name = filename.replace(&bin, alias);
429 let d = dirname(&first.install_path);
430
431 first.install_path = clean(&(d + "/" + &alias_name));
432}
433
434pub(crate) fn install_output_files(files: &mut [OutputFile], alias: Option<String>) -> Result<()> {
435 if let Some(alias) = alias {
436 rename_alias(files, &alias);
437 }
438
439 guess_executable(files);
440 for OutputFile {
441 install_path,
442 buffer,
443 mode,
444 origin_path,
445 ..
446 } in files.iter()
447 {
448 if origin_path.starts_with("__MACOSX") {
450 continue;
451 }
452 write_to_file(install_path, buffer, mode)?;
453 }
454
455 #[cfg(not(windows))]
456 {
457 let maybe_exe = files
458 .iter()
459 .filter(|i| !is_skip(&i.origin_path))
460 .collect::<Vec<_>>();
461
462 if let [single_exe] = maybe_exe.as_slice() {
463 add_execute_permission(&single_exe.install_path)?;
464 }
465 }
466 Ok(())
467}
468
469pub(crate) fn name_no_ext(s: &str) -> String {
470 let mut exts: Vec<_> = Fmt::iter().flat_map(|i| i.extensions().to_vec()).collect();
471 exts.sort_by_key(|b| std::cmp::Reverse(b.len()));
472 for ext in exts {
473 if s.ends_with(ext) {
474 return s[0..s.len() - ext.len()].to_string();
475 }
476 }
477
478 let all: &[&[&str]] = &[
479 &WINDOWS_EXE_EXTS[..],
480 &INSTALLER_EXTS[..],
481 &TEXT_FILE_EXTS[..],
482 &MAYBE_EXECUTABLE_EXTS[..],
483 &SKIP_FMT_LIST[..],
484 ];
485
486 for i in all {
487 for ext in i.iter() {
488 if s.ends_with(ext) {
489 return s[0..s.len() - ext.len()].to_string();
490 }
491 }
492 }
493
494 s.to_string()
495}
496
497#[cfg(not(windows))]
498pub(crate) fn add_execute_permission(file_path: &str) -> Result<()> {
499 use std::os::unix::fs::PermissionsExt;
500 if let Ok(metadata) = std::fs::metadata(file_path).context("metadata failed") {
501 if metadata.is_dir() {
502 return Ok(());
503 }
504
505 let mut permissions = metadata.permissions();
506 let current_mode = permissions.mode();
507
508 let new_mode = current_mode | EXEC_MASK;
509 permissions.set_mode(new_mode);
510
511 if std::fs::set_permissions(file_path, permissions)
512 .context("set_permissions failed")
513 .is_ok()
514 {
515 return Ok(());
516 }
517 }
518 std::process::Command::new("chmod")
519 .args(["+x", file_path])
520 .output()?;
521 Ok(())
522}
523
524pub(crate) fn is_archive_file(s: &str) -> bool {
525 Fmt::guess(s).is_some()
526}
527
528pub(crate) fn ends_with_exe(s: &str) -> bool {
529 WINDOWS_EXE_EXTS.iter().any(|i| s.ends_with(i))
530}
531pub(crate) fn is_exe_file(s: &str) -> Result<bool> {
532 if ends_with_exe(s) {
533 return Ok(true);
534 }
535 let re_latest =
536 Regex::new(r"^https://github\.com/([^/]+)/([^/]+)/releases/latest/download/([^/]+)$")?;
537 let re_tag =
538 Regex::new(r"^https://github\.com/([^/]+)/([^/]+)/releases/download/([^/]+)/([^/]+)$")?;
539 let re_tag2 = Regex::new(
540 r"^https://github\.com/([^/]+)/([^/]+)/releases/download/([^/]+)/([^/]+)/([^/]+)$",
541 )?;
542 for (re, n) in [(re_tag2, 5), (re_tag, 4), (re_latest, 3)] {
543 if let Some(cap) = re.captures(s)
544 && let Some(name) = cap.get(n)
545 {
546 if is_archive_file(name.as_str()) {
547 return Ok(false);
548 }
549 if !name.as_str().contains(".") {
550 return Ok(true);
551 }
552 }
553 }
554 Ok(false)
555}
556
557pub(crate) fn is_url(s: &str) -> bool {
558 s.starts_with("http://") || s.starts_with("https://")
559}
560
561pub(crate) fn is_dist_manfiest(s: &str) -> bool {
562 s.ends_with(".json")
563}
564
565pub(crate) fn path_to_str(p: &Path) -> String {
566 p.to_str().unwrap().replace("\\", "/")
567}
568
569pub(crate) fn replace_filename(base_url: &str, name: &str) -> String {
570 if let Some(pos) = base_url.rfind('/') {
571 format!("{}{}", &base_url[..pos + 1], name)
572 } else {
573 name.to_string()
574 }
575}
576
577pub(crate) fn get_artifact_url(
578 artifacts: GhArtifacts,
579 config: &InstallConfig,
580) -> Result<Vec<(String, String)>> {
581 use crate::ty::Repo;
582
583 let mut v = vec![];
584 let local_target = get_local_target();
585
586 for i in artifacts.assets {
587 if is_skip(&i.browser_download_url) {
588 continue;
589 }
590 if ends_with_exe(&i.browser_download_url)
591 && local_target.iter().any(|t| t.os() != Os::Windows)
592 {
593 continue;
594 }
595 let filename = get_filename(&i.browser_download_url);
596 let name = name_no_ext(&filename);
597 let guess = guess_target(&name);
598 if let Some(t) = config.target
599 && let Some(item) = guess.iter().find(|i| t == i.target)
600 {
601 v.push((item.rank, item.name.clone(), i.browser_download_url.clone()));
602 } else if let Some(item) = guess.iter().find(|i| local_target.contains(&i.target)) {
603 let hack_musl = match (item.target.arch(), item.target.abi()) {
605 (Arch::Aarch64, Some(Abi::Musl)) => 10,
606 _ => 0,
607 };
608 v.push((
609 item.rank + hack_musl,
610 item.name.clone(),
611 i.browser_download_url.clone(),
612 ));
613 }
614 }
615 let max_rank = v.iter().fold(0, |pre, cur| pre.max(cur.0));
616 let mut filter = vec![];
617 let mut list = vec![];
618 for (rank, name, url) in v {
620 if rank < max_rank {
621 continue;
622 }
623 if filter.contains(&name) {
624 continue;
625 }
626
627 filter.push(name.clone());
628 let proxied_url = Repo::convert_github_url_to_proxy(&url, config.proxy);
629 list.push((name, proxied_url));
630 }
631 Ok(list)
632}
633#[cfg(test)]
634mod test {
635 use anyhow::Context;
636
637 use crate::{
638 download::download_dist_manfiest,
639 tool::{dirname, get_artifact_url_from_manfiest, is_archive_file, is_exe_file, is_url},
640 ty::Repo,
641 };
642 use github_proxy::Proxy;
643
644 use super::{get_bin_name, get_common_prefix_len};
645
646 #[test]
647 fn test_is_file() {
648 assert!(!is_archive_file("https://github.com/ahaoboy/ansi2"));
649 assert!(!is_archive_file(
650 "https://api.github.com/repos/ahaoboy/ansi2/releases/latest"
651 ));
652 assert!(!is_archive_file(
653 "https://github.com/ahaoboy/ansi2/releases/tag/v0.2.11"
654 ));
655 assert!(is_archive_file(
656 "https://github.com/ahaoboy/ansi2/releases/download/v0.2.11/ansi2-x86_64-unknown-linux-musl.tar.gz"
657 ));
658 assert!(is_archive_file(
659 "https://github.com/ahaoboy/ansi2/releases/download/v0.2.11/ansi2-x86_64-pc-windows-msvc.zip"
660 ));
661 }
662
663 #[test]
664 fn test_is_github() {
665 let repo = Repo {
666 owner: "ahaoboy".to_string(),
667 name: "ansi2".to_string(),
668 tag: None,
669 };
670 assert_eq!(
671 Repo::try_from("https://github.com/ahaoboy/ansi2")
672 .context("failed to try_from")
673 .unwrap(),
674 repo
675 );
676
677 assert!(
678 Repo::try_from("https://api.github.com/repos/ahaoboy/ansi2/releases/latest")
679 .context("failed to try_from")
680 .is_err()
681 );
682 assert_eq!(
683 Repo::try_from("ahaoboy/ansi2")
684 .context("failed to try_from")
685 .unwrap(),
686 repo
687 );
688 let repo = Repo {
689 owner: "ahaoboy".to_string(),
690 name: "ansi2".to_string(),
691 tag: Some("v0.2.11".to_string()),
692 };
693 assert_eq!(
694 Repo::try_from("ahaoboy/ansi2@v0.2.11")
695 .context("failed to try_from")
696 .unwrap(),
697 repo
698 );
699 assert_eq!(
700 Repo::try_from("https://github.com/ahaoboy/ansi2/releases/tag/v0.2.11")
701 .context("failed to try_from")
702 .unwrap(),
703 repo
704 );
705
706 assert_eq!(
707 Repo::try_from("https://github.com/ahaoboy/ansi2/releases/download/v0.2.11/ansi2-x86_64-unknown-linux-musl.tar.gz").context("failed to try_from").unwrap(),
708 repo
709 );
710
711 assert_eq!(
712 Repo::try_from("https://github.com/ahaoboy/ansi2/releases/download/v0.2.11/ansi2-x86_64-pc-windows-msvc.zip").context("failed to try_from").unwrap(),
713 repo
714 );
715
716 }
726
727 #[test]
728 fn test_is_url() {
729 assert!(is_url("https://github.com/ahaoboy/ansi2"));
730 assert!(!is_url("ansi2"));
731 }
732
733 #[tokio::test]
734 async fn test_get_artifact_api() {
735 let repo = Repo::try_from("https://github.com/axodotdev/cargo-dist").unwrap();
736 let url = repo.get_artifact_api();
737 assert_eq!(
738 url,
739 "https://api.github.com/repos/axodotdev/cargo-dist/releases/latest"
740 );
741 }
742 #[tokio::test]
743 async fn test_get_manfiest() {
744 let repo =
754 Repo::try_from("https://github.com/axodotdev/cargo-dist/releases/tag/v0.25.1").unwrap();
755 let url = repo.get_manfiest_url(Proxy::Github, 3, 600).await.unwrap();
756 assert_eq!(
757 url,
758 "https://github.com/axodotdev/cargo-dist/releases/download/v0.25.1/dist-manifest.json"
759 );
760
761 let manfiest = repo.get_manfiest(3, Proxy::Github, 30).await.unwrap();
762 assert!(!manfiest.artifacts.is_empty());
763
764 let repo =
765 Repo::try_from("https://github.com/ahaoboy/mujs-build/releases/tag/v0.0.2").unwrap();
766 let url = repo.get_manfiest_url(Proxy::Github, 3, 600).await.unwrap();
767 assert_eq!(
768 url,
769 "https://github.com/ahaoboy/mujs-build/releases/download/v0.0.2/dist-manifest.json"
770 );
771
772 let manfiest = repo.get_manfiest(3, Proxy::Github, 30).await.unwrap();
773 assert!(!manfiest.artifacts.is_empty())
774 }
775
776 #[tokio::test]
789 async fn test_cargo_dist() {
790 let url = "https://github.com/axodotdev/cargo-dist/releases/download/v1.0.0-rc.1/dist-manifest.json";
791 let manfiest = download_dist_manfiest(url, 3, 30).await.unwrap();
792 let art_url = get_artifact_url_from_manfiest(url, &manfiest);
793 assert!(!art_url.is_empty())
794 }
795
796 #[tokio::test]
797 async fn test_deno() {
798 let url = "https://github.com/denoland/deno";
799 let repo = Repo::try_from(url).unwrap();
800 let artifact_url = repo.get_artifact_url(&Default::default()).await.unwrap();
801 println!("artifact_url{artifact_url:?}");
802 assert_eq!(artifact_url.len(), 2);
803 }
804
805 #[tokio::test]
806 async fn test_starship() {
807 let repo = Repo::try_from("https://github.com/starship/starship").unwrap();
808 let artifact_url = repo.get_artifact_url(&Default::default()).await.unwrap();
809 println!("{artifact_url:?}");
810 assert_eq!(artifact_url.len(), 1);
811 }
812
813 #[test]
814 fn test_is_exe_file() {
815 for (a, b) in [
816 (
817 "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe",
818 true,
819 ),
820 (
821 "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-win-x64.exe",
822 true,
823 ),
824 (
825 "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-win-x64",
826 true,
827 ),
828 (
829 "https://github.com/easy-install/easy-install/releases/download/v0.1.5/ei-x86_64-apple-darwin.tar.gz",
830 false,
831 ),
832 ("https://github.com/easy-install/easy-install", false),
833 (
834 "https://github.com/easy-install/easy-install/releases/tag/v0.1.5",
835 false,
836 ),
837 (
838 "https://github.com/biomejs/biome/releases/download/cli/v1.9.4/biome-darwin-arm64",
839 true,
840 ),
841 (
842 "https://github.com/biomejs/biome/releases/download/cli/v1.9.4/biome-darwin-arm64.zip",
843 false,
844 ),
845 (
846 "https://github.com/biomejs/biome/releases/download/cli/v1.9.4/biome-darwin-arm64.msi",
847 false,
848 ),
849 ] {
850 assert_eq!(
851 is_exe_file(a)
852 .context("failed to check is_exe_file")
853 .unwrap(),
854 b
855 );
856 }
857 }
858
859 #[test]
860 fn test_get_common_prefix() {
861 assert_eq!(get_common_prefix_len(&["/a/ab/c", "/a/ad", "/a/ab/d",]), 3);
862 assert_eq!(get_common_prefix_len(&["a",]), 0);
863 assert_eq!(get_common_prefix_len(&["/a",]), 1);
864 assert_eq!(get_common_prefix_len(&["/a/b"]), 3);
865 }
866
867 #[test]
868 fn test_get_bin_name() {
869 for (a, b) in [
870 ("a", if cfg!(windows) { "a.exe" } else { "a" }),
871 ("a.bat", "a.bat"),
872 ("a.ps1", "a.ps1"),
873 ("a.msi", "a.msi"),
874 ] {
875 let s = get_bin_name(a);
876 assert_eq!(b, s)
877 }
878 }
879
880 #[test]
881 fn test_dirname() {
882 for (a, b) in [("a", "a"), ("/a", "/"), ("/a/b", "/a/"), ("a/b/c", "a/b/")] {
883 assert_eq!(dirname(a), b);
884 }
885 }
886}
887
888pub(crate) fn not_found_asset_message(url: &str) {
889 println!(
890 "Not found asset for os:{} arch:{} target:{} on {}",
891 std::env::consts::OS,
892 std::env::consts::ARCH,
893 get_local_target()
894 .iter()
895 .map(|i| i.to_str().to_string())
896 .collect::<Vec<_>>()
897 .join(", "),
898 url
899 );
900}