use crate::pkg::{db, dependencies, hooks, local, recorder, resolve, types};
use crate::utils;
use anyhow::anyhow;
use colored::*;
use mlua::Lua;
use std::fs;
use std::path::PathBuf;
fn get_bin_root(scope: types::Scope) -> anyhow::Result<PathBuf> {
match scope {
types::Scope::User => {
let home_dir =
home::home_dir().ok_or_else(|| anyhow!("Could not find home directory."))?;
Ok(crate::pkg::sysroot::apply_sysroot(
home_dir.join(".zoi/pkgs/bin"),
))
}
types::Scope::System => {
if cfg!(target_os = "windows") {
Ok(crate::pkg::sysroot::apply_sysroot(PathBuf::from(
"C:\\ProgramData\\zoi\\pkgs\\bin",
)))
} else {
Ok(crate::pkg::sysroot::apply_sysroot(PathBuf::from(
"/usr/local/bin",
)))
}
}
types::Scope::Project => {
let current_dir = std::env::current_dir()?;
Ok(current_dir.join(".zoi").join("pkgs").join("bin"))
}
}
}
fn uninstall_collection(
pkg: &types::Package,
manifest: &types::InstallManifest,
scope: types::Scope,
registry_handle: Option<String>,
yes: bool,
) -> anyhow::Result<types::InstallManifest> {
println!("Uninstalling collection '{}'...", pkg.name.bold());
let dependencies_to_uninstall = &manifest.installed_dependencies;
if dependencies_to_uninstall.is_empty() {
println!("Collection has no dependencies to uninstall.");
} else {
println!("Uninstalling dependencies of the collection...");
for dep_str in dependencies_to_uninstall {
let dep = dependencies::parse_dependency_string(dep_str)?;
if dep.manager != "zoi" {
let prompt = format!(
"Uninstall native dependency '{}' ({})?",
dep.package.cyan(),
dep.manager.yellow()
);
let warning = "Warning: Zoi cannot track if other non-Zoi applications depend on this package.";
if yes {
println!(
"\n{} Uninstalling native dependency: {}...",
"::".bold().blue(),
dep_str.bold()
);
println!("{}: {}", "Note".yellow(), warning);
} else if utils::ask_for_confirmation(
&format!("{}\n {}", prompt, warning.dimmed()),
false,
) {
println!(
"\n{} Uninstalling dependency: {}...",
"::".bold().blue(),
dep_str.bold()
);
} else {
println!(
"Skipping uninstallation of native dependency: {}",
dep.package.yellow()
);
continue;
}
} else {
println!(
"\n{} Uninstalling zoi dependency: {}...",
"::".bold().blue(),
dep_str.bold()
);
}
if let Err(e) = dependencies::uninstall_dependency(dep_str, &move |name| {
run(name, Some(scope), yes).map(|_| ())
}) {
eprintln!(
"Warning: Could not uninstall dependency '{}': {}",
dep_str, e
);
}
}
}
let handle = registry_handle.as_deref().unwrap_or("local");
let package_dir = local::get_package_dir(scope, handle, &pkg.repo, &pkg.name)?;
if package_dir.exists() {
let _ = crate::pkg::service::cleanup_service(&pkg.name, scope);
fs::remove_dir_all(&package_dir)?;
}
if let Err(e) = recorder::remove_package_from_record(manifest) {
eprintln!(
"{} Failed to remove package from lockfile: {}",
"Warning:".yellow(),
e
);
}
if let Ok(conn) = db::open_connection("local") {
let _ = db::delete_package(&conn, &pkg.name, None, &pkg.repo, Some(scope));
}
match crate::pkg::telemetry::posthog_capture_event(
"uninstall",
pkg,
env!("CARGO_PKG_VERSION"),
registry_handle.as_deref().unwrap_or("local"),
None,
) {
Ok(true) => println!("{} telemetry sent", "Info:".green()),
Ok(false) => (),
Err(e) => eprintln!("{} telemetry failed: {}", "Warning:".yellow(), e),
}
Ok(manifest.clone())
}
fn find_installed_manifest(
request: &resolve::PackageRequest,
scope_override: Option<types::Scope>,
) -> anyhow::Result<(types::InstallManifest, types::Scope)> {
let scopes = if let Some(scope) = scope_override {
vec![scope]
} else {
vec![
types::Scope::Project,
types::Scope::User,
types::Scope::System,
]
};
for scope in scopes {
let mut matches = local::find_installed_manifests_matching(request, scope)?;
match matches.len() {
0 => continue,
1 => return Ok((matches.remove(0), scope)),
_ => {
return Err(anyhow!(
"Package '{}' is ambiguous in {:?} scope. Use an explicit source like '#handle@repo/name[:sub]@version'.",
request.name,
scope
));
}
}
}
if scope_override.is_some() {
Err(anyhow!(
"Package '{}' is not installed in the specified scope.",
request.name
))
} else {
Err(anyhow!(
"Package '{}' is not installed by Zoi.",
request.name
))
}
}
fn load_installed_package(
manifest: &types::InstallManifest,
yes: bool,
) -> anyhow::Result<(types::Package, PathBuf)> {
let installed_source_path = local::get_package_source_path(manifest)?;
if installed_source_path.exists() {
let path = installed_source_path
.to_str()
.ok_or_else(|| anyhow!("Stored package source path contains invalid UTF-8"))?;
let mut pkg =
crate::pkg::lua::parser::parse_lua_package(path, Some(&manifest.version), true)?;
pkg.repo = manifest.repo.clone();
pkg.scope = manifest.scope;
pkg.registry_handle = Some(manifest.registry_handle.clone());
pkg.sub_package = manifest.sub_package.clone();
return Ok((pkg, installed_source_path));
}
let source = local::installed_manifest_source(manifest);
let (mut pkg, _, _, pkg_lua_path, _, _) =
resolve::resolve_package_and_version(&source, true, yes)?;
pkg.scope = manifest.scope;
pkg.sub_package = manifest.sub_package.clone();
Ok((pkg, pkg_lua_path))
}
pub fn run(
package_name: &str,
scope_override: Option<types::Scope>,
yes: bool,
) -> anyhow::Result<types::InstallManifest> {
let request = resolve::parse_source_string(package_name)?;
let (manifest, scope) = find_installed_manifest(&request, scope_override)?;
let sub_package_to_uninstall = manifest.sub_package.clone();
let registry_handle = Some(manifest.registry_handle.clone());
let (pkg, pkg_lua_path) = load_installed_package(&manifest, yes)?;
if pkg.package_type == types::PackageType::Collection {
return uninstall_collection(&pkg, &manifest, scope, registry_handle.clone(), yes);
}
if let Some(hooks) = &pkg.hooks
&& let Err(e) = hooks::run_hooks(hooks, hooks::HookType::PreRemove)
{
return Err(anyhow::anyhow!("Pre-remove hook failed: {}", e));
}
let handle = manifest.registry_handle.as_str();
let package_dir = local::get_package_dir(scope, handle, &pkg.repo, &pkg.name)?;
let version_dir = package_dir.join(&manifest.version);
let dependents = local::get_dependents(&package_dir)?;
if !dependents.is_empty() {
return Err(anyhow::anyhow!(
"Cannot uninstall '{}' because other packages depend on it:\n -{}\n\nPlease uninstall these packages first.",
&pkg.name,
dependents.join("\n - ")
));
}
let lua = Lua::new();
crate::pkg::lua::functions::setup_lua_environment(
&lua,
&utils::get_platform()?,
Some(&manifest.version),
pkg_lua_path.to_str(),
None,
sub_package_to_uninstall.as_deref(),
true,
)
.map_err(|e| anyhow!(e.to_string()))?;
let lua_code = fs::read_to_string(pkg_lua_path)?;
lua.load(&lua_code)
.exec()
.map_err(|e| anyhow!(e.to_string()))?;
if let Ok(uninstall_fn) = lua.globals().get::<mlua::Function>("uninstall") {
println!("Running uninstall() script...");
uninstall_fn
.call::<()>(())
.map_err(|e| anyhow!(e.to_string()))?;
}
if let Ok(uninstall_ops) = lua.globals().get::<mlua::Table>("__ZoiUninstallOperations") {
for op in uninstall_ops.sequence_values::<mlua::Table>() {
let op = op.map_err(|e| anyhow!(e.to_string()))?;
if let Ok(op_type) = op.get::<String>("op")
&& op_type == "zrm"
{
let mut path_to_remove: String =
op.get("path").map_err(|e| anyhow!(e.to_string()))?;
path_to_remove =
path_to_remove.replace("${pkgstore}", &version_dir.to_string_lossy());
if let Some(home_dir) = home::home_dir() {
path_to_remove =
path_to_remove.replace("${usrhome}", &home_dir.to_string_lossy());
}
path_to_remove = path_to_remove.replace(
"${usrroot}",
&crate::pkg::sysroot::apply_sysroot(PathBuf::from("/")).to_string_lossy(),
);
let path = std::path::PathBuf::from(path_to_remove);
if path.exists() {
println!("Removing {}...", path.display());
if path.is_dir() {
fs::remove_dir_all(path)?;
} else {
fs::remove_file(path)?;
}
}
}
}
}
if let Some(backup_files) = &manifest.backup {
println!("Saving configuration files...");
for backup_file_rel in backup_files {
let backup_src = version_dir.join(backup_file_rel);
if backup_src.exists() {
let backup_dest = version_dir
.parent()
.expect("version_dir should have a parent (package_dir)")
.join(format!("{}.zoisave", backup_file_rel));
if let Some(p) = backup_dest.parent()
&& let Err(e) = fs::create_dir_all(p)
{
eprintln!(
"Warning: could not create backup directory {}: {}",
p.display(),
e
);
continue;
}
println!(
"Saving {} to {}",
backup_src.display(),
backup_dest.display()
);
if let Err(e) = fs::rename(&backup_src, &backup_dest) {
eprintln!("Warning: failed to save {}: {}", backup_src.display(), e);
}
}
}
}
println!(
"Uninstalling '{}'...",
if let Some(sub) = &manifest.sub_package {
format!("{}:{}", pkg.name, sub)
} else {
pkg.name.clone()
}
.bold()
);
if let Some(bins) = &manifest.bins {
let bin_root = get_bin_root(scope)?;
for bin in bins {
let symlink_path = bin_root.join(bin);
if symlink_path.is_symlink() || symlink_path.exists() {
let other_providers = db::find_provides("local", bin)?;
let still_provided = other_providers
.iter()
.any(|(p, _)| p.name != pkg.name || (p.sub_package != manifest.sub_package));
if !still_provided {
println!(
"Removing shim for {} from {}...",
bin.cyan(),
symlink_path.display()
);
fs::remove_file(&symlink_path)?;
} else {
println!(
"Keeping shim for {} as it is still provided by other packages.",
bin.cyan()
);
}
}
}
} else if manifest.sub_package.is_none() {
let bin = &pkg.name;
let symlink_path = get_bin_root(scope)?.join(bin);
if symlink_path.is_symlink() || symlink_path.exists() {
let other_providers = db::find_provides("local", bin)?;
let still_provided = other_providers
.iter()
.any(|(p, _)| p.name != pkg.name || (p.sub_package != manifest.sub_package));
if !still_provided {
println!(
"Removing shim for {} from {}...",
bin.cyan(),
symlink_path.display()
);
fs::remove_file(symlink_path)?;
}
}
}
for file_path_str in &manifest.installed_files {
let file_path = PathBuf::from(file_path_str);
if file_path.exists() {
if file_path.is_dir() {
let _ = fs::remove_dir_all(&file_path);
} else {
let _ = fs::remove_file(&file_path);
}
}
}
let manifest_filename = if let Some(sub) = &manifest.sub_package {
format!("manifest-{}.yaml", sub)
} else {
"manifest.yaml".to_string()
};
let manifest_path = version_dir.join(manifest_filename);
if manifest_path.exists() {
fs::remove_file(manifest_path)?;
}
if version_dir.exists() {
let mut has_other_manifests = false;
if let Ok(entries) = fs::read_dir(&version_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("manifest") && name.ends_with(".yaml") {
has_other_manifests = true;
break;
}
}
}
if !has_other_manifests {
println!(
"Removing empty version directory: {}",
version_dir.display()
);
fs::remove_dir_all(&version_dir)?;
}
}
if package_dir.exists() {
let _ = crate::pkg::service::cleanup_service(&pkg.name, scope);
let mut has_other_versions = false;
if let Ok(entries) = fs::read_dir(&package_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name != "latest" && name != "dependents" {
has_other_versions = true;
break;
}
}
}
if !has_other_versions {
println!("Removing package store: {}", package_dir.display());
fs::remove_dir_all(&package_dir)?;
}
}
let parent_id = format!(
"#{}@{}/{}@{}",
manifest.registry_handle, manifest.repo, manifest.name, manifest.version
);
for dep_str in &manifest.installed_dependencies {
if let Ok(dep) = dependencies::parse_dependency_string(dep_str)
&& dep.manager == "zoi"
{
let dep_req = resolve::parse_source_string(dep.package)?;
let dep_matches = local::find_installed_manifests_matching(&dep_req, scope)?;
if dep_matches.len() == 1 {
let dep_manifest = &dep_matches[0];
match local::get_package_dir(
dep_manifest.scope,
&dep_manifest.registry_handle,
&dep_manifest.repo,
&dep_manifest.name,
) {
Ok(dep_pkg_dir) => {
if let Err(e) = local::remove_dependent(&dep_pkg_dir, &parent_id) {
eprintln!(
"Warning: failed to remove dependent link for {}: {}",
dep.package, e
);
}
}
Err(e) => {
eprintln!(
"Warning: failed to get package dir for {}: {}",
dep.package, e
);
}
}
}
}
}
if let Err(e) = recorder::remove_package_from_record(&manifest) {
eprintln!(
"{} Failed to remove package from lockfile: {}",
"Warning:".yellow(),
e
);
}
if let Ok(conn) = db::open_connection("local") {
let _ = db::delete_package(
&conn,
&pkg.name,
sub_package_to_uninstall.as_deref(),
&pkg.repo,
Some(scope),
);
}
println!("Removed manifest for '{}'.", pkg.name);
match crate::pkg::telemetry::posthog_capture_event(
"uninstall",
&pkg,
env!("CARGO_PKG_VERSION"),
&manifest.registry_handle,
None,
) {
Ok(true) => println!("{} telemetry sent", "Info:".green()),
Ok(false) => (),
Err(e) => eprintln!("{} telemetry failed: {}", "Warning:".yellow(), e),
}
if let Some(hooks) = &pkg.hooks
&& let Err(e) = hooks::run_hooks(hooks, hooks::HookType::PostRemove)
{
eprintln!("{} post-remove hook failed: {}", "Warning:".yellow(), e);
}
Ok(manifest)
}