/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: remove.reef Authors: Chris Tusa License: Description: Remove command - uninstall packages ******************************************************************************/ module commands.remove import sys.args import sys.process import io.file import io.dir import io.path import core.str import core.config import core.database import core.pkgdb import core.package import core.port import util.prompt import util.mtree import util.checksum import types import util.color import fs.stat import fs.ops import util.priv import exitcodes as ec import core.result as res export fn execute(opts: types.GlobalOptions): int end export fn execute(opts: types.GlobalOptions): int let argc = args.count() // Package arguments should be after the command if argc <= opts.cmd_index + 1 print_usage() return ec.EXIT_USAGE() end if // Load configuration and apply command-line overrides let base_cfg = config.load() let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix, opts.no_chroot) let root_path = config.get_root(cfg) // Show root if set (verbose mode) if opts.verbose and str.length(root_path) > 0 color.print_info("Using root: " + root_path) end if // Collect packages to remove mut packages: [string] = new [string](256) mut pkg_count = 0 mut i = opts.cmd_index + 1 while i < argc and pkg_count < 256 let pkg_name = args.get(i) // Skip flags and their values if pkg_name == "--root" or pkg_name == "--prefix" i = i + 2 // Skip flag and its value continue elif pkg_name == "--no-chroot-scripts" i = i + 1 // valueless flag: skip only itself continue end if if str.starts_with(pkg_name, "-") i = i + 1 continue end if // Check if installed (use rooted version if root is set) let installed = check_installed(pkg_name, root_path) if installed packages[pkg_count] = pkg_name pkg_count = pkg_count + 1 else color.print_warning(pkg_name + " is not installed") end if i = i + 1 end while if pkg_count == 0 color.print_info("No packages to remove") return ec.EXIT_SUCCESS() end if // Check for reverse dependencies (packages that depend on these) mut dependents: [string] = new [string](256) mut dependent_count = 0 i = 0 while i < pkg_count let pkg_name = packages[i] let count = get_reverse_dependencies(pkg_name, dependents, dependent_count, 256) dependent_count = count i = i + 1 end while // Warn if there are dependents if dependent_count > 0 color.print_warning("The following installed packages depend on packages being removed:") i = 0 while i < dependent_count println(" - " + dependents[i]) i = i + 1 end while // Require --force to proceed if not opts.force color.print_error("Use --force to remove packages with dependents") return ec.EXIT_DEPS_CONFLICT() end if color.print_warning("Proceeding with removal (--force specified)") end if // Ask for confirmation (unless -y flag) if not prompt.confirm_remove(packages, pkg_count, dependents, dependent_count, opts) color.print_info("Removal cancelled") return ec.EXIT_CANCELLED() end if // Dry-run: show what would be done and exit if opts.dry_run color.print_info("Dry run — no changes made") return ec.EXIT_SUCCESS() end if // Remove packages mut all_success = true i = 0 while i < pkg_count if not remove_package(packages[i], root_path, cfg) color.print_error("Failed to remove: " + packages[i]) all_success = false end if i = i + 1 end while if all_success return ec.EXIT_SUCCESS() else return ec.EXIT_REMOVE_FAILED() end if end execute // Check if package is installed, using rooted check if root is set fn check_installed(name: string, root_path: string): bool if str.length(root_path) > 0 return database.is_installed_rooted(name, root_path) end if return database.is_installed(name) end check_installed proc print_usage() println("Usage: coral remove ") println("") println("Remove one or more installed packages.") println("") println("Arguments:") println(" Name of installed package to remove") println("") println("Options:") println(" -y, --yes Don't ask for confirmation") println(" -f, --force Remove even if other packages depend on it") println("") println("Examples:") println(" coral remove vim") println(" coral remove vim nano htop") println(" coral remove --force libfoo") end print_usage fn remove_package(pkg_name: string, root_path: string, cfg: config.Config): bool // Check if package is installed (use rooted version if root is set) if not check_installed(pkg_name, root_path) color.print_error(pkg_name + " is not installed") return false end if color.print_action("Removing " + pkg_name + "...") let install_root = get_actual_root(root_path) // Resolve script execution context; abort early if confinement is required but impossible. // Defense-in-depth: main.reef already bars non-root for all non-query commands, so this abort // is currently unreachable via the CLI. Kept so confinement fails safely if that global gate is // ever relaxed. if package.script_wants_chroot(install_root, config.get_chroot_scripts(cfg)) and not priv.is_root() color.print_error("--root " + install_root + " runs maintainer scripts confined to the target via chroot, which requires root privileges.") color.print_error("Re-run as root, or pass --no-chroot-scripts to run them on the host (unconfined).") return false end if let script_ctx = package.resolve_script_ctx(install_root, config.get_chroot_scripts(cfg), priv.is_root()) // Get package info (use rooted version if root is set) let pkg = get_installed_pkg(pkg_name, root_path) color.print_info("Package: " + pkg.info.name + " " + pkg.info.version) // Get file list from database (use rooted version if root is set) mut files: [string] = new [string](4096) let file_count = get_files_list(pkg_name, files, 4096, root_path) if file_count == 0 color.print_warning("No files recorded for package") else color.print_info("Removing " + int_to_str(file_count) + " files...") end if // Run pre-remove script if present (abort on failure) let scripts_dir = database.get_scripts_dir(pkg_name) if database.has_stored_scripts(pkg_name) color.print_info("Running pre-remove script...") if not package.run_stored_pre_remove(script_ctx, scripts_dir, pkg.info.version) color.print_error("Pre-remove script failed, aborting removal of " + pkg_name) return false end if end if // Load config file list and manifest checksums for config protection mut config_files: [string] = new [string](256) let config_count = database.get_config_files(pkg_name, config_files, 256) mut manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) mut manifest_count = 0 let manifest_path = database.get_manifest_path(pkg_name) if file.fileExists(manifest_path) let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") if str.length(manifest_content) > 0 manifest_count = mtree.parse_manifest(manifest_content, manifest_entries, 8192) end if end if // Remove files (with proper root path, respecting config protection) let actual_root = get_actual_root(root_path) if not remove_files(files, file_count, actual_root, config_files, config_count, manifest_entries, manifest_count) color.print_warning("Some files could not be removed") // Continue anyway - unregister from database end if // Run post-remove script if present (warn on failure, don't abort) if database.has_stored_scripts(pkg_name) color.print_info("Running post-remove script...") if not package.run_stored_post_remove(script_ctx, scripts_dir, pkg.info.version) color.print_warning("Post-remove script failed") end if end if // Unregister from database (use rooted version if root is set) color.print_info("Unregistering package...") if not unregister_pkg(pkg_name, root_path) color.print_error("Failed to unregister package from database") return false end if // Update package database indexes if not pkgdb.rebuild_indexes_rooted(root_path) color.print_warning("Failed to update package indexes") end if color.print_success("Removed " + pkg_name) return true end remove_package // Get installed package info, using rooted check if root is set fn get_installed_pkg(name: string, root_path: string): types.InstalledPackage if str.length(root_path) > 0 return database.get_installed_rooted(name, root_path) end if return database.get_installed(name) end get_installed_pkg // Get files list, using rooted check if root is set fn get_files_list(name: string, files: [string], max_count: int, root_path: string): int if str.length(root_path) > 0 return database.get_files_rooted(name, files, max_count, root_path) end if return database.get_files(name, files, max_count) end get_files_list // Unregister package, using rooted version if root is set fn unregister_pkg(name: string, root_path: string): bool if str.length(root_path) > 0 return database.unregister_package_rooted(name, root_path) end if return database.unregister_package(name) end unregister_pkg // Get actual root path (returns "/" if empty) fn get_actual_root(root_path: string): string if str.length(root_path) == 0 return "/" end if return root_path end get_actual_root // Remove files from system, respecting config file protection fn remove_files(files: [string], file_count: int, root_path: string, config_files: [string], config_count: int, manifest_entries: [mtree.ManifestEntry], manifest_count: int): bool mut all_success = true // Remove files in reverse order mut i = file_count - 1 while i >= 0 let rel_path = files[i] let full_path = path.join_path(root_path, rel_path) if stat.is_symlink(full_path) or stat.exists(full_path) // Check if this is a config file that the user has modified if config_count > 0 and package.is_config_file(rel_path, config_files, config_count) // Look up the package's sha256 from the manifest let pkg_sha = find_manifest_sha256(rel_path, manifest_entries, manifest_count) if str.length(pkg_sha) > 0 let installed_sha = checksum.sha256_file(full_path) if not str.equals(installed_sha, pkg_sha) // User has modified this config — preserve it color.print_info("Preserving modified config: " + rel_path) i = i - 1 continue end if end if end if if not res.is_ok(ops.remove_file(full_path)) color.print_warning("Failed to remove: " + full_path) all_success = false end if end if i = i - 1 end while // Try to remove empty parent directories i = file_count - 1 while i >= 0 let rel_path = files[i] let full_path = path.join_path(root_path, rel_path) let parent = path.dirname(full_path) if str.length(parent) > str.length(root_path) and dir.dir_exists(parent) dir.remove_dir(parent) end if i = i - 1 end while return all_success end remove_files // Look up the sha256 for a given relative path in the manifest entries fn find_manifest_sha256(rel_path: string, entries: [mtree.ManifestEntry], count: int): string mut i = 0 while i < count let epath = mtree.entry_path(entries[i]) // Compare with and without ./ prefix mut entry_rel = epath if str.starts_with(epath, "./") entry_rel = str.substring(epath, 2, str.length(epath) - 2) end if if str.equals(entry_rel, rel_path) return mtree.entry_sha256(entries[i]) end if i = i + 1 end while return "" end find_manifest_sha256 // Get packages that depend on the given package // Returns new total count (existing + new dependents) fn get_reverse_dependencies(pkg_name: string, dependents: [string], current_count: int, max_count: int): int mut count = current_count // List all installed packages mut installed: [string] = new [string](512) let installed_count = database.list_installed(installed, 512) mut i = 0 while i < installed_count and count < max_count let other_pkg = installed[i] // Skip the package being removed and already listed dependents if str.equals(other_pkg, pkg_name) i = i + 1 continue end if // Check if already in dependents list if array_contains(dependents, count, other_pkg) i = i + 1 continue end if // Try to find this package's port to get its dependencies let port_result = port.find(other_pkg) if port_result.success let p = port_result.port // Check runtime dependencies if depends_on(p.deps.runtime, p.deps.runtime_count, pkg_name) dependents[count] = other_pkg count = count + 1 end if end if i = i + 1 end while return count end get_reverse_dependencies // Check if a dependency list contains the given package fn depends_on(deps: [string], dep_count: int, pkg_name: string): bool mut i = 0 while i < dep_count let dep = deps[i] // Handle alternative dependencies (format: base:opt1,opt2) let colon_pos = str.index_of_char(dep, ':') mut base_dep = dep if colon_pos > 0 base_dep = str.substring(dep, 0, colon_pos) end if if str.equals(base_dep, pkg_name) return true end if i = i + 1 end while return false end depends_on // Helper to check if array contains a string fn array_contains(arr: [string], count: int, s: string): bool mut i = 0 while i < count if str.equals(arr[i], s) return true end if i = i + 1 end while return false end array_contains // Helper: convert int to string fn int_to_str(n: int): string if n == 0 return "0" end if mut negative = false mut value = n if n < 0 negative = true value = 0 - n end if mut result = "" while value > 0 let digit = value % 10 result = str.concat(str.substring("0123456789", digit, 1), result) value = value / 10 end while if negative result = str.concat("-", result) end if return result end int_to_str end module