avalanche_installer/avalanchego/
github.rs

1use std::{
2    env, fmt,
3    fs::{self, File},
4    io::{self, copy, Cursor, Error, ErrorKind},
5    os::unix::fs::PermissionsExt,
6    path::Path,
7};
8
9use compress_manager::DirDecoder;
10use tokio::time::{sleep, Duration};
11
12/// Downloads the latest "avalanchego" from the github release page.
13pub async fn download_latest(arch: Option<Arch>, os: Option<Os>) -> io::Result<String> {
14    download(arch, os, None).await
15}
16
17/// ref. <https://github.com/ava-labs/avalanchego/releases>
18pub const DEFAULT_TAG_NAME: &str = "v1.10.3";
19
20/// Downloads the official "avalanchego" binaries from the GitHub release page.
21/// Returns the path to the binary path.
22///
23/// Leave "release_tag" none to download the latest.
24///
25/// Leave "arch" and "os" empty to auto-detect from its local system.
26/// "arch" must be either "amd64" or "arm64".
27/// "os" must be either "macos", "linux", or "win".
28/// ref. <https://github.com/ava-labs/avalanchego/releases>
29pub async fn download(
30    arch: Option<Arch>,
31    os: Option<Os>,
32    release_tag: Option<String>,
33) -> io::Result<String> {
34    // e.g., "v1.10.0"
35    let tag_name = if let Some(v) = release_tag {
36        // "https://github.com/ava-labs/avalanchego/releases" doesn't have "latest" tag
37        if v.eq("latest") {
38            log::warn!("falling back 'latest' to {DEFAULT_TAG_NAME}");
39            DEFAULT_TAG_NAME.to_owned()
40        } else {
41            v
42        }
43    } else {
44        log::info!("fetching the latest git tags");
45        let mut release_info = crate::github::ReleaseResponse::default();
46        for round in 0..10 {
47            let info = match crate::github::fetch_latest_release("ava-labs", "avalanchego").await {
48                Ok(v) => v,
49                Err(e) => {
50                    log::warn!(
51                        "failed fetch_latest_release {} -- retrying {}...",
52                        e,
53                        round + 1
54                    );
55                    sleep(Duration::from_secs((round + 1) * 3)).await;
56                    continue;
57                }
58            };
59
60            release_info = info;
61            if release_info.tag_name.is_some() {
62                break;
63            }
64
65            log::warn!("release_info.tag_name is None -- retrying {}...", round + 1);
66            sleep(Duration::from_secs((round + 1) * 3)).await;
67        }
68
69        if release_info.tag_name.is_none() {
70            log::warn!("release_info.tag_name not found -- defaults to {DEFAULT_TAG_NAME}");
71            release_info.tag_name = Some(DEFAULT_TAG_NAME.to_string());
72        }
73
74        if release_info.prerelease {
75            log::warn!(
76                "latest release '{}' is prerelease, falling back to default tag name '{}'",
77                release_info.tag_name.unwrap(),
78                DEFAULT_TAG_NAME
79            );
80            DEFAULT_TAG_NAME.to_string()
81        } else {
82            release_info.tag_name.unwrap()
83        }
84    };
85
86    // ref. <https://github.com/ava-labs/avalanchego/releases>
87    log::info!(
88        "detecting arch and platform for the release version tag {}",
89        tag_name
90    );
91    let arch = {
92        if arch.is_none() {
93            match env::consts::ARCH {
94                "x86_64" => String::from("amd64"),
95                "aarch64" => String::from("arm64"),
96                _ => String::from(""),
97            }
98        } else {
99            let arch = arch.unwrap();
100            arch.to_string()
101        }
102    };
103
104    // TODO: handle Apple arm64 when the official binary is available
105    // ref. <https://github.com/ava-labs/avalanchego/releases>
106    let (file_name, dir_decoder) = {
107        if os.is_none() {
108            if cfg!(target_os = "macos") {
109                (
110                    format!("avalanchego-macos-{}.zip", tag_name),
111                    DirDecoder::Zip,
112                )
113            } else if cfg!(unix) {
114                (
115                    format!("avalanchego-linux-{}-{}.tar.gz", arch, tag_name),
116                    DirDecoder::TarGzip,
117                )
118            } else if cfg!(windows) {
119                (
120                    format!("avalanchego-win-{}-experimental.zip", tag_name),
121                    DirDecoder::Zip,
122                )
123            } else {
124                (String::new(), DirDecoder::Zip)
125            }
126        } else {
127            let os = os.unwrap();
128            match os {
129                Os::MacOs => (
130                    format!("avalanchego-macos-{}.zip", tag_name),
131                    DirDecoder::Zip,
132                ),
133                Os::Linux => (
134                    format!("avalanchego-linux-{}-{}.tar.gz", arch, tag_name),
135                    DirDecoder::TarGzip,
136                ),
137                Os::Windows => (
138                    format!("avalanchego-win-{}-experimental.zip", tag_name),
139                    DirDecoder::Zip,
140                ),
141            }
142        }
143    };
144    if file_name.is_empty() {
145        return Err(Error::new(
146            ErrorKind::Other,
147            format!("unknown platform '{}'", env::consts::OS),
148        ));
149    }
150
151    log::info!("downloading latest avalanchego '{}'", file_name);
152    let download_url = format!(
153        "https://github.com/ava-labs/avalanchego/releases/download/{}/{}",
154        tag_name, file_name
155    );
156    let tmp_file_path = random_manager::tmp_path(10, Some(dir_decoder.suffix()))?;
157    download_file(&download_url, &tmp_file_path).await?;
158
159    let dst_dir_path = random_manager::tmp_path(10, None)?;
160    log::info!("unpacking {} to {}", tmp_file_path, dst_dir_path);
161    compress_manager::unpack_directory(&tmp_file_path, &dst_dir_path, dir_decoder.clone())?;
162
163    // TODO: this can fail due to files being still busy...
164    log::info!("cleaning up downloaded file {}", tmp_file_path);
165    match fs::remove_file(&tmp_file_path) {
166        Ok(_) => log::info!("removed downloaded file {}", tmp_file_path),
167        Err(e) => log::warn!(
168            "failed to remove downloaded file {} ({}), skipping for now...",
169            tmp_file_path,
170            e
171        ),
172    }
173
174    let avalanchego_path = if dir_decoder.clone().suffix() == DirDecoder::Zip.suffix() {
175        Path::new(&dst_dir_path).join("build").join("avalanchego")
176    } else {
177        Path::new(&dst_dir_path)
178            .join(format!("avalanchego-{}", tag_name))
179            .join("avalanchego")
180    };
181
182    {
183        let f = File::open(&avalanchego_path)?;
184        f.set_permissions(PermissionsExt::from_mode(0o777))?;
185    }
186    Ok(String::from(avalanchego_path.as_os_str().to_str().unwrap()))
187}
188
189/// Represents the AvalancheGo release "arch".
190#[derive(Eq, PartialEq, Clone)]
191pub enum Arch {
192    Amd64,
193    Arm64,
194}
195
196/// ref. https://doc.rust-lang.org/std/string/trait.ToString.html
197/// ref. https://doc.rust-lang.org/std/fmt/trait.Display.html
198/// Use "Self.to_string()" to directly invoke this
199impl fmt::Display for Arch {
200    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
201        match self {
202            Arch::Amd64 => write!(f, "amd64"),
203            Arch::Arm64 => write!(f, "arm64"),
204        }
205    }
206}
207
208impl Arch {
209    pub fn new(arch: &str) -> io::Result<Self> {
210        match arch {
211            "amd64" => Ok(Arch::Amd64),
212            "arm64" => Ok(Arch::Arm64),
213            _ => Err(Error::new(
214                ErrorKind::InvalidInput,
215                format!("unknown arch {}", arch),
216            )),
217        }
218    }
219}
220
221/// Represents the AvalancheGo release "os".
222#[derive(Eq, PartialEq, Clone)]
223pub enum Os {
224    MacOs,
225    Linux,
226    Windows,
227}
228
229/// ref. https://doc.rust-lang.org/std/string/trait.ToString.html
230/// ref. https://doc.rust-lang.org/std/fmt/trait.Display.html
231/// Use "Self.to_string()" to directly invoke this
232impl fmt::Display for Os {
233    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
234        match self {
235            Os::MacOs => write!(f, "macos"),
236            Os::Linux => write!(f, "linux"),
237            Os::Windows => write!(f, "win"),
238        }
239    }
240}
241
242impl Os {
243    pub fn new(os: &str) -> io::Result<Self> {
244        match os {
245            "macos" => Ok(Os::MacOs),
246            "linux" => Ok(Os::Linux),
247            "win" => Ok(Os::Windows),
248            _ => Err(Error::new(
249                ErrorKind::InvalidInput,
250                format!("unknown os {}", os),
251            )),
252        }
253    }
254}
255
256/// Downloads a file to the "file_path".
257pub async fn download_file(ep: &str, file_path: &str) -> io::Result<()> {
258    log::info!("downloading the file via {}", ep);
259    let resp = reqwest::get(ep)
260        .await
261        .map_err(|e| Error::new(ErrorKind::Other, format!("failed reqwest::get {}", e)))?;
262
263    let mut content = Cursor::new(
264        resp.bytes()
265            .await
266            .map_err(|e| Error::new(ErrorKind::Other, format!("failed bytes {}", e)))?,
267    );
268
269    let mut f = File::create(file_path)?;
270    copy(&mut content, &mut f)?;
271
272    Ok(())
273}