1pub mod extract;
26pub mod fetch;
27pub mod lock;
28pub mod manifest;
29
30use std::path::{Path, PathBuf};
31use std::process::ExitCode;
32use std::time::Duration;
33
34use clap::Args;
35use thiserror::Error;
36
37#[derive(Args, Debug)]
38pub struct InstallArgs {
39 #[arg(short = 'f', long, default_value = "./Manifest.lua")]
41 pub manifest: PathBuf,
42
43 #[arg(long)]
46 pub cache_dir: Option<PathBuf>,
47
48 #[arg(long)]
51 pub bin_dir: Option<PathBuf>,
52
53 #[arg(long)]
56 pub lib_dir: Option<PathBuf>,
57
58 #[arg(long)]
60 pub offline: bool,
61
62 #[arg(long)]
64 pub dry_run: bool,
65
66 #[arg(long)]
68 pub no_progress: bool,
69}
70
71#[derive(Debug, Error)]
72pub enum InstallError {
73 #[error("read {path}: {source}")]
74 ReadManifest {
75 path: PathBuf,
76 #[source]
77 source: std::io::Error,
78 },
79
80 #[error(transparent)]
81 Manifest(#[from] manifest::ManifestError),
82
83 #[error(transparent)]
84 Fetch(#[from] fetch::FetchError),
85
86 #[error(transparent)]
87 Extract(#[from] extract::ExtractError),
88
89 #[error("{count} dep(s) failed to fetch; see errors above")]
90 FetchFailed { count: usize },
91
92 #[error("write {path}: {source}")]
93 WriteLock {
94 path: PathBuf,
95 #[source]
96 source: std::io::Error,
97 },
98}
99
100pub async fn run(args: InstallArgs) -> ExitCode {
102 match execute(args).await {
103 Ok(()) => ExitCode::SUCCESS,
104 Err(e) => {
105 eprintln!("assay install: {e}");
106 let mut src = std::error::Error::source(&e);
107 while let Some(s) = src {
108 eprintln!(" caused by: {s}");
109 src = std::error::Error::source(s);
110 }
111 ExitCode::from(1)
112 }
113 }
114}
115
116pub async fn execute(args: InstallArgs) -> Result<(), InstallError> {
120 let source =
122 std::fs::read_to_string(&args.manifest).map_err(|e| InstallError::ReadManifest {
123 path: args.manifest.clone(),
124 source: e,
125 })?;
126 let m = manifest::parse(&source, &args.manifest)?;
127
128 let cache_dir = args.cache_dir.clone().unwrap_or_else(default_cache_dir);
130 let bin_dir = args.bin_dir.clone().unwrap_or_else(default_bin_dir);
131 let lib_dir = args.lib_dir.clone().unwrap_or_else(default_lib_dir);
132 let arch = std::env::consts::ARCH.to_string();
133
134 let mut ext_plans: Vec<(manifest::Extension, fetch::FetchPlan)> = Vec::new();
136 for e in &m.extensions {
137 let plan = fetch::FetchPlan::for_extension(e, &arch, &cache_dir)?;
138 ext_plans.push((e.clone(), plan));
139 }
140 let mut lib_plans: Vec<(manifest::Lib, fetch::FetchPlan)> = Vec::new();
141 for l in &m.libs {
142 lib_plans.push((l.clone(), fetch::FetchPlan::for_lib(l, &cache_dir)));
143 }
144
145 if args.dry_run {
147 print_plan(&m, &ext_plans, &lib_plans, &cache_dir, &bin_dir, &lib_dir);
148 return Ok(());
149 }
150
151 let client = reqwest::Client::builder()
153 .timeout(Duration::from_secs(60))
154 .build()
155 .expect("build reqwest client");
156
157 let mut handles = Vec::with_capacity(ext_plans.len() + lib_plans.len());
158 let all_plans = ext_plans
159 .iter()
160 .map(|(_, p)| p)
161 .chain(lib_plans.iter().map(|(_, p)| p));
162 for plan in all_plans {
163 let plan = plan.clone();
164 let client = client.clone();
165 let offline = args.offline;
166 handles.push(tokio::spawn(async move {
167 let result = fetch::fetch(&plan, &client, offline).await;
168 (plan.display_name.clone(), result)
169 }));
170 }
171
172 let mut failures = 0;
173 for h in handles {
174 let (name, res) = h.await.expect("fetch task panicked");
175 match res {
176 Ok(()) => {
177 if !args.no_progress {
178 eprintln!(" ✓ fetched {name}");
179 }
180 }
181 Err(e) => {
182 eprintln!(" ✗ {name}: {e}");
183 failures += 1;
184 }
185 }
186 }
187 if failures > 0 {
188 return Err(InstallError::FetchFailed { count: failures });
189 }
190
191 for (e, plan) in &ext_plans {
193 extract::install_extension_binary(&plan.cache_path, &bin_dir, &e.name)?;
194 if !args.no_progress {
195 eprintln!(" → installed {} → {}", e.name, bin_dir.join(&e.name).display());
196 }
197 }
198 for (l, plan) in &lib_plans {
199 extract::install_lib_tree(&plan.cache_path, &lib_dir, &l.name)?;
200 if !args.no_progress {
201 eprintln!(" → installed {} → {}/", l.name, lib_dir.join(&l.name).display());
202 }
203 }
204
205 let lockfile = lock::Lockfile {
207 assay: m.assay.clone(),
208 extensions: ext_plans
209 .iter()
210 .map(|(e, p)| lock::LockExtension::new(e, p))
211 .collect(),
212 libs: lib_plans
213 .iter()
214 .map(|(l, p)| lock::LockLib::new(l, p))
215 .collect(),
216 };
217 let lock_path = args.manifest.with_file_name("Manifest.lock");
218 std::fs::write(&lock_path, lockfile.to_lua()).map_err(|e| InstallError::WriteLock {
219 path: lock_path.clone(),
220 source: e,
221 })?;
222 if !args.no_progress {
223 eprintln!(" → wrote {}", lock_path.display());
224 }
225
226 Ok(())
227}
228
229fn print_plan(
230 m: &manifest::Manifest,
231 ext_plans: &[(manifest::Extension, fetch::FetchPlan)],
232 lib_plans: &[(manifest::Lib, fetch::FetchPlan)],
233 cache_dir: &Path,
234 bin_dir: &Path,
235 lib_dir: &Path,
236) {
237 println!("# assay install (dry run)");
238 if let Some(v) = &m.assay {
239 println!("assay = {v}");
240 }
241 println!("cache_dir = {}", cache_dir.display());
242 println!("bin_dir = {}", bin_dir.display());
243 println!("lib_dir = {}", lib_dir.display());
244 println!();
245 println!("extensions:");
246 for (_, p) in ext_plans {
247 println!(" {} (sha256 {})", p.display_name, p.expected_sha256);
248 println!(" url = {}", p.url);
249 println!(" cache = {}", p.cache_path.display());
250 }
251 println!();
252 println!("libs:");
253 for (_, p) in lib_plans {
254 println!(" {} (sha256 {})", p.display_name, p.expected_sha256);
255 println!(" url = {}", p.url);
256 println!(" cache = {}", p.cache_path.display());
257 }
258}
259
260fn default_cache_dir() -> PathBuf {
263 if is_root() {
264 PathBuf::from("/var/cache/assay")
265 } else if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") {
266 PathBuf::from(xdg).join("assay")
267 } else if let Some(home) = std::env::var_os("HOME") {
268 PathBuf::from(home).join(".cache/assay")
269 } else {
270 PathBuf::from("/tmp/assay-cache")
271 }
272}
273
274fn default_bin_dir() -> PathBuf {
275 if is_root() {
276 PathBuf::from("/usr/local/bin")
277 } else if let Some(home) = std::env::var_os("HOME") {
278 PathBuf::from(home).join(".local/bin")
279 } else {
280 PathBuf::from("/usr/local/bin")
281 }
282}
283
284fn default_lib_dir() -> PathBuf {
285 if is_root() {
286 PathBuf::from("/opt/assay/libs")
287 } else if let Some(xdg) = std::env::var_os("XDG_DATA_HOME") {
288 PathBuf::from(xdg).join("assay/libs")
289 } else if let Some(home) = std::env::var_os("HOME") {
290 PathBuf::from(home).join(".local/share/assay/libs")
291 } else {
292 PathBuf::from("/opt/assay/libs")
293 }
294}
295
296#[cfg(unix)]
297fn is_root() -> bool {
298 unsafe { libc::getuid() == 0 }
300}
301
302#[cfg(not(unix))]
303fn is_root() -> bool {
304 false
305}