Skip to main content

easy_install/
tool.rs

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 mut filter = vec![];
271    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        // let guess = guess_target(&filename);
284
285        // if let Some(item) = guess.iter().find(|i| local_target.contains(&i.target)) {
286        //     if filter.contains(&item.name) {
287        //         continue;
288        //     }
289        //     if !is_url(key) {
290        //         v.push((item.rank, item.name.clone(), replace_filename(url, key)));
291        //     } else {
292        //         v.push((item.rank, item.name.clone(), key.clone()));
293        //     }
294        //     filter.push(item.name.clone());
295        //     continue;
296        // }
297
298        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    // let max_rank = v.iter().fold(0, |pre, cur| pre.max(cur.0));
322    // v.into_iter()
323    //     .filter_map(|i| {
324    //         if i.0 < max_rank {
325    //             None
326    //         } else {
327    //             Some((i.1, i.2))
328    //         }
329    //     })
330    //     .collect()
331    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; // owner execute
367    const S_IXGRP: u32 = 0o010; // group execute
368    const S_IXOTH: u32 = 0o001; // others execute
369
370    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
377// if no executable file is found, then the only possible executable program is set to executable
378pub(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        // FIXME: skip __MACOSX
449        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            // HACK: Prioritize using musl on the arm platform
604            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    // FIXME: Need user to select eg: llrt-no-sdk llrt-full-sdk
619    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        // let repo = Repo {
717        //     owner: "Ryubing".to_string(),
718        //     name: "Ryujinx".to_string(),
719        //     tag: Some("1.2.78".to_string()),
720        // };
721        // assert_eq!(
722        //   Repo::try_from("https://github.com/Ryubing/Ryujinx/releases/download/1.2.78/ryujinx-*.*.*-win_x64.zip").context("failed to try_from").unwrap(),
723        //   repo
724        // );
725    }
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        // TODO: support latest tag
745        // let repo = Repo::try_from("https://github.com/axodotdev/cargo-dist/releases").unwrap();
746        // let url = repo.get_manfiest_url(Proxy::Github, 3, 600).await.unwrap();
747        // assert_eq!(
748        //     url,
749        //     "https://github.com/axodotdev/cargo-dist/releases/latest/download/dist-manifest.json"
750        // );
751        // assert!(repo.get_manfiest(3, Proxy::Github, 30).await.is_ok());
752
753        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]
777    // async fn test_install_from_manfiest() {
778    //     let url =
779    //         "https://github.com/ahaoboy/mujs-build/releases/latest/download/dist-manifest.json";
780    //     let manfiest = download_dist_manfiest(url)
781    //         .await
782    //         .context("failed to download_dist_manfiest")
783    //         .unwrap();
784    //     let art_url = get_artifact_url_from_manfiest(url, &manfiest);
785    //     assert!(!art_url.is_empty())
786    // }
787
788    #[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}