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!(
196 " → installed {} → {}",
197 e.name,
198 bin_dir.join(&e.name).display()
199 );
200 }
201 }
202 for (l, plan) in &lib_plans {
203 extract::install_lib_tree(&plan.cache_path, &lib_dir, &l.name)?;
204 if !args.no_progress {
205 eprintln!(
206 " → installed {} → {}/",
207 l.name,
208 lib_dir.join(&l.name).display()
209 );
210 }
211 }
212
213 let lockfile = lock::Lockfile {
215 assay: m.assay.clone(),
216 extensions: ext_plans
217 .iter()
218 .map(|(e, p)| lock::LockExtension::new(e, p))
219 .collect(),
220 libs: lib_plans
221 .iter()
222 .map(|(l, p)| lock::LockLib::new(l, p))
223 .collect(),
224 };
225 let lock_path = args.manifest.with_file_name("Manifest.lock");
226 std::fs::write(&lock_path, lockfile.to_lua()).map_err(|e| InstallError::WriteLock {
227 path: lock_path.clone(),
228 source: e,
229 })?;
230 if !args.no_progress {
231 eprintln!(" → wrote {}", lock_path.display());
232 }
233
234 Ok(())
235}
236
237fn print_plan(
238 m: &manifest::Manifest,
239 ext_plans: &[(manifest::Extension, fetch::FetchPlan)],
240 lib_plans: &[(manifest::Lib, fetch::FetchPlan)],
241 cache_dir: &Path,
242 bin_dir: &Path,
243 lib_dir: &Path,
244) {
245 println!("# assay install (dry run)");
246 if let Some(v) = &m.assay {
247 println!("assay = {v}");
248 }
249 println!("cache_dir = {}", cache_dir.display());
250 println!("bin_dir = {}", bin_dir.display());
251 println!("lib_dir = {}", lib_dir.display());
252 println!();
253 println!("extensions:");
254 for (_, p) in ext_plans {
255 println!(" {} (sha256 {})", p.display_name, p.expected_sha256);
256 println!(" url = {}", p.url);
257 println!(" cache = {}", p.cache_path.display());
258 }
259 println!();
260 println!("libs:");
261 for (_, p) in lib_plans {
262 println!(" {} (sha256 {})", p.display_name, p.expected_sha256);
263 println!(" url = {}", p.url);
264 println!(" cache = {}", p.cache_path.display());
265 }
266}
267
268fn default_cache_dir() -> PathBuf {
271 if is_root() {
272 PathBuf::from("/var/cache/assay")
273 } else if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") {
274 PathBuf::from(xdg).join("assay")
275 } else if let Some(home) = std::env::var_os("HOME") {
276 PathBuf::from(home).join(".cache/assay")
277 } else {
278 PathBuf::from("/tmp/assay-cache")
279 }
280}
281
282fn default_bin_dir() -> PathBuf {
283 if is_root() {
284 PathBuf::from("/usr/local/bin")
285 } else if let Some(home) = std::env::var_os("HOME") {
286 PathBuf::from(home).join(".local/bin")
287 } else {
288 PathBuf::from("/usr/local/bin")
289 }
290}
291
292fn default_lib_dir() -> PathBuf {
293 if let Some(explicit) = std::env::var_os("ASSAY_LIB_DIR") {
294 return PathBuf::from(explicit);
295 }
296 if is_root() {
297 PathBuf::from("/opt/assay/libs")
298 } else if let Some(xdg) = std::env::var_os("XDG_DATA_HOME") {
299 PathBuf::from(xdg).join("assay/libs")
300 } else if let Some(home) = std::env::var_os("HOME") {
301 PathBuf::from(home).join(".local/share/assay/libs")
302 } else {
303 PathBuf::from("/opt/assay/libs")
304 }
305}
306
307#[cfg(unix)]
308fn is_root() -> bool {
309 unsafe { libc::getuid() == 0 }
311}
312
313#[cfg(not(unix))]
314fn is_root() -> bool {
315 false
316}