proto_installer/
lib.rs

1mod error;
2#[cfg(unix)]
3mod unix;
4#[cfg(windows)]
5mod windows;
6
7use starbase_archive::Archiver;
8use starbase_styles::color;
9use starbase_utils::fs;
10use starbase_utils::net::{self, DownloadOptions, NetError};
11use std::env;
12use std::env::consts;
13use std::fmt::Debug;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use std::time::SystemTime;
17use system_env::SystemLibc;
18use tracing::{instrument, trace};
19#[cfg(unix)]
20pub use unix::*;
21#[cfg(windows)]
22pub use windows::*;
23
24pub use error::ProtoInstallerError;
25
26#[instrument]
27pub fn determine_triple() -> Result<String, ProtoInstallerError> {
28    let target = match (consts::OS, consts::ARCH) {
29        ("linux", arch) => format!(
30            "{arch}-unknown-linux-{}",
31            if SystemLibc::is_musl() { "musl" } else { "gnu" }
32        ),
33        ("macos", arch) => format!("{arch}-apple-darwin"),
34        ("windows", "x86_64") => "x86_64-pc-windows-msvc".to_owned(),
35        (os, arch) => {
36            return Err(ProtoInstallerError::InvalidPlatform {
37                arch: arch.to_owned(),
38                os: os.to_owned(),
39            });
40        }
41    };
42
43    Ok(target)
44}
45
46#[derive(Debug)]
47pub struct DownloadResult {
48    pub archive_file: PathBuf,
49    pub file: String,
50    pub file_stem: String,
51    pub url: String,
52}
53
54#[instrument(skip(on_chunk))]
55pub async fn download_release(
56    triple: &str,
57    version: &str,
58    temp_dir: impl AsRef<Path> + Debug,
59    on_chunk: impl Fn(u64, u64) + Send + Sync + 'static,
60) -> Result<DownloadResult, ProtoInstallerError> {
61    let target_ext = if cfg!(windows) { "zip" } else { "tar.xz" };
62    let target_file = format!("proto_cli-{triple}");
63
64    let download_file = format!("{target_file}.{target_ext}");
65    let download_url =
66        format!("https://github.com/moonrepo/proto/releases/download/v{version}/{download_file}");
67
68    let archive_file = temp_dir.as_ref().join(&download_file);
69
70    trace!(
71        version,
72        triple,
73        "Downloading proto release from {}",
74        color::url(&download_url)
75    );
76
77    net::download_from_url_with_options(
78        &download_url,
79        &archive_file,
80        DownloadOptions {
81            on_chunk: Some(Arc::new(on_chunk)),
82            ..DownloadOptions::default()
83        },
84    )
85    .await
86    .map_err(|error| match error {
87        NetError::Http { url, error } => ProtoInstallerError::FailedDownload { url, error },
88        NetError::DownloadFailed { status, .. } => ProtoInstallerError::DownloadNotAvailable {
89            version: version.to_owned(),
90            status,
91        },
92        _ => ProtoInstallerError::Net(Box::new(error)),
93    })?;
94
95    Ok(DownloadResult {
96        archive_file,
97        file: download_file,
98        file_stem: target_file,
99        url: download_url,
100    })
101}
102
103#[instrument]
104pub fn replace_binaries(
105    source_dir: impl AsRef<Path> + Debug,
106    target_dir: impl AsRef<Path> + Debug,
107    relocate_current: bool,
108) -> Result<bool, ProtoInstallerError> {
109    let source_dir = source_dir.as_ref();
110    let target_dir = target_dir.as_ref();
111    let bin_names = if cfg!(windows) {
112        vec!["proto.exe", "proto-shim.exe"]
113    } else {
114        vec!["proto", "proto-shim"]
115    };
116
117    let mut output_dirs = vec![target_dir.to_path_buf()];
118
119    if relocate_current {
120        if let Ok(current) = env::current_exe() {
121            let current_dir = current.parent().unwrap();
122
123            if current_dir != target_dir {
124                output_dirs.push(current_dir.to_path_buf());
125            }
126        }
127    }
128
129    let mut replaced = false;
130
131    for bin_name in &bin_names {
132        let input_path = source_dir.join(bin_name);
133
134        if !input_path.exists() {
135            continue;
136        }
137
138        for output_dir in &output_dirs {
139            let output_path = output_dir.join(bin_name);
140            let relocate_path = output_dir.join(format!("{bin_name}.backup"));
141
142            if output_path.exists() {
143                self_replace(&output_path, &input_path, &relocate_path)?;
144            } else {
145                fs::copy_file(&input_path, &output_path)?;
146                fs::update_perms(&output_path, None)?;
147            }
148
149            replaced = true;
150        }
151    }
152
153    Ok(replaced)
154}
155
156#[instrument]
157pub fn install_release(
158    download: DownloadResult,
159    install_dir: impl AsRef<Path> + Debug,
160    relocate_dir: impl AsRef<Path> + Debug,
161    relocate_current: bool,
162) -> Result<bool, ProtoInstallerError> {
163    let temp_dir = download
164        .archive_file
165        .parent()
166        .unwrap()
167        .join(&download.file_stem);
168    let install_dir = install_dir.as_ref();
169    let relocate_dir = relocate_dir.as_ref();
170    let bin_names = if cfg!(windows) {
171        vec!["proto.exe", "proto-shim.exe"]
172    } else {
173        vec!["proto", "proto-shim"]
174    };
175
176    trace!(
177        source = ?download.archive_file,
178        target = ?temp_dir,
179        "Unpacking downloaded and installing proto release"
180    );
181
182    // Unpack the downloaded file
183    Archiver::new(&temp_dir, &download.archive_file).unpack_from_ext()?;
184
185    // Move the new binary to the install directory
186    let mut installed = false;
187
188    trace!(install_dir = ?install_dir, "Moving unpacked proto binaries to the install directory");
189
190    let input_dirs = vec![temp_dir.join(&download.file_stem), temp_dir.clone()];
191    let mut output_dirs = vec![install_dir.to_path_buf()];
192
193    if relocate_current {
194        if let Ok(current) = env::current_exe() {
195            let current_dir = current.parent().unwrap();
196
197            if current_dir != install_dir {
198                output_dirs.push(current_dir.to_path_buf());
199            }
200        }
201    }
202
203    for bin_name in &bin_names {
204        for input_dir in &input_dirs {
205            let input_path = input_dir.join(bin_name);
206
207            if !input_path.exists() {
208                continue;
209            }
210
211            for output_dir in &output_dirs {
212                let output_path = output_dir.join(bin_name);
213                let relocate_path = relocate_dir.join(bin_name);
214
215                if output_path.exists() {
216                    self_replace(&output_path, &input_path, &relocate_path)?;
217                } else {
218                    fs::copy_file(&input_path, &output_path)?;
219                    fs::update_perms(&output_path, None)?;
220                }
221
222                installed = true;
223            }
224        }
225    }
226
227    fs::remove(temp_dir)?;
228    fs::remove(download.archive_file)?;
229
230    // Track last used so operations like clean continue to work
231    // correctly, otherwise we get into a weird state!
232    if installed && relocate_dir.exists() {
233        fs::write_file(
234            relocate_dir.join(".last-used"),
235            SystemTime::now()
236                .duration_since(SystemTime::UNIX_EPOCH)
237                .map(|d| d.as_millis())
238                .unwrap_or(0)
239                .to_string(),
240        )?;
241    }
242
243    Ok(installed)
244}