chur_build/
dependency.rs

1use std::fmt::Display;
2use std::fs;
3use std::io::{Read, Write};
4
5use sha::sha256::Sha256;
6use sha::utils::DigestExt;
7
8use crate::defined_constants::{DEPENDENCY_CACHE_DIR, DEPENDENCY_INCLUDE_DIR};
9use crate::error::{ChurError, ChurResult};
10
11use archiver_rs::{Archive, Compressed};
12
13#[derive(Debug)]
14pub enum DependencyFormat {
15    Gzip,
16}
17
18#[derive(Debug)]
19pub struct Dependency {
20    pub(crate) url: String,
21    pub(crate) format: DependencyFormat,
22    pub(crate) subdir: Option<String>,
23}
24
25impl Dependency {
26    pub fn new(
27        url: impl Display,
28        format: DependencyFormat,
29        subdir: impl Into<Option<String>>,
30    ) -> Self {
31        Self {
32            url: url.to_string(),
33            format,
34            subdir: subdir.into(),
35        }
36    }
37
38    pub fn tarball(
39        url: impl Display,
40        format: DependencyFormat,
41        subdir: impl Into<Option<String>>,
42    ) -> Self {
43        Self {
44            url: url.to_string(),
45            format,
46            subdir: subdir.into(),
47        }
48    }
49
50    pub fn github(repo: impl Display, branch_or_hash: impl Into<Option<String>>) -> Self {
51        let branch_opt: Option<String> = branch_or_hash.into();
52        let branch_or_hash_unwrapped = branch_opt.unwrap_or("main".to_string());
53
54        let repo_string = repo.to_string();
55        let mut repo_split = repo_string.split("/");
56
57        let _user = repo_split.next().unwrap();
58        let repo = repo_split.next().unwrap();
59
60        // the subdir of a github tarball depends on the repos name and the commit hash
61        //
62        // e.g. this would contain a subdir called chur-<branch_or_hash> which
63        // has the repo in it.
64        Self::new(
65            format!("https://github.com/{repo_string}/archive/{branch_or_hash_unwrapped}.tar.gz"),
66            DependencyFormat::Gzip,
67            format!("{repo}-{branch_or_hash_unwrapped}"),
68        )
69    }
70
71    pub(crate) fn fetch(&self) -> ChurResult<String> {
72        match self.format {
73            DependencyFormat::Gzip => self.fetch_gzip_tar(),
74        }
75    }
76
77    pub(crate) fn fetch_gzip_tar(&self) -> ChurResult<String> {
78        let agent = ureq::Agent::new();
79
80        let response = agent.get(&self.url).call()?;
81        if response.status() != 200 {
82            ChurError::Dependency(format!(
83                "Dependency with URL \"{}\" returns a result of {}",
84                self.url,
85                response.status()
86            ));
87        }
88
89        // 2MB seems like a decent default limit.
90        let response_len = if let Some(header) = response.header("Content-Length") {
91            header.parse().unwrap_or(2_000_000)
92        } else {
93            2_000_000
94        };
95
96        let mut buf = Vec::with_capacity(response_len);
97        response
98            .into_reader()
99            .take(response_len as u64)
100            .read_to_end(&mut buf)?;
101
102        let mut hasher = Sha256::default();
103        hasher.write_all(&buf)?;
104
105        let hash = hasher.to_hex();
106
107        let mut archive = archiver_rs::Gzip::new(buf.as_slice())?;
108        fs::create_dir_all(DEPENDENCY_CACHE_DIR.as_path())?;
109        let tarball_file = DEPENDENCY_CACHE_DIR.join(&format!("tarball_{}", &hash));
110        archive.decompress(&tarball_file)?;
111        drop(archive);
112
113        let mut tarball = archiver_rs::Tar::open(&tarball_file)?;
114        let unpacked_dir = DEPENDENCY_INCLUDE_DIR.join(&hash);
115        fs::create_dir_all(&unpacked_dir)?;
116        tarball.extract(&unpacked_dir)?;
117
118        Ok(hash)
119    }
120}