/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: build.reef Authors: Chris Tusa License: Description: Build command - compile packages from ports ******************************************************************************/ module commands.build import sys.args import sys.process import io.file import io.dir import io.path import io.console import core.str import core.port import core.config import core.database import core.resolver import util.version as ver import types import util.color import util.prompt import util.checksum import util.archive import util.mtree import util.http as uhttp import exitcodes as ec import core.result as res export fn execute(opts: types.GlobalOptions): int end export // Build context type BuildContext = struct port: types.Port work_dir: string pkg_dir: string src_dir: string cfg: config.Config noclean: bool force_clean: bool log_file: string logging: bool quiet: bool end BuildContext // Load a port from path or find it by name fn load_or_find_port(port_path: string): types.PortResult // Check if it's a valid port directory (must contain package.toml) // This prevents confusion with binary files or other directories with same name if dir.dir_exists(port_path) let toml_path = path.join_path(port_path, "package.toml") if file.fileExists(toml_path) color.print_action("Loading port from: " + port_path) return port.load(port_path) end if end if // Not a valid port directory, search by name color.print_action("Finding port: " + port_path) return port.find(port_path) end load_or_find_port fn execute(opts: types.GlobalOptions): int // Parse arguments let argc = args.count() // Port argument should be after the command if argc <= opts.cmd_index + 1 print_usage() return ec.EXIT_USAGE() end if // Parse flags mut noclean = false mut force_clean = false mut quiet_flag = false mut verbose_flag = false mut install_flag = false mut port_path = "" mut i = opts.cmd_index + 1 while i < argc let arg = args.get(i) // Skip global flags with values if arg == "--root" or arg == "--prefix" i = i + 2 continue elif arg == "--no-chroot-scripts" i = i + 1 // valueless flag: skip only itself continue end if if arg == "--noclean" or arg == "-n" noclean = true elif arg == "--clean" or arg == "-c" force_clean = true elif arg == "--quiet" or arg == "-q" quiet_flag = true elif arg == "--verbose" or arg == "-v" verbose_flag = true elif arg == "--install" install_flag = true elif arg == "--no-install" install_flag = false elif not str.starts_with(arg, "-") if str.length(port_path) == 0 port_path = arg end if end if i = i + 1 end while if str.length(port_path) == 0 print_usage() return ec.EXIT_USAGE() end if // Handle "." for current directory if port_path == "." port_path = res.unwrap_or(dir.current_dir(), "") end if // Load configuration let cfg = config.load() uhttp.enable_proxy(config.get_use_proxy(cfg)) // Try to find the port by name if path doesn't exist let result = load_or_find_port(port_path) if not result.success color.print_error(result.error) return ec.EXIT_PKG_NOT_FOUND() end if let p = result.port color.print_success("Loaded port: " + p.info.name + " " + p.info.version) // Dry-run: show build plan without executing if opts.dry_run color.print_info("Dry run — would build: " + p.info.name + " " + p.info.version) let dry_resolve = resolver.resolve_build_deps(p.info.name) if dry_resolve.success and dry_resolve.count > 1 color.print_info("Dependencies to build first:") mut di = 0 while di < dry_resolve.count - 1 let dep_port = port.find(dry_resolve.packages[di]) if dep_port.success color.print_info(" " + dry_resolve.packages[di] + " " + dep_port.port.info.version) else color.print_info(" " + dry_resolve.packages[di]) end if di = di + 1 end while end if color.print_info("Dry run — no changes made") return ec.EXIT_SUCCESS() end if // Resolve, build, and install missing dependencies before building if not resolve_and_build_deps(p, opts, cfg) // Check if the failure was due to circular dependencies let check_result = resolver.resolve_build_deps(p.info.name) if not check_result.success and str.starts_with(check_result.error, "Circular") color.print_error("Cannot build " + p.info.name + " — circular dependency") return ec.EXIT_DEPS_CIRCULAR() end if color.print_error("Cannot build " + p.info.name + " — missing dependencies") return ec.EXIT_DEPS_UNMET() end if // Determine quiet mode: CLI flag overrides config, verbose overrides quiet mut quiet_mode = config.get_quiet(cfg) if quiet_flag quiet_mode = true end if if verbose_flag quiet_mode = false end if // Create build context let ctx = create_context(p, cfg, noclean, force_clean, quiet_mode) // Step 1: Setup directories if not setup_directories(ctx) color.print_error("Failed to create build directories") return ec.EXIT_ERROR() end if // Initialize logging after directories are created init_log(ctx) if ctx.logging and str.length(ctx.log_file) > 0 log_info(ctx, "Log file: " + ctx.log_file) end if // Step 2: Download sources if not download_sources(ctx) log_error(ctx, "Failed to download sources") print_log_location(ctx) cleanup_build(ctx) return ec.EXIT_DOWNLOAD_FAILED() end if // Step 3: Verify checksums if not verify_checksums(ctx) log_error(ctx, "Checksum verification failed") print_log_location(ctx) cleanup_build(ctx) return ec.EXIT_CHECKSUM_FAILED() end if // Step 4: Extract sources if not extract_sources(ctx) log_error(ctx, "Failed to extract sources") print_log_location(ctx) cleanup_build(ctx) return ec.EXIT_EXTRACT_FAILED() end if // Step 5: Apply patches if not apply_patches(ctx) log_error(ctx, "Failed to apply patches") print_log_location(ctx) cleanup_build(ctx) return ec.EXIT_PATCH_FAILED() end if // Step 6: Run build script if not run_build(ctx) log_error(ctx, "Build failed") print_log_location(ctx) cleanup_build(ctx) return ec.EXIT_BUILD_FAILED() end if // Step 7: Compress man pages (if enabled) if config.get_compress_man(ctx.cfg) compress_man_pages(ctx) end if // Step 8: Create package if not create_package(ctx) log_error(ctx, "Failed to create package") print_log_location(ctx) cleanup_build(ctx) return ec.EXIT_PACKAGE_FAILED() end if // Success! let pkg_name = p.info.name + "-" + p.info.version + "-" + int_to_str(p.info.release) + ".pkg.tar.xz" let pkg_path = path.join_path(config.get_packages_dir(cfg), pkg_name) log_success(ctx, "Package created: " + pkg_path) // Cleanup work directory on success cleanup_build(ctx) // Step 9: Install the built package (only with --install) if install_flag color.print_action("Installing " + p.info.name + "...") let self_exe = args.get(0) let install_cmd = self_exe + " install -y -f \"" + pkg_path + "\"" let install_pid = process.process_spawn_shell(install_cmd) if install_pid < 0 color.print_error("Failed to launch installer for " + p.info.name) return ec.EXIT_INSTALL_FAILED() end if let install_exit = process.process_wait(install_pid) if install_exit != 0 color.print_error("Failed to install " + p.info.name) return ec.EXIT_INSTALL_FAILED() end if else color.print_success("Package ready (use --install to install, or: coral install \"" + pkg_path + "\")") end if return ec.EXIT_SUCCESS() end execute proc print_usage() println("Usage: coral build [options] ") println("") println("Build a package from a port directory.") println("") println("Arguments:") println(" Path to port directory (use . for current)") println("") println("Options:") println(" --install Install the package after building") println(" --no-install Build only, do not install (default)") println(" -n, --noclean Don't clean work/pkg directories after build") println(" -c, --clean Force cleanup even if disabled in config") println(" -q, --quiet Hide subprocess output (still logged to file)") println(" -v, --verbose Show all output (overrides quiet)") println("") println("Examples:") println(" coral build /usr/zports/base/vim") println(" coral build --install base/vim") println(" coral build --noclean .") println(" coral build --quiet vim") end print_usage // Resolve all build dependencies recursively, build from source, and install each. // Returns true if all dependencies are satisfied (either already installed or built+installed). fn resolve_and_build_deps(p: types.Port, opts: types.GlobalOptions, cfg: config.Config): bool // Resolve the full dependency tree (runtime + build deps, recursive) let resolve_result = resolver.resolve_build_deps(p.info.name) if not resolve_result.success color.print_error("Dependency resolution failed: " + resolve_result.error) return false end if // The result includes the target package as the last element — strip it mut dep_count = resolve_result.count if dep_count > 0 let last = resolve_result.packages[dep_count - 1] if str.equals(last, p.info.name) dep_count = dep_count - 1 end if end if // No deps to build if dep_count == 0 return true end if // Look up version info for each dep to display in the build plan mut dep_versions: [string] = new [string](256) mut i = 0 while i < dep_count let dep_port_result = port.find(resolve_result.packages[i]) if dep_port_result.success dep_versions[i] = dep_port_result.port.info.version else dep_versions[i] = "" end if i = i + 1 end while // Show build plan and ask for confirmation if not prompt.confirm_build_plan(resolve_result.packages, dep_versions, dep_count, p.info.name, opts) color.print_info("Build cancelled") return false end if // Build and install each dependency in topological order i = 0 while i < dep_count let dep_name = resolve_result.packages[i] println("") color.print_action("[" + int_to_str(i + 1) + "/" + int_to_str(dep_count) + "] Building dependency: " + dep_name) // Build the dependency from source if not build_single_port(dep_name, cfg, opts) color.print_error("Failed to build dependency: " + dep_name) return false end if // Install the built package let dep_port_result = port.find(dep_name) if not dep_port_result.success color.print_error("Cannot find port after build: " + dep_name) return false end if let dp = dep_port_result.port let pkg_filename = dp.info.name + "-" + dp.info.version + "-" + int_to_str(dp.info.release) + ".pkg.tar.xz" let pkg_path = path.join_path(config.get_packages_dir(cfg), pkg_filename) if not file.fileExists(pkg_path) color.print_error("Built package not found: " + pkg_path) return false end if color.print_action("Installing " + dep_name + "...") let self_exe = args.get(0) let install_cmd = self_exe + " install -y \"" + pkg_path + "\"" let pid = process.process_spawn_shell(install_cmd) if pid < 0 color.print_error("Failed to launch installer for " + dep_name) return false end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Failed to install " + dep_name) return false end if color.print_success("Dependency ready: " + dep_name) i = i + 1 end while println("") color.print_success("All " + int_to_str(dep_count) + " dependencies built and installed") return true end resolve_and_build_deps // Build a single port from source. Used for dependency builds. // Returns true if build succeeds and package archive is created. fn build_single_port(name: string, cfg: config.Config, opts: types.GlobalOptions): bool let port_result = port.find(name) if not port_result.success color.print_error("Port not found: " + name) return false end if let p = port_result.port // Determine quiet mode mut quiet_mode = config.get_quiet(cfg) if opts.verbose quiet_mode = false end if if opts.quiet quiet_mode = true end if let ctx = create_context(p, cfg, false, false, quiet_mode) // Run the full build pipeline if not setup_directories(ctx) color.print_error("Failed to create build directories for " + name) return false end if init_log(ctx) if not download_sources(ctx) log_error(ctx, "Failed to download sources for " + name) print_log_location(ctx) cleanup_build(ctx) return false end if if not verify_checksums(ctx) log_error(ctx, "Checksum verification failed for " + name) print_log_location(ctx) cleanup_build(ctx) return false end if if not extract_sources(ctx) log_error(ctx, "Failed to extract sources for " + name) print_log_location(ctx) cleanup_build(ctx) return false end if if not apply_patches(ctx) log_error(ctx, "Failed to apply patches for " + name) print_log_location(ctx) cleanup_build(ctx) return false end if if not run_build(ctx) log_error(ctx, "Build failed for " + name) print_log_location(ctx) cleanup_build(ctx) return false end if // Compress man pages (if enabled) if config.get_compress_man(cfg) compress_man_pages(ctx) end if if not create_package(ctx) log_error(ctx, "Failed to create package for " + name) print_log_location(ctx) cleanup_build(ctx) return false end if let pkg_filename = p.info.name + "-" + p.info.version + "-" + int_to_str(p.info.release) + ".pkg.tar.xz" let pkg_path = path.join_path(config.get_packages_dir(cfg), pkg_filename) log_success(ctx, "Package created: " + pkg_path) cleanup_build(ctx) return true end build_single_port fn create_context(p: types.Port, cfg: config.Config, noclean: bool, force_clean: bool, quiet: bool): BuildContext // New directory structure: // /usr/zports/pkgs/build//src/ - extracted sources, compilation // /usr/zports/pkgs/build//stage/ - DESTDIR for make install let build_base = config.get_build_dir(cfg) let pkg_build_dir = path.join_path(build_base, p.info.name) let work_dir = path.join_path(pkg_build_dir, "src") let pkg_dir = path.join_path(pkg_build_dir, "stage") // Generate log file path with timestamp (in package build root, not src/) let logging_enabled = config.get_logging(cfg) mut log_path = "" if logging_enabled log_path = path.join_path(pkg_build_dir, "coral-build-" + p.info.name + "-" + get_timestamp() + ".log") end if return BuildContext{ port: p, work_dir: work_dir, pkg_dir: pkg_dir, src_dir: "", cfg: cfg, noclean: noclean, force_clean: force_clean, log_file: log_path, logging: logging_enabled, quiet: quiet } end create_context // Get current timestamp for log filename fn get_timestamp(): string // Use date command to get timestamp let tmp_file = "/tmp/coral_ts_" + int_to_str(process.getpid()) + ".txt" let cmd = "date '+%Y%m%d-%H%M%S' > \"" + tmp_file + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if let ts = res.unwrap_or(file.readFile(tmp_file), "") // Cleanup let rm_cmd = "rm -f \"" + tmp_file + "\"" let rm_pid = process.process_spawn_shell(rm_cmd) if rm_pid > 0 process.process_wait(rm_pid) end if return str.trim(ts, '\n') end get_timestamp // Initialize log file with header proc init_log(ctx: BuildContext) if not ctx.logging return end if mut header = "=== Coral Build Log ===\n" header = header + "Package: " + ctx.port.info.name + "\n" header = header + "Version: " + ctx.port.info.version + "\n" header = header + "Work Dir: " + ctx.work_dir + "\n" header = header + "Log File: " + ctx.log_file + "\n" header = header + "========================\n\n" file.writeFile(ctx.log_file, header) end init_log // Log a message to file and optionally to screen proc log_msg(ctx: BuildContext, msg: string, show_screen: bool) // Always write to log file if logging enabled if ctx.logging and str.length(ctx.log_file) > 0 file.appendFile(ctx.log_file, msg + "\n") end if // Show on screen unless quiet mode suppresses it if show_screen println(msg) end if end log_msg // Log info message (always show on screen) proc log_info(ctx: BuildContext, msg: string) if ctx.logging and str.length(ctx.log_file) > 0 file.appendFile(ctx.log_file, "[INFO] " + msg + "\n") end if color.print_info(msg) end log_info // Log success message (always show on screen) proc log_success(ctx: BuildContext, msg: string) if ctx.logging and str.length(ctx.log_file) > 0 file.appendFile(ctx.log_file, "[OK] " + msg + "\n") end if color.print_success(msg) end log_success // Log error message (always show on screen) proc log_error(ctx: BuildContext, msg: string) if ctx.logging and str.length(ctx.log_file) > 0 file.appendFile(ctx.log_file, "[ERROR] " + msg + "\n") end if color.print_error(msg) end log_error // Log action message (always show on screen) proc log_action(ctx: BuildContext, msg: string) if ctx.logging and str.length(ctx.log_file) > 0 file.appendFile(ctx.log_file, "[ACTION] " + msg + "\n") end if color.print_action(msg) end log_action // Log warning message (always show on screen) proc log_warning(ctx: BuildContext, msg: string) if ctx.logging and str.length(ctx.log_file) > 0 file.appendFile(ctx.log_file, "[WARN] " + msg + "\n") end if color.print_warning(msg) end log_warning // Print log file location (for debugging failed builds) proc print_log_location(ctx: BuildContext) if ctx.logging and str.length(ctx.log_file) > 0 color.print_info("Build log: " + ctx.log_file) end if end print_log_location // Run a shell command with output captured to log // Returns exit code fn run_logged_cmd(ctx: BuildContext, cmd: string): int if ctx.logging and str.length(ctx.log_file) > 0 file.appendFile(ctx.log_file, "[CMD] " + cmd + "\n") end if // Build command that logs output to file // Redirects all output to log file to avoid tee pipeline issues // (tee caused SIGPIPE on long builds and masked exit codes) mut full_cmd = cmd if ctx.logging and str.length(ctx.log_file) > 0 // Both quiet and normal mode: log to file only // Real-time terminal output is sacrificed for build reliability full_cmd = "(" + cmd + ") >> \"" + ctx.log_file + "\" 2>&1" else if ctx.quiet // No logging, quiet mode: suppress output full_cmd = "(" + cmd + ") >/dev/null 2>&1" end if // No logging, not quiet: just run normally end if // Flush stdout before spawning subprocess to ensure proper output ordering console.flushOutput() let pid = process.process_spawn_shell(full_cmd) if pid < 0 return -1 end if return process.process_wait(pid) end run_logged_cmd fn setup_directories(ctx: BuildContext): bool color.print_info("Setting up build directories...") // Create work directory if dir.dir_exists(ctx.work_dir) // Clean existing work dir let cmd = "rm -rf \"" + ctx.work_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if end if if not res.is_ok(dir.create_dir_all(ctx.work_dir)) return false end if // Create pkg directory (for DESTDIR) if dir.dir_exists(ctx.pkg_dir) let cmd = "rm -rf \"" + ctx.pkg_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if end if if not res.is_ok(dir.create_dir_all(ctx.pkg_dir)) return false end if // Ensure sources directory exists let sources_dir = config.get_sources_dir(ctx.cfg) if not dir.dir_exists(sources_dir) if not res.is_ok(dir.create_dir_all(sources_dir)) return false end if end if // Ensure packages directory exists let packages_dir = config.get_packages_dir(ctx.cfg) if not dir.dir_exists(packages_dir) if not res.is_ok(dir.create_dir_all(packages_dir)) return false end if end if return true end setup_directories fn download_sources(ctx: BuildContext): bool if ctx.port.sources.count == 0 color.print_info("No sources to download") return true end if color.print_info("Downloading sources...") let sources_dir = config.get_sources_dir(ctx.cfg) mut i = 0 while i < ctx.port.sources.count let src = ctx.port.sources.sources[i] let filename = src.file let dest = path.join_path(sources_dir, filename) // Check if already cached with valid checksum if file.fileExists(dest) if verify_single_checksum(dest, src.checksum) color.print_info("Using cached: " + filename) i = i + 1 continue else // Cached file has wrong checksum, re-download color.print_warning("Cached file checksum mismatch, re-downloading: " + filename) end if end if // Try each mirror URL in order mut download_success = false mut url_idx = 0 while url_idx < src.url_count let url = src.urls[url_idx] if url_idx > 0 color.print_info("Trying mirror: " + url) end if // Check if this is a local file (no http:// or https:// prefix) if is_local_source(url) // Local file - copy from port directory to sources cache if copy_local_source(url, dest, ctx.port.port_dir) if verify_single_checksum(dest, src.checksum) download_success = true break else color.print_warning("Checksum mismatch for local file, trying next...") let rm_cmd = "rm -f \"" + dest + "\"" let rm_pid = process.process_spawn_shell(rm_cmd) if rm_pid > 0 process.process_wait(rm_pid) end if end if end if else // Remote URL - download via HTTP if uhttp.download(url, dest) // Verify checksum after download if verify_single_checksum(dest, src.checksum) download_success = true break else color.print_warning("Checksum mismatch from mirror, trying next...") // Delete bad file let rm_cmd = "rm -f \"" + dest + "\"" let rm_pid = process.process_spawn_shell(rm_cmd) if rm_pid > 0 process.process_wait(rm_pid) end if end if end if end if url_idx = url_idx + 1 end while if not download_success color.print_error("Failed to download: " + filename + " (all sources failed)") return false end if i = i + 1 end while return true end download_sources // Check if a URL is a local file path (not http:// or https://) fn is_local_source(url: string): bool if str.starts_with(url, "http://") return false end if if str.starts_with(url, "https://") return false end if return true end is_local_source // Copy a local source file from port directory to sources cache fn copy_local_source(source_path: string, dest: string, port_dir: string): bool // Resolve the source path mut full_path = source_path // If it's a relative path, resolve relative to port directory if not str.starts_with(source_path, "/") full_path = path.join_path(port_dir, source_path) end if // Check if file exists if not file.fileExists(full_path) color.print_error("Local source file not found: " + full_path) return false end if color.print_info("Copying local source: " + source_path) // Copy the file let cmd = "cp \"" + full_path + "\" \"" + dest + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Failed to copy local source: " + source_path) return false end if color.print_success("Copied: " + source_path) return true end copy_local_source // Verify a single file's checksum fn verify_single_checksum(file_path: string, expected: string): bool // Handle special checksum values if str.equals(expected, "SKIP") or str.equals(expected, "BOOTSTRAP") or str.equals(expected, "skip") return true end if if str.length(expected) == 0 return true end if return checksum.verify_checksum_with_algo(file_path, expected) end verify_single_checksum fn verify_checksums(ctx: BuildContext): bool if ctx.port.sources.count == 0 return true end if color.print_info("Verifying checksums...") let sources_dir = config.get_sources_dir(ctx.cfg) mut i = 0 while i < ctx.port.sources.count let src = ctx.port.sources.sources[i] let filename = src.file let file_path = path.join_path(sources_dir, filename) let expected = src.checksum // Handle special checksum values if str.equals(expected, "SKIP") or str.equals(expected, "BOOTSTRAP") or str.equals(expected, "skip") color.print_warning("Checksum verification skipped: " + filename) elif str.length(expected) > 0 if not checksum.verify_checksum_with_algo(file_path, expected) color.print_error("Checksum mismatch: " + filename) return false end if color.print_success("Checksum verified: " + filename) else color.print_warning("No checksum for: " + filename) end if i = i + 1 end while return true end verify_checksums fn extract_sources(ctx: BuildContext): bool if ctx.port.sources.count == 0 return true end if color.print_info("Extracting sources...") let sources_dir = config.get_sources_dir(ctx.cfg) mut i = 0 while i < ctx.port.sources.count let src = ctx.port.sources.sources[i] let filename = src.file let archive_path = path.join_path(sources_dir, filename) // Check extract flag if not src.extract // Copy file to work_dir instead of extracting (for patches, etc.) let dest_path = path.join_path(ctx.work_dir, filename) let cp_cmd = "cp \"" + archive_path + "\" \"" + dest_path + "\"" let cp_pid = process.process_spawn_shell(cp_cmd) if cp_pid > 0 let exit_code = process.process_wait(cp_pid) if exit_code != 0 color.print_error("Failed to copy: " + filename) return false end if end if color.print_info("Copied (no extract): " + filename) i = i + 1 continue end if if not archive.extract(archive_path, ctx.work_dir) color.print_error("Failed to extract: " + filename) return false end if color.print_success("Extracted: " + filename) i = i + 1 end while return true end extract_sources fn apply_patches(ctx: BuildContext): bool if ctx.port.patches.count == 0 return true end if log_info(ctx, "Applying patches...") let strip = ctx.port.patches.strip // Find the source directory (first subdirectory in work_dir) let find_srcdir = "cd \"" + ctx.work_dir + "\" && ls -d */ 2>/dev/null | head -1 | tr -d '/'" mut i = 0 while i < ctx.port.patches.count let patch_file = ctx.port.patches.files[i] let patch_path = path.join_path(ctx.port.port_dir, patch_file) if not file.fileExists(patch_path) log_error(ctx, "Patch file not found: " + patch_file) return false end if // Apply patch from within the source directory let cmd = "cd \"" + ctx.work_dir + "/$(" + find_srcdir + ")\" && patch -p" + int_to_str(strip) + " < \"" + patch_path + "\"" let exit_code = run_logged_cmd(ctx, cmd) if exit_code != 0 log_error(ctx, "Failed to apply patch: " + patch_file) return false end if log_success(ctx, "Applied patch: " + patch_file) i = i + 1 end while return true end apply_patches fn run_build(ctx: BuildContext): bool log_action(ctx, "Building " + ctx.port.info.name + "...") // Look for build.sh in port directory let build_script = path.join_path(ctx.port.port_dir, "build.sh") if not file.fileExists(build_script) log_error(ctx, "No build.sh found in port directory") return false end if // Set up environment let destdir = ctx.pkg_dir let cflags = config.get_cflags(ctx.cfg) let cxxflags = config.get_cxxflags(ctx.cfg) let jobs = config.get_jobs(ctx.cfg) mut jobs_str = "" if jobs > 0 jobs_str = int_to_str(jobs) else // Auto-detect: use getconf (portable across illumos/Linux, unlike nproc) jobs_str = "$(getconf NPROCESSORS_ONLN 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1)" end if // Find the extracted source directory (first directory in work_dir) // Typically the tarball extracts to a subdirectory like "zlib-1.3.1" let find_srcdir_cmd = "cd \"" + ctx.work_dir + "\" && ls -d */ 2>/dev/null | head -1 | tr -d '/'" // Determine PREFIX: package.toml override → coral.conf → default (/usr) mut prefix = config.get_prefix(ctx.cfg) if str.length(ctx.port.prefix) > 0 prefix = ctx.port.prefix end if // Determine SYSCONFDIR: package.toml override → coral.conf → default (/etc) mut sysconfdir = config.get_sysconfdir(ctx.cfg) if str.length(ctx.port.sysconfdir) > 0 sysconfdir = ctx.port.sysconfdir end if // PKG_ROOT is the alternate root (empty means /) let pkg_root = config.get_root(ctx.cfg) // Build the command with all environment variables // Per Hammerhead porting guide: no uname wrappers, replace bundled config.guess/config.sub, // use system config.guess for BUILD_TRIPLE (returns x86_64-zygaena-hammerhead) mut cmd = "cd \"" + ctx.work_dir + "\" && " cmd = cmd + "export SRCDIR=\"" + ctx.work_dir + "/$(" + find_srcdir_cmd + ")\" && " cmd = cmd + "export PKGDIR=\"" + destdir + "\" && " cmd = cmd + "export DESTDIR=\"" + destdir + "\" && " cmd = cmd + "export PREFIX=\"" + prefix + "\" && " cmd = cmd + "export SYSCONFDIR=\"" + sysconfdir + "\" && " cmd = cmd + "export PKG_ROOT=\"" + pkg_root + "\" && " cmd = cmd + "export JOBS=\"" + jobs_str + "\" && " cmd = cmd + "export CFLAGS=\"" + cflags + "\" && " cmd = cmd + "export CXXFLAGS=\"" + cxxflags + "\" && " cmd = cmd + "export LDFLAGS=\"-R" + prefix + "/lib\" && " cmd = cmd + "export PKG_CONFIG_PATH=\"" + prefix + "/lib/pkgconfig\" && " cmd = cmd + "export MAKEFLAGS=\"-j" + jobs_str + "\" && " cmd = cmd + "export PKG_NAME=\"" + ctx.port.info.name + "\" && " cmd = cmd + "export PKG_VERSION=\"" + ctx.port.info.version + "\" && " cmd = cmd + "export BUILD_TRIPLE=\"$(/usr/share/autoconf/build-aux/config.guess 2>/dev/null || echo x86_64-zygaena-hammerhead)\" && " cmd = cmd + "export PATH=\"/usr/gnu/bin:$PATH\" && " cmd = cmd + "export MAKE=gmake && " cmd = cmd + "export TAR=gtar && " cmd = cmd + "for _f in config.guess config.sub; do find \"$SRCDIR\" -name \"$_f\" -exec cp /usr/share/autoconf/build-aux/\"$_f\" {} \\; 2>/dev/null; done; " cmd = cmd + "zsh \"" + build_script + "\"" log_info(ctx, "Running build script...") let exit_code = run_logged_cmd(ctx, cmd) if exit_code != 0 log_error(ctx, "Build script failed with exit code: " + int_to_str(exit_code)) return false end if log_success(ctx, "Build completed successfully") return true end run_build // Compress uncompressed man pages in the staging directory proc compress_man_pages(ctx: BuildContext) // Find uncompressed man pages and gzip them // Uses -exec {} + (batch mode) to avoid shell escaping issues with \; // Uses -type f to skip symlinks (avoids creating dangling refs) let script = "find '" + ctx.pkg_dir + "' -type f -path '*/man/man*/*' ! -name '*.gz' ! -name '*.bz2' ! -name '*.xz' -exec gzip -9 {} +" let pid = process.process_spawn_shell(script) if pid > 0 let exit_code = process.process_wait(pid) if exit_code == 0 log_info(ctx, "Compressed man pages") end if end if end compress_man_pages fn create_package(ctx: BuildContext): bool color.print_action("Creating package...") // Validate that staging directory has content (not just metadata files) if not validate_staging_dir(ctx.pkg_dir) color.print_error("Staging directory is empty - build produced no files") color.print_error("Check that build.sh installs files to $DESTDIR or $PKGDIR") return false end if // Generate .PKGINFO if not generate_pkginfo(ctx) return false end if // Generate .MANIFEST (mtree file inventory) if not generate_manifest_file(ctx) return false end if // Create the package archive let pkg_name = ctx.port.info.name + "-" + ctx.port.info.version + "-" + int_to_str(ctx.port.info.release) + ".pkg.tar.xz" let pkg_path = path.join_path(config.get_packages_dir(ctx.cfg), pkg_name) if not archive.create_package(ctx.pkg_dir, pkg_path) color.print_error("Failed to create package archive") return false end if return true end create_package // Validate that staging directory has actual content (not empty) fn validate_staging_dir(pkg_dir: string): bool // Count files in staging directory, excluding metadata files // A valid package should have at least one real file let cmd = "find \"" + pkg_dir + "\" -type f ! -name '.PKGINFO' ! -name '.MANIFEST' ! -name '.FOOTPRINT' | head -1" let tmp_file = "/tmp/coral_validate_" + int_to_str(process.getpid()) + ".txt" let full_cmd = cmd + " > \"" + tmp_file + "\"" let pid = process.process_spawn_shell(full_cmd) if pid < 0 return false end if process.process_wait(pid) // Read result let content = res.unwrap_or(file.readFile(tmp_file), "") // Clean up let rm_cmd = "rm -f \"" + tmp_file + "\"" let rm_pid = process.process_spawn_shell(rm_cmd) if rm_pid > 0 process.process_wait(rm_pid) end if // If content is empty, staging dir has no real files if str.length(content) == 0 return false end if // Trim newlines and check again let trimmed = str.trim(content, '\n') if str.length(trimmed) == 0 return false end if return true end validate_staging_dir fn generate_pkginfo(ctx: BuildContext): bool let pkginfo_path = path.join_path(ctx.pkg_dir, ".PKGINFO") mut content = "[package]\n" content = content + "name = \"" + ctx.port.info.name + "\"\n" content = content + "version = \"" + ctx.port.info.version + "\"\n" content = content + "release = " + int_to_str(ctx.port.info.release) + "\n" content = content + "description = \"" + ctx.port.info.description + "\"\n" content = content + "url = \"" + ctx.port.info.url + "\"\n" content = content + "license = \"" + ctx.port.info.license + "\"\n" content = content + "maintainer = \"" + ctx.port.info.maintainer + "\"\n" content = content + "arch = \"" + ctx.port.info.arch + "\"\n" // Emit [dependencies] section if there are any let has_runtime = ctx.port.deps.runtime_count > 0 let has_build = ctx.port.deps.build_count > 0 if has_runtime or has_build content = content + "\n[dependencies]\n" if has_runtime content = content + "runtime = [" mut i = 0 while i < ctx.port.deps.runtime_count if i > 0 content = content + ", " end if content = content + "\"" + ctx.port.deps.runtime[i] + "\"" i = i + 1 end while content = content + "]\n" end if if has_build content = content + "build = [" mut i = 0 while i < ctx.port.deps.build_count if i > 0 content = content + ", " end if content = content + "\"" + ctx.port.deps.build[i] + "\"" i = i + 1 end while content = content + "]\n" end if end if return res.is_ok(file.writeFile(pkginfo_path, content)) end generate_pkginfo fn generate_manifest_file(ctx: BuildContext): bool let manifest_path = path.join_path(ctx.pkg_dir, ".MANIFEST") // Generate mtree manifest using native fs operations // BUG-033 (GC candidate overflow) fixed in Reef 0.5.6 // uid/gid resolution now uses zero-allocation stat.uid_name()/gid_name() let content = mtree.generate_manifest(ctx.pkg_dir) if str.length(content) == 0 return false end if return res.is_ok(file.writeFile(manifest_path, content)) end generate_manifest_file proc cleanup_build(ctx: BuildContext) // Check if cleanup is disabled by --noclean flag if ctx.noclean color.print_info("Skipping cleanup (--noclean specified)") return end if // Determine cleanup behavior from config and flags mut clean_work = config.get_clean_work(ctx.cfg) mut clean_pkg = config.get_clean_pkg(ctx.cfg) // --clean flag forces cleanup if ctx.force_clean clean_work = true clean_pkg = true end if // Clean work directory if clean_work and dir.dir_exists(ctx.work_dir) color.print_info("Cleaning work directory: " + ctx.work_dir) let cmd = "rm -rf \"" + ctx.work_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 let exit_code = process.process_wait(pid) if exit_code == 0 color.print_success("Cleaned work directory") else color.print_warning("Failed to clean work directory") end if end if end if // Clean pkg directory (DESTDIR staging) if clean_pkg and dir.dir_exists(ctx.pkg_dir) color.print_info("Cleaning pkg directory: " + ctx.pkg_dir) let cmd = "rm -rf \"" + ctx.pkg_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 let exit_code = process.process_wait(pid) if exit_code == 0 color.print_success("Cleaned pkg directory") else color.print_warning("Failed to clean pkg directory") end if end if end if end cleanup_build // Helper: extract filename from URL fn extract_filename(url: string): string let last_slash = str.last_index_of_char(url, '/') if last_slash < 0 return url end if let filename = str.substring(url, last_slash + 1, str.length(url) - last_slash - 1) // Remove query string if present let query_pos = str.index_of_char(filename, '?') if query_pos > 0 return str.substring(filename, 0, query_pos) end if return filename end extract_filename // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: clean.reef Authors: Chris Tusa License: Description: Clean command - remove build artifacts and caches ******************************************************************************/ module commands.clean import sys.args import sys.process import io.file import io.dir import io.path import core.str import core.config import types import util.color import exitcodes as ec import core.result as res export fn execute(): int end export fn execute(): int let argc = args.count() if argc < 3 print_usage() return ec.EXIT_USAGE() end if let subcommand = args.get(2) // Load config let cfg = config.load() if subcommand == "status" return show_status(cfg) elif subcommand == "sources" return clean_sources(cfg) elif subcommand == "packages" return clean_packages(cfg) elif subcommand == "build" return clean_build(cfg) elif subcommand == "all" return clean_all(cfg) else color.print_error("Unknown subcommand: " + subcommand) print_usage() return ec.EXIT_USAGE() end if end execute fn show_status(cfg: config.Config): int color.print_action("Build artifact status:") println("") // Built packages let packages_dir = config.get_packages_dir(cfg) let pkg_count = count_files(packages_dir, ".pkg.tar.xz") mut pkg_protected = "" if config.get_keep_packages(cfg) pkg_protected = " [protected]" end if println(" Packages (" + packages_dir + "):" + pkg_protected) println(" " + int_to_str(pkg_count) + " built package(s)") // Source tarballs let sources_dir = config.get_sources_dir(cfg) let src_count = count_files(sources_dir, "") mut src_protected = "" if config.get_keep_sources(cfg) src_protected = " [protected]" end if println(" Sources (" + sources_dir + "):" + src_protected) println(" " + int_to_str(src_count) + " source archive(s)") // Build workspace let build_dir = config.get_build_dir(cfg) let build_count = count_subdirs(build_dir) println(" Build (" + build_dir + "):") println(" " + int_to_str(build_count) + " build workspace(s)") return ec.EXIT_SUCCESS() end show_status fn clean_sources(cfg: config.Config): int // Respect keep_sources config unless --force is passed if config.get_keep_sources(cfg) and not args.has_flag("force") color.print_warning("Source cache is configured to be preserved (keep_sources = true)") color.print_info("Use --force to override") return ec.EXIT_SUCCESS() end if let sources_dir = config.get_sources_dir(cfg) color.print_action("Cleaning source archives...") println(" Location: " + sources_dir) if not dir.dir_exists(sources_dir) color.print_info("No sources directory to clean") return ec.EXIT_SUCCESS() end if let cmd = "rm -rf \"" + sources_dir + "\"/*" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if color.print_success("Source archives cleaned") return ec.EXIT_SUCCESS() end clean_sources fn clean_packages(cfg: config.Config): int // Respect keep_packages config unless --force is passed if config.get_keep_packages(cfg) and not args.has_flag("force") color.print_warning("Package cache is configured to be preserved (keep_packages = true)") color.print_info("Use --force to override") return ec.EXIT_SUCCESS() end if let packages_dir = config.get_packages_dir(cfg) color.print_action("Cleaning built packages...") println(" Location: " + packages_dir) if not dir.dir_exists(packages_dir) color.print_info("No packages directory to clean") return ec.EXIT_SUCCESS() end if let cmd = "rm -rf \"" + packages_dir + "\"/*" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if color.print_success("Built packages cleaned") return ec.EXIT_SUCCESS() end clean_packages fn clean_build(cfg: config.Config): int let build_dir = config.get_build_dir(cfg) color.print_action("Cleaning build workspaces...") println(" Location: " + build_dir) if not dir.dir_exists(build_dir) color.print_info("No build directory to clean") return ec.EXIT_SUCCESS() end if let cmd = "rm -rf \"" + build_dir + "\"/*" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if color.print_success("Build workspaces cleaned") return ec.EXIT_SUCCESS() end clean_build fn clean_all(cfg: config.Config): int color.print_action("Cleaning all build artifacts...") println("") clean_sources(cfg) clean_packages(cfg) clean_build(cfg) println("") color.print_success("All build artifacts cleaned") return ec.EXIT_SUCCESS() end clean_all fn count_files(dir_path: string, extension: string): int if not dir.dir_exists(dir_path) return 0 end if let entries = res.unwrap_or(dir.list_dir(dir_path), new [string](0)) let count = entries.length() if str.length(extension) == 0 return count end if mut matched = 0 mut i = 0 while i < count if str.ends_with(entries[i], extension) matched = matched + 1 end if i = i + 1 end while return matched end count_files fn count_subdirs(dir_path: string): int if not dir.dir_exists(dir_path) return 0 end if let entries = res.unwrap_or(dir.list_dir(dir_path), new [string](0)) let count = entries.length() // Count only directories (packages being built) mut dirs = 0 mut i = 0 while i < count let entry = entries[i] if str.length(entry) > 0 and entry[0] != '.' let full_path = path.join_path(dir_path, entry) if dir.dir_exists(full_path) dirs = dirs + 1 end if end if i = i + 1 end while return dirs end count_subdirs proc print_usage() println("Usage: coral clean ") println("") println("Remove build artifacts and cached files.") println("") println("Targets:") println(" status Show what can be cleaned") println(" sources Remove downloaded source archives") println(" packages Remove built binary packages") println(" build Remove build workspaces") println(" all Remove all of the above") println("") println("Locations:") println(" sources: /usr/zports/pkgs/sources/") println(" packages: /usr/zports/pkgs/packages/") println(" build: /usr/zports/pkgs/build/") println("") println("Options:") println(" --force Override keep_packages/keep_sources config protection") println("") println("Note: This does NOT affect installed packages or the package database.") println(" Use 'coral remove' to uninstall packages.") println("") println("Examples:") println(" coral clean status") println(" coral clean build") println(" coral clean all") end print_usage // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: db.reef Authors: Chris Tusa License: Description: Database command - manage package database indexes ******************************************************************************/ module commands.db import sys.args import io.file import io.dir import core.str import core.config import core.pkgdb import core.portindex import types import util.color import exitcodes as ec export fn execute(opts: types.GlobalOptions): int end export fn execute(opts: types.GlobalOptions): int let argc = args.count() // Subcommand should be at cmd_index + 1 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 let subcommand = args.get(opts.cmd_index + 1) if subcommand == "init" return db_init(root_path) elif subcommand == "rebuild" return db_rebuild(root_path) elif subcommand == "status" return db_status(root_path) elif subcommand == "dump" return db_dump(root_path) else color.print_error("Unknown subcommand: " + subcommand) print_usage() return ec.EXIT_USAGE() end if end execute fn db_init(root_path: string): int if str.length(root_path) > 0 color.print_action("Initializing package database in " + root_path + "...") else color.print_action("Initializing package database...") end if println("") // Check if database already exists (use rooted version if root is set) let exists = check_indexes_exist(root_path) if exists color.print_warning("Database indexes already exist") color.print_info("Use 'coral db rebuild' to regenerate indexes") return ec.EXIT_SUCCESS() end if // Initialize the database directory (use rooted version if root is set) if not init_pkgdb(root_path) color.print_error("Failed to create database directory") return ec.EXIT_DB_ERROR() end if // Build initial indexes (use rooted version if root is set) if not rebuild_pkgdb(root_path) color.print_error("Failed to build initial indexes") return ec.EXIT_DB_ERROR() end if // Build ports index portindex.rebuild_port_index() let stats = get_pkgdb_stats(root_path) let port_count = portindex.get_port_count() color.print_success("Database initialized successfully") println(" Packages indexed: " + int_to_str(stats.package_count)) println(" Files indexed: " + int_to_str(stats.file_count)) println(" Ports indexed: " + int_to_str(port_count)) return ec.EXIT_SUCCESS() end db_init fn db_rebuild(root_path: string): int if str.length(root_path) > 0 color.print_action("Rebuilding package database indexes in " + root_path + "...") else color.print_action("Rebuilding package database indexes...") end if println("") // Check if db directory exists (use rooted version if root is set) if not init_pkgdb(root_path) color.print_error("Failed to access database directory") color.print_info("Run 'coral db init' first") return ec.EXIT_DB_ERROR() end if // Rebuild indexes from source of truth (use rooted version if root is set) if not rebuild_pkgdb(root_path) color.print_error("Failed to rebuild indexes") return ec.EXIT_DB_ERROR() end if // Rebuild ports index portindex.rebuild_port_index() let stats = get_pkgdb_stats(root_path) let port_count = portindex.get_port_count() color.print_success("Database indexes rebuilt successfully") println(" Packages indexed: " + int_to_str(stats.package_count)) println(" Files indexed: " + int_to_str(stats.file_count)) println(" Ports indexed: " + int_to_str(port_count)) return ec.EXIT_SUCCESS() end db_rebuild fn db_dump(root_path: string): int color.print_action("Dumping files index contents...") println("") mut paths: [string] = new [string](256) mut owners: [string] = new [string](256) let count = pkgdb.dump_files_index_rooted(root_path, paths, owners, 256) if count == 0 color.print_warning("No files in index or index not loaded") return ec.EXIT_DB_ERROR() end if println("Files in index: " + int_to_str(count)) println("") mut i = 0 while i < count println(" [" + int_to_str(i) + "] '" + paths[i] + "' -> '" + owners[i] + "'") i = i + 1 end while return ec.EXIT_SUCCESS() end db_dump fn db_status(root_path: string): int if str.length(root_path) > 0 color.print_action("Package database status for " + root_path + ":") else color.print_action("Package database status:") end if println("") let stats = get_pkgdb_stats(root_path) if not check_indexes_exist(root_path) color.print_warning("Database indexes not found") color.print_info("Run 'coral db init' to create indexes") println("") if str.length(root_path) > 0 println(" Source of truth: " + root_path + "/var/lib/coral/installed/") else println(" Source of truth: /var/lib/coral/installed/") end if println(" Packages installed: " + int_to_str(stats.package_count)) return ec.EXIT_DB_INDEX_STALE() end if if str.length(root_path) > 0 println(" Index location: " + root_path + types.get_index_dir()) else println(" Index location: " + types.get_index_dir()) end if println(" Packages indexed: " + int_to_str(stats.package_count)) println(" Files indexed: " + int_to_str(stats.file_count)) if stats.packages_db_size > 0 println(" packages.db size: " + format_size(stats.packages_db_size)) end if if stats.files_db_size > 0 println(" files.db size: " + format_size(stats.files_db_size)) end if // Ports index status if portindex.port_index_exists() let port_count = portindex.get_port_count() println(" Ports indexed: " + int_to_str(port_count)) else println(" Ports index: not built") end if return ec.EXIT_SUCCESS() end db_status // Helper functions for rooted pkgdb operations fn check_indexes_exist(root_path: string): bool if str.length(root_path) > 0 return pkgdb.indexes_exist_rooted(root_path) end if return pkgdb.indexes_exist() end check_indexes_exist fn init_pkgdb(root_path: string): bool if str.length(root_path) > 0 return pkgdb.init_db_rooted(root_path) end if return pkgdb.init_db() end init_pkgdb fn rebuild_pkgdb(root_path: string): bool if str.length(root_path) > 0 return pkgdb.rebuild_indexes_rooted(root_path) end if return pkgdb.rebuild_indexes() end rebuild_pkgdb fn get_pkgdb_stats(root_path: string): pkgdb.IndexStats if str.length(root_path) > 0 return pkgdb.get_index_stats_rooted(root_path) end if return pkgdb.get_index_stats() end get_pkgdb_stats fn format_size(bytes: int): string if bytes < 1024 return int_to_str(bytes) + " B" elif bytes < 1024 * 1024 return int_to_str(bytes / 1024) + " KB" elif bytes < 1024 * 1024 * 1024 return int_to_str(bytes / (1024 * 1024)) + " MB" else return int_to_str(bytes / (1024 * 1024 * 1024)) + " GB" end if end format_size proc print_usage() println("Usage: coral db ") println("") println("Manage package database indexes.") println("") println("Commands:") println(" init Initialize database (run once during system setup)") println(" rebuild Rebuild indexes from installed packages") println(" status Show database status and statistics") println("") println("The package database provides fast lookups for installed packages") println("and file ownership. The source of truth is always the installed/") println("directory; indexes can be safely rebuilt at any time.") println("") println("Examples:") println(" coral db init") println(" coral db rebuild") println(" coral db status") end print_usage // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: depends.reef Authors: Chris Tusa License: Description: Depends command - show package dependency tree ******************************************************************************/ module commands.depends import sys.args import core.str import core.port import core.portindex import core.database import core.resolver import types import util.color import exitcodes as ec export fn execute(): int end export fn execute(): int let argc = args.count() if argc < 3 print_usage() return ec.EXIT_USAGE() end if let pkg_name = args.get(2) // Check for --reverse flag (show what depends on this package) if args.has_flag("reverse") or args.has_flag("r") show_reverse_deps(pkg_name) return ec.EXIT_SUCCESS() end if // Show forward dependencies show_deps(pkg_name) return ec.EXIT_SUCCESS() end execute proc show_deps(name: string) // Find the port let result = port.find(name) if not result.success color.print_error("Port not found: " + name) return end if let p = result.port color.print_action("Dependencies for " + name + ":") println("") // Show runtime dependencies if p.deps.runtime_count == 0 color.print_info("No runtime dependencies") else color.print_info("Runtime dependencies:") show_dep_tree(name, 0) end if println("") // Show build dependencies if p.deps.build_count > 0 color.print_info("Build dependencies:") mut i = 0 while i < p.deps.build_count print(" ") let dep = p.deps.build[i] print(dep) if database.is_installed(dep) println(" [installed]") else println("") end if i = i + 1 end while end if end show_deps proc show_dep_tree(name: string, depth: int) if depth > 10 return end if let result = port.find(name) if not result.success return end if let p = result.port mut i = 0 while i < p.deps.runtime_count let dep = p.deps.runtime[i] // Print with indentation print_indent(depth) print(dep) if database.is_installed(dep) println(" [installed]") else println("") end if // Recurse show_dep_tree(dep, depth + 1) i = i + 1 end while end show_dep_tree proc show_reverse_deps(name: string) color.print_action("Packages that depend on " + name + ":") println("") mut found = 0 // Try ports index first (fast path) if portindex.port_index_exists() mut results: [portindex.PortIndexEntry] = new [portindex.PortIndexEntry](512) let result_count = portindex.find_reverse_deps(name, results, 512) mut i = 0 while i < result_count let entry = results[i] print(" ") print(entry.category) print("/") print(entry.name) if database.is_installed(entry.name) println(" [installed]") else println("") end if found = found + 1 i = i + 1 end while else // Fall back to filesystem scan mut cats: [string] = new [string](32) let cat_count = port.get_categories(cats, 32) mut i = 0 while i < cat_count let category = cats[i] mut ports: [string] = new [string](256) let port_count = port.list_category(category, ports, 256) mut j = 0 while j < port_count let port_name = ports[j] if depends_on(port_name, name) print(" ") print(category) print("/") print(port_name) if database.is_installed(port_name) println(" [installed]") else println("") end if found = found + 1 end if j = j + 1 end while i = i + 1 end while end if if found == 0 color.print_info("No packages depend on " + name) else println("") color.print_info("Total: " + int_to_str(found) + " package(s)") end if end show_reverse_deps fn depends_on(pkg_name: string, dep_name: string): bool let result = port.find(pkg_name) if not result.success return false end if let p = result.port // Check runtime deps mut i = 0 while i < p.deps.runtime_count if p.deps.runtime[i] == dep_name return true end if i = i + 1 end while // Check build deps i = 0 while i < p.deps.build_count if p.deps.build[i] == dep_name return true end if i = i + 1 end while return false end depends_on proc print_indent(depth: int) mut i = 0 while i < depth print(" ") i = i + 1 end while print(" ") // Base indent end print_indent proc print_usage() println("Usage: coral depends [options]") println("") println("Show dependency tree for a package.") println("") println("Arguments:") println(" Name of package to show dependencies for") println("") println("Options:") println(" -r, --reverse Show packages that depend on this package") println("") println("Examples:") println(" coral depends vim") println(" coral depends vim --reverse") end print_usage // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: files.reef Authors: Chris Tusa License: Description: Files command - list files owned by a package ******************************************************************************/ module commands.files import sys.args import core.str import core.config import core.database import types import util.color import exitcodes as ec export fn execute(opts: types.GlobalOptions): int end export fn execute(opts: types.GlobalOptions): int let argc = args.count() // Package name should be at cmd_index + 1 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 let pkg_name = args.get(opts.cmd_index + 1) // Check if 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 ec.EXIT_PKG_NOT_FOUND() end if // Get files (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_info("No files recorded for " + pkg_name) return ec.EXIT_SUCCESS() end if color.print_action("Files owned by " + pkg_name + ":") println("") mut i = 0 while i < file_count println("/" + files[i]) i = i + 1 end while println("") color.print_info("Total: " + int_to_str(file_count) + " files") return ec.EXIT_SUCCESS() end execute // Helper functions for rooted operations 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 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 proc print_usage() println("Usage: coral files ") println("") println("List all files owned by an installed package.") println("") println("Arguments:") println(" Name of installed package") println("") println("Examples:") println(" coral files vim") println(" coral files hello") end print_usage // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: info.reef Authors: Chris Tusa License: Description: Info command - display package information ******************************************************************************/ module commands.info import sys.args import core.str import core.config import core.database import core.port import types import util.color import exitcodes as ec export fn execute(opts: types.GlobalOptions): int end export fn execute(opts: types.GlobalOptions): int let argc = args.count() // Package name should be at cmd_index + 1 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 let pkg_name = args.get(opts.cmd_index + 1) // Check if installed (use rooted version if root is set) let is_installed = check_installed(pkg_name, root_path) // Find port let port_result = port.find(pkg_name) if not is_installed and not port_result.success color.print_error("Package not found: " + pkg_name) return ec.EXIT_PKG_NOT_FOUND() end if // Print package info if is_installed let installed_pkg = get_installed_pkg(pkg_name, root_path) print_installed_info(installed_pkg) if port_result.success let p = port_result.port // Check if upgrade available if str.compare(installed_pkg.info.version, p.info.version) < 0 println("") color.print_info("Upgrade available: " + p.info.version) end if end if else // Not installed, show port info let p = port_result.port print_port_info(p) end if return ec.EXIT_SUCCESS() end execute // Helper functions for rooted operations 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 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 proc print_usage() println("Usage: coral info ") println("") println("Display detailed information about a package.") println("") println("Arguments:") println(" Name of package to show info for") println("") println("Examples:") println(" coral info vim") println(" coral info hello") end print_usage proc print_installed_info(pkg: types.InstalledPackage) color.print_action("Package: " + pkg.info.name) println("") println("Status: installed") println("Version: " + pkg.info.version + "-" + int_to_str(pkg.info.release)) println("Description: " + pkg.info.description) println("URL: " + pkg.info.url) println("License: " + pkg.info.license) println("Maintainer: " + pkg.info.maintainer) println("Architecture: " + pkg.info.arch) println("") println("Install date: " + pkg.install_date) println("Reason: " + pkg.install_reason) println("Files: " + int_to_str(pkg.files_count)) end print_installed_info proc print_port_info(p: types.Port) color.print_action("Port: " + p.info.name) println("") println("Status: not installed") println("Version: " + p.info.version + "-" + int_to_str(p.info.release)) println("Description: " + p.info.description) println("URL: " + p.info.url) println("License: " + p.info.license) println("Maintainer: " + p.info.maintainer) println("Architecture: " + p.info.arch) // Show dependencies if any if p.deps.runtime_count > 0 println("") print("Depends on: ") mut i = 0 while i < p.deps.runtime_count if i > 0 print(", ") end if print(p.deps.runtime[i]) i = i + 1 end while println("") end if if p.deps.build_count > 0 println("") print("Build deps: ") mut i = 0 while i < p.deps.build_count if i > 0 print(", ") end if print(p.deps.build[i]) i = i + 1 end while println("") end if end print_port_info // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: install.reef Authors: Chris Tusa License: Description: Install command - install packages from archives ******************************************************************************/ module commands.install 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.groups import core.repository import core.signing import core.resolver import core.port import core.portindex import util.checksum import util.mtree import util.http as uhttp import util.prompt import util.version as ver import types import util.color 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 start 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) uhttp.enable_proxy(config.get_use_proxy(cfg)) // Auto-refresh port index if configured if config.get_auto_refresh(cfg) portindex.rebuild_port_index() end if // Show root/prefix if set (verbose mode) if opts.verbose let root = config.get_root(cfg) let prefix = config.get_prefix(cfg) if str.length(root) > 0 color.print_info("Using root: " + root) end if if str.length(prefix) > 0 color.print_info("Using prefix: " + prefix) end if end if // Collect all packages to install (expanding groups) mut packages: [string] = new [string](256) mut pkg_count = 0 mut direct_files: [string] = new [string](16) mut direct_count = 0 // Process each package argument (starting after the command) mut i = opts.cmd_index + 1 while i < argc and pkg_count < 256 let pkg_arg = args.get(i) // Skip flags and their values if pkg_arg == "--root" or pkg_arg == "--prefix" i = i + 2 // Skip flag and its value continue elif pkg_arg == "--no-chroot-scripts" i = i + 1 // valueless flag: skip only itself continue end if if str.starts_with(pkg_arg, "-") i = i + 1 continue end if // Check if this is a direct file path to a .pkg.tar.xz if str.ends_with(pkg_arg, ".pkg.tar.xz") and file.fileExists(pkg_arg) direct_files[direct_count] = pkg_arg direct_count = direct_count + 1 i = i + 1 continue end if // Check if this is a group reference if groups.is_group(pkg_arg) // Expand group to member packages mut group_members: [string] = new [string](128) let member_count = groups.expand_group(pkg_arg, group_members, 128) if member_count == 0 color.print_error("Group not found or empty: " + pkg_arg) i = i + 1 continue end if color.print_info("Expanding group " + pkg_arg + " (" + int_to_str(member_count) + " packages)") // Add all group members to packages list mut j = 0 while j < member_count and pkg_count < 256 packages[pkg_count] = group_members[j] pkg_count = pkg_count + 1 j = j + 1 end while else // Regular package packages[pkg_count] = pkg_arg pkg_count = pkg_count + 1 end if i = i + 1 end while // Handle direct file installs — bypass resolver, install immediately if direct_count > 0 let root = get_root(cfg) let root_for_db = config.get_root(cfg) if str.length(root_for_db) > 0 if not database.init_db_rooted(root_for_db) color.print_error("Failed to initialize database in " + root_for_db) return ec.EXIT_DB_ERROR() end if else if not database.init_db() color.print_error("Failed to initialize package database") return ec.EXIT_DB_ERROR() end if end if mut di = 0 while di < direct_count if not install_package(direct_files[di], cfg, root_for_db, opts) color.print_error("Failed to install: " + direct_files[di]) return ec.EXIT_INSTALL_FAILED() end if di = di + 1 end while if pkg_count == 0 return ec.EXIT_SUCCESS() end if end if // Resolve dependencies for all packages // Use rooted resolver if installing to alternate root mut install_order: [string] = new [string](512) let root_for_resolve = config.get_root(cfg) mut install_count = 0 if str.length(root_for_resolve) > 0 install_count = resolver.get_install_order_rooted(packages, pkg_count, install_order, 512, root_for_resolve) else install_count = resolver.get_install_order(packages, pkg_count, install_order, 512) end if // When force/reinstall is set, add requested packages that the resolver // skipped (because they're already installed) back into the install order if opts.force and install_count < pkg_count mut fi = 0 while fi < pkg_count let pkg = packages[fi] if not array_contains(install_order, install_count, pkg) install_order[install_count] = pkg install_count = install_count + 1 end if fi = fi + 1 end while end if if install_count == 0 and pkg_count > 0 color.print_warning("No packages to install (all already installed or not found)") return ec.EXIT_SUCCESS() end if // Separate new installs from already-installed (for display) mut to_install: [string] = new [string](512) mut install_idx = 0 mut dep_idx = 0 while dep_idx < install_count to_install[install_idx] = install_order[dep_idx] install_idx = install_idx + 1 dep_idx = dep_idx + 1 end while // Ask for confirmation (unless -y flag) mut empty_upgrade: [string] = new [string](1) if not prompt.confirm_install(to_install, install_idx, empty_upgrade, 0, opts) color.print_info("Installation 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 // Get root path for installation let root = get_root(cfg) // Initialize database in the target root let root_for_db = config.get_root(cfg) if str.length(root_for_db) > 0 if not database.init_db_rooted(root_for_db) color.print_error("Failed to initialize database in " + root_for_db) return ec.EXIT_DB_ERROR() end if else if not database.init_db() color.print_error("Failed to initialize package database") return ec.EXIT_DB_ERROR() end if end if // Install all packages in resolved order (dependencies first) i = 0 while i < install_count let pkg_arg = install_order[i] // Install this package if not install_package(pkg_arg, cfg, root_for_db, opts) color.print_error("Failed to install: " + pkg_arg) color.print_error("Aborting installation - dependency failed") return ec.EXIT_INSTALL_FAILED() end if i = i + 1 end while return ec.EXIT_SUCCESS() end execute // 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 proc print_usage() println("Usage: coral install ") println("") println("Install one or more packages.") println("") println("Arguments:") println(" Package name or path to .pkg.tar.xz file") println("") println("Options:") println(" -y, --yes Don't ask for confirmation") println(" --force Force reinstall if already installed") println("") println("Examples:") println(" coral install vim") println(" coral install /path/to/package.pkg.tar.xz") println(" coral install vim nano htop") end print_usage fn install_package(pkg_arg: string, cfg: config.Config, root_path: string, opts: types.GlobalOptions): bool // Determine if this is a file path or package name let pkg_path = resolve_package_path(pkg_arg, cfg) if str.length(pkg_path) == 0 color.print_error("Package not found: " + pkg_arg) return false end if // Verify signature if required (for local packages not already verified by resolve_package_path) let require_signed = config.get_require_signed_packages(cfg) if require_signed let sig_path = pkg_path + ".sig" if file.fileExists(sig_path) color.print_info("Verifying package signature...") let verify_result = signing.verify_package(pkg_path, sig_path) if not verify_result.success or not verify_result.valid color.print_error("Signature verification failed: " + verify_result.error) return false end if color.print_success("Signature verified (key: " + verify_result.keyid + ")") else color.print_error("Package signature required but not found: " + sig_path) return false end if end if color.print_action("Installing " + pkg_arg + "...") // Create extraction directory let extract_dir = create_extract_dir(cfg) if str.length(extract_dir) == 0 color.print_error("Failed to create extraction directory") return false end if // Extract package color.print_info("Extracting package...") let result = package.extract_package(pkg_path, extract_dir) if not result.success color.print_error(result.error) cleanup_extract_dir(extract_dir) return false end if // Read package info let info = package.read_pkginfo(extract_dir) if str.length(info.name) == 0 color.print_error("Failed to read package info") cleanup_extract_dir(extract_dir) return false end if color.print_info("Package: " + info.name + " " + info.version) // Check for conflicts with installed packages if info.conflicts_count > 0 mut ci = 0 while ci < info.conflicts_count let conflict = info.conflicts[ci] if check_installed(conflict, root_path) color.print_error("Package " + info.name + " conflicts with installed package: " + conflict) if not opts.force color.print_error("Use --force to override conflict check") cleanup_extract_dir(extract_dir) return false end if color.print_warning("Proceeding despite conflict (--force specified)") end if ci = ci + 1 end while end if // Check if already installed (in target root) let already_installed = check_installed(info.name, root_path) mut is_upgrade = false mut old_version = "" if already_installed // Get old package info to check version let old_pkg = get_old_installed(info.name, root_path) old_version = old_pkg.info.version if not str.equals(old_version, info.version) // Different version — upgrade is_upgrade = true color.print_info("Upgrading " + info.name + " " + old_version + " -> " + info.version) else // Same version — skip or reinstall with --force if not opts.force color.print_warning(info.name + " " + info.version + " is already installed (use --force to reinstall)") cleanup_extract_dir(extract_dir) return true end if color.print_info("Reinstalling " + info.name + "...") end if end if let install_root = get_root(cfg) // 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).") cleanup_extract_dir(extract_dir) return false end if let script_ctx = package.resolve_script_ctx(install_root, config.get_chroot_scripts(cfg), priv.is_root()) if is_upgrade // Upgrade flow: pre-upgrade → remove old files → install new files → post-upgrade // Run pre-upgrade from stored scripts (old package) let old_scripts = database.get_scripts_dir(info.name) if database.has_stored_scripts(info.name) color.print_info("Running pre-upgrade script...") if not package.run_stored_pre_upgrade(script_ctx, old_scripts, old_version, info.version) color.print_error("Pre-upgrade script failed, aborting") cleanup_extract_dir(extract_dir) return false end if end if // Remove old files color.print_info("Removing old version files...") mut old_files: [string] = new [string](4096) let old_file_count = get_old_files(info.name, old_files, 4096, root_path) if old_file_count > 0 package.remove_files(old_files, old_file_count, install_root) end if // Load old manifest for config file checksum protection mut old_manifest_entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) let old_manifest_count = load_old_manifest(info.name, root_path, old_manifest_entries, 8192) // Install new files (with upgrade config protection) color.print_info("Installing files...") if old_manifest_count > 0 if not package.install_files_upgrade(extract_dir, install_root, old_manifest_entries, old_manifest_count) color.print_error("Failed to install files") cleanup_extract_dir(extract_dir) return false end if else if not package.install_files(extract_dir, install_root) color.print_error("Failed to install files") cleanup_extract_dir(extract_dir) return false end if end if // Run post-upgrade from new package's scripts if package.has_scripts(extract_dir) color.print_info("Running post-upgrade script...") if not package.run_post_upgrade(script_ctx, extract_dir, old_version, info.version) color.print_warning("Post-upgrade script failed") end if end if else // Fresh install flow // Run pre-install script (abort on failure) if package.has_scripts(extract_dir) color.print_info("Running pre-install script...") if not package.run_pre_install(script_ctx, extract_dir, info.version) color.print_error("Pre-install script failed") cleanup_extract_dir(extract_dir) return false end if end if // Install files to target root color.print_info("Installing files...") if not package.install_files(extract_dir, install_root) color.print_error("Failed to install files") cleanup_extract_dir(extract_dir) return false end if // Run post-install script (warn on failure, don't abort) if package.has_scripts(extract_dir) color.print_info("Running post-install script...") if not package.run_post_install(script_ctx, extract_dir, info.version) color.print_warning("Post-install script failed") end if end if end if // Register in database (use rooted version if root is set) color.print_info("Registering package...") if not register_installed_fast_rooted(info, extract_dir, root_path) color.print_error("Failed to register package in database") // Files are installed but not registered - warn but don't fail end if // Store scripts for later use (removal) if package.has_scripts(extract_dir) if not store_scripts_rooted(info.name, extract_dir, root_path) color.print_warning("Failed to store scripts for removal") end if end if // Store config files list for upgrade handling if not store_config_files_rooted(info.name, extract_dir, root_path) color.print_warning("Failed to store config files list") end if // Update package database indexes if not pkgdb.rebuild_indexes_rooted(root_path) color.print_warning("Failed to update package indexes") end if // Cleanup cleanup_extract_dir(extract_dir) color.print_success("Installed " + info.name + " " + info.version) return true end install_package // 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 // Register package using rooted version if root is set fn register_installed_fast_rooted(info: types.PackageInfo, extract_dir: string, root_path: string): bool mut files: [string] = new [string](1) mut pkg = new_installed_pkg(info, files, 0) if str.length(root_path) > 0 return database.register_from_extract_dir_rooted(pkg, extract_dir, root_path) end if return database.register_from_extract_dir(pkg, extract_dir) end register_installed_fast_rooted // Store scripts using rooted version if root is set fn store_scripts_rooted(name: string, extract_dir: string, root_path: string): bool if str.length(root_path) > 0 return database.store_scripts_rooted(name, extract_dir, root_path) end if return database.store_scripts(name, extract_dir) end store_scripts_rooted // Store config files using rooted version if root is set fn store_config_files_rooted(name: string, extract_dir: string, root_path: string): bool if str.length(root_path) > 0 return database.store_config_files_rooted(name, extract_dir, root_path) end if return database.store_config_files(name, extract_dir) end store_config_files_rooted // Find package file - either direct path, repository, or search in packages directory fn resolve_package_path(pkg_arg: string, cfg: config.Config): string // If it's a file path that exists, use it directly if file.fileExists(pkg_arg) return pkg_arg end if // Search in local packages cache first let packages_dir = config.get_packages_dir(cfg) // Find all matching packages and select highest version let entries = res.unwrap_or(dir.list_dir(packages_dir), new [string](0)) let entry_count = entries.length() // Collect all matching package files mut best_file = "" mut best_version = "" mut best_release = 0 mut i = 0 while i < entry_count let entry = entries[i] // Check if entry starts with package name and has correct suffix if str.starts_with(entry, pkg_arg + "-") and str.ends_with(entry, ".pkg.tar.xz") // Extract version from filename let entry_version = ver.extract_version_from_filename(entry, pkg_arg) let entry_release = ver.extract_release_from_filename(entry) // Compare with current best if str.length(best_file) == 0 // First match best_file = entry best_version = entry_version best_release = entry_release else // Compare versions let cmp = ver.compare_versions(entry_version, best_version) if cmp > 0 // Entry has higher version best_file = entry best_version = entry_version best_release = entry_release elif cmp == 0 and entry_release > best_release // Same version but higher release best_file = entry best_version = entry_version best_release = entry_release end if end if end if i = i + 1 end while // Return the best matching package if str.length(best_file) > 0 return path.join_path(packages_dir, best_file) end if // Also try with .pkg.tar.xz appended let direct_path = path.join_path(packages_dir, pkg_arg + ".pkg.tar.xz") if file.fileExists(direct_path) return direct_path end if // Try to build from source if configured if config.get_fallback_to_source(cfg) let port_result = port.find(pkg_arg) if port_result.success color.print_info("No binary package found for " + pkg_arg + ", building from source...") let build_cmd = "coral build -y " + pkg_arg let build_pid = process.process_spawn_shell(build_cmd) if build_pid > 0 let build_exit = process.process_wait(build_pid) if build_exit == 0 // Build succeeded, look for the package it produced let p = port_result.port let built_pkg = p.info.name + "-" + p.info.version + "-" + int_to_str(p.info.release) + ".pkg.tar.xz" let built_path = path.join_path(packages_dir, built_pkg) if file.fileExists(built_path) return built_path end if end if end if color.print_warning("Source build failed for " + pkg_arg + ", trying repositories...") end if end if // Try to find in repositories and download let remote_result = repository.find_remote_package(pkg_arg) if remote_result.success let remote_pkg = remote_result.pkg color.print_info("Found " + pkg_arg + " " + remote_pkg.version + " in repository: " + remote_pkg.repo_name) // Download the package let downloaded_path = repository.download_package(remote_pkg) if str.length(downloaded_path) > 0 // Verify checksum if available if str.length(remote_pkg.sha256) > 0 color.print_info("Verifying checksum...") if not checksum.verify_checksum(downloaded_path, remote_pkg.sha256) color.print_error("Checksum verification failed!") return "" end if color.print_success("Checksum verified") end if // Verify signature if required or available let require_signed = config.get_require_signed_packages(cfg) if require_signed or str.length(remote_pkg.signature) > 0 let sig_path = downloaded_path + ".sig" // Download signature file if we don't have it if not file.fileExists(sig_path) and str.length(remote_pkg.signature) > 0 color.print_info("Downloading signature...") let sig_url = get_sig_url(remote_pkg) if not download_signature(sig_url, sig_path) if require_signed color.print_error("Failed to download package signature") return "" else color.print_warning("Could not download signature, skipping verification") end if end if end if // Verify signature if we have it if file.fileExists(sig_path) color.print_info("Verifying signature...") let verify_result = signing.verify_package(downloaded_path, sig_path) if verify_result.success and verify_result.valid color.print_success("Signature verified (key: " + verify_result.keyid + ")") else if require_signed color.print_error("Signature verification failed: " + verify_result.error) return "" else color.print_warning("Signature verification failed: " + verify_result.error) end if end if elif require_signed color.print_error("Package signature required but not available") return "" end if end if return downloaded_path end if end if return "" end resolve_package_path // Create a temporary extraction directory fn create_extract_dir(cfg: config.Config): string let work_dir = config.get_work_dir(cfg) let pid = process.getpid() let extract_dir = path.join_path(work_dir, "install_" + int_to_str(pid)) if dir.dir_exists(extract_dir) // Clean existing let cmd = "rm -rf \"" + extract_dir + "\"" let rm_pid = process.process_spawn_shell(cmd) if rm_pid > 0 process.process_wait(rm_pid) end if end if if not res.is_ok(dir.create_dir_all(extract_dir)) return "" end if return extract_dir end create_extract_dir // Cleanup extraction directory fn cleanup_extract_dir(dir_path: string): bool if str.length(dir_path) == 0 return true end if let cmd = "rm -rf \"" + dir_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if process.process_wait(pid) return true end cleanup_extract_dir // Get installation root from config (can be overridden with --root) fn get_root(cfg: config.Config): string let root = config.get_root(cfg) if str.length(root) == 0 return "/" end if return root end get_root // Check for file conflicts with other installed packages // TODO: Implement efficient conflict detection using a file database fn check_conflicts(files: [string], file_count: int, pkg_name: string): bool // Skip conflict checking for performance - file overwrites are handled // by the manifest-driven installer and dependencies are managed separately return true end check_conflicts // Helper to create InstalledPackage fn new_installed_pkg(info: types.PackageInfo, files: [string], file_count: int): types.InstalledPackage mut pkg = types.new_installed_package() pkg.info = info pkg.install_date = get_date_string() pkg.install_reason = "explicit" pkg.files = files pkg.files_count = file_count return pkg end new_installed_pkg // Get current date as string (simplified) fn get_date_string(): string // Use date command for simplicity // TODO: Use proper time functions when available return "2025-01-24" end get_date_string // Get installed package info (root-aware) fn get_old_installed(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_old_installed // Get old file list for a package (root-aware) fn get_old_files(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_old_files // Load old manifest from database for upgrade config protection fn load_old_manifest(name: string, root_path: string, entries: [mtree.ManifestEntry], max_count: int): int let manifest_path = database.get_manifest_path(name) if not file.fileExists(manifest_path) return 0 end if let content = res.unwrap_or(file.readFile(manifest_path), "") if str.length(content) == 0 return 0 end if return mtree.parse_manifest(content, entries, max_count) end load_old_manifest // 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 // Build signature URL from remote package info fn get_sig_url(pkg: repository.RemotePackage): string mut base_url = pkg.repo_url if not str.ends_with(base_url, "/") base_url = base_url + "/" end if // If signature field contains a full URL, use it if str.starts_with(pkg.signature, "http") return pkg.signature end if // Otherwise construct from filename return base_url + "packages/" + pkg.filename + ".sig" end get_sig_url // Download a signature file fn download_signature(url: string, dest: string): bool return uhttp.download(url, dest) end download_signature end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: key.reef Authors: Chris Tusa License: Description: Key command - manage signing keys ******************************************************************************/ module commands.key import sys.args import sys.process import io.file import io.dir import core.str import core.signing import util.color import exitcodes as ec import core.result as res export fn execute(): int end export fn execute(): int let argc = args.count() if argc < 3 print_usage() return ec.EXIT_USAGE() end if let subcommand = args.get(2) if subcommand == "list" list_keys() return ec.EXIT_SUCCESS() elif subcommand == "generate" if argc < 4 color.print_error("Missing key name") println("Usage: coral key generate ") return ec.EXIT_USAGE() end if generate_key(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "import" if argc < 4 color.print_error("Missing key file") println("Usage: coral key import [name]") return ec.EXIT_USAGE() end if let key_file = args.get(3) mut name = "" if argc >= 5 name = args.get(4) end if import_key(key_file, name) return ec.EXIT_SUCCESS() elif subcommand == "export" if argc < 4 color.print_error("Missing key name") println("Usage: coral key export ") return ec.EXIT_USAGE() end if export_key(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "delete" if argc < 4 color.print_error("Missing key name") println("Usage: coral key delete ") return ec.EXIT_USAGE() end if delete_key(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "init" init_keyring() return ec.EXIT_SUCCESS() elif subcommand == "trust" if argc < 4 color.print_error("Missing key name") println("Usage: coral key trust ") return ec.EXIT_USAGE() end if trust_key(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "revoke" if argc < 4 color.print_error("Missing key name") println("Usage: coral key revoke ") return ec.EXIT_USAGE() end if revoke_key(args.get(3)) return ec.EXIT_SUCCESS() else color.print_error("Unknown subcommand: " + subcommand) print_usage() return ec.EXIT_USAGE() end if end execute proc list_keys() color.print_action("Signing keys:") println("") mut keys: [signing.KeyPair] = new [signing.KeyPair](64) let count = signing.list_keys(keys, 64) if count == 0 color.print_info("No keys found") println("") color.print_info("Generate a key with: coral key generate ") return end if mut i = 0 while i < count let key = keys[i] print(" ") print(key.name) if str.length(key.private_key) > 0 print(" [private+public]") else print(" [public only]") end if if key.trusted print(" [trusted]") end if println("") i = i + 1 end while println("") color.print_info("Total: " + int_to_str(count) + " key(s)") end list_keys proc generate_key(name: string) color.print_action("Generating Ed25519 keypair: " + name) let result = signing.generate_keypair(name) if result.success color.print_success("Keypair generated successfully") println("") color.print_info("Private key: " + signing.get_keyring_dir() + "/" + name + ".key") color.print_info("Public key: " + signing.get_keyring_dir() + "/" + name + ".pub") else color.print_error(result.error) end if end generate_key proc import_key(key_file: string, name: string) if not file.fileExists(key_file) color.print_error("Key file not found: " + key_file) return end if // Determine name from filename if not provided mut key_name = name if str.length(key_name) == 0 key_name = extract_name(key_file) end if if str.length(key_name) == 0 color.print_error("Could not determine key name. Please specify one.") return end if color.print_action("Importing key: " + key_name) let key_data = res.unwrap_or(file.readFile(key_file), "") let result = signing.import_key(key_data, key_name) if result.success color.print_success("Key imported successfully") else color.print_error(result.error) end if end import_key proc export_key(name: string) let keyring = signing.get_keyring_dir() let pub_path = keyring + "/" + name + ".pub" if not file.fileExists(pub_path) color.print_error("Key not found: " + name) return end if color.print_action("Public key for " + name + ":") println("") let key_data = res.unwrap_or(file.readFile(pub_path), "") println(key_data) end export_key proc delete_key(name: string) let keyring = signing.get_keyring_dir() // Check if key exists let pub_path = keyring + "/" + name + ".pub" let priv_path = keyring + "/" + name + ".key" if not file.fileExists(pub_path) and not file.fileExists(priv_path) color.print_error("Key not found: " + name) return end if // Ask for confirmation if private key exists if file.fileExists(priv_path) color.print_warning("This will delete the private key! Are you sure?") color.print_info("Use --force to confirm deletion") if not args.has_flag("force") return end if end if // Delete files if file.fileExists(pub_path) let cmd1 = "rm -f \"" + pub_path + "\"" let pid1 = process.process_spawn_shell(cmd1) if pid1 > 0 process.process_wait(pid1) end if end if if file.fileExists(priv_path) let cmd2 = "rm -f \"" + priv_path + "\"" let pid2 = process.process_spawn_shell(cmd2) if pid2 > 0 process.process_wait(pid2) end if end if color.print_success("Deleted key: " + name) end delete_key fn extract_name(path: string): string // Extract filename without extension let len = str.length(path) if len == 0 return "" end if // Find last slash mut start = 0 mut i = 0 while i < len if path[i] == '/' start = i + 1 end if i = i + 1 end while // Find last dot mut end_pos = len i = len - 1 while i >= start if path[i] == '.' end_pos = i end if i = i - 1 end while if end_pos <= start return str.substring(path, start, len - start) end if return str.substring(path, start, end_pos - start) end extract_name // Initialize the keyring directory structure proc init_keyring() color.print_action("Initializing keyring...") let keyring = signing.get_keyring_dir() let trusted_keys_dir = "/var/lib/coral/trusted-keys" // Create keyring directory if not dir.dir_exists(keyring) if not res.is_ok(dir.create_dir_all(keyring)) color.print_error("Failed to create keyring directory: " + keyring) return end if color.print_info("Created: " + keyring) else color.print_info("Keyring already exists: " + keyring) end if // Create trusted-keys directory if not dir.dir_exists(trusted_keys_dir) if not res.is_ok(dir.create_dir_all(trusted_keys_dir)) color.print_error("Failed to create trusted-keys directory") return end if color.print_info("Created: " + trusted_keys_dir) else color.print_info("Trusted keys directory already exists: " + trusted_keys_dir) end if // Set permissions (keyring should be root-only for private keys) let cmd = "chmod 700 \"" + keyring + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if color.print_success("Keyring initialized") color.print_info("Generate keys with: coral key generate ") end init_keyring // Add a key to the trusted keys list proc trust_key(name: string) let keyring = signing.get_keyring_dir() let trusted_dir = "/var/lib/coral/trusted-keys" let pub_path = keyring + "/" + name + ".pub" // Check if key exists if not file.fileExists(pub_path) color.print_error("Key not found: " + name) color.print_info("Import or generate the key first") return end if // Ensure trusted-keys directory exists if not dir.dir_exists(trusted_dir) if not res.is_ok(dir.create_dir_all(trusted_dir)) color.print_error("Failed to create trusted-keys directory") return end if end if // Check if already trusted let trusted_path = trusted_dir + "/" + name + ".pub" if file.fileExists(trusted_path) color.print_warning("Key already trusted: " + name) return end if // Copy public key to trusted directory let cmd = "cp \"" + pub_path + "\" \"" + trusted_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to copy key") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Failed to trust key") return end if color.print_success("Trusted key: " + name) color.print_info("Packages signed with this key will now be accepted") end trust_key // Remove a key from the trusted keys list proc revoke_key(name: string) let trusted_dir = "/var/lib/coral/trusted-keys" let trusted_path = trusted_dir + "/" + name + ".pub" // Check if key is trusted if not file.fileExists(trusted_path) color.print_error("Key is not in trusted list: " + name) return end if // Confirm revocation if not args.has_flag("force") and not args.has_flag("yes") color.print_warning("This will revoke trust for key: " + name) color.print_info("Use --force or -y to confirm") return end if // Remove from trusted keys let cmd = "rm -f \"" + trusted_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to revoke key") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Failed to remove key from trusted list") return end if color.print_success("Revoked trust for key: " + name) color.print_info("Packages signed with this key will no longer be accepted") end revoke_key // List trusted keys proc list_trusted_keys() let trusted_dir = "/var/lib/coral/trusted-keys" if not dir.dir_exists(trusted_dir) color.print_info("No trusted keys (directory does not exist)") return end if let entries = res.unwrap_or(dir.list_dir(trusted_dir), new [string](0)) let count = entries.length() if count == 0 color.print_info("No trusted keys") return end if println("") color.print_action("Trusted keys:") mut i = 0 while i < count let entry = entries[i] if str.ends_with(entry, ".pub") let name = str.substring(entry, 0, str.length(entry) - 4) println(" " + name) end if i = i + 1 end while end list_trusted_keys proc print_usage() println("Usage: coral key [options]") println("") println("Manage signing keys for package verification.") println("") println("Commands:") println(" list List all keys in keyring") println(" generate Generate a new Ed25519 keypair") println(" import [name] Import a public key") println(" export Export a public key") println(" delete Delete a key (use --force for private keys)") println("") println("Trust management:") println(" init Initialize keyring directories") println(" trust Add key to trusted list") println(" revoke Remove key from trusted list") println("") println("Examples:") println(" coral key init") println(" coral key generate maintainer") println(" coral key import repo.pub community") println(" coral key trust community") end print_usage // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: list.reef Authors: Chris Tusa License: Description: List installed or available packages ******************************************************************************/ module commands.list import sys.args import core.str import core.config import core.database import core.port import core.portindex import types import util.color import exitcodes as ec export fn execute(opts: types.GlobalOptions): int end export fn execute(opts: types.GlobalOptions): int let argc = args.count() // 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 // Check for --available flag if args.has_flag("available") or args.has_flag("a") list_available() return ec.EXIT_SUCCESS() end if // Default: list installed packages list_installed(root_path) return ec.EXIT_SUCCESS() end execute proc list_installed(root_path: string) mut packages: [string] = new [string](512) let pkg_count = list_installed_pkgs(packages, 512, root_path) if pkg_count == 0 color.print_info("No packages installed") return end if color.print_action("Installed packages:") println("") mut i = 0 while i < pkg_count let pkg_name = packages[i] let pkg = get_installed_pkg(pkg_name, root_path) // Format: name version print(" ") print(pkg.info.name) print(" ") print(pkg.info.version) print("-") println(int_to_str(pkg.info.release)) i = i + 1 end while println("") color.print_info("Total: " + int_to_str(pkg_count) + " packages") end list_installed // Helper functions for rooted operations fn list_installed_pkgs(names: [string], max_count: int, root_path: string): int if str.length(root_path) > 0 return database.list_installed_rooted(names, max_count, root_path) end if return database.list_installed(names, max_count) end list_installed_pkgs 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 proc list_available() color.print_action("Available ports:") println("") mut total = 0 // Try ports index first (fast path) if portindex.port_index_exists() mut entries: [portindex.PortIndexEntry] = new [portindex.PortIndexEntry](4096) let entry_count = portindex.list_all_ports(entries, 4096) // Group by category for display mut current_cat = "" mut i = 0 while i < entry_count let entry = entries[i] if not str.equals(entry.category, current_cat) if str.length(current_cat) > 0 println("") end if current_cat = entry.category color.print_info(current_cat + "/") end if print(" ") print(entry.name) print(" ") print(entry.version) print(" - ") println(entry.description) total = total + 1 i = i + 1 end while else // Fall back to filesystem scan mut cats: [string] = new [string](32) let cat_count = port.get_categories(cats, 32) mut i = 0 while i < cat_count let category = cats[i] mut ports: [string] = new [string](256) let port_count = port.list_category(category, ports, 256) if port_count > 0 color.print_info(category + "/") mut j = 0 while j < port_count let port_name = ports[j] let result = port.find(port_name) if result.success let p = result.port print(" ") print(p.info.name) print(" ") print(p.info.version) print(" - ") println(p.info.description) end if j = j + 1 total = total + 1 end while println("") end if i = i + 1 end while end if println("") color.print_info("Total: " + int_to_str(total) + " ports") end list_available // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: outdated.reef Authors: Chris Tusa License: Description: Outdated command - show packages with available updates ******************************************************************************/ module commands.outdated import sys.args import core.str import core.database import core.port import types import util.color import exitcodes as ec export fn execute(): int end export fn execute(): int color.print_action("Checking for available updates...") println("") // Get list of installed packages mut installed: [string] = new [string](512) let installed_count = database.list_installed(installed, 512) if installed_count == 0 color.print_info("No packages installed") return ec.EXIT_SUCCESS() end if // Check each package for updates mut outdated_count = 0 mut i = 0 while i < installed_count let pkg_name = installed[i] let installed_pkg = database.get_installed(pkg_name) // Find port let port_result = port.find(pkg_name) if port_result.success let p = port_result.port // Compare versions let cmp = compare_versions(installed_pkg.info.version, p.info.version) if cmp < 0 // Outdated if outdated_count == 0 color.print_info("Outdated packages:") println("") end if print(" ") print(pkg_name) print(" ") print(installed_pkg.info.version) print("-") print(int_to_str(installed_pkg.info.release)) print(" -> ") print(p.info.version) print("-") println(int_to_str(p.info.release)) outdated_count = outdated_count + 1 end if end if i = i + 1 end while if outdated_count == 0 color.print_success("All packages are up to date") // Return special code for scripting - no updates available if args.has_flag("check") return ec.EXIT_NO_UPDATES() end if return ec.EXIT_SUCCESS() else println("") color.print_info(int_to_str(outdated_count) + " package(s) can be upgraded") println("") color.print_info("Run 'coral upgrade' to upgrade all packages") color.print_info("Run 'coral upgrade ' to upgrade specific packages") // Return special code for scripting - updates available if args.has_flag("check") return ec.EXIT_UPDATE_AVAILABLE() end if return ec.EXIT_SUCCESS() end if end execute // Compare two version strings // Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2 fn compare_versions(v1: string, v2: string): int // Split versions into parts mut parts1: [string] = new [string](10) mut parts2: [string] = new [string](10) let count1 = str.split(v1, '.', parts1, 10) let count2 = str.split(v2, '.', parts2, 10) // Compare each part mut i = 0 let max_parts = max_int(count1, count2) while i < max_parts mut n1 = 0 mut n2 = 0 if i < count1 n1 = parse_int(parts1[i]) end if if i < count2 n2 = parse_int(parts2[i]) end if if n1 < n2 return 0 - 1 elif n1 > n2 return 1 end if i = i + 1 end while return 0 end compare_versions fn max_int(a: int, b: int): int if a > b return a end if return b end max_int // Parse integer from string fn parse_int(s: string): int let len = str.length(s) if len == 0 return 0 end if mut result = 0 mut i = 0 while i < len let c = s[i] if c >= '0' and c <= '9' result = result * 10 + (c - '0') else // Stop at non-digit return result end if i = i + 1 end while return result end parse_int // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: owner.reef Authors: Chris Tusa License: Description: Owner command - find which package owns a file ******************************************************************************/ module commands.owner import sys.args import core.str import core.config import core.database import core.pkgdb import types import util.color import exitcodes as ec export fn execute(opts: types.GlobalOptions): int end export fn execute(opts: types.GlobalOptions): int let argc = args.count() // File path should be at cmd_index + 1 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 let file_path = args.get(opts.cmd_index + 1) // Find owner (use rooted version if root is set) let owner = find_owner(file_path, root_path) if str.length(owner) == 0 color.print_error("No package owns: " + file_path) return ec.EXIT_PKG_NOT_FOUND() end if // Get package info (use rooted version if root is set) let pkg = get_installed_pkg(owner, root_path) color.print_action("File: " + file_path) println("") println("Owned by: " + owner + " " + pkg.info.version) return ec.EXIT_SUCCESS() end execute // Helper functions for rooted operations // Uses pkgdb for fast indexed lookup with fallback to database scan fn find_owner(file_path: string, root_path: string): string return pkgdb.find_file_owner_rooted(file_path, root_path) end find_owner 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 proc print_usage() println("Usage: coral owner ") println("") println("Find which package owns a given file.") println("") println("Arguments:") println(" Path to the file to look up") println("") println("Examples:") println(" coral owner /usr/bin/vim") println(" coral owner /usr/bin/hello") end print_usage end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: ports.reef Authors: Chris Tusa License: Description: Ports command - port scaffolding and cache management ******************************************************************************/ module commands.ports import sys.args import io.file import io.dir import io.path import core.str import core.config import core.portindex import types import util.color 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() if argc <= opts.cmd_index + 1 print_usage() return ec.EXIT_USAGE() end if let subcommand = args.get(opts.cmd_index + 1) if subcommand == "new" return ports_new(opts) elif subcommand == "cache" return ports_cache(opts) else color.print_error("Unknown subcommand: " + subcommand) print_usage() return ec.EXIT_USAGE() end if end execute // ============================================================================ // coral ports new // ============================================================================ fn ports_new(opts: types.GlobalOptions): int let argc = args.count() if argc <= opts.cmd_index + 2 color.print_error("Missing argument: category/name") println("Usage: coral ports new ") return ec.EXIT_USAGE() end if let arg = args.get(opts.cmd_index + 2) // Require slash format let slash_idx = str.index_of(arg, "/") if slash_idx <= 0 color.print_error("Invalid format: '" + arg + "'") println("Use category/name format, e.g.: coral ports new hammerhead/my-port") return ec.EXIT_USAGE() end if let category = str.substring(arg, 0, slash_idx) let name = str.substring(arg, slash_idx + 1, str.length(arg) - slash_idx - 1) if str.length(name) == 0 color.print_error("Port name cannot be empty") return ec.EXIT_USAGE() end if // Get ports directory let cfg = config.load() let ports_dir = config.get_ports_dir(cfg) // Create port directory let port_dir = path.join_path(ports_dir, path.join_path(category, name)) if dir.dir_exists(port_dir) color.print_error("Port directory already exists: " + port_dir) return ec.EXIT_PKG_ALREADY_INSTALLED() end if if not res.is_ok(dir.create_dir_all(port_dir)) color.print_error("Failed to create directory: " + port_dir) return ec.EXIT_PERMISSION_DENIED() end if // Generate package.toml let toml_path = path.join_path(port_dir, "package.toml") let toml_content = "[package]\nname = \"" + name + "\"\nversion = \"0.1.0\"\nrelease = 1\ndescription = \"\"\nurl = \"\"\nlicense = \"\"\nmaintainer = \"\"\narch = \"x86_64\"\n\n[dependencies]\nruntime = []\nbuild = []\n\n[[source]]\nfile = \"\"\nurls = []\nchecksum = \"SKIP\"\n\n[build]\nparallel = true\njobs = 0\n" if not res.is_ok(file.writeFile(toml_path, toml_content)) color.print_error("Failed to write package.toml") return ec.EXIT_PERMISSION_DENIED() end if // Generate build.sh let build_path = path.join_path(port_dir, "build.sh") let dollar = "$" let build_content = "#!/bin/sh\n# Build script for " + name + "\nset -e\n\n# cd " + name + "-" + dollar + "{VERSION}\n\nMAKE=gmake\nif [ \"" + dollar + "(uname -s)\" != \"SunOS\" ]; then\n MAKE=make\nfi\n\n# ./configure --prefix=/usr\n# " + dollar + "MAKE " + dollar + "{MAKEFLAGS}\n# " + dollar + "MAKE DESTDIR=\"" + dollar + "{DESTDIR}\" install\n" if not res.is_ok(file.writeFile(build_path, build_content)) color.print_error("Failed to write build.sh") return ec.EXIT_PERMISSION_DENIED() end if color.print_success("Port created: " + category + "/" + name) println("") println(" Directory: " + port_dir) println(" package.toml: " + toml_path) println(" build.sh: " + build_path) println("") color.print_info("Next steps:") println(" 1. Edit package.toml with package details") println(" 2. Add source URLs and checksums") println(" 3. Write the build.sh script") println(" 4. Test with: coral build " + category + "/" + name) return ec.EXIT_SUCCESS() end ports_new // ============================================================================ // coral ports cache [status|rebuild] // ============================================================================ fn ports_cache(opts: types.GlobalOptions): int let argc = args.count() if argc <= opts.cmd_index + 2 // Default to status return cache_status() end if let subcmd = args.get(opts.cmd_index + 2) if subcmd == "status" return cache_status() elif subcmd == "rebuild" return cache_rebuild() else color.print_error("Unknown cache subcommand: " + subcmd) println("Usage: coral ports cache [status|rebuild]") return ec.EXIT_USAGE() end if end ports_cache fn cache_status(): int color.print_action("Ports index status:") println("") if portindex.port_index_exists() let count = portindex.get_port_count() println(" Status: built") println(" Ports indexed: " + int_to_str(count)) else println(" Status: not built") color.print_info("Run 'coral ports cache rebuild' to build the index") end if return ec.EXIT_SUCCESS() end cache_status fn cache_rebuild(): int color.print_action("Rebuilding ports index...") println("") if portindex.rebuild_port_index() let count = portindex.get_port_count() color.print_success("Ports index rebuilt successfully") println(" Ports indexed: " + int_to_str(count)) return ec.EXIT_SUCCESS() else color.print_error("Failed to rebuild ports index") return ec.EXIT_DB_ERROR() end if end cache_rebuild // ============================================================================ // Usage // ============================================================================ proc print_usage() println("Usage: coral ports [args]") println("") println("Port development and cache management.") println("") println("Commands:") println(" new Scaffold a new port") println(" cache [status] Show ports index status") println(" cache rebuild Rebuild ports index") println("") println("Examples:") println(" coral ports new hammerhead/my-port") println(" coral ports cache status") println(" coral ports cache rebuild") end print_usage // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: repo.reef Authors: Chris Tusa License: Description: Repo command - manage package repositories ******************************************************************************/ module commands.repo import sys.args import sys.process import io.file import io.dir import core.str import core.repository import util.color import exitcodes as ec import core.result as res export fn execute(): int end export fn execute(): int let argc = args.count() if argc < 3 print_usage() return ec.EXIT_USAGE() end if let subcommand = args.get(2) if subcommand == "list" list_repos() return ec.EXIT_SUCCESS() elif subcommand == "add" if argc < 4 color.print_error("Missing repository URL") println("Usage: coral repo add [name]") return ec.EXIT_USAGE() end if let url = args.get(3) mut name = "" if argc >= 5 name = args.get(4) end if add_repo(url, name) return ec.EXIT_SUCCESS() elif subcommand == "remove" if argc < 4 color.print_error("Missing repository name") println("Usage: coral repo remove ") return ec.EXIT_USAGE() end if remove_repo(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "enable" if argc < 4 color.print_error("Missing repository name") return ec.EXIT_USAGE() end if enable_repo(args.get(3), true) return ec.EXIT_SUCCESS() elif subcommand == "disable" if argc < 4 color.print_error("Missing repository name") return ec.EXIT_USAGE() end if enable_repo(args.get(3), false) return ec.EXIT_SUCCESS() elif subcommand == "init" if argc < 4 color.print_error("Missing repository path") println("Usage: coral repo init ") return ec.EXIT_USAGE() end if init_repo(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "rebuild" if argc < 4 color.print_error("Missing repository path") println("Usage: coral repo rebuild ") return ec.EXIT_USAGE() end if rebuild_repo(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "sign" if argc < 4 color.print_error("Missing repository path") println("Usage: coral repo sign [keyname]") return ec.EXIT_USAGE() end if let repo_path = args.get(3) mut key_name = "default" if argc >= 5 key_name = args.get(4) end if sign_repo(repo_path, key_name) return ec.EXIT_SUCCESS() elif subcommand == "verify" if argc < 4 color.print_error("Missing repository path") println("Usage: coral repo verify ") return ec.EXIT_USAGE() end if verify_repo(args.get(3)) return ec.EXIT_SUCCESS() elif subcommand == "mirror" if argc < 5 color.print_error("Missing source or destination") println("Usage: coral repo mirror ") return ec.EXIT_USAGE() end if mirror_repo(args.get(3), args.get(4)) return ec.EXIT_SUCCESS() elif subcommand == "export" if argc < 5 color.print_error("Missing repository path or output") println("Usage: coral repo export ") return ec.EXIT_USAGE() end if export_repo(args.get(3), args.get(4)) return ec.EXIT_SUCCESS() else color.print_error("Unknown subcommand: " + subcommand) print_usage() return ec.EXIT_USAGE() end if end execute proc list_repos() color.print_action("Configured repositories:") println("") mut repos: [repository.Repository] = new [repository.Repository](32) let count = repository.list_repos(repos, 32) if count == 0 color.print_info("No repositories configured") println("") color.print_info("Add a repository with: coral repo add ") return end if mut i = 0 while i < count let repo = repos[i] print(" ") print(repo.name) print(" - ") print(repo.url) if not repo.enabled print(" [disabled]") end if if repo.is_signed print(" [signed]") end if println("") i = i + 1 end while println("") color.print_info("Total: " + int_to_str(count) + " repository(ies)") end list_repos proc add_repo(url: string, name: string) color.print_action("Adding repository...") // Generate name from URL if not provided mut repo_name = name if str.length(repo_name) == 0 repo_name = extract_repo_name(url) end if if str.length(repo_name) == 0 color.print_error("Could not determine repository name. Please specify one.") return end if // Create repo file let repos_dir = "/etc/coral/repos.d" let repo_path = repos_dir + "/" + repo_name + ".repo" // Check if it exists if file.fileExists(repo_path) color.print_error("Repository already exists: " + repo_name) return end if // Ensure directory exists if not dir.dir_exists(repos_dir) if not res.is_ok(dir.create_dir_all(repos_dir)) color.print_error("Failed to create repos directory") return end if end if // Create repo file content mut content = "[repository]\n" content = content + "name = \"" + repo_name + "\"\n" content = content + "url = \"" + url + "\"\n" content = content + "priority = 100\n" content = content + "enabled = true\n" content = content + "signed = false\n" if not res.is_ok(file.writeFile(repo_path, content)) color.print_error("Failed to write repository file") return end if color.print_success("Added repository: " + repo_name) end add_repo proc remove_repo(name: string) let repos_dir = "/etc/coral/repos.d" let repo_path = repos_dir + "/" + name + ".repo" if not file.fileExists(repo_path) color.print_error("Repository not found: " + name) return end if // Remove the file let cmd = "rm -f \"" + repo_path + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if color.print_success("Removed repository: " + name) end remove_repo proc enable_repo(name: string, enable: bool) let repos_dir = "/etc/coral/repos.d" let repo_path = repos_dir + "/" + name + ".repo" if not file.fileExists(repo_path) color.print_error("Repository not found: " + name) return end if // Read current content let content = res.unwrap_or(file.readFile(repo_path), "") // Replace enabled line mut new_content = "" if enable new_content = str.replace(content, "enabled = false", "enabled = true") else new_content = str.replace(content, "enabled = true", "enabled = false") end if if not res.is_ok(file.writeFile(repo_path, new_content)) color.print_error("Failed to update repository file") return end if if enable color.print_success("Enabled repository: " + name) else color.print_success("Disabled repository: " + name) end if end enable_repo fn extract_repo_name(url: string): string // Extract name from URL (e.g., https://repo.example.com/zygaena -> zygaena) let len = str.length(url) if len == 0 return "" end if // Find last slash mut last_slash = 0 - 1 mut i = 0 while i < len if url[i] == '/' last_slash = i end if i = i + 1 end while if last_slash < 0 or last_slash >= len - 1 return "repo" end if return str.substring(url, last_slash + 1, len - last_slash - 1) end extract_repo_name // Initialize a new repository at the given path proc init_repo(repo_path: string) color.print_action("Initializing repository at " + repo_path) // Create the repository directory if not dir.dir_exists(repo_path) if not res.is_ok(dir.create_dir_all(repo_path)) color.print_error("Failed to create repository directory") return end if end if // Create packages subdirectory let packages_dir = repo_path + "/packages" if not dir.dir_exists(packages_dir) if not res.is_ok(dir.create_dir_all(packages_dir)) color.print_error("Failed to create packages directory") return end if end if // Create repo.toml manifest let manifest_path = repo_path + "/repo.toml" if file.fileExists(manifest_path) color.print_warning("repo.toml already exists, skipping") else mut content = "# Coral Repository Manifest\n" content = content + "# Generated by coral repo init\n\n" content = content + "[repository]\n" content = content + "name = \"unnamed\"\n" content = content + "description = \"A Coral package repository\"\n" content = content + "url = \"\"\n" content = content + "arch = \"x86_64\"\n" content = content + "created = \"2025-01-24\"\n" content = content + "signed = false\n\n" content = content + "# Packages will be listed below by coral repo rebuild\n" content = content + "[packages]\n" if not res.is_ok(file.writeFile(manifest_path, content)) color.print_error("Failed to write repo.toml") return end if end if color.print_success("Repository initialized at " + repo_path) color.print_info("Place packages in: " + packages_dir) color.print_info("Run 'coral repo rebuild " + repo_path + "' to update manifest") end init_repo // Rebuild repository manifest by scanning packages proc rebuild_repo(repo_path: string) color.print_action("Rebuilding repository manifest...") let packages_dir = repo_path + "/packages" if not dir.dir_exists(packages_dir) color.print_error("Packages directory not found: " + packages_dir) return end if // Scan for package files let entries = res.unwrap_or(dir.list_dir(packages_dir), new [string](0)) let entry_count = entries.length() mut pkg_count = 0 mut packages_content = "" mut i = 0 while i < entry_count let entry = entries[i] // Look for .pkg.tar.xz files if str.ends_with(entry, ".pkg.tar.xz") // Parse package name and version from filename // Format: name-version-release.arch.pkg.tar.xz let info = parse_package_filename(entry) if str.length(info) > 0 packages_content = packages_content + info + "\n" pkg_count = pkg_count + 1 end if end if i = i + 1 end while // Generate new repo.toml let manifest_path = repo_path + "/repo.toml" mut content = "# Coral Repository Manifest\n" content = content + "# Rebuilt by coral repo rebuild\n\n" content = content + "[repository]\n" content = content + "name = \"unnamed\"\n" content = content + "description = \"A Coral package repository\"\n" content = content + "url = \"\"\n" content = content + "arch = \"x86_64\"\n" content = content + "created = \"2025-01-24\"\n" content = content + "signed = false\n\n" content = content + "[packages]\n" content = content + packages_content if not res.is_ok(file.writeFile(manifest_path, content)) color.print_error("Failed to write repo.toml") return end if color.print_success("Repository rebuilt: " + int_to_str(pkg_count) + " package(s) found") end rebuild_repo // Parse package filename into TOML entry fn parse_package_filename(filename: string): string // Extract name-version-release from filename // Example: vim-9.0-1.x86_64.pkg.tar.xz -> vim = { version = "9.0", release = 1 } // Remove .pkg.tar.xz suffix let base = str.substring(filename, 0, str.length(filename) - 11) // Find arch (e.g., .x86_64) let arch_pos = str.last_index_of_char(base, '.') if arch_pos < 0 return "" end if let name_ver_rel = str.substring(base, 0, arch_pos) // Find release number (last hyphen) let rel_pos = str.last_index_of_char(name_ver_rel, '-') if rel_pos < 0 return "" end if let release = str.substring(name_ver_rel, rel_pos + 1, str.length(name_ver_rel) - rel_pos - 1) // Find version (second to last hyphen) let name_ver = str.substring(name_ver_rel, 0, rel_pos) let ver_pos = str.last_index_of_char(name_ver, '-') if ver_pos < 0 return "" end if let name = str.substring(name_ver, 0, ver_pos) let version = str.substring(name_ver, ver_pos + 1, str.length(name_ver) - ver_pos - 1) return name + " = { version = \"" + version + "\", release = " + release + ", file = \"" + filename + "\" }" end parse_package_filename // Sign the repository manifest proc sign_repo(repo_path: string, key_name: string) color.print_action("Signing repository manifest...") let manifest_path = repo_path + "/repo.toml" if not file.fileExists(manifest_path) color.print_error("repo.toml not found in: " + repo_path) return end if // Find the key let keyring = "/etc/coral/keys" let key_path = keyring + "/" + key_name + ".key" if not file.fileExists(key_path) color.print_error("Key not found: " + key_name) color.print_info("Generate a key with: coral key generate " + key_name) return end if // Sign the manifest using openssl let sig_path = repo_path + "/repo.toml.sig" let cmd = "openssl pkeyutl -sign -inkey \"" + key_path + "\" -in \"" + manifest_path + "\" -out \"" + sig_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to start signing process") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Signing failed") return end if // Update repo.toml to indicate it's signed let content = res.unwrap_or(file.readFile(manifest_path), "") let new_content = str.replace(content, "signed = false", "signed = true") file.writeFile(manifest_path, new_content) color.print_success("Repository signed with key: " + key_name) color.print_info("Signature: " + sig_path) end sign_repo // Verify repository signature proc verify_repo(repo_path: string) color.print_action("Verifying repository signature...") let manifest_path = repo_path + "/repo.toml" let sig_path = repo_path + "/repo.toml.sig" if not file.fileExists(manifest_path) color.print_error("repo.toml not found") return end if if not file.fileExists(sig_path) color.print_error("No signature found (repo.toml.sig)") return end if // Try to find a matching public key let keyring = "/etc/coral/keys" if not dir.dir_exists(keyring) color.print_error("No keys in keyring") return end if // List all public keys and try each let entries = res.unwrap_or(dir.list_dir(keyring), new [string](0)) let count = entries.length() mut i = 0 mut verified = false while i < count and not verified let entry = entries[i] if str.ends_with(entry, ".pub") let key_path = keyring + "/" + entry let cmd = "openssl pkeyutl -verify -pubin -inkey \"" + key_path + "\" -in \"" + manifest_path + "\" -sigfile \"" + sig_path + "\" 2>/dev/null" let pid = process.process_spawn_shell(cmd) if pid > 0 let exit_code = process.process_wait(pid) if exit_code == 0 let key_name = str.substring(entry, 0, str.length(entry) - 4) color.print_success("Signature verified with key: " + key_name) verified = true end if end if end if i = i + 1 end while if not verified color.print_error("Signature verification failed - no matching key found") end if end verify_repo // Mirror repository to remote destination proc mirror_repo(source: string, dest: string) color.print_action("Mirroring repository...") color.print_info("Source: " + source) color.print_info("Destination: " + dest) // Use rsync for mirroring let cmd = "rsync -av --delete \"" + source + "/\" \"" + dest + "/\"" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to start rsync") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Mirror sync failed") return end if color.print_success("Repository mirrored to: " + dest) end mirror_repo // Export repository for offline distribution (ISO/USB) proc export_repo(repo_path: string, output_path: string) color.print_action("Exporting repository for offline distribution...") if not dir.dir_exists(repo_path) color.print_error("Repository not found: " + repo_path) return end if // Determine output format based on extension if str.ends_with(output_path, ".tar.xz") // Create compressed tarball let cmd = "gtar -C \"" + repo_path + "\" -cJf \"" + output_path + "\" ." let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to create archive") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Archive creation failed") return end if elif str.ends_with(output_path, ".tar.gz") let cmd = "gtar -C \"" + repo_path + "\" -czf \"" + output_path + "\" ." let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to create archive") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Archive creation failed") return end if else // Copy directory let cmd = "cp -r \"" + repo_path + "\" \"" + output_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to copy repository") return end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Copy failed") return end if end if color.print_success("Repository exported to: " + output_path) end export_repo proc print_usage() println("Usage: coral repo [options]") println("") println("Manage package repositories.") println("") println("Commands:") println(" list List configured repositories") println(" add [name] Add a new repository") println(" remove Remove a repository") println(" enable Enable a repository") println(" disable Disable a repository") println("") println("Repository maintenance:") println(" init Create a new repository structure") println(" rebuild Scan packages and rebuild manifest") println(" sign [key] Sign repository with Ed25519 key") println(" verify Verify repository signature") println(" mirror Sync repository to mirror (rsync)") println(" export Export for offline distribution") println("") println("Examples:") println(" coral repo list") println(" coral repo add https://repo.zygaena.org/packages") println(" coral repo init /var/www/repo/myrepo") println(" coral repo rebuild /var/www/repo/myrepo") println(" coral repo sign /var/www/repo/myrepo maintainer") end print_usage // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: search.reef Authors: Chris Tusa License: Description: Search command - search for packages by name or description ******************************************************************************/ module commands.search import sys.args import core.str import core.port import core.portindex import core.database import types import util.color import exitcodes as ec export fn execute(): int end export fn execute(): int let argc = args.count() if argc < 3 print_usage() return ec.EXIT_USAGE() end if let search_term = args.get(2) color.print_action("Searching for: " + search_term) println("") mut total = 0 // Try ports index first (fast path) if portindex.port_index_exists() mut results: [portindex.PortIndexEntry] = new [portindex.PortIndexEntry](512) let result_count = portindex.search_ports(search_term, results, 512) mut i = 0 while i < result_count let entry = results[i] print_index_match(entry) total = total + 1 i = i + 1 end while else // Fall back to filesystem scan mut cats: [string] = new [string](32) let cat_count = port.get_categories(cats, 32) mut i = 0 while i < cat_count let category = cats[i] mut ports: [string] = new [string](256) let port_count = port.list_category(category, ports, 256) mut j = 0 while j < port_count let port_name = ports[j] let result = port.find(port_name) if result.success let p = result.port // Check if search term matches name or description if matches_search(p, search_term) print_match(p, category) total = total + 1 end if end if j = j + 1 end while i = i + 1 end while end if if total == 0 color.print_info("No packages found matching: " + search_term) else println("") color.print_info("Found " + int_to_str(total) + " package(s)") end if return ec.EXIT_SUCCESS() end execute fn matches_search(p: types.Port, term: string): bool let lower_term = str.to_lower(term) let lower_name = str.to_lower(p.info.name) let lower_desc = str.to_lower(p.info.description) // Check name if str.contains(lower_name, lower_term) return true end if // Check description if str.contains(lower_desc, lower_term) return true end if return false end matches_search proc print_index_match(entry: portindex.PortIndexEntry) let is_installed = database.is_installed(entry.name) print(entry.category) print("/") print(entry.name) print(" ") print(entry.version) if is_installed print(" [installed]") end if println("") print(" ") println(entry.description) end print_index_match proc print_match(p: types.Port, category: string) // Check if installed let is_installed = database.is_installed(p.info.name) print(category) print("/") print(p.info.name) print(" ") print(p.info.version) if is_installed print(" [installed]") end if println("") print(" ") println(p.info.description) end print_match proc print_usage() println("Usage: coral search ") println("") println("Search for packages by name or description.") println("") println("Arguments:") println(" Search term to look for") println("") println("Examples:") println(" coral search vim") println(" coral search editor") println(" coral search hello") end print_usage // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: sync.reef Authors: Chris Tusa License: Description: Sync command - update ports collection from repository ******************************************************************************/ module commands.sync import sys.args import sys.process import io.dir import io.path import io.file import core.str import core.config import core.port import core.portindex import util.color import exitcodes as ec import core.result as res export fn execute(): int end export fn execute(): int // Load configuration let cfg = config.load() let ports_dir = config.get_ports_dir(cfg) color.print_action("Syncing ports collection...") println("") // Check if ports directory exists if not dir.dir_exists(ports_dir) color.print_error("Ports directory does not exist: " + ports_dir) color.print_info("You may need to clone the ports repository first:") println(" git clone " + ports_dir) return ec.EXIT_CONFIG_ERROR() end if // Check if it's a git repository let git_dir = path.join_path(ports_dir, ".git") if not dir.dir_exists(git_dir) color.print_warning("Ports directory is not a git repository") color.print_info("Cannot sync - no version control detected") return ec.EXIT_CONFIG_ERROR() end if // Run git pull color.print_info("Updating from remote repository...") let cmd = "cd \"" + ports_dir + "\" && git pull" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to start git pull") return ec.EXIT_REPO_UNREACHABLE() end if let exit_code = process.process_wait(pid) if exit_code == 0 color.print_success("Ports collection updated successfully") // Rebuild ports index after successful sync portindex.rebuild_port_index() // Show summary of changes show_update_summary(ports_dir) return ec.EXIT_SUCCESS() else color.print_error("Git pull failed with exit code " + int_to_str(exit_code)) color.print_info("Check your network connection and repository access") return ec.EXIT_REPO_UNREACHABLE() end if end execute proc show_update_summary(ports_dir: string) // Count ports in each category (dynamically discovered) mut cats: [string] = new [string](32) let cat_count = port.get_categories(cats, 32) mut total = 0 mut i = 0 while i < cat_count let cat = cats[i] let cat_path = path.join_path(ports_dir, cat) if dir.dir_exists(cat_path) let entries = res.unwrap_or(dir.list_dir(cat_path), new [string](0)) let count = entries.length() // Count valid ports (directories with package.toml) mut port_count = 0 mut j = 0 while j < count let entry = entries[j] if str.length(entry) > 0 and entry[0] != '.' let toml_path = path.join_path(path.join_path(cat_path, entry), "package.toml") if file.fileExists(toml_path) port_count = port_count + 1 end if end if j = j + 1 end while if port_count > 0 println(" " + cat + ": " + int_to_str(port_count) + " ports") total = total + port_count end if end if i = i + 1 end while println("") color.print_info("Total: " + int_to_str(total) + " ports available") end show_update_summary // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: upgrade.reef Authors: Chris Tusa License: Description: Upgrade command - upgrade installed packages ******************************************************************************/ module commands.upgrade 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.port import core.portindex import core.package import types import util.color import util.version as ver 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() // 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) // Auto-refresh port index if configured if config.get_auto_refresh(cfg) portindex.rebuild_port_index() end if // Show root if set (verbose mode) if opts.verbose and str.length(root_path) > 0 color.print_info("Using root: " + root_path) end if // Count non-flag arguments (package names) // Skip flag values (--root, --prefix take a value) mut pkg_count = 0 mut i = opts.cmd_index + 1 while i < argc let arg = args.get(i) if arg == "--root" or arg == "--prefix" i = i + 2 // Skip flag and its value continue elif arg == "--no-chroot-scripts" i = i + 1 // valueless flag: skip only itself continue end if if not str.starts_with(arg, "-") pkg_count = pkg_count + 1 end if i = i + 1 end while if pkg_count == 0 // No packages specified - upgrade all return upgrade_all(cfg) else // Upgrade specified packages mut all_success = true i = opts.cmd_index + 1 while i < argc 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 // Upgrade this package if not upgrade_package(pkg_name, cfg) color.print_error("Failed to upgrade: " + pkg_name) all_success = false end if i = i + 1 end while if all_success return ec.EXIT_SUCCESS() else return ec.EXIT_UPGRADE_FAILED() end if end if end execute fn upgrade_all(cfg: config.Config): int color.print_action("Checking for upgrades...") // Get root path for rooted operations let root_path = config.get_root(cfg) // List all installed packages (use rooted version if root is set) mut installed: [string] = new [string](512) let installed_count = list_installed_pkgs(installed, 512, root_path) if installed_count == 0 color.print_info("No packages installed") return ec.EXIT_SUCCESS() end if // Check each package for updates mut upgradable: [string] = new [string](512) mut upgradable_count = 0 mut i = 0 while i < installed_count let pkg_name = installed[i] if has_upgrade(pkg_name, root_path) upgradable[upgradable_count] = pkg_name upgradable_count = upgradable_count + 1 end if i = i + 1 end while if upgradable_count == 0 color.print_success("All packages are up to date") return ec.EXIT_SUCCESS() end if color.print_info("Packages to upgrade: " + int_to_str(upgradable_count)) // List packages to upgrade i = 0 while i < upgradable_count let pkg_name = upgradable[i] let installed_pkg = get_installed_pkg(pkg_name, root_path) let port_result = port.find(pkg_name) if port_result.success let p = port_result.port println(" " + pkg_name + " " + installed_pkg.info.version + " -> " + p.info.version) end if i = i + 1 end while // Ask for confirmation unless -y flag if not args.has_flag("y") and not args.has_flag("yes") color.print_info("Use -y flag to skip confirmation and proceed with upgrade") return ec.EXIT_CANCELLED() end if // Dry-run: show what would be done and exit if args.has_flag("n") or args.has_flag("dry-run") color.print_info("Dry run — no changes made") return ec.EXIT_SUCCESS() end if // Perform upgrades mut all_success = true i = 0 while i < upgradable_count if not upgrade_package(upgradable[i], cfg) color.print_error("Failed to upgrade: " + upgradable[i]) all_success = false end if i = i + 1 end while if all_success return ec.EXIT_SUCCESS() else return ec.EXIT_UPGRADE_FAILED() end if end upgrade_all fn has_upgrade(pkg_name: string, root_path: string): bool // Get installed version (use rooted version if root is set) let installed_pkg = get_installed_pkg(pkg_name, root_path) if str.length(installed_pkg.info.name) == 0 return false end if // Find port let port_result = port.find(pkg_name) if not port_result.success return false end if let p = port_result.port // Compare versions using semantic versioning return ver.compare_versions(installed_pkg.info.version, p.info.version) < 0 end has_upgrade // Helper functions for rooted operations fn list_installed_pkgs(names: [string], max_count: int, root_path: string): int if str.length(root_path) > 0 return database.list_installed_rooted(names, max_count, root_path) end if return database.list_installed(names, max_count) end list_installed_pkgs 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 fn is_installed_pkg(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 is_installed_pkg 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 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 fn register_pkg(pkg: types.InstalledPackage, root_path: string): bool if str.length(root_path) > 0 return database.register_from_extract_dir_rooted(pkg, "", root_path) end if return database.register_package(pkg) end register_pkg fn get_actual_root(root_path: string): string if str.length(root_path) == 0 return "/" end if return root_path end get_actual_root fn upgrade_package(pkg_name: string, cfg: config.Config): bool // Get root path for rooted operations let root_path = config.get_root(cfg) // Check if installed (use rooted version if root is set) if not is_installed_pkg(pkg_name, root_path) color.print_error(pkg_name + " is not installed") return false end if // Get installed info (use rooted version if root is set) let installed_pkg = get_installed_pkg(pkg_name, root_path) // Find port let port_result = port.find(pkg_name) if not port_result.success color.print_error("Port not found: " + pkg_name) return false end if let p = port_result.port // Check if upgrade is available using semantic versioning let cmp = ver.compare_versions(installed_pkg.info.version, p.info.version) if cmp >= 0 color.print_info(pkg_name + " is already at version " + installed_pkg.info.version) return true end if color.print_action("Upgrading " + pkg_name + " from " + installed_pkg.info.version + " to " + p.info.version + "...") // Look for pre-built package let pkg_path = find_package(pkg_name, p.info.version, p.info.release, cfg) if str.length(pkg_path) == 0 color.print_error("Package not found. Build it first with: coral build") return false end if // Remove old version (keep database entry for now) mut files: [string] = new [string](4096) let file_count = get_files_list(pkg_name, files, 4096, root_path) color.print_info("Removing old files...") let actual_root = get_actual_root(root_path) remove_files(files, file_count, actual_root) // Install new version color.print_info("Installing new version...") // Create extraction directory let extract_dir = create_extract_dir(cfg) if str.length(extract_dir) == 0 color.print_error("Failed to create extraction directory") return false end if // Extract package let result = package.extract_package(pkg_path, extract_dir) if not result.success color.print_error(result.error) cleanup_extract_dir(extract_dir) return false end if // Read new file list mut new_files: [string] = new [string](4096) let new_file_count = package.read_footprint(extract_dir, new_files, 4096) // Install files to proper root if not package.install_files(extract_dir, actual_root) color.print_error("Failed to install files") cleanup_extract_dir(extract_dir) return false end if // Update database (remove and re-add) using rooted versions if needed unregister_pkg(pkg_name, root_path) let new_info = package.read_pkginfo(extract_dir) mut new_pkg = types.new_installed_package() new_pkg.info = new_info new_pkg.install_date = "2025-01-24" new_pkg.install_reason = "explicit" new_pkg.files = new_files new_pkg.files_count = new_file_count // Register using rooted version if root is set if str.length(root_path) > 0 if not database.register_from_extract_dir_rooted(new_pkg, extract_dir, root_path) color.print_warning("Failed to update database") end if else if not database.register_from_extract_dir(new_pkg, extract_dir) color.print_warning("Failed to update database") end if end if // Cleanup cleanup_extract_dir(extract_dir) color.print_success("Upgraded " + pkg_name + " to " + p.info.version) return true end upgrade_package // Find a package file by name and version fn find_package(name: string, version: string, release: int, cfg: config.Config): string let packages_dir = config.get_packages_dir(cfg) let pkg_name = name + "-" + version + "-" + int_to_str(release) + ".pkg.tar.xz" let pkg_path = path.join_path(packages_dir, pkg_name) if file.fileExists(pkg_path) return pkg_path end if return "" end find_package // Remove files from system fn remove_files(files: [string], file_count: int, root_path: string): bool mut i = file_count - 1 while i >= 0 let rel_path = files[i] let full_path = path.join_path(root_path, rel_path) if file.fileExists(full_path) let cmd = "rm -f \"" + full_path + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if end if i = i - 1 end while return true end remove_files // Create extraction directory fn create_extract_dir(cfg: config.Config): string let work_dir = config.get_work_dir(cfg) let pid = process.getpid() let extract_dir = path.join_path(work_dir, "upgrade_" + int_to_str(pid)) if dir.dir_exists(extract_dir) let cmd = "rm -rf \"" + extract_dir + "\"" let rm_pid = process.process_spawn_shell(cmd) if rm_pid > 0 process.process_wait(rm_pid) end if end if if not res.is_ok(dir.create_dir_all(extract_dir)) return "" end if return extract_dir end create_extract_dir // Cleanup extraction directory fn cleanup_extract_dir(dir_path: string): bool if str.length(dir_path) == 0 return true end if let cmd = "rm -rf \"" + dir_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if process.process_wait(pid) return true end cleanup_extract_dir // 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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: verify.reef Authors: Chris Tusa License: Description: Verify command - check installed package integrity ******************************************************************************/ module commands.verify import sys.args import io.file import io.dir import io.path import core.str import core.config import core.database import util.mtree import util.checksum import types import util.color import exitcodes as ec import fs.stat import fs.link import core.result as res export fn execute(opts: types.GlobalOptions): int end export fn execute(opts: types.GlobalOptions): int let argc = args.count() 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) if opts.verbose and str.length(root_path) > 0 color.print_info("Using root: " + root_path) end if let pkg_name = args.get(opts.cmd_index + 1) // Check if installed if not check_installed(pkg_name, root_path) color.print_error(pkg_name + " is not installed") return ec.EXIT_PKG_NOT_FOUND() end if color.print_action("Verifying " + pkg_name + "...") // Load manifest from database let manifest_path = database.get_manifest_path(pkg_name) if not file.fileExists(manifest_path) color.print_error("No manifest found for " + pkg_name + " (legacy package without .MANIFEST)") return ec.EXIT_PKG_INVALID() end if let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") if str.length(manifest_content) == 0 color.print_error("Empty manifest for " + pkg_name) return ec.EXIT_PKG_INVALID() end if mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) let entry_count = mtree.parse_manifest(manifest_content, entries, 8192) if entry_count == 0 color.print_error("Failed to parse manifest for " + pkg_name) return ec.EXIT_PKG_INVALID() end if // Determine actual root let actual_root = get_actual_root(root_path) // Verify each entry mut missing_count = 0 mut checksum_fail_count = 0 mut mode_fail_count = 0 mut link_fail_count = 0 mut ok_count = 0 mut i = 0 while i < entry_count let entry = entries[i] let etype = mtree.entry_type(entry) let epath = mtree.entry_path(entry) mut rel_path = epath if str.starts_with(epath, "./") rel_path = str.substring(epath, 2, str.length(epath) - 2) end if let full_path = path.join_path(actual_root, rel_path) if str.equals(etype, "dir") if not dir.dir_exists(full_path) color.print_warning("MISSING dir: " + rel_path) missing_count = missing_count + 1 else // Check mode let expected_mode = mtree.entry_mode(entry) let actual_mode = res.unwrap_or(stat.file_mode(full_path), 0) // Compare lower 12 bits (permissions + setuid/setgid/sticky) if (actual_mode % 4096) != (expected_mode % 4096) if opts.verbose color.print_warning("MODE " + rel_path + " (expected " + int_to_octal(expected_mode) + ", got " + int_to_octal(actual_mode) + ")") else color.print_warning("MODE " + rel_path) end if mode_fail_count = mode_fail_count + 1 else ok_count = ok_count + 1 end if end if elif str.equals(etype, "file") if not stat.exists(full_path) color.print_warning("MISSING file: " + rel_path) missing_count = missing_count + 1 else mut file_ok = true // Check sha256 let expected_sha = mtree.entry_sha256(entry) if str.length(expected_sha) > 0 let actual_sha = checksum.sha256_file(full_path) if not str.equals(actual_sha, expected_sha) color.print_warning("CHECKSUM " + rel_path) checksum_fail_count = checksum_fail_count + 1 file_ok = false end if end if // Check mode let expected_mode = mtree.entry_mode(entry) let actual_mode = res.unwrap_or(stat.file_mode(full_path), 0) if (actual_mode % 4096) != (expected_mode % 4096) if opts.verbose color.print_warning("MODE " + rel_path + " (expected " + int_to_octal(expected_mode) + ", got " + int_to_octal(actual_mode) + ")") else color.print_warning("MODE " + rel_path) end if mode_fail_count = mode_fail_count + 1 file_ok = false end if // Check ownership let expected_uid = mtree.name_to_uid(mtree.entry_uname(entry)) let expected_gid = mtree.name_to_gid(mtree.entry_gname(entry)) let actual_uid = res.unwrap_or(stat.file_uid(full_path), 0) let actual_gid = res.unwrap_or(stat.file_gid(full_path), 0) if actual_uid != expected_uid or actual_gid != expected_gid if opts.verbose color.print_warning("OWNER " + rel_path + " (expected " + mtree.entry_uname(entry) + ":" + mtree.entry_gname(entry) + ")") else color.print_warning("OWNER " + rel_path) end if mode_fail_count = mode_fail_count + 1 file_ok = false end if if file_ok ok_count = ok_count + 1 end if end if elif str.equals(etype, "link") if not stat.is_symlink(full_path) if not stat.exists(full_path) color.print_warning("MISSING link: " + rel_path) missing_count = missing_count + 1 else color.print_warning("NOT A LINK " + rel_path) link_fail_count = link_fail_count + 1 end if else let expected_target = mtree.entry_link(entry) let actual_target = res.unwrap_or(link.readlink(full_path), "") if not str.equals(actual_target, expected_target) if opts.verbose color.print_warning("LINK " + rel_path + " -> " + actual_target + " (expected " + expected_target + ")") else color.print_warning("LINK TARGET " + rel_path) end if link_fail_count = link_fail_count + 1 else ok_count = ok_count + 1 end if end if end if i = i + 1 end while // Summary println("") let total_issues = missing_count + checksum_fail_count + mode_fail_count + link_fail_count if total_issues == 0 color.print_success(pkg_name + ": " + int_to_str(ok_count) + " entries verified, all OK") return ec.EXIT_SUCCESS() else color.print_error(pkg_name + ": " + int_to_str(total_issues) + " issue(s) found") if missing_count > 0 println(" Missing: " + int_to_str(missing_count)) end if if checksum_fail_count > 0 println(" Checksum: " + int_to_str(checksum_fail_count)) end if if mode_fail_count > 0 println(" Mode/Owner: " + int_to_str(mode_fail_count)) end if if link_fail_count > 0 println(" Symlinks: " + int_to_str(link_fail_count)) end if println(" OK: " + int_to_str(ok_count)) return ec.EXIT_PKG_CHECKSUM_MISMATCH() end if end execute 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 fn get_actual_root(root_path: string): string if str.length(root_path) == 0 return "/" end if return root_path end get_actual_root proc print_usage() println("Usage: coral verify ") println("") println("Verify integrity of an installed package against its manifest.") println("") println("Checks:") println(" - Missing files, directories, and symlinks") println(" - File checksum (sha256) mismatches") println(" - Permission and ownership changes") println(" - Broken or incorrect symlink targets") println("") println("Arguments:") println(" Name of installed package to verify") println("") println("Options:") println(" -v, --verbose Show expected vs actual values for failures") println("") println("Examples:") println(" coral verify vim") println(" coral verify -v openssl") end print_usage // 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 // Helper: convert int to octal string (for mode display) fn int_to_octal(n: int): string if n == 0 return "0" end if mut value = n % 4096 mut result = "" while value > 0 let digit = value % 8 result = str.concat(str.substring("01234567", digit, 1), result) value = value / 8 end while return result end int_to_octal end module