easy_install/
install.rs

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 targets = detect_targets().await;
101    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        // println!("{}", display_output(&output));
179        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            // && is_archive_file(name)
198            && 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        // FIXME: The full path should be used
251        // but the cargo-dist path has a prefix
252        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                // && is_archive_file(&name)
313                && 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        // Bin
714    ] {
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}