use crate::pkg::{
cache, config, db, hooks,
install::{manifest, plan, post_install, prebuilt, resolver::InstallNode, util},
local, pgp, pkgdir, recorder, resolve, types,
};
use anyhow::{Result, anyhow};
use colored::Colorize;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::fs;
use std::path::{Path, PathBuf};
pub fn download_and_cache_archive(
_node: &InstallNode,
details: &plan::PrebuiltDetails,
pb: Option<&ProgressBar>,
) -> Result<PathBuf> {
let config = config::read_config()?;
let signature_policy = config.policy.signature_enforcement.filter(|p| p.enable);
let archive_cache_root = cache::get_archive_cache_root()?;
fs::create_dir_all(&archive_cache_root)?;
let archive_filename = details
.info
.final_url
.split('/')
.next_back()
.unwrap_or("archive.pkg.tar.zst");
let cached_archive_path = archive_cache_root.join(archive_filename);
let sig_filename = format!("{}.sig", archive_filename);
let cached_sig_path = archive_cache_root.join(&sig_filename);
let archive_path = if let Some(path) = pkgdir::find_in_pkg_dirs(archive_filename) {
if pb.is_none() {
println!("Found archive in pkg-dir: {}", path.display());
}
path
} else if cached_archive_path.exists() {
if pb.is_none() {
println!("Using cached archive: {}", cached_archive_path.display());
}
cached_archive_path.clone()
} else {
if crate::pkg::offline::is_offline() {
return Err(anyhow!(
"Archive not found in cache and cannot download: Zoi is in offline mode. Missing: {}",
archive_filename
));
}
let temp_dir = tempfile::Builder::new().prefix("zoi-dl-").tempdir()?;
let temp_archive_path = temp_dir.path().join(archive_filename);
util::download_file_with_progress(
&details.info.final_url,
&temp_archive_path,
pb,
Some(details.download_size),
)
.map_err(|e| {
anyhow!(
"Failed to download package archive from {}: {}",
details.info.final_url,
e
)
})?;
fs::copy(&temp_archive_path, &cached_archive_path)?;
cached_archive_path.clone()
};
if let Some(hash_url) = &details.info.hash_url {
let hash = util::get_expected_hash(hash_url, Some(archive_filename))?;
if !hash.is_empty() && !util::verify_file_hash(&archive_path, &hash, pb)? {
return Err(anyhow!("Hash verification failed"));
}
}
if let Some(policy) = &signature_policy {
if let Some(pgp_url) = &details.info.pgp_url {
let sig_path = if cached_sig_path.exists() {
cached_sig_path.clone()
} else {
if crate::pkg::offline::is_offline() {
return Err(anyhow!(
"Signature not found in cache and cannot download: Zoi is in offline mode."
));
}
let temp_dir = tempfile::Builder::new().prefix("zoi-sig-dl-").tempdir()?;
let temp_sig_path = temp_dir.path().join(&sig_filename);
util::download_file_with_progress(pgp_url, &temp_sig_path, pb, None)
.map_err(|e| anyhow!("Failed to download signature from {}: {}", pgp_url, e))?;
fs::copy(&temp_sig_path, &cached_sig_path)?;
cached_sig_path.clone()
};
println!("Verifying signature...");
let trusted_certs = pgp::get_certs_by_name_or_fingerprint(&policy.trusted_keys)?;
pgp::verify_detached_signature_multi_key(&archive_path, &sig_path, trusted_certs)?;
println!("{}", "Signature verified successfully.".green());
} else {
return Err(anyhow!(
"Signature enforcement is active, but no PGP URL found for package"
));
}
}
Ok(archive_path)
}
pub fn install_node(
node: &InstallNode,
action: &plan::InstallAction,
m: Option<&MultiProgress>,
build_type: Option<&str>,
yes: bool,
link_bins: bool,
) -> Result<types::InstallManifest> {
let pkg = &node.pkg;
let version = &node.version;
let handle = &node.registry_handle;
let is_direct = matches!(node.reason, types::InstallReason::Direct);
let pb_style = ProgressStyle::default_bar()
.template("{spinner:.green} {msg:30.cyan} [{bar:40.cyan/blue}] {percent}%")?
.progress_chars("#>-");
let main_pb = if let Some(m_inner) = m {
if !is_direct {
let pb = m_inner.add(ProgressBar::new(100));
pb.set_style(pb_style.clone());
let name = if let Some(sub) = &node.sub_package {
format!("{}:{}", pkg.name, sub)
} else {
pkg.name.clone()
};
pb.set_message(format!("zoi: @{}:{}", name, version));
Some(pb)
} else {
None
}
} else {
None
};
let step_pb = if is_direct && let Some(m_inner) = m {
let pb = m_inner.add(ProgressBar::new(100));
pb.set_style(pb_style);
Some(pb)
} else {
None
};
if let Some(hooks) = &pkg.hooks {
if let Some(pb) = &step_pb {
pb.set_message("Running pre-install hooks...");
}
hooks::run_hooks(hooks, hooks::HookType::PreInstall)?;
}
let request = resolve::parse_source_string(&node.source)?;
let sub_package_to_install = request.sub_package;
let sub_packages_vec = sub_package_to_install.clone().map(|s| vec![s]);
let (installed_files, install_method) = match action {
plan::InstallAction::DownloadAndInstall(details) => {
let pb_for_step = step_pb.as_ref().or(main_pb.as_ref());
if let Some(pb) = pb_for_step {
pb.set_message("Downloading package...");
}
let archive_path = download_and_cache_archive(node, details, pb_for_step)?;
if let Some(pb) = pb_for_step {
pb.set_message("Installing package...");
pb.set_position(0);
}
let files = crate::pkg::package::install::run(
&archive_path,
Some(pkg.scope),
&node.registry_handle,
Some(&node.version),
yes,
sub_packages_vec,
link_bins,
pb_for_step,
)?;
(files, "pre-compiled".to_string())
}
plan::InstallAction::InstallFromArchive(archive_path) => {
let pb_for_step = step_pb.as_ref().or(main_pb.as_ref());
if let Some(pb) = pb_for_step {
pb.set_message("Installing package...");
}
let files = crate::pkg::package::install::run(
archive_path,
Some(pkg.scope),
&node.registry_handle,
Some(&node.version),
yes,
sub_packages_vec,
link_bins,
pb_for_step,
)?;
(files, "pre-compiled".to_string())
}
plan::InstallAction::BuildAndInstall => {
let pb_for_step = step_pb.as_ref().or(main_pb.as_ref());
if let Some(pb) = pb_for_step {
pb.set_message("Building package...");
}
let pkg_lua_path = Path::new(&node.source);
let files = prebuilt::try_build_install(
pkg_lua_path,
pkg,
&node.registry_handle,
build_type,
yes,
sub_package_to_install.clone(),
pb_for_step,
)?;
(files, "source".to_string())
}
};
if let types::InstallReason::Dependency { ref parent } = node.reason {
let package_dir = local::get_package_dir(pkg.scope, handle, &pkg.repo, &pkg.name)?;
local::add_dependent(&package_dir, parent)?;
}
if let Err(e) =
post_install::install_manual_if_available(pkg, version, handle, step_pb.as_ref())
{
let msg = format!(
"Warning: failed to install manual for '{}': {}",
pkg.name, e
);
if let Some(p) = &step_pb {
p.println(msg);
} else {
eprintln!("{}", msg);
}
}
let manifest = manifest::create_manifest(
pkg,
node.reason.clone(),
node.dependencies.clone(),
Some(install_method.clone()),
installed_files,
handle,
&node.chosen_options,
&node.chosen_optionals,
sub_package_to_install.clone(),
)?;
local::write_manifest(&manifest)?;
if let Ok(conn) = db::open_connection("local") {
let _ = db::update_package(
&conn,
pkg,
handle,
Some(pkg.scope),
sub_package_to_install.as_deref(),
Some(&node.reason),
);
}
if let Err(e) = recorder::record_package(
pkg,
&node.reason,
&node.dependencies,
handle,
&node.chosen_options,
&node.chosen_optionals,
sub_package_to_install.clone(),
) {
eprintln!(
"Warning: failed to record package installation for '{}': {}",
pkg.name, e
);
}
if let Some(hooks) = &pkg.hooks {
if let Some(pb) = &step_pb {
pb.set_message("Running post-install hooks...");
}
hooks::run_hooks(hooks, hooks::HookType::PostInstall)?;
}
if let Some(pb) = main_pb {
pb.finish();
}
if let Some(pb) = step_pb {
pb.finish();
}
util::send_telemetry("install", pkg, handle, Some(&install_method));
Ok(manifest)
}