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 Archiver::new(&temp_dir, &download.archive_file).unpack_from_ext()?;
184
185 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 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}