pub mod extract;
pub mod fetch;
pub mod lock;
pub mod manifest;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::Duration;
use clap::Args;
use thiserror::Error;
#[derive(Args, Debug)]
pub struct InstallArgs {
#[arg(short = 'f', long, default_value = "./Manifest.lua")]
pub manifest: PathBuf,
#[arg(long)]
pub cache_dir: Option<PathBuf>,
#[arg(long)]
pub bin_dir: Option<PathBuf>,
#[arg(long)]
pub lib_dir: Option<PathBuf>,
#[arg(long)]
pub offline: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub no_progress: bool,
}
#[derive(Debug, Error)]
pub enum InstallError {
#[error("read {path}: {source}")]
ReadManifest {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error(transparent)]
Manifest(#[from] manifest::ManifestError),
#[error(transparent)]
Fetch(#[from] fetch::FetchError),
#[error(transparent)]
Extract(#[from] extract::ExtractError),
#[error("{count} dep(s) failed to fetch; see errors above")]
FetchFailed { count: usize },
#[error("write {path}: {source}")]
WriteLock {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
pub async fn run(args: InstallArgs) -> ExitCode {
match execute(args).await {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("assay install: {e}");
let mut src = std::error::Error::source(&e);
while let Some(s) = src {
eprintln!(" caused by: {s}");
src = std::error::Error::source(s);
}
ExitCode::from(1)
}
}
}
pub async fn execute(args: InstallArgs) -> Result<(), InstallError> {
let source =
std::fs::read_to_string(&args.manifest).map_err(|e| InstallError::ReadManifest {
path: args.manifest.clone(),
source: e,
})?;
let m = manifest::parse(&source, &args.manifest)?;
let cache_dir = args.cache_dir.clone().unwrap_or_else(default_cache_dir);
let bin_dir = args.bin_dir.clone().unwrap_or_else(default_bin_dir);
let lib_dir = args.lib_dir.clone().unwrap_or_else(default_lib_dir);
let arch = std::env::consts::ARCH.to_string();
let mut ext_plans: Vec<(manifest::Extension, fetch::FetchPlan)> = Vec::new();
for e in &m.extensions {
let plan = fetch::FetchPlan::for_extension(e, &arch, &cache_dir)?;
ext_plans.push((e.clone(), plan));
}
let mut lib_plans: Vec<(manifest::Lib, fetch::FetchPlan)> = Vec::new();
for l in &m.libs {
lib_plans.push((l.clone(), fetch::FetchPlan::for_lib(l, &cache_dir)));
}
if args.dry_run {
print_plan(&m, &ext_plans, &lib_plans, &cache_dir, &bin_dir, &lib_dir);
return Ok(());
}
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(60))
.build()
.expect("build reqwest client");
let mut handles = Vec::with_capacity(ext_plans.len() + lib_plans.len());
let all_plans = ext_plans
.iter()
.map(|(_, p)| p)
.chain(lib_plans.iter().map(|(_, p)| p));
for plan in all_plans {
let plan = plan.clone();
let client = client.clone();
let offline = args.offline;
handles.push(tokio::spawn(async move {
let result = fetch::fetch(&plan, &client, offline).await;
(plan.display_name.clone(), result)
}));
}
let mut failures = 0;
for h in handles {
let (name, res) = h.await.expect("fetch task panicked");
match res {
Ok(()) => {
if !args.no_progress {
eprintln!(" ✓ fetched {name}");
}
}
Err(e) => {
eprintln!(" ✗ {name}: {e}");
failures += 1;
}
}
}
if failures > 0 {
return Err(InstallError::FetchFailed { count: failures });
}
for (e, plan) in &ext_plans {
extract::install_extension_binary(&plan.cache_path, &bin_dir, &e.name)?;
if !args.no_progress {
eprintln!(
" → installed {} → {}",
e.name,
bin_dir.join(&e.name).display()
);
}
}
for (l, plan) in &lib_plans {
extract::install_lib_tree(&plan.cache_path, &lib_dir, &l.name)?;
if !args.no_progress {
eprintln!(
" → installed {} → {}/",
l.name,
lib_dir.join(&l.name).display()
);
}
}
let lockfile = lock::Lockfile {
assay: m.assay.clone(),
extensions: ext_plans
.iter()
.map(|(e, p)| lock::LockExtension::new(e, p))
.collect(),
libs: lib_plans
.iter()
.map(|(l, p)| lock::LockLib::new(l, p))
.collect(),
};
let lock_path = args.manifest.with_file_name("Manifest.lock");
std::fs::write(&lock_path, lockfile.to_lua()).map_err(|e| InstallError::WriteLock {
path: lock_path.clone(),
source: e,
})?;
if !args.no_progress {
eprintln!(" → wrote {}", lock_path.display());
}
Ok(())
}
fn print_plan(
m: &manifest::Manifest,
ext_plans: &[(manifest::Extension, fetch::FetchPlan)],
lib_plans: &[(manifest::Lib, fetch::FetchPlan)],
cache_dir: &Path,
bin_dir: &Path,
lib_dir: &Path,
) {
println!("# assay install (dry run)");
if let Some(v) = &m.assay {
println!("assay = {v}");
}
println!("cache_dir = {}", cache_dir.display());
println!("bin_dir = {}", bin_dir.display());
println!("lib_dir = {}", lib_dir.display());
println!();
println!("extensions:");
for (_, p) in ext_plans {
println!(" {} (sha256 {})", p.display_name, p.expected_sha256);
println!(" url = {}", p.url);
println!(" cache = {}", p.cache_path.display());
}
println!();
println!("libs:");
for (_, p) in lib_plans {
println!(" {} (sha256 {})", p.display_name, p.expected_sha256);
println!(" url = {}", p.url);
println!(" cache = {}", p.cache_path.display());
}
}
fn default_cache_dir() -> PathBuf {
if is_root() {
PathBuf::from("/var/cache/assay")
} else if let Some(xdg) = std::env::var_os("XDG_CACHE_HOME") {
PathBuf::from(xdg).join("assay")
} else if let Some(home) = std::env::var_os("HOME") {
PathBuf::from(home).join(".cache/assay")
} else {
PathBuf::from("/tmp/assay-cache")
}
}
fn default_bin_dir() -> PathBuf {
if is_root() {
PathBuf::from("/usr/local/bin")
} else if let Some(home) = std::env::var_os("HOME") {
PathBuf::from(home).join(".local/bin")
} else {
PathBuf::from("/usr/local/bin")
}
}
fn default_lib_dir() -> PathBuf {
if let Some(explicit) = std::env::var_os("ASSAY_LIB_DIR") {
return PathBuf::from(explicit);
}
if is_root() {
PathBuf::from("/opt/assay/libs")
} else if let Some(xdg) = std::env::var_os("XDG_DATA_HOME") {
PathBuf::from(xdg).join("assay/libs")
} else if let Some(home) = std::env::var_os("HOME") {
PathBuf::from(home).join(".local/share/assay/libs")
} else {
PathBuf::from("/opt/assay/libs")
}
}
#[cfg(unix)]
fn is_root() -> bool {
unsafe { libc::getuid() == 0 }
}
#[cfg(not(unix))]
fn is_root() -> bool {
false
}