use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use colored::Colorize;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use zoi_core::{cache, config, pgp, pkgdir, recorder, types, utils};
use zoi_db as db;
use zoi_hooks as hooks;
use zoi_resolver::local;
use crate::resolver::InstallNode;
use crate::{manifest, plan, prebuilt, util};
pub fn download_and_cache_archive(
node: &InstallNode,
details: &plan::PrebuiltDetails,
pb: Option<&ProgressBar>,
verbose: bool,
force_redownload: bool
) -> 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 = util::get_filename_from_url(&details.info.final_url)
.unwrap_or("archive.zpa");
let cached_archive_path =
utils::safe_join(&archive_cache_root, Path::new(archive_filename))?;
let sig_filename = format!("{archive_filename}.sig");
let cached_sig_path =
utils::safe_join(&archive_cache_root, Path::new(&sig_filename))?;
let authorities = config
.default_registry
.as_ref()
.filter(|r| r.handle == node.registry_handle)
.and_then(|r| r.authorities.as_ref())
.or_else(|| {
config
.added_registries
.iter()
.find(|r| r.handle == node.registry_handle)
.and_then(|r| r.authorities.as_ref())
});
let has_authorities = authorities.is_some_and(|a| !a.is_empty());
let pgp_identifiers: Option<Vec<String>> = signature_policy
.as_ref()
.map(|p| p.trusted_keys.clone())
.or_else(|| authorities.cloned());
let has_pgp_identifiers = pgp_identifiers
.as_ref()
.is_some_and(|keys| !keys.is_empty());
let mut delta_applied = false;
if force_redownload {
fs::remove_file(&cached_archive_path).ok();
fs::remove_file(&cached_sig_path).ok();
fs::remove_file(
archive_cache_root.join(format!("{archive_filename}.part"))
)
.ok();
if pb.is_none() {
println!(
"Re-downloading archive (--force): {}",
archive_filename.cyan()
);
}
}
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 !force_redownload && cached_archive_path.exists() {
if pb.is_none() {
println!("Using cached archive: {}", cached_archive_path.display());
}
cached_archive_path.clone()
} else {
if zoi_core::offline::is_offline() {
return Err(anyhow!(
"Archive not found in cache and cannot download: Zoi is in \
offline mode. Missing: {archive_filename}"
));
}
delta_applied = if force_redownload {
false
} else {
match try_delta_upgrade_sequence(
node,
&archive_cache_root,
&cached_archive_path,
pb,
verbose,
pgp_identifiers.as_deref(),
config.max_delta_steps
) {
Ok(applied) => applied,
Err(e) => {
if verbose {
println!(
"{} {e}; falling back to full download.",
"Warning:".yellow()
);
}
false
}
}
};
if delta_applied {
cached_archive_path.clone()
} else {
let part_path =
archive_cache_root.join(format!("{archive_filename}.part"));
if part_path.exists() && pb.is_none() {
println!("Resuming partial download: {}", part_path.display());
}
let mut last_error = None;
let candidate_urls =
cache::mirror_candidate_urls(&details.info.final_url);
let mut downloaded = false;
for candidate_url in candidate_urls {
match util::download_file_with_progress(
&candidate_url,
&part_path,
pb,
Some(details.download_size)
) {
Ok(()) => {
downloaded = true;
break;
}
Err(e) => last_error = Some((candidate_url, e))
}
}
if !downloaded {
let (url, error) = last_error.ok_or_else(|| {
anyhow!("archive download failed but no error recorded")
})?;
return Err(anyhow!(
"Failed to download package archive from {url}: {error}"
));
}
fs::rename(&part_path, &cached_archive_path)?;
cached_archive_path.clone()
}
};
if !delta_applied && let Some(hash_url) = &details.info.hash_url {
let hash = db::get_package_hash_from_db(
&node.registry_handle,
&node.pkg.name,
node.sub_package.as_deref(),
&node.pkg.repo
)
.unwrap_or(None)
.filter(|hash| !hash.is_empty());
let hash = match hash {
Some(hash) => hash,
None => util::get_expected_hash(hash_url, Some(archive_filename))
.map_err(|error| {
anyhow!(
"Unable to obtain the required checksum for '{}': \
{error}",
node.pkg.name
)
})?
};
if !util::verify_file_hash(&archive_path, &hash, pb)? {
return Err(anyhow!("Hash verification failed"));
}
}
validate_signature_requirements(
signature_policy.is_some(),
details.info.pgp_url.is_some(),
has_pgp_identifiers
)?;
if delta_applied {
if verbose {
println!(
"{}",
"Skipping archive signature re-verification for delta-rebuilt \
archive (delta chain already verified)."
.dimmed()
);
}
} else {
let mut embedded_verified = false;
if let Some(ref identifiers) = pgp_identifiers
&& !identifiers.is_empty()
&& !matches!(archive_filename.rsplit('.').next(), Some("zsa"))
{
let trusted_certs =
pgp::get_certs_by_name_or_fingerprint(identifiers)?;
match try_verify_embedded_signature(&archive_path, trusted_certs) {
Ok(true) => {
if verbose {
println!(
"{}",
"Embedded signature verified successfully.".green()
);
}
embedded_verified = true;
}
Ok(false) => {}
Err(e) => {
return Err(anyhow!(
"Embedded signature verification failed: {e}"
));
}
}
}
if !embedded_verified
&& let Some(pgp_url) = &details.info.pgp_url
&& let Some(ref identifiers) = pgp_identifiers
&& !identifiers.is_empty()
{
let sig_path = if cached_sig_path.exists() {
cached_sig_path.clone()
} else {
if zoi_core::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);
let mut last_error = None;
let mut downloaded = false;
for candidate_url in cache::mirror_candidate_urls(pgp_url) {
match util::download_file_with_progress(
&candidate_url,
&temp_sig_path,
pb,
None
) {
Ok(()) => {
downloaded = true;
break;
}
Err(e) => last_error = Some((candidate_url, e))
}
}
if !downloaded {
let (url, error) = last_error.ok_or_else(|| {
anyhow!(
"signature download failed but no error recorded"
)
})?;
return Err(anyhow!(
"Failed to download signature from {url}: {error}"
));
}
fs::copy(&temp_sig_path, &cached_sig_path)?;
cached_sig_path.clone()
};
if verbose {
println!("Verifying signature...");
}
let trusted_certs =
pgp::get_certs_by_name_or_fingerprint(identifiers)?;
pgp::verify_detached_signature_multi_key(
&archive_path,
&sig_path,
trusted_certs
)?;
if verbose {
println!("{}", "Signature verified successfully.".green());
}
} else if !embedded_verified
&& details.info.pgp_url.is_none()
&& has_authorities
{
let msg = format!(
"Warning: Installing unsigned package '{}' from a registry \
that claims to be secure.",
node.pkg.name
);
if let Some(p) = pb {
p.println(msg.yellow().to_string());
} else {
println!("{}", msg.yellow());
}
if signature_policy.is_some() {
return Err(anyhow!(
"Signature enforcement is active, but no PGP URL found \
for package"
));
}
}
}
Ok(archive_path)
}
fn fetch_and_apply_delta(
node: &InstallNode,
base_path: &Path,
base_version: &str,
to_version: &str,
work_dir: &Path,
pb: Option<&ProgressBar>,
verbose: bool,
pgp_identifiers: Option<&[String]>
) -> Result<PathBuf> {
let Some(delta_info) = crate::util::find_delta_info(
&node.pkg,
&node.registry_handle,
base_version,
to_version
)?
else {
return Err(anyhow!(
"No delta configuration found for '{}' v{to_version}",
node.pkg.name
));
};
if verbose {
println!(
"Attempting delta upgrade from v{base_version} to v{to_version}..."
);
}
let delta_path = work_dir.join(format!("upgrade-{to_version}.zdelta"));
let mut downloaded = false;
for candidate_url in cache::mirror_candidate_urls(&delta_info.final_url) {
if util::download_file_with_progress(
&candidate_url,
&delta_path,
pb,
None
)
.is_ok()
{
downloaded = true;
break;
}
}
if !downloaded {
return Err(anyhow!(
"No delta available for '{}' v{base_version} -> v{to_version}",
node.pkg.name
));
}
let expected_delta_hash =
delta_info.hash_url.as_ref().ok_or_else(|| {
anyhow!(
"Delta configuration missing required hash URL for package \
'{}'",
node.pkg.name
)
})?;
let expected = util::get_expected_hash(expected_delta_hash, None)?;
if !util::verify_file_hash(&delta_path, &expected, pb)? {
return Err(anyhow!(
"Delta hash verification failed for '{}' v{to_version}",
node.pkg.name
));
}
if let Some(pgp_url) = &delta_info.pgp_url
&& let Some(identifiers) = pgp_identifiers
&& !identifiers.is_empty()
{
let temp_sig_path = work_dir.join(format!("upgrade-{to_version}.sig"));
let mut downloaded = false;
for candidate_url in cache::mirror_candidate_urls(pgp_url) {
if util::download_file_with_progress(
&candidate_url,
&temp_sig_path,
pb,
None
)
.is_ok()
{
downloaded = true;
break;
}
}
if downloaded {
let trusted_certs =
pgp::get_certs_by_name_or_fingerprint(identifiers)?;
pgp::verify_detached_signature_multi_key(
&delta_path,
&temp_sig_path,
trusted_certs
)?;
if verbose {
println!(
"{}",
"Delta signature verified successfully.".green()
);
}
} else if verbose {
println!(
"{} Delta signature file not found; skipping signature \
verification.",
"Warning:".yellow()
);
}
} else if verbose {
println!(
"{} No trusted PGP keys for delta signature verification.",
"Warning:".yellow()
);
}
let rebuilt_path = work_dir.join(format!("rebuilt-{to_version}.zpa"));
zoi_package::delta::apply_zpa_delta(base_path, &delta_path, &rebuilt_path)?;
if verbose {
println!("{}", "Delta applied successfully.".green());
}
Ok(rebuilt_path)
}
fn try_delta_upgrade_sequence(
node: &InstallNode,
archive_cache_root: &Path,
target_path: &Path,
pb: Option<&ProgressBar>,
verbose: bool,
pgp_identifiers: Option<&[String]>,
max_delta_steps: u32
) -> Result<bool> {
let Ok(target_version) = semver::Version::parse(&node.version) else {
return Ok(false);
};
let platform = zoi_core::utils::get_platform().unwrap_or_default();
let prefix = format!("{}-", node.pkg.name);
let suffix = format!("-{platform}.zpa");
let mut best: Option<(semver::Version, PathBuf)> = None;
let entries = std::fs::read_dir(archive_cache_root).ok();
let Some(entries) = entries else {
return Ok(false);
};
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if !name.starts_with(&prefix) || !name.ends_with(&suffix) {
continue;
}
let middle = &name[prefix.len()..name.len() - suffix.len()];
let version_str = middle.trim_start_matches('v');
let Ok(version) = semver::Version::parse(version_str) else {
continue;
};
if version >= target_version {
continue;
}
if best.as_ref().is_none_or(|(v, _)| version > *v) {
best = Some((version, entry.path()));
}
}
let Some((base_version, base_path)) = best else {
return Ok(false);
};
let mut steps: Vec<(semver::Version, String)> = Vec::new();
if let Ok(db_versions) = db::get_all_versions(
&node.registry_handle,
&node.pkg.name,
&node.pkg.repo
) {
for version_string in db_versions {
let parsed =
semver::Version::parse(&version_string).or_else(|_| {
semver::Version::parse(
version_string.trim_start_matches('v')
)
});
let Ok(version) = parsed else {
continue;
};
if version > base_version && version <= target_version {
steps.push((version, version_string));
}
}
steps.sort_by(|a, b| a.0.cmp(&b.0));
steps.dedup_by(|a, b| a.0 == b.0);
}
if steps.is_empty() {
steps.push((target_version.clone(), node.version.clone()));
}
if steps.len() as u32 > max_delta_steps {
if verbose {
println!(
"{} Upgrade spans {} versions but at most {max_delta_steps} \
delta steps are configured; falling back to full download.",
"Warning:".yellow(),
steps.len()
);
}
return Ok(false);
}
let work_dir = tempfile::Builder::new()
.prefix("zoi-delta-")
.tempdir_in(archive_cache_root)?;
let mut current_base = base_path;
let mut current_version = base_version.to_string();
for (_, step_string) in &steps {
match fetch_and_apply_delta(
node,
¤t_base,
¤t_version,
step_string,
work_dir.path(),
pb,
verbose,
pgp_identifiers
) {
Ok(rebuilt) => {
current_base = rebuilt;
current_version.clone_from(step_string);
}
Err(e) => {
if verbose {
println!(
"{} {e}; falling back to full download.",
"Warning:".yellow()
);
}
return Ok(false);
}
}
}
persist_rebuilt_archive(¤t_base, target_path)?;
if verbose {
println!("{}", "All delta steps applied successfully.".green());
}
Ok(true)
}
fn persist_rebuilt_archive(src: &Path, dst: &Path) -> Result<()> {
if fs::rename(src, dst).is_err() {
fs::copy(src, dst)?;
fs::remove_file(src).ok();
}
Ok(())
}
fn try_verify_embedded_signature(
archive_path: &Path,
trusted_certs: Vec<pgp::sequoia_openpgp::Cert>
) -> Result<bool> {
use std::io::Read;
let file = fs::File::open(archive_path)?;
let decoder = zstd::stream::read::Decoder::new(file)?;
let mut archive = tar::Archive::new(decoder);
let mut manifest_bytes: Option<Vec<u8>> = None;
let mut sig_bytes: Option<Vec<u8>> = None;
for entry in archive.entries()? {
let mut entry = entry?;
let name = entry.path()?.to_string_lossy().to_string();
match Path::new(&name).file_name().and_then(|f| f.to_str()) {
Some("manifest.json") if manifest_bytes.is_none() => {
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
manifest_bytes = Some(buf);
}
Some("manifest.sig") => {
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
sig_bytes = Some(buf);
}
_ => {}
}
}
let (Some(manifest), Some(sig)) = (manifest_bytes, sig_bytes) else {
return Ok(false);
};
let temp_dir = tempfile::Builder::new()
.prefix("zoi-embed-sig-")
.tempdir()?;
let manifest_path = temp_dir.path().join("manifest.json");
let sig_path = temp_dir.path().join("manifest.sig");
fs::write(&manifest_path, &manifest)?;
fs::write(&sig_path, &sig)?;
pgp::verify_detached_signature_multi_key(
&manifest_path,
&sig_path,
trusted_certs
)?;
Ok(true)
}
fn validate_signature_requirements(
enforcement_enabled: bool,
has_signature_url: bool,
has_trusted_keys: bool
) -> Result<()> {
if !enforcement_enabled {
return Ok(());
}
if !has_signature_url {
return Err(anyhow!(
"Signature enforcement is active, but no PGP URL was found for \
the package"
));
}
if !has_trusted_keys {
return Err(anyhow!(
"Signature enforcement is active, but no trusted PGP keys are \
configured"
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{persist_rebuilt_archive, validate_signature_requirements};
#[test]
fn signature_enforcement_requires_a_signature_url() {
assert!(validate_signature_requirements(true, false, true).is_err());
}
#[test]
fn signature_enforcement_requires_trusted_keys() {
assert!(validate_signature_requirements(true, true, false).is_err());
}
#[test]
fn disabled_signature_enforcement_allows_missing_metadata() {
assert!(validate_signature_requirements(false, false, false).is_ok());
}
#[test]
fn persist_rebuilt_archive_moves_content_to_target() {
let dir = tempfile::tempdir().expect("tempdir");
let src = dir.path().join("rebuilt-1.6.3.zpa");
let dst = dir.path().join("zbsdiff-1.6.3-linux-amd64.zpa");
std::fs::write(&src, b"rebuilt-archive-bytes").expect("write src");
persist_rebuilt_archive(&src, &dst).expect("persist");
assert!(!src.exists(), "source should be moved away");
assert_eq!(
std::fs::read(&dst).expect("read dst"),
b"rebuilt-archive-bytes"
);
}
#[test]
fn persist_rebuilt_archive_overwrites_stale_target() {
let dir = tempfile::tempdir().expect("tempdir");
let src = dir.path().join("rebuilt-1.6.3.zpa");
let dst = dir.path().join("zbsdiff-1.6.3-linux-amd64.zpa");
std::fs::write(&src, b"new-bytes").expect("write src");
std::fs::write(&dst, b"stale-bytes").expect("write dst");
persist_rebuilt_archive(&src, &dst).expect("persist");
assert_eq!(std::fs::read(&dst).expect("read dst"), b"new-bytes");
}
}
#[derive(Clone)]
pub struct PreparedNode {
pub archive_path: PathBuf,
pub install_method: String,
pub is_build: bool
}
pub fn prepare_node(
node: &InstallNode,
action: &plan::InstallAction,
m: Option<&MultiProgress>,
build_type: Option<&str>,
verbose: bool,
force_redownload: bool
) -> Result<PreparedNode> {
let pkg = &node.pkg;
let version = &node.version;
let pb_style = ProgressStyle::default_bar()
.template(
"{spinner:.green} {msg:30.cyan} [{bar:40.cyan/blue}] {percent}%"
)?
.progress_chars("#>-");
let spinner_style = ProgressStyle::default_spinner()
.template("{spinner:.green} {msg:30.cyan}")?
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ ");
let display_name = if let Some(sub) = &node.sub_package {
format!("{}:{}", pkg.name, sub)
} else {
pkg.name.clone()
};
let version_display = if node.revision == "1" {
version.clone()
} else {
format!("{}-{}", version, node.revision)
};
let message = format!("zoi:{display_name}@{version_display}");
let pb = if let Some(m_inner) = m {
let pb = m_inner.add(ProgressBar::new(100));
pb.set_style(pb_style);
pb.set_message(message.clone());
Some(pb)
} else {
None
};
let (archive_path, install_method, is_build) = match action {
plan::InstallAction::DownloadAndInstall(details) => {
if let Some(p) = &pb {
p.set_message("Downloading package...");
}
let archive_path = download_and_cache_archive(
node,
details,
pb.as_ref(),
verbose,
force_redownload
)?;
(archive_path, "pre-compiled".to_string(), false)
}
plan::InstallAction::InstallFromArchive(archive_path) => {
if archive_path.to_string_lossy().ends_with(".zsa") {
if let Some(p) = &pb {
p.set_style(spinner_style);
p.enable_steady_tick(std::time::Duration::from_millis(100));
p.set_message(format!("Building {display_name}..."));
}
let archive_path = prebuilt::build_archive(
archive_path,
pkg,
node.sub_package.as_deref(),
build_type,
pb.as_ref(),
!verbose
)?;
match archive_path {
Some(path) => (path, "source".to_string(), true),
None => (PathBuf::new(), "meta".to_string(), false)
}
} else {
if let Some(p) = &pb {
p.set_message("Using local archive...");
p.finish();
}
(archive_path.clone(), "pre-compiled".to_string(), false)
}
}
plan::InstallAction::BuildAndInstall => {
if let Some(p) = &pb {
p.set_style(spinner_style);
p.enable_steady_tick(std::time::Duration::from_millis(100));
p.set_message(format!("Building {display_name}..."));
}
let pkg_lua_path = Path::new(&node.source);
let archive_path = prebuilt::build_archive(
pkg_lua_path,
pkg,
node.sub_package.as_deref(),
build_type,
pb.as_ref(),
!verbose
)?;
match archive_path {
Some(path) => (path, "source".to_string(), true),
None => (PathBuf::new(), "meta".to_string(), false)
}
}
};
if let Some(p) = pb {
p.finish_and_clear();
}
Ok(PreparedNode {
archive_path,
install_method,
is_build
})
}
pub fn install_prepared_node(
node: &InstallNode,
prepared: &PreparedNode,
m: Option<&MultiProgress>,
yes: bool,
record: bool,
link_bins: bool,
_verbose: 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_spinner()
.template("{spinner:.green} {msg:30.cyan}")?
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ ");
let main_pb = if let Some(m_inner) = m {
if is_direct {
None
} else {
let pb = m_inner.add(ProgressBar::new_spinner());
pb.set_style(pb_style.clone());
let name = if let Some(sub) = &node.sub_package {
format!("{}:{}", pkg.name, sub)
} else {
pkg.name.clone()
};
let version_display = if node.revision == "1" {
version.clone()
} else {
format!("{}-{}", version, node.revision)
};
pb.set_message(format!("zoi:{name}@{version_display}"));
pb.enable_steady_tick(std::time::Duration::from_millis(100));
Some(pb)
}
} else {
None
};
let step_pb = if is_direct && let Some(m_inner) = m {
let pb = m_inner.add(ProgressBar::new_spinner());
pb.set_style(pb_style);
pb.enable_steady_tick(std::time::Duration::from_millis(100));
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, pkg.scope)?;
}
let sub_package_to_install = node.sub_package.clone();
let sub_packages_vec = sub_package_to_install.clone().map(|s| vec![s]);
let archive_path = &prepared.archive_path;
let install_method = &prepared.install_method;
let needs_escalation =
pkg.scope == types::Scope::System && !zoi_core::utils::is_admin();
let install_manifest = if needs_escalation {
let escalator =
zoi_core::utils::get_privilege_escalator().ok_or_else(|| {
anyhow!(
"Root privileges required for system scope installation, \
but neither 'sudo' nor 'doas' was found."
)
})?;
if let Some(pb) = step_pb.as_ref().or(main_pb.as_ref()) {
pb.set_message(format!(
"Waiting for {escalator} privileges to install system \
package..."
));
}
let node_json = serde_json::to_string(node)?;
let mut temp_file = tempfile::NamedTempFile::new()?;
temp_file.write_all(node_json.as_bytes())?;
let temp_path = temp_file.path();
let mut cmd = std::process::Command::new(escalator);
cmd.arg(std::env::current_exe()?);
cmd.arg("helper").arg("elevate-install-node");
cmd.arg("--node-json").arg(temp_path);
cmd.arg("--archive").arg(archive_path);
cmd.arg("--install-method").arg(install_method);
if yes {
cmd.arg("--yes");
}
if link_bins {
cmd.arg("--link-bins");
}
let status = cmd
.status()
.map_err(|e| anyhow!("Failed to spawn privilege escalator: {e}"))?;
if !status.success() {
return Err(anyhow!("Escalated installation failed."));
}
let version_dir = local::get_package_version_dir(
pkg.scope,
&node.registry_handle,
&pkg.repo,
&pkg.name,
&node.version
)?;
let manifest_path = local::find_store_manifest(
&version_dir,
node.sub_package.as_deref()
)
.ok_or_else(|| {
anyhow!(
"Escalated install did not leave a manifest in '{}'",
version_dir.display()
)
})?;
local::read_store_manifest(&manifest_path)?
} else {
if let Some(pb) = step_pb.as_ref().or(main_pb.as_ref()) {
pb.set_message(format!("Installing {}...", pkg.name.cyan()));
}
let (installed_files, file_digests) = crate::pkg_install::run(
archive_path,
Some(pkg.scope),
&node.registry_handle,
Some(&node.version),
yes,
sub_packages_vec,
link_bins,
step_pb.as_ref().or(main_pb.as_ref())
)?;
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)?;
}
let install_manifest = manifest::create_manifest(
pkg,
node.reason.clone(),
node.dependencies.clone(),
Some(install_method.clone()),
installed_files,
handle,
node.repo_type.clone(),
&node.chosen_options,
&node.chosen_optionals,
sub_package_to_install.clone(),
file_digests
)?;
if record {
local::write_manifest(&install_manifest)?;
local::persist_package_source(
&install_manifest,
Path::new(&node.source)
)?;
}
install_manifest
};
if prepared.is_build {
let _ = fs::remove_file(archive_path);
}
if record {
if let Ok(conn) = db::open_connection("local")
&& let Ok(pkg_id) = db::update_package(
&conn,
pkg,
handle,
Some(pkg.scope),
sub_package_to_install.as_deref(),
Some(&node.reason)
)
{
let _ = db::clear_package_files(&conn, pkg_id);
let _ = db::index_package_files(
&conn,
pkg_id,
&install_manifest.installed_files
);
}
if let Err(e) = recorder::record_package(
pkg,
&node.reason,
&node.dependencies,
handle,
&node.repo_type,
&node.chosen_options,
&node.chosen_optionals,
sub_package_to_install.as_deref(),
&node.source,
node.git_sha.as_deref()
) {
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, pkg.scope)?;
}
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(install_manifest)
}
pub fn install_node(
node: &InstallNode,
action: &plan::InstallAction,
m: Option<&MultiProgress>,
build_type: Option<&str>,
yes: bool,
record: bool,
link_bins: bool,
verbose: bool,
force_redownload: bool
) -> Result<types::InstallManifest> {
let prepared =
prepare_node(node, action, m, build_type, verbose, force_redownload)?;
install_prepared_node(node, &prepared, m, yes, record, link_bins, verbose)
}