easy_install/
install.rs

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