/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: config.reef Authors: Chris Tusa License: Description: Configuration file loading and access ******************************************************************************/ module core.config import types import io.file import core.str import encoding.toml import core.result as res export // Configuration structure type Config // Load configuration from file (with defaults if not found) fn load(): Config // Access functions for common settings fn get_arch(cfg: Config): string fn get_jobs(cfg: Config): int fn get_confirm(cfg: Config): bool fn get_color(cfg: Config): bool // Path accessors fn get_ports_dir(cfg: Config): string fn get_packages_dir(cfg: Config): string fn get_sources_dir(cfg: Config): string fn get_database_dir(cfg: Config): string fn get_build_dir(cfg: Config): string fn get_index_dir(cfg: Config): string // Legacy (for compatibility) fn get_work_dir(cfg: Config): string fn get_pkg_dir(cfg: Config): string // Root and prefix accessors fn get_root(cfg: Config): string fn get_prefix(cfg: Config): string fn get_sysconfdir(cfg: Config): string // Root-aware path accessors (prepend root to path) fn get_database_dir_rooted(cfg: Config): string fn get_install_prefix(cfg: Config): string // Apply command-line overrides to config fn apply_overrides(cfg: Config, root: string, prefix: string, no_chroot: bool): Config // Build settings fn get_cflags(cfg: Config): string fn get_cxxflags(cfg: Config): string fn get_makeflags(cfg: Config): string fn get_strip(cfg: Config): bool fn get_fallback_to_source(cfg: Config): bool fn get_compress_man(cfg: Config): bool fn get_clean_work(cfg: Config): bool fn get_clean_pkg(cfg: Config): bool fn get_logging(cfg: Config): bool fn get_quiet(cfg: Config): bool // Network settings fn get_timeout(cfg: Config): int fn get_retries(cfg: Config): int fn get_use_proxy(cfg: Config): bool // Sync settings fn get_auto_refresh(cfg: Config): bool // Cache settings fn get_keep_packages(cfg: Config): bool fn get_keep_sources(cfg: Config): bool // Security settings fn get_require_signed_repos(cfg: Config): bool fn get_require_signed_packages(cfg: Config): bool // Install settings fn get_chroot_scripts(cfg: Config): bool end export // Configuration structure type Config = struct // [general] arch: string jobs: int confirm: bool color: bool // [paths] root: string prefix: string sysconfdir: string ports_dir: string packages_dir: string sources_dir: string database_dir: string build_dir: string index_dir: string // Legacy (kept for compatibility) work_dir: string pkg_dir: string // [build] cflags: string cxxflags: string makeflags: string strip: bool compress_man: bool fallback_to_source: bool clean_work: bool clean_pkg: bool logging: bool quiet: bool // [network] timeout: int retries: int use_proxy: bool // [sync] auto_refresh: bool // [cache] keep_packages: bool keep_sources: bool // [security] require_signed_repos: bool require_signed_packages: bool // [install] chroot_scripts: bool end Config // Create a default configuration fn default_config(): Config return Config{ // General arch: "amd64", jobs: 0, // 0 = auto-detect confirm: true, color: true, // Paths (defaults from types) root: "", prefix: "/usr/local", sysconfdir: "/etc", ports_dir: types.get_ports_dir(), packages_dir: types.get_packages_dir(), sources_dir: types.get_sources_dir(), database_dir: types.get_db_dir(), build_dir: types.get_build_dir(), index_dir: types.get_index_dir(), // Legacy (for compatibility) work_dir: types.get_build_dir(), pkg_dir: types.get_build_dir(), // Build cflags: "-O2 -pipe", cxxflags: "-O2 -pipe", makeflags: "", strip: true, compress_man: true, fallback_to_source: true, clean_work: true, clean_pkg: true, logging: true, quiet: false, // Network timeout: 30, retries: 3, use_proxy: false, // Sync auto_refresh: false, // Cache keep_packages: true, keep_sources: true, // Security require_signed_repos: false, require_signed_packages: false, // Install chroot_scripts: true } end default_config // Load configuration from file fn load(): Config mut cfg = default_config() // Check if config file exists if not file.fileExists(types.get_config_file()) return cfg end if // Read config file let content = res.unwrap_or(file.readFile(types.get_config_file()), "") if str.length(content) == 0 return cfg end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return cfg end if // [general] section if toml.toml_has_key(keys, vals, entry_count, "general.arch") cfg.arch = toml.toml_get(keys, vals, entry_count, "general.arch") end if if toml.toml_has_key(keys, vals, entry_count, "general.jobs") cfg.jobs = toml.toml_get_int(keys, vals, entry_count, "general.jobs") end if if toml.toml_has_key(keys, vals, entry_count, "general.confirm") cfg.confirm = toml.toml_get_bool(keys, vals, entry_count, "general.confirm") end if if toml.toml_has_key(keys, vals, entry_count, "general.color") cfg.color = toml.toml_get_bool(keys, vals, entry_count, "general.color") end if // [paths] section if toml.toml_has_key(keys, vals, entry_count, "paths.root") cfg.root = toml.toml_get(keys, vals, entry_count, "paths.root") end if if toml.toml_has_key(keys, vals, entry_count, "paths.prefix") cfg.prefix = toml.toml_get(keys, vals, entry_count, "paths.prefix") end if if toml.toml_has_key(keys, vals, entry_count, "paths.sysconfdir") cfg.sysconfdir = toml.toml_get(keys, vals, entry_count, "paths.sysconfdir") end if if toml.toml_has_key(keys, vals, entry_count, "paths.ports") cfg.ports_dir = toml.toml_get(keys, vals, entry_count, "paths.ports") end if if toml.toml_has_key(keys, vals, entry_count, "paths.packages") cfg.packages_dir = toml.toml_get(keys, vals, entry_count, "paths.packages") end if if toml.toml_has_key(keys, vals, entry_count, "paths.sources") cfg.sources_dir = toml.toml_get(keys, vals, entry_count, "paths.sources") end if if toml.toml_has_key(keys, vals, entry_count, "paths.database") cfg.database_dir = toml.toml_get(keys, vals, entry_count, "paths.database") end if if toml.toml_has_key(keys, vals, entry_count, "paths.build") cfg.build_dir = toml.toml_get(keys, vals, entry_count, "paths.build") end if if toml.toml_has_key(keys, vals, entry_count, "paths.index") cfg.index_dir = toml.toml_get(keys, vals, entry_count, "paths.index") end if // Legacy path support (maps to build_dir) if toml.toml_has_key(keys, vals, entry_count, "paths.work") cfg.work_dir = toml.toml_get(keys, vals, entry_count, "paths.work") cfg.build_dir = cfg.work_dir // Also set new field end if if toml.toml_has_key(keys, vals, entry_count, "paths.pkg") cfg.pkg_dir = toml.toml_get(keys, vals, entry_count, "paths.pkg") end if // [build] section if toml.toml_has_key(keys, vals, entry_count, "build.cflags") cfg.cflags = toml.toml_get(keys, vals, entry_count, "build.cflags") end if if toml.toml_has_key(keys, vals, entry_count, "build.cxxflags") cfg.cxxflags = toml.toml_get(keys, vals, entry_count, "build.cxxflags") end if if toml.toml_has_key(keys, vals, entry_count, "build.makeflags") cfg.makeflags = toml.toml_get(keys, vals, entry_count, "build.makeflags") end if if toml.toml_has_key(keys, vals, entry_count, "build.strip") cfg.strip = toml.toml_get_bool(keys, vals, entry_count, "build.strip") end if if toml.toml_has_key(keys, vals, entry_count, "build.compress_man") cfg.compress_man = toml.toml_get_bool(keys, vals, entry_count, "build.compress_man") end if if toml.toml_has_key(keys, vals, entry_count, "build.fallback_to_source") cfg.fallback_to_source = toml.toml_get_bool(keys, vals, entry_count, "build.fallback_to_source") end if if toml.toml_has_key(keys, vals, entry_count, "build.clean_work") cfg.clean_work = toml.toml_get_bool(keys, vals, entry_count, "build.clean_work") end if if toml.toml_has_key(keys, vals, entry_count, "build.clean_pkg") cfg.clean_pkg = toml.toml_get_bool(keys, vals, entry_count, "build.clean_pkg") end if if toml.toml_has_key(keys, vals, entry_count, "build.logging") cfg.logging = toml.toml_get_bool(keys, vals, entry_count, "build.logging") end if if toml.toml_has_key(keys, vals, entry_count, "build.quiet") cfg.quiet = toml.toml_get_bool(keys, vals, entry_count, "build.quiet") end if // [network] section if toml.toml_has_key(keys, vals, entry_count, "network.timeout") cfg.timeout = toml.toml_get_int(keys, vals, entry_count, "network.timeout") end if if toml.toml_has_key(keys, vals, entry_count, "network.retries") cfg.retries = toml.toml_get_int(keys, vals, entry_count, "network.retries") end if if toml.toml_has_key(keys, vals, entry_count, "network.use_proxy") cfg.use_proxy = toml.toml_get_bool(keys, vals, entry_count, "network.use_proxy") end if // [sync] section if toml.toml_has_key(keys, vals, entry_count, "sync.auto_refresh") cfg.auto_refresh = toml.toml_get_bool(keys, vals, entry_count, "sync.auto_refresh") end if // [cache] section if toml.toml_has_key(keys, vals, entry_count, "cache.keep_packages") cfg.keep_packages = toml.toml_get_bool(keys, vals, entry_count, "cache.keep_packages") end if if toml.toml_has_key(keys, vals, entry_count, "cache.keep_sources") cfg.keep_sources = toml.toml_get_bool(keys, vals, entry_count, "cache.keep_sources") end if // [security] section if toml.toml_has_key(keys, vals, entry_count, "security.require_signed_repos") cfg.require_signed_repos = toml.toml_get_bool(keys, vals, entry_count, "security.require_signed_repos") end if if toml.toml_has_key(keys, vals, entry_count, "security.require_signed_packages") cfg.require_signed_packages = toml.toml_get_bool(keys, vals, entry_count, "security.require_signed_packages") end if // [install] section if toml.toml_has_key(keys, vals, entry_count, "install.chroot_scripts") cfg.chroot_scripts = toml.toml_get_bool(keys, vals, entry_count, "install.chroot_scripts") end if return cfg end load // Accessor functions fn get_arch(cfg: Config): string return cfg.arch end get_arch fn get_jobs(cfg: Config): int return cfg.jobs end get_jobs fn get_confirm(cfg: Config): bool return cfg.confirm end get_confirm fn get_color(cfg: Config): bool return cfg.color end get_color fn get_ports_dir(cfg: Config): string return cfg.ports_dir end get_ports_dir fn get_packages_dir(cfg: Config): string return cfg.packages_dir end get_packages_dir fn get_sources_dir(cfg: Config): string return cfg.sources_dir end get_sources_dir fn get_database_dir(cfg: Config): string return cfg.database_dir end get_database_dir fn get_build_dir(cfg: Config): string return cfg.build_dir end get_build_dir fn get_index_dir(cfg: Config): string return cfg.index_dir end get_index_dir // Legacy accessors (for compatibility) fn get_work_dir(cfg: Config): string return cfg.build_dir end get_work_dir fn get_pkg_dir(cfg: Config): string return cfg.build_dir end get_pkg_dir fn get_cflags(cfg: Config): string return cfg.cflags end get_cflags fn get_cxxflags(cfg: Config): string return cfg.cxxflags end get_cxxflags fn get_makeflags(cfg: Config): string return cfg.makeflags end get_makeflags fn get_strip(cfg: Config): bool return cfg.strip end get_strip fn get_fallback_to_source(cfg: Config): bool return cfg.fallback_to_source end get_fallback_to_source fn get_compress_man(cfg: Config): bool return cfg.compress_man end get_compress_man fn get_clean_work(cfg: Config): bool return cfg.clean_work end get_clean_work fn get_clean_pkg(cfg: Config): bool return cfg.clean_pkg end get_clean_pkg fn get_logging(cfg: Config): bool return cfg.logging end get_logging fn get_quiet(cfg: Config): bool return cfg.quiet end get_quiet fn get_timeout(cfg: Config): int return cfg.timeout end get_timeout fn get_retries(cfg: Config): int return cfg.retries end get_retries fn get_use_proxy(cfg: Config): bool return cfg.use_proxy end get_use_proxy fn get_auto_refresh(cfg: Config): bool return cfg.auto_refresh end get_auto_refresh fn get_keep_packages(cfg: Config): bool return cfg.keep_packages end get_keep_packages fn get_keep_sources(cfg: Config): bool return cfg.keep_sources end get_keep_sources fn get_require_signed_repos(cfg: Config): bool return cfg.require_signed_repos end get_require_signed_repos fn get_require_signed_packages(cfg: Config): bool return cfg.require_signed_packages end get_require_signed_packages fn get_chroot_scripts(cfg: Config): bool return cfg.chroot_scripts end get_chroot_scripts // Get the alternate root path (empty string means /) fn get_root(cfg: Config): string return cfg.root end get_root // Get the installation prefix (default /usr/local) fn get_prefix(cfg: Config): string return cfg.prefix end get_prefix // Get the system configuration directory (default /etc) fn get_sysconfdir(cfg: Config): string return cfg.sysconfdir end get_sysconfdir // Get database directory with root prepended fn get_database_dir_rooted(cfg: Config): string if str.length(cfg.root) == 0 return cfg.database_dir end if return cfg.root + cfg.database_dir end get_database_dir_rooted // Get the full install prefix (root + prefix) // This is where package files should be installed fn get_install_prefix(cfg: Config): string if str.length(cfg.root) == 0 return cfg.prefix end if return cfg.root + cfg.prefix end get_install_prefix // Apply command-line overrides to config // Call this after loading config to override with --root and --prefix flags fn apply_overrides(cfg: Config, root: string, prefix: string, no_chroot: bool): Config mut result = cfg if str.length(root) > 0 result.root = root end if if str.length(prefix) > 0 result.prefix = prefix end if if no_chroot result.chroot_scripts = false end if return result end apply_overrides end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: database.reef Authors: Chris Tusa License: Description: Installed package database management ******************************************************************************/ module core.database import types import io.file import io.dir import io.path import core.str import encoding.toml import util.mtree import sys.process import core.result as res export // Check if a package is installed fn is_installed(name: string): bool // List all installed packages (returns count, fills names array) fn list_installed(names: [string], max_count: int): int // Get installed package details fn get_installed(name: string): types.InstalledPackage // Register a new installed package fn register_package(pkg: types.InstalledPackage): bool // Register package with file list copied from extract dir (more efficient) fn register_from_extract_dir(pkg: types.InstalledPackage, extract_dir: string): bool // Unregister (remove) a package from database fn unregister_package(name: string): bool // Get list of files owned by a package (returns count, fills files array) fn get_files(name: string, files: [string], max_count: int): int // Find which package owns a given file path fn find_owner(file_path: string): string // Find which package provides an alternative fn find_provider(alternative: string): string // Ensure database directories exist fn init_db(): bool // Scripts support fn get_scripts_dir(name: string): string fn store_scripts(name: string, extract_dir: string): bool fn has_stored_scripts(name: string): bool // Config files support fn store_config_files(name: string, extract_dir: string): bool fn get_config_files(name: string, config_files: [string], max_count: int): int // Manifest support fn get_manifest_path(name: string): string // Root-aware versions for alternate root installation fn init_db_rooted(root: string): bool fn register_from_extract_dir_rooted(pkg: types.InstalledPackage, extract_dir: string, root: string): bool fn store_scripts_rooted(name: string, extract_dir: string, root: string): bool fn store_config_files_rooted(name: string, extract_dir: string, root: string): bool fn is_installed_rooted(name: string, root: string): bool fn list_installed_rooted(names: [string], max_count: int, root: string): int fn get_installed_rooted(name: string, root: string): types.InstalledPackage fn get_files_rooted(name: string, files: [string], max_count: int, root: string): int fn find_owner_rooted(file_path: string, root: string): string fn unregister_package_rooted(name: string, root: string): bool end export // Get the package database directory for a specific package fn pkg_db_dir(name: string): string return path.join_path(types.get_db_dir(), name) end pkg_db_dir // Get the pkg.toml path for a package fn pkg_toml_path(name: string): string return path.join_path(pkg_db_dir(name), "pkg.toml") end pkg_toml_path // Get the files.txt path for a package fn pkg_files_path(name: string): string return path.join_path(pkg_db_dir(name), "files.txt") end pkg_files_path // Get the manifest.mtree path for a package fn get_manifest_path(name: string): string return path.join_path(pkg_db_dir(name), "manifest.mtree") end get_manifest_path // Initialize the database directory structure fn init_db(): bool if not dir.dir_exists(types.get_db_dir()) return res.is_ok(dir.create_dir_all(types.get_db_dir())) end if return true end init_db // Check if a package is installed by looking for its database entry fn is_installed(name: string): bool let pkg_dir = pkg_db_dir(name) if not dir.dir_exists(pkg_dir) return false end if // Also verify pkg.toml exists return file.fileExists(pkg_toml_path(name)) end is_installed // List all installed packages by scanning the database directory fn list_installed(names: [string], max_count: int): int if not dir.dir_exists(types.get_db_dir()) return 0 end if let entries = res.unwrap_or(dir.list_dir(types.get_db_dir()), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Skip hidden files if str.length(entry) > 0 and entry[0] != '.' // Verify it's a valid package (has pkg.toml) let toml_path = pkg_toml_path(entry) if file.fileExists(toml_path) names[count] = entry count = count + 1 end if end if i = i + 1 end while return count end list_installed // Create an empty InstalledPackage fn new_installed_package(): types.InstalledPackage return types.new_installed_package() end new_installed_package // Get details of an installed package fn get_installed(name: string): types.InstalledPackage if not is_installed(name) return new_installed_package() end if // Read pkg.toml let toml_path = pkg_toml_path(name) let content = res.unwrap_or(file.readFile(toml_path), "") if str.length(content) == 0 return new_installed_package() end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return new_installed_package() end if // Build package info mut pkg = new_installed_package() pkg.info.name = toml.toml_get(keys, vals, entry_count, "package.name") pkg.info.version = toml.toml_get(keys, vals, entry_count, "package.version") pkg.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release") pkg.info.description = toml.toml_get(keys, vals, entry_count, "package.description") pkg.info.url = toml.toml_get(keys, vals, entry_count, "package.url") pkg.info.license = toml.toml_get(keys, vals, entry_count, "package.license") pkg.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer") pkg.info.arch = toml.toml_get(keys, vals, entry_count, "package.arch") // Install metadata pkg.install_date = toml.toml_get(keys, vals, entry_count, "install.date") pkg.install_reason = toml.toml_get(keys, vals, entry_count, "install.reason") // Read files list let files_path = pkg_files_path(name) if file.fileExists(files_path) let files_content = res.unwrap_or(file.readFile(files_path), "") if str.length(files_content) > 0 let alloc_size = count_newlines(files_content) + 16 mut files_arr: [string] = new [string](alloc_size) let files_count = str.split(files_content, '\n', files_arr, alloc_size) // Filter out empty lines mut valid_count = 0 mut j = 0 while j < files_count if str.length(files_arr[j]) > 0 valid_count = valid_count + 1 end if j = j + 1 end while pkg.files = files_arr pkg.files_count = valid_count end if end if return pkg end get_installed // Generate TOML content for a package fn generate_pkg_toml(pkg: types.InstalledPackage): string mut content = "[package]\n" content = content + "name = \"" + pkg.info.name + "\"\n" content = content + "version = \"" + pkg.info.version + "\"\n" content = content + "release = " + int_to_str(pkg.info.release) + "\n" content = content + "description = \"" + pkg.info.description + "\"\n" content = content + "url = \"" + pkg.info.url + "\"\n" content = content + "license = \"" + pkg.info.license + "\"\n" content = content + "maintainer = \"" + pkg.info.maintainer + "\"\n" content = content + "arch = \"" + pkg.info.arch + "\"\n" content = content + "\n" content = content + "[install]\n" content = content + "date = \"" + pkg.install_date + "\"\n" content = content + "reason = \"" + pkg.install_reason + "\"\n" return content end generate_pkg_toml // 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 // Register a newly installed package fn register_package(pkg: types.InstalledPackage): bool // Ensure database directory exists if not init_db() return false end if // Create package directory let pkg_dir = pkg_db_dir(pkg.info.name) if not dir.dir_exists(pkg_dir) if not res.is_ok(dir.create_dir_all(pkg_dir)) return false end if end if // Write pkg.toml let toml_content = generate_pkg_toml(pkg) let toml_path = pkg_toml_path(pkg.info.name) if not res.is_ok(file.writeFile(toml_path, toml_content)) return false end if // Note: files.txt is written by register_from_extract_dir for performance return true end register_package // Count newline characters in a string to estimate line count fn count_newlines(s: string): int let slen = str.length(s) mut count = 0 mut i = 0 while i < slen if s[i] == '\n' count = count + 1 end if i = i + 1 end while return count end count_newlines // Process a .FOOTPRINT file content: filter and normalize paths // Uses str.join() for O(n) single-allocation string building fn process_footprint(content: string): string let newline_count = count_newlines(content) let alloc_size = newline_count + 16 mut lines: [string] = new [string](alloc_size) let line_count = str.split(content, '\n', lines, alloc_size) mut result_lines: [string] = new [string](alloc_size) mut result_count = 0 mut i = 0 while i < line_count mut line = str.trim_ws(lines[i]) if str.length(line) == 0 i = i + 1 continue end if // Strip leading ./ if str.starts_with(line, "./") line = str.substring(line, 2, str.length(line) - 2) end if // Skip metadata files if str.equals(line, ".PKGINFO") or str.equals(line, ".FOOTPRINT") i = i + 1 continue end if if str.starts_with(line, ".SCRIPTS") i = i + 1 continue end if if str.equals(line, ".CONFIG") i = i + 1 continue end if // Skip directories (end with /) let len = str.length(line) if len > 0 and line[len - 1] == '/' i = i + 1 continue end if result_lines[result_count] = line result_count = result_count + 1 i = i + 1 end while return str.join(result_lines, result_count, "\n") end process_footprint // Extract file paths from mtree manifest content for files.txt // Parses manifest entries and returns a newline-joined list of relative paths // (files and symlinks only, no directories, no metadata) fn extract_paths_from_manifest(content: string): string mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) let count = mtree.parse_manifest(content, entries, 8192) mut result_lines: [string] = new [string](8192) mut result_count = 0 mut i = 0 while i < count let etype = mtree.entry_type(entries[i]) let epath = mtree.entry_path(entries[i]) // Only include files and symlinks (matches legacy files.txt behavior) if str.equals(etype, "file") or str.equals(etype, "link") // Strip leading ./ if str.starts_with(epath, "./") result_lines[result_count] = str.substring(epath, 2, str.length(epath) - 2) else result_lines[result_count] = epath end if result_count = result_count + 1 end if i = i + 1 end while return str.join(result_lines, result_count, "\n") end extract_paths_from_manifest // Register package and copy files list from extracted package directory fn register_from_extract_dir(pkg: types.InstalledPackage, extract_dir: string): bool // Register the package metadata if not register_package(pkg) return false end if let files_path = pkg_files_path(pkg.info.name) // Try .MANIFEST first (v2 format), fall back to .FOOTPRINT (legacy) let manifest_path = path.join_path(extract_dir, ".MANIFEST") if file.fileExists(manifest_path) // Store the full manifest for future use (verify, manifest-based remove) let manifest_store = path.join_path(pkg_db_dir(pkg.info.name), "manifest.mtree") let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") file.writeFile(manifest_store, manifest_content) // Also generate files.txt for backward compatibility let result = extract_paths_from_manifest(manifest_content) return res.is_ok(file.writeFile(files_path, result)) end if // Legacy: .FOOTPRINT let footprint_path = path.join_path(extract_dir, ".FOOTPRINT") if not file.fileExists(footprint_path) file.writeFile(files_path, "") return true end if let content = res.unwrap_or(file.readFile(footprint_path), "") if str.length(content) == 0 file.writeFile(files_path, "") return true end if let result = process_footprint(content) return res.is_ok(file.writeFile(files_path, result)) end register_from_extract_dir // Helper to remove a file fn remove_file(file_path: string): bool let cmd = "rm -f \"" + file_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if let exit_code = process.process_wait(pid) return exit_code == 0 end remove_file // Unregister a package from the database fn unregister_package(name: string): bool if not is_installed(name) return true // Already not installed end if let pkg_dir = pkg_db_dir(name) // Remove pkg.toml let toml_path = pkg_toml_path(name) if file.fileExists(toml_path) if not remove_file(toml_path) return false end if end if // Remove files.txt let files_path = pkg_files_path(name) if file.fileExists(files_path) if not remove_file(files_path) return false end if end if // Remove manifest.mtree let manifest_path = get_manifest_path(name) if file.fileExists(manifest_path) remove_file(manifest_path) end if // Remove scripts directory if exists let scripts_dir = get_scripts_dir(name) if dir.dir_exists(scripts_dir) let cmd = "rm -rf \"" + scripts_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if end if // Remove config.txt if exists let config_path = pkg_config_path(name) if file.fileExists(config_path) remove_file(config_path) end if // Remove the package directory return res.is_ok(dir.remove_dir(pkg_dir)) end unregister_package // Get list of files owned by a package fn get_files(name: string, files: [string], max_count: int): int if not is_installed(name) return 0 end if let files_path = pkg_files_path(name) if not file.fileExists(files_path) return 0 end if let content = res.unwrap_or(file.readFile(files_path), "") if str.length(content) == 0 return 0 end if // Split by newlines — size dynamically let alloc_size = count_newlines(content) + 16 mut all_lines: [string] = new [string](alloc_size) let line_count = str.split(content, '\n', all_lines, alloc_size) // Copy non-empty lines to output mut count = 0 mut i = 0 while i < line_count and count < max_count let line = str.trim_ws(all_lines[i]) if str.length(line) > 0 files[count] = line count = count + 1 end if i = i + 1 end while return count end get_files // Find which package owns a given file fn find_owner(file_path: string): string // Normalize the path - strip leading "/" if present mut search_path = file_path if str.length(search_path) > 0 and search_path[0] == '/' search_path = str.substring(search_path, 1, str.length(search_path) - 1) end if // Get list of all installed packages mut packages: [string] = new [string](256) let pkg_count = list_installed(packages, 256) mut i = 0 while i < pkg_count let pkg_name = packages[i] // Get files for this package mut files: [string] = new [string](4096) let file_count = get_files(pkg_name, files, 4096) // Search for matching file mut j = 0 while j < file_count if files[j] == search_path return pkg_name end if j = j + 1 end while i = i + 1 end while return "" // No owner found end find_owner // Find which package provides an alternative fn find_provider(alternative: string): string // Get list of all installed packages mut packages: [string] = new [string](256) let pkg_count = list_installed(packages, 256) mut i = 0 while i < pkg_count let pkg_name = packages[i] let pkg = get_installed(pkg_name) // Check alternatives array // Note: We'd need to parse the alternatives array from pkg.toml // For now, check if this package provides the alternative let toml_path = pkg_toml_path(pkg_name) let content = res.unwrap_or(file.readFile(toml_path), "") if str.length(content) > 0 let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if toml.toml_has_key(keys, vals, entry_count, "package.alternatives") let alts_val = toml.toml_get(keys, vals, entry_count, "package.alternatives") // Check if the alternative is in the array if str.contains(alts_val, alternative) return pkg_name end if end if end if i = i + 1 end while return "" // No provider found end find_provider // Get the scripts directory path for a package in the database fn get_scripts_dir(name: string): string return path.join_path(pkg_db_dir(name), "scripts") end get_scripts_dir // Check if a package has stored scripts in the database fn has_stored_scripts(name: string): bool let scripts_path = get_scripts_dir(name) return dir.dir_exists(scripts_path) end has_stored_scripts // Store scripts from an extracted package to the database fn store_scripts(name: string, extract_dir: string): bool let src_scripts = path.join_path(extract_dir, ".SCRIPTS") // Check if source has scripts if not dir.dir_exists(src_scripts) return true // No scripts is not an error end if let dest_scripts = get_scripts_dir(name) // Create scripts directory in database if not dir.dir_exists(dest_scripts) if not res.is_ok(dir.create_dir_all(dest_scripts)) return false end if end if // Copy all script files let cmd = "cp -r \"" + src_scripts + "/\"* \"" + dest_scripts + "/\" 2>/dev/null" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if let exit_code = process.process_wait(pid) // Don't fail if cp fails (might be empty directory) return true end store_scripts // Get the config.txt path for a package fn pkg_config_path(name: string): string return path.join_path(pkg_db_dir(name), "config.txt") end pkg_config_path // Store config files list from an extracted package to the database fn store_config_files(name: string, extract_dir: string): bool let src_config = path.join_path(extract_dir, ".CONFIG") // Check if source has config files list if not file.fileExists(src_config) return true // No config files is not an error end if let dest_config = pkg_config_path(name) // Copy the config files list let content = res.unwrap_or(file.readFile(src_config), "") if str.length(content) > 0 return res.is_ok(file.writeFile(dest_config, content)) end if return true end store_config_files // Get list of config files for a package fn get_config_files(name: string, config_files: [string], max_count: int): int let config_path = pkg_config_path(name) if not file.fileExists(config_path) return 0 end if let content = res.unwrap_or(file.readFile(config_path), "") if str.length(content) == 0 return 0 end if // Split by newlines mut lines: [string] = new [string](max_count) let line_count = str.split(content, '\n', lines, max_count) // Filter out empty lines and comments mut count = 0 mut i = 0 while i < line_count and count < max_count let line = str.trim_ws(lines[i]) if str.length(line) > 0 and line[0] != '#' config_files[count] = line count = count + 1 end if i = i + 1 end while return count end get_config_files // ============================================================================ // Root-aware functions for alternate root installation // ============================================================================ // Get the rooted database directory fn get_db_dir_rooted(root: string): string if str.length(root) == 0 return types.get_db_dir() end if return root + types.get_db_dir() end get_db_dir_rooted // Get the rooted package database directory fn pkg_db_dir_rooted(name: string, root: string): string return path.join_path(get_db_dir_rooted(root), name) end pkg_db_dir_rooted // Get the rooted pkg.toml path fn pkg_toml_path_rooted(name: string, root: string): string return path.join_path(pkg_db_dir_rooted(name, root), "pkg.toml") end pkg_toml_path_rooted // Get the rooted files.txt path fn pkg_files_path_rooted(name: string, root: string): string return path.join_path(pkg_db_dir_rooted(name, root), "files.txt") end pkg_files_path_rooted // Initialize database with alternate root fn init_db_rooted(root: string): bool let db_dir = get_db_dir_rooted(root) if not dir.dir_exists(db_dir) return res.is_ok(dir.create_dir_all(db_dir)) end if return true end init_db_rooted // Check if package is installed in alternate root fn is_installed_rooted(name: string, root: string): bool let pkg_dir = pkg_db_dir_rooted(name, root) if not dir.dir_exists(pkg_dir) return false end if return file.fileExists(pkg_toml_path_rooted(name, root)) end is_installed_rooted // Register package with alternate root fn register_package_rooted(pkg: types.InstalledPackage, root: string): bool // Ensure database directory exists if not init_db_rooted(root) return false end if // Create package directory let pkg_dir = pkg_db_dir_rooted(pkg.info.name, root) if not dir.dir_exists(pkg_dir) if not res.is_ok(dir.create_dir_all(pkg_dir)) return false end if end if // Write pkg.toml let toml_content = generate_pkg_toml(pkg) let toml_path = pkg_toml_path_rooted(pkg.info.name, root) if not res.is_ok(file.writeFile(toml_path, toml_content)) return false end if return true end register_package_rooted // Register package from extract dir with alternate root fn register_from_extract_dir_rooted(pkg: types.InstalledPackage, extract_dir: string, root: string): bool // Register the package metadata if not register_package_rooted(pkg, root) return false end if let files_path = pkg_files_path_rooted(pkg.info.name, root) // Try .MANIFEST first (v2 format), fall back to .FOOTPRINT (legacy) let manifest_path = path.join_path(extract_dir, ".MANIFEST") if file.fileExists(manifest_path) // Store the full manifest let manifest_store = path.join_path(pkg_db_dir_rooted(pkg.info.name, root), "manifest.mtree") let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") file.writeFile(manifest_store, manifest_content) // Also generate files.txt for backward compatibility let result = extract_paths_from_manifest(manifest_content) return res.is_ok(file.writeFile(files_path, result)) end if // Legacy: .FOOTPRINT let footprint_path = path.join_path(extract_dir, ".FOOTPRINT") if not file.fileExists(footprint_path) file.writeFile(files_path, "") return true end if let content = res.unwrap_or(file.readFile(footprint_path), "") if str.length(content) == 0 file.writeFile(files_path, "") return true end if let result = process_footprint(content) return res.is_ok(file.writeFile(files_path, result)) end register_from_extract_dir_rooted // Get scripts directory with alternate root fn get_scripts_dir_rooted(name: string, root: string): string return path.join_path(pkg_db_dir_rooted(name, root), "scripts") end get_scripts_dir_rooted // Store scripts with alternate root fn store_scripts_rooted(name: string, extract_dir: string, root: string): bool let src_scripts = path.join_path(extract_dir, ".SCRIPTS") if not dir.dir_exists(src_scripts) return true end if let dest_scripts = get_scripts_dir_rooted(name, root) if not dir.dir_exists(dest_scripts) if not res.is_ok(dir.create_dir_all(dest_scripts)) return false end if end if let cmd = "cp -r \"" + src_scripts + "/\"* \"" + dest_scripts + "/\" 2>/dev/null" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if process.process_wait(pid) return true end store_scripts_rooted // Get config path with alternate root fn pkg_config_path_rooted(name: string, root: string): string return path.join_path(pkg_db_dir_rooted(name, root), "config.txt") end pkg_config_path_rooted // Store config files with alternate root fn store_config_files_rooted(name: string, extract_dir: string, root: string): bool let src_config = path.join_path(extract_dir, ".CONFIG") if not file.fileExists(src_config) return true end if let dest_config = pkg_config_path_rooted(name, root) let content = res.unwrap_or(file.readFile(src_config), "") if str.length(content) > 0 return res.is_ok(file.writeFile(dest_config, content)) end if return true end store_config_files_rooted // List all installed packages in alternate root fn list_installed_rooted(names: [string], max_count: int, root: string): int let db_dir = get_db_dir_rooted(root) if not dir.dir_exists(db_dir) return 0 end if let entries = res.unwrap_or(dir.list_dir(db_dir), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Skip hidden files if str.length(entry) > 0 and entry[0] != '.' // Verify it's a valid package (has pkg.toml) let toml_path = pkg_toml_path_rooted(entry, root) if file.fileExists(toml_path) names[count] = entry count = count + 1 end if end if i = i + 1 end while return count end list_installed_rooted // Get details of an installed package in alternate root fn get_installed_rooted(name: string, root: string): types.InstalledPackage if not is_installed_rooted(name, root) return new_installed_package() end if // Read pkg.toml let toml_path = pkg_toml_path_rooted(name, root) let content = res.unwrap_or(file.readFile(toml_path), "") if str.length(content) == 0 return new_installed_package() end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return new_installed_package() end if // Build package info mut pkg = new_installed_package() pkg.info.name = toml.toml_get(keys, vals, entry_count, "package.name") pkg.info.version = toml.toml_get(keys, vals, entry_count, "package.version") pkg.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release") pkg.info.description = toml.toml_get(keys, vals, entry_count, "package.description") pkg.info.url = toml.toml_get(keys, vals, entry_count, "package.url") pkg.info.license = toml.toml_get(keys, vals, entry_count, "package.license") pkg.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer") pkg.info.arch = toml.toml_get(keys, vals, entry_count, "package.arch") // Install metadata pkg.install_date = toml.toml_get(keys, vals, entry_count, "install.date") pkg.install_reason = toml.toml_get(keys, vals, entry_count, "install.reason") // Read files list let files_path = pkg_files_path_rooted(name, root) if file.fileExists(files_path) let files_content = res.unwrap_or(file.readFile(files_path), "") if str.length(files_content) > 0 let alloc_size = count_newlines(files_content) + 16 mut files_arr: [string] = new [string](alloc_size) let files_count = str.split(files_content, '\n', files_arr, alloc_size) // Filter out empty lines mut valid_count = 0 mut j = 0 while j < files_count if str.length(files_arr[j]) > 0 valid_count = valid_count + 1 end if j = j + 1 end while pkg.files = files_arr pkg.files_count = valid_count end if end if return pkg end get_installed_rooted // Get list of files owned by a package in alternate root fn get_files_rooted(name: string, files: [string], max_count: int, root: string): int if not is_installed_rooted(name, root) return 0 end if let files_path = pkg_files_path_rooted(name, root) if not file.fileExists(files_path) return 0 end if let content = res.unwrap_or(file.readFile(files_path), "") if str.length(content) == 0 return 0 end if // Split by newlines — size dynamically let alloc_size = count_newlines(content) + 16 mut all_lines: [string] = new [string](alloc_size) let line_count = str.split(content, '\n', all_lines, alloc_size) // Copy non-empty lines to output mut count = 0 mut i = 0 while i < line_count and count < max_count let line = str.trim_ws(all_lines[i]) if str.length(line) > 0 files[count] = line count = count + 1 end if i = i + 1 end while return count end get_files_rooted // Find which package owns a given file in alternate root fn find_owner_rooted(file_path: string, root: string): string // Normalize the path - strip leading "/" if present mut search_path = file_path if str.length(search_path) > 0 and search_path[0] == '/' search_path = str.substring(search_path, 1, str.length(search_path) - 1) end if // Get list of all installed packages in this root mut packages: [string] = new [string](256) let pkg_count = list_installed_rooted(packages, 256, root) mut i = 0 while i < pkg_count let pkg_name = packages[i] // Get files for this package mut files: [string] = new [string](4096) let file_count = get_files_rooted(pkg_name, files, 4096, root) // Search for matching file mut j = 0 while j < file_count if files[j] == search_path return pkg_name end if j = j + 1 end while i = i + 1 end while return "" // No owner found end find_owner_rooted // Unregister a package from the database in alternate root fn unregister_package_rooted(name: string, root: string): bool if not is_installed_rooted(name, root) return true // Already not installed end if let pkg_dir = pkg_db_dir_rooted(name, root) // Remove pkg.toml let toml_path = pkg_toml_path_rooted(name, root) if file.fileExists(toml_path) if not remove_file(toml_path) return false end if end if // Remove files.txt let files_path = pkg_files_path_rooted(name, root) if file.fileExists(files_path) if not remove_file(files_path) return false end if end if // Remove scripts directory if exists let scripts_dir = get_scripts_dir_rooted(name, root) if dir.dir_exists(scripts_dir) let cmd = "rm -rf \"" + scripts_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if end if // Remove config.txt if exists let config_path = pkg_config_path_rooted(name, root) if file.fileExists(config_path) remove_file(config_path) end if // Remove the package directory return res.is_ok(dir.remove_dir(pkg_dir)) end unregister_package_rooted end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: groups.reef Authors: Chris Tusa License: Description: Package groups support for Coral (@groupname) ******************************************************************************/ module core.groups import core.str import io.file import io.dir import io.path import encoding.toml import types import core.result as res export type GroupResult fn load_group(name: string): GroupResult fn list_groups(groups: [types.PackageGroup], max_count: int): int fn expand_group(name: string, packages: [string], max_count: int): int fn is_group(name: string): bool end export // Result for group loading type GroupResult = struct success: bool group: types.PackageGroup error: string end GroupResult // Get the groups directory fn get_groups_dir(): string return types.get_groups_dir() end get_groups_dir // Create empty group fn new_group(): types.PackageGroup return types.new_package_group() end new_group // Create success result fn success_result(g: types.PackageGroup): GroupResult return GroupResult{ success: true, group: g, error: "" } end success_result // Create error result fn error_result(msg: string): GroupResult return GroupResult{ success: false, group: new_group(), error: msg } end error_result // Check if a package name is a group reference (starts with @) fn is_group(name: string): bool if str.length(name) < 2 return false end if return name[0] == '@' end is_group // Load a group definition from TOML file fn load_group(name: string): GroupResult // Remove @ prefix if present mut group_name = name if name[0] == '@' group_name = str.substring(name, 1, str.length(name) - 1) end if let groups_dir = get_groups_dir() let group_file = path.join_path(groups_dir, group_name + ".toml") if not file.fileExists(group_file) return error_result("Group not found: " + group_name) end if let content = res.unwrap_or(file.readFile(group_file), "") if str.length(content) == 0 return error_result("Failed to read group file") end if // Parse TOML using key/value arrays let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return error_result("Invalid group file format") end if mut group = new_group() group.name = group_name // Get description if toml.toml_has_key(keys, vals, entry_count, "group.description") group.description = toml.toml_get(keys, vals, entry_count, "group.description") end if // Get members list (packages array) if toml.toml_has_key(keys, vals, entry_count, "group.packages") let members_str = toml.toml_get(keys, vals, entry_count, "group.packages") group.members_count = parse_toml_array(members_str, group.members, 256) end if return success_result(group) end load_group // List all available groups fn list_groups(groups: [types.PackageGroup], max_count: int): int let groups_dir = get_groups_dir() if not dir.dir_exists(groups_dir) return 0 end if let entries = res.unwrap_or(dir.list_dir(groups_dir), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Look for .toml files if str.ends_with(entry, ".toml") let name_len = str.length(entry) - 5 let name = str.substring(entry, 0, name_len) let result = load_group(name) if result.success groups[count] = result.group count = count + 1 end if end if i = i + 1 end while return count end list_groups // Expand a group to its member packages fn expand_group(name: string, packages: [string], max_count: int): int let result = load_group(name) if not result.success return 0 end if let group = result.group mut count = 0 while count < group.members_count and count < max_count packages[count] = group.members[count] count = count + 1 end while return count end expand_group // Parse TOML array value into string array // Format: ["item1", "item2", "item3"] fn parse_toml_array(value: string, result: [string], max_items: int): int let len = str.length(value) if len < 2 return 0 end if // Check for array brackets if value[0] != '[' return 0 end if mut count = 0 mut i = 1 mut in_string = false mut item_start = 0 while i < len and count < max_items let c = value[i] if c == '"' and not in_string in_string = true item_start = i + 1 elif c == '"' and in_string // End of string item let item_len = i - item_start if item_len > 0 result[count] = str.substring(value, item_start, item_len) count = count + 1 end if in_string = false elif c == ']' and not in_string // End of array return count end if i = i + 1 end while return count end parse_toml_array end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: package.reef Authors: Chris Tusa License: Description: Package archive handling - extract, read metadata, install files ******************************************************************************/ module core.package import types import io.file import io.dir import io.path import core.str import encoding.toml import util.archive import util.mtree import util.checksum import sys.process import fs.stat import fs.perm import fs.link import fs.ops import core.result as res export // Package extraction result type ExtractResult // Extract a package to a temporary directory fn extract_package(pkg_path: string, dest_dir: string): ExtractResult // Read package info from an extracted package or archive fn read_pkginfo(pkg_path: string): types.PackageInfo // Read file list from an extracted package (legacy .FOOTPRINT or .MANIFEST) fn read_footprint(pkg_path: string, files: [string], max_count: int): int // Read manifest entries from an extracted package fn read_manifest(pkg_dir: string, entries: [mtree.ManifestEntry], max_count: int): int // Install files from extracted package to root (manifest-driven) fn install_files(src_dir: string, root: string): bool // Remove files listed in footprint from root fn remove_files(files: [string], file_count: int, root: string): bool // Remove files using manifest entries (with explicit directory tracking) fn remove_files_manifest(entries: [mtree.ManifestEntry], entry_count: int, root: string): bool // Verify package integrity (check all files exist in extracted dir) fn verify_package(pkg_dir: string): bool // Install files with old manifest for upgrade config protection fn install_files_upgrade(src_dir: string, root: string, old_entries: [mtree.ManifestEntry], old_entry_count: int): bool // Script execution context (chroot-confined maintainer scripts) type ScriptCtx fn new_script_ctx(root: string, do_chroot: bool): ScriptCtx fn script_wants_chroot(root: string, chroot_scripts: bool): bool fn build_script_cmd(ctx: ScriptCtx, script_path: string, script_name: string, args: string, is_sh: bool): string fn resolve_script_ctx(root: string, chroot_scripts: bool, caller_is_root: bool): ScriptCtx // Install scripts support (version passed as $VERSION to scripts) fn has_scripts(pkg_dir: string): bool fn run_pre_install(ctx: ScriptCtx, pkg_dir: string, version: string): bool fn run_post_install(ctx: ScriptCtx, pkg_dir: string, version: string): bool fn run_pre_remove(ctx: ScriptCtx, pkg_dir: string, version: string): bool fn run_post_remove(ctx: ScriptCtx, pkg_dir: string, version: string): bool // Upgrade scripts ($OLD_VERSION $NEW_VERSION passed to scripts) fn run_pre_upgrade(ctx: ScriptCtx, pkg_dir: string, old_version: string, new_version: string): bool fn run_post_upgrade(ctx: ScriptCtx, pkg_dir: string, old_version: string, new_version: string): bool // Run scripts from a stored scripts directory (for remove/upgrade operations) fn run_stored_pre_remove(ctx: ScriptCtx, scripts_dir: string, version: string): bool fn run_stored_post_remove(ctx: ScriptCtx, scripts_dir: string, version: string): bool fn run_stored_pre_upgrade(ctx: ScriptCtx, scripts_dir: string, old_version: string, new_version: string): bool fn run_stored_post_upgrade(ctx: ScriptCtx, scripts_dir: string, old_version: string, new_version: string): bool // Config file protection fn read_config_files(pkg_dir: string, config_files: [string], max_count: int): int fn is_config_file(rel_path: string, config_files: [string], config_count: int): bool fn install_config_file(src_path: string, dest_path: string, manifest_sha256: string, stored_sha256: string): bool end export // Extraction result type ExtractResult = struct success: bool extract_dir: string error: string end ExtractResult // Create a failed extraction result fn extract_error(msg: string): ExtractResult return ExtractResult{ success: false, extract_dir: "", error: msg } end extract_error // Create a successful extraction result fn extract_success(dir: string): ExtractResult return ExtractResult{ success: true, extract_dir: dir, error: "" } end extract_success // Extract a package archive to destination directory fn extract_package(pkg_path: string, dest_dir: string): ExtractResult // Verify package exists if not file.fileExists(pkg_path) return extract_error("Package file not found: " + pkg_path) end if // Ensure destination directory exists if not dir.dir_exists(dest_dir) if not res.is_ok(dir.create_dir_all(dest_dir)) return extract_error("Failed to create extraction directory: " + dest_dir) end if end if // Extract using archive module if not archive.extract(pkg_path, dest_dir) return extract_error("Failed to extract package: " + pkg_path) end if // Verify .PKGINFO exists let pkginfo_path = path.join_path(dest_dir, ".PKGINFO") if not file.fileExists(pkginfo_path) return extract_error("Invalid package: missing .PKGINFO") end if return extract_success(dest_dir) end extract_package // Read package info from .PKGINFO in an extracted package directory fn read_pkginfo(pkg_dir: string): types.PackageInfo mut info = types.new_package_info() let pkginfo_path = path.join_path(pkg_dir, ".PKGINFO") if not file.fileExists(pkginfo_path) return info end if let content = res.unwrap_or(file.readFile(pkginfo_path), "") if str.length(content) == 0 return info end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return info end if // Extract fields info.name = toml.toml_get(keys, vals, entry_count, "package.name") info.version = toml.toml_get(keys, vals, entry_count, "package.version") info.release = toml.toml_get_int(keys, vals, entry_count, "package.release") info.description = toml.toml_get(keys, vals, entry_count, "package.description") info.url = toml.toml_get(keys, vals, entry_count, "package.url") info.license = toml.toml_get(keys, vals, entry_count, "package.license") info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer") info.arch = toml.toml_get(keys, vals, entry_count, "package.arch") // Parse [dependencies] section (optional — legacy packages won't have it) if toml.toml_has_key(keys, vals, entry_count, "dependencies.runtime") let rt_val = toml.toml_get(keys, vals, entry_count, "dependencies.runtime") info.runtime_deps_count = parse_toml_array(rt_val, info.runtime_deps, 64) end if if toml.toml_has_key(keys, vals, entry_count, "dependencies.build") let bd_val = toml.toml_get(keys, vals, entry_count, "dependencies.build") info.build_deps_count = parse_toml_array(bd_val, info.build_deps, 64) end if return info end read_pkginfo // Read file list from .MANIFEST (v2) or .FOOTPRINT (legacy) // Returns a flat list of relative file paths (files and symlinks only) fn read_footprint(pkg_dir: string, files: [string], max_count: int): int // Try .MANIFEST first (v2 format) let manifest_path = path.join_path(pkg_dir, ".MANIFEST") if file.fileExists(manifest_path) let content = res.unwrap_or(file.readFile(manifest_path), "") if str.length(content) > 0 mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](max_count) let entry_count = mtree.parse_manifest(content, entries, max_count) mut count = 0 mut i = 0 while i < entry_count and count < max_count let etype = mtree.entry_type(entries[i]) // Only include files and symlinks (matches legacy behavior) if str.equals(etype, "file") or str.equals(etype, "link") let epath = mtree.entry_path(entries[i]) if str.starts_with(epath, "./") files[count] = str.substring(epath, 2, str.length(epath) - 2) else files[count] = epath end if count = count + 1 end if i = i + 1 end while return count end if end if // Fallback: legacy .FOOTPRINT let footprint_path = path.join_path(pkg_dir, ".FOOTPRINT") if not file.fileExists(footprint_path) return 0 end if let content = res.unwrap_or(file.readFile(footprint_path), "") if str.length(content) == 0 return 0 end if mut lines: [string] = new [string](max_count) let line_count = str.split(content, '\n', lines, max_count) mut count = 0 mut i = 0 while i < line_count and count < max_count let line = str.trim_ws(lines[i]) if str.length(line) == 0 i = i + 1 continue end if if str.equals(line, "./.PKGINFO") or str.equals(line, "./.FOOTPRINT") i = i + 1 continue end if if str.equals(line, ".PKGINFO") or str.equals(line, ".FOOTPRINT") i = i + 1 continue end if let len = str.length(line) if len > 0 and line[len - 1] == '/' i = i + 1 continue end if if str.starts_with(line, "./") files[count] = str.substring(line, 2, len - 2) else files[count] = line end if count = count + 1 i = i + 1 end while return count end read_footprint // Read manifest entries from .MANIFEST in an extracted package directory fn read_manifest(pkg_dir: string, entries: [mtree.ManifestEntry], max_count: int): int let manifest_path = path.join_path(pkg_dir, ".MANIFEST") 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 read_manifest // Install files from extracted package to root directory using manifest fn install_files(src_dir: string, root: string): bool // Read config file list for protection checks mut config_files: [string] = new [string](256) let config_count = read_config_files(src_dir, config_files, 256) // Parse .MANIFEST mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) let entry_count = read_manifest(src_dir, entries, 8192) if entry_count == 0 return false end if // Walk manifest entries in order (dirs before their contents) mut i = 0 while i < entry_count let entry = entries[i] let etype = mtree.entry_type(entry) let epath = mtree.entry_path(entry) // Strip leading ./ from manifest path to get relative path mut rel_path = epath if str.starts_with(epath, "./") rel_path = str.substring(epath, 2, str.length(epath) - 2) end if let dest_path = path.join_path(root, rel_path) let src_path = path.join_path(src_dir, rel_path) let mode = mtree.entry_mode(entry) let uid = mtree.name_to_uid(mtree.entry_uname(entry)) let gid = mtree.name_to_gid(mtree.entry_gname(entry)) if str.equals(etype, "dir") // Create directory if it doesn't exist if not dir.dir_exists(dest_path) if not res.is_ok(dir.create_dir_all(dest_path)) return false end if end if perm.chmod(dest_path, mode) perm.chown(dest_path, uid, gid) elif str.equals(etype, "file") // Check if this is a config-protected file if is_config_file(rel_path, config_files, config_count) // Config file — use checksum-based protection let manifest_sha = mtree.entry_sha256(entry) install_config_file(src_path, dest_path, manifest_sha, "") else // Ensure parent directory exists let parent = path.dirname(dest_path) if str.length(parent) > 0 and not dir.dir_exists(parent) dir.create_dir_all(parent) end if // Remove any conflicting target (symlink, file, or dir) if stat.is_symlink(dest_path) ops.remove_file(dest_path) elif stat.exists(dest_path) ops.remove_file(dest_path) end if // Copy file preserving permissions from source if not res.is_ok(ops.copy_file_preserve(src_path, dest_path)) return false end if // Set permissions and ownership from manifest perm.chmod(dest_path, mode) perm.chown(dest_path, uid, gid) // Verify sha256 after copy let manifest_sha = mtree.entry_sha256(entry) if str.length(manifest_sha) > 0 let installed_sha = checksum.sha256_file(dest_path) if not str.equals(installed_sha, manifest_sha) return false end if end if end if elif str.equals(etype, "link") let link_target = mtree.entry_link(entry) // Remove any conflicting target if stat.is_symlink(dest_path) or stat.exists(dest_path) ops.remove_file(dest_path) end if // Ensure parent directory exists let parent = path.dirname(dest_path) if str.length(parent) > 0 and not dir.dir_exists(parent) dir.create_dir_all(parent) end if // Create symlink if not res.is_ok(link.symlink(link_target, dest_path)) return false end if end if i = i + 1 end while return true end install_files // Remove files from root directory based on flat file list (legacy) fn remove_files(files: [string], file_count: int, root: string): bool // 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, rel_path) if stat.is_symlink(full_path) or stat.exists(full_path) ops.remove_file(full_path) 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, rel_path) let parent = path.dirname(full_path) if str.length(parent) > 0 and dir.dir_exists(parent) dir.remove_dir(parent) end if i = i - 1 end while return true end remove_files // Remove files using manifest entries (with explicit directory tracking) // Processes in reverse order: files/symlinks first, then directories fn remove_files_manifest(entries: [mtree.ManifestEntry], entry_count: int, root: string): bool mut all_success = true // First pass: remove files and symlinks in reverse order mut i = entry_count - 1 while i >= 0 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(root, rel_path) if str.equals(etype, "file") or str.equals(etype, "link") if stat.is_symlink(full_path) or stat.exists(full_path) if not res.is_ok(ops.remove_file(full_path)) all_success = false end if end if end if i = i - 1 end while // Second pass: remove directories in reverse order (deepest first) i = entry_count - 1 while i >= 0 let entry = entries[i] let etype = mtree.entry_type(entry) let epath = mtree.entry_path(entry) if str.equals(etype, "dir") 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(root, rel_path) // Only remove empty directories (remove_dir fails on non-empty) if dir.dir_exists(full_path) dir.remove_dir(full_path) end if end if i = i - 1 end while return all_success end remove_files_manifest // Verify package has required files fn verify_package(pkg_dir: string): bool // Check for .PKGINFO let pkginfo_path = path.join_path(pkg_dir, ".PKGINFO") if not file.fileExists(pkginfo_path) return false end if // Check for .MANIFEST (or legacy .FOOTPRINT) let manifest_path = path.join_path(pkg_dir, ".MANIFEST") let footprint_path = path.join_path(pkg_dir, ".FOOTPRINT") if not file.fileExists(manifest_path) and not file.fileExists(footprint_path) return false end if // Verify we can read package info let info = read_pkginfo(pkg_dir) if str.length(info.name) == 0 return false end if return true end verify_package // Check if package has install/remove scripts fn has_scripts(pkg_dir: string): bool let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") return dir.dir_exists(scripts_dir) end has_scripts // Context describing where a package's maintainer scripts run. // root == "/" -> normal install on the live system // do_chroot -> confine scripts to root via chroot (root must be != "/") type ScriptCtx = struct root: string do_chroot: bool end ScriptCtx // Construct a ScriptCtx. fn new_script_ctx(root: string, do_chroot: bool): ScriptCtx return ScriptCtx{ root: root, do_chroot: do_chroot } end new_script_ctx // Pure predicate: should scripts be chrooted for this root + config? fn script_wants_chroot(root: string, chroot_scripts: bool): bool if str.equals(root, "/") return false end if return chroot_scripts end script_wants_chroot // Resolve the script execution context for a command flow. // want_chroot requires both a non-"/" root with chroot enabled AND a root caller; // the caller is responsible for aborting first when chroot is wanted but caller_is_root // is false (see resolve in the command flows). fn resolve_script_ctx(root: string, chroot_scripts: bool, caller_is_root: bool): ScriptCtx mut want_chroot = script_wants_chroot(root, chroot_scripts) if not caller_is_root want_chroot = false end if return new_script_ctx(root, want_chroot) end resolve_script_ctx // The single place a maintainer-script shell command is assembled. // is_sh true -> run under zsh; false -> execute the file directly. fn build_script_cmd(ctx: ScriptCtx, script_path: string, script_name: string, args: string, is_sh: bool): string mut cmd = "" if ctx.do_chroot // Confined: run inside ; from the script's view the target is "/". let in_root_path = "/.SCRIPTS/" + script_name mut body = "" if is_sh body = "zsh \"" + in_root_path + "\"" else body = "\"" + in_root_path + "\"" end if cmd = "chroot \"" + ctx.root + "\" env CORAL_ROOT=\"/\" " + body else // Host: normal install or --no-chroot-scripts. Export CORAL_ROOT=ctx.root. mut body = "" if is_sh body = "zsh \"" + script_path + "\"" else body = "\"" + script_path + "\"" end if cmd = "CORAL_ROOT=\"" + ctx.root + "\" " + body end if if str.length(args) > 0 cmd = cmd + " " + args end if return cmd end build_script_cmd // Copy a package's .SCRIPTS dir into /.SCRIPTS so it is reachable post-chroot. fn stage_scripts_in_root(pkg_scripts_dir: string, root: string): bool let dest = path.join_path(root, ".SCRIPTS") // Clean any stale staging first. let rm_cmd = "rm -rf \"" + dest + "\"" let rm_pid = process.process_spawn_shell(rm_cmd) process.process_wait(rm_pid) // Copy preserving perms/owner/symlinks. let cp_cmd = "cp -a \"" + pkg_scripts_dir + "\" \"" + dest + "\"" let pid = process.process_spawn_shell(cp_cmd) if pid < 0 return false end if let code = process.process_wait(pid) return code == 0 end stage_scripts_in_root // Remove staged scripts from /.SCRIPTS. proc unstage_scripts_in_root(root: string) let dest = path.join_path(root, ".SCRIPTS") let cmd = "rm -rf \"" + dest + "\"" let pid = process.process_spawn_shell(cmd) process.process_wait(pid) end unstage_scripts_in_root // Run a script from the .SCRIPTS directory fn run_script(ctx: ScriptCtx, pkg_dir: string, script_name: string, script_args: string): bool let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let script_path = path.join_path(scripts_dir, script_name) if not file.fileExists(script_path) return true end if if ctx.do_chroot if not stage_scripts_in_root(scripts_dir, ctx.root) // Staging may have left a partial /.SCRIPTS; clean it before bailing. unstage_scripts_in_root(ctx.root) return false end if end if let is_sh = str.ends_with(script_name, ".sh") let cmd = build_script_cmd(ctx, script_path, script_name, script_args, is_sh) let pid = process.process_spawn_shell(cmd) mut ok = false if pid >= 0 let exit_code = process.process_wait(pid) ok = exit_code == 0 end if if ctx.do_chroot unstage_scripts_in_root(ctx.root) end if return ok end run_script // Run pre-install script (passes $VERSION) fn run_pre_install(ctx: ScriptCtx, pkg_dir: string, version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") // Try .sh first, then bare executable let sh_path = path.join_path(scripts_dir, "pre-install.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "pre-install.sh", args) end if return run_script(ctx, pkg_dir, "pre-install", args) end run_pre_install // Run post-install script (passes $VERSION) fn run_post_install(ctx: ScriptCtx, pkg_dir: string, version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let sh_path = path.join_path(scripts_dir, "post-install.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "post-install.sh", args) end if return run_script(ctx, pkg_dir, "post-install", args) end run_post_install // Run pre-remove script (passes $VERSION) fn run_pre_remove(ctx: ScriptCtx, pkg_dir: string, version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let sh_path = path.join_path(scripts_dir, "pre-remove.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "pre-remove.sh", args) end if return run_script(ctx, pkg_dir, "pre-remove", args) end run_pre_remove // Run post-remove script (passes $VERSION) fn run_post_remove(ctx: ScriptCtx, pkg_dir: string, version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let sh_path = path.join_path(scripts_dir, "post-remove.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "post-remove.sh", args) end if return run_script(ctx, pkg_dir, "post-remove", args) end run_post_remove // Run pre-upgrade script (passes $OLD_VERSION $NEW_VERSION) fn run_pre_upgrade(ctx: ScriptCtx, pkg_dir: string, old_version: string, new_version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + old_version + "\" \"" + new_version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let sh_path = path.join_path(scripts_dir, "pre-upgrade.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "pre-upgrade.sh", args) end if return run_script(ctx, pkg_dir, "pre-upgrade", args) end run_pre_upgrade // Run post-upgrade script (passes $OLD_VERSION $NEW_VERSION) fn run_post_upgrade(ctx: ScriptCtx, pkg_dir: string, old_version: string, new_version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + old_version + "\" \"" + new_version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let sh_path = path.join_path(scripts_dir, "post-upgrade.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "post-upgrade.sh", args) end if return run_script(ctx, pkg_dir, "post-upgrade", args) end run_post_upgrade // Run a script directly from a stored scripts directory fn run_stored_script(ctx: ScriptCtx, scripts_dir: string, script_name: string, script_args: string): bool let script_path = path.join_path(scripts_dir, script_name) if not file.fileExists(script_path) return true end if if ctx.do_chroot if not stage_scripts_in_root(scripts_dir, ctx.root) // Staging may have left a partial /.SCRIPTS; clean it before bailing. unstage_scripts_in_root(ctx.root) return false end if end if let is_sh = str.ends_with(script_name, ".sh") let cmd = build_script_cmd(ctx, script_path, script_name, script_args, is_sh) let pid = process.process_spawn_shell(cmd) mut ok = false if pid >= 0 let exit_code = process.process_wait(pid) ok = exit_code == 0 end if if ctx.do_chroot unstage_scripts_in_root(ctx.root) end if return ok end run_stored_script // Run pre-remove script from stored scripts directory (passes $VERSION) fn run_stored_pre_remove(ctx: ScriptCtx, scripts_dir: string, version: string): bool if not dir.dir_exists(scripts_dir) return true end if let args = "\"" + version + "\"" let sh_path = path.join_path(scripts_dir, "pre-remove.sh") if file.fileExists(sh_path) return run_stored_script(ctx, scripts_dir, "pre-remove.sh", args) end if return run_stored_script(ctx, scripts_dir, "pre-remove", args) end run_stored_pre_remove // Run post-remove script from stored scripts directory (passes $VERSION) fn run_stored_post_remove(ctx: ScriptCtx, scripts_dir: string, version: string): bool if not dir.dir_exists(scripts_dir) return true end if let args = "\"" + version + "\"" let sh_path = path.join_path(scripts_dir, "post-remove.sh") if file.fileExists(sh_path) return run_stored_script(ctx, scripts_dir, "post-remove.sh", args) end if return run_stored_script(ctx, scripts_dir, "post-remove", args) end run_stored_post_remove // Run pre-upgrade script from stored scripts directory (passes $OLD_VERSION $NEW_VERSION) fn run_stored_pre_upgrade(ctx: ScriptCtx, scripts_dir: string, old_version: string, new_version: string): bool if not dir.dir_exists(scripts_dir) return true end if let args = "\"" + old_version + "\" \"" + new_version + "\"" let sh_path = path.join_path(scripts_dir, "pre-upgrade.sh") if file.fileExists(sh_path) return run_stored_script(ctx, scripts_dir, "pre-upgrade.sh", args) end if return run_stored_script(ctx, scripts_dir, "pre-upgrade", args) end run_stored_pre_upgrade // Run post-upgrade script from stored scripts directory (passes $OLD_VERSION $NEW_VERSION) fn run_stored_post_upgrade(ctx: ScriptCtx, scripts_dir: string, old_version: string, new_version: string): bool if not dir.dir_exists(scripts_dir) return true end if let args = "\"" + old_version + "\" \"" + new_version + "\"" let sh_path = path.join_path(scripts_dir, "post-upgrade.sh") if file.fileExists(sh_path) return run_stored_script(ctx, scripts_dir, "post-upgrade.sh", args) end if return run_stored_script(ctx, scripts_dir, "post-upgrade", args) end run_stored_post_upgrade // Read list of config files from .CONFIG file in extracted package fn read_config_files(pkg_dir: string, config_files: [string], max_count: int): int let config_path = path.join_path(pkg_dir, ".CONFIG") if not file.fileExists(config_path) return 0 end if let content = res.unwrap_or(file.readFile(config_path), "") if str.length(content) == 0 return 0 end if // Split by newlines mut lines: [string] = new [string](max_count) let line_count = str.split(content, '\n', lines, max_count) // Filter out empty lines mut count = 0 mut i = 0 while i < line_count and count < max_count let line = str.trim_ws(lines[i]) if str.length(line) > 0 and line[0] != '#' // Normalize path (remove leading ./) if str.starts_with(line, "./") config_files[count] = str.substring(line, 2, str.length(line) - 2) else config_files[count] = line end if count = count + 1 end if i = i + 1 end while return count end read_config_files // Check if a file path is in the config files list fn is_config_file(rel_path: string, config_files: [string], config_count: int): bool mut i = 0 while i < config_count if str.equals(rel_path, config_files[i]) return true end if i = i + 1 end while return false end is_config_file // Install a config file with checksum-based protection. // manifest_sha256: checksum of the new package's config file // stored_sha256: checksum from the previously installed package version (empty on fresh install) fn install_config_file(src_path: string, dest_path: string, manifest_sha256: string, stored_sha256: string): bool if not stat.exists(dest_path) // Fresh install — file doesn't exist yet // Create parent directory if needed let dest_dir = path.dirname(dest_path) if str.length(dest_dir) > 0 and not dir.dir_exists(dest_dir) dir.create_dir_all(dest_dir) end if return res.is_ok(ops.copy_file_preserve(src_path, dest_path)) end if // File exists — check if user has modified it if str.length(stored_sha256) == 0 // No stored checksum (fresh install over existing file or legacy package) // Be safe: install as .pkg-new, preserve user's file let new_path = dest_path + ".pkg-new" return res.is_ok(ops.copy_file_preserve(src_path, new_path)) end if let installed_sha = checksum.sha256_file(dest_path) if str.equals(installed_sha, stored_sha256) // User has not modified the config — safe to overwrite return res.is_ok(ops.copy_file_preserve(src_path, dest_path)) else // User has modified the config — preserve their version let new_path = dest_path + ".pkg-new" return res.is_ok(ops.copy_file_preserve(src_path, new_path)) end if end install_config_file // Look up sha256 for a path in 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]) 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 // Install files from extracted package with old manifest for upgrade config protection fn install_files_upgrade(src_dir: string, root: string, old_entries: [mtree.ManifestEntry], old_entry_count: int): bool mut config_files: [string] = new [string](256) let config_count = read_config_files(src_dir, config_files, 256) mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) let entry_count = read_manifest(src_dir, entries, 8192) if entry_count == 0 return false end if 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 dest_path = path.join_path(root, rel_path) let src_path = path.join_path(src_dir, rel_path) let mode = mtree.entry_mode(entry) let uid = mtree.name_to_uid(mtree.entry_uname(entry)) let gid = mtree.name_to_gid(mtree.entry_gname(entry)) if str.equals(etype, "dir") if not dir.dir_exists(dest_path) if not res.is_ok(dir.create_dir_all(dest_path)) return false end if end if perm.chmod(dest_path, mode) perm.chown(dest_path, uid, gid) elif str.equals(etype, "file") if is_config_file(rel_path, config_files, config_count) let manifest_sha = mtree.entry_sha256(entry) // Look up old sha256 from previous version's manifest let stored_sha = find_manifest_sha256(rel_path, old_entries, old_entry_count) install_config_file(src_path, dest_path, manifest_sha, stored_sha) else let parent = path.dirname(dest_path) if str.length(parent) > 0 and not dir.dir_exists(parent) dir.create_dir_all(parent) end if if stat.is_symlink(dest_path) ops.remove_file(dest_path) elif stat.exists(dest_path) ops.remove_file(dest_path) end if if not res.is_ok(ops.copy_file_preserve(src_path, dest_path)) return false end if perm.chmod(dest_path, mode) perm.chown(dest_path, uid, gid) let manifest_sha = mtree.entry_sha256(entry) if str.length(manifest_sha) > 0 let installed_sha = checksum.sha256_file(dest_path) if not str.equals(installed_sha, manifest_sha) return false end if end if end if elif str.equals(etype, "link") let link_target = mtree.entry_link(entry) if stat.is_symlink(dest_path) or stat.exists(dest_path) ops.remove_file(dest_path) end if let parent = path.dirname(dest_path) if str.length(parent) > 0 and not dir.dir_exists(parent) dir.create_dir_all(parent) end if if not res.is_ok(link.symlink(link_target, dest_path)) return false end if end if i = i + 1 end while return true end install_files_upgrade // Parse a TOML inline array string like ["foo", "bar"] into a string array fn parse_toml_array(value: string, result: [string], max_items: int): int let len = str.length(value) if len < 2 return 0 end if if value[0] != '[' if len > 0 result[0] = value return 1 end if return 0 end if mut pos = 1 mut count = 0 while pos < len and count < max_items while pos < len and (value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n') pos = pos + 1 end while if pos >= len or value[pos] == ']' break end if if value[pos] == '"' pos = pos + 1 mut item_start = pos while pos < len and value[pos] != '"' pos = pos + 1 end while if pos > item_start result[count] = str.substring(value, item_start, pos - item_start) count = count + 1 end if pos = pos + 1 elif value[pos] != ']' and value[pos] != ',' mut item_start = pos while pos < len and value[pos] != ',' and value[pos] != ']' and value[pos] != ' ' pos = pos + 1 end while if pos > item_start result[count] = str.substring(value, item_start, pos - item_start) count = count + 1 end if end if while pos < len and (value[pos] == ',' or value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n') pos = pos + 1 end while end while return count end parse_toml_array end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: pkgdb.reef Authors: Chris Tusa License: Description: MsgPack-based package database for fast lookups The package database provides fast indexed access to: - Installed packages (packages.db) - File ownership (files.db) Source of truth remains in /var/lib/coral/installed//pkg.toml These indexes can be rebuilt with 'coral db rebuild' Index files are stored as Base64-encoded MsgPack data for compatibility with Reef's string-based file I/O. ******************************************************************************/ module core.pkgdb import types import io.file import io.dir import io.path import core.str import core.database import encoding.msgpack import encoding.base64 import core.result as res export // Package index entry (for packages.db) type PkgIndexEntry // Initialize/ensure db directory exists fn init_db(): bool // Check if indexes exist and are valid fn indexes_exist(): bool // Get package count from index (fast) fn get_package_count(): int // List all packages from index (fast) fn list_packages(names: [string], versions: [string], max_count: int): int // Check if package is in index fn is_indexed(name: string): bool // Get package info from index fn get_indexed_package(name: string): PkgIndexEntry // Find file owner from index (fast) fn find_file_owner(file_path: string): string // Update index when package is installed fn index_add_package(name: string, version: string, release: int, description: string): bool // Update index when package is removed fn index_remove_package(name: string): bool // Add files to file index fn index_add_files(pkg_name: string, files: [string], file_count: int): bool // Remove files from file index fn index_remove_files(pkg_name: string): bool // Rebuild all indexes from installed packages (source of truth) fn rebuild_indexes(): bool // Get index statistics fn get_index_stats(): IndexStats // Root-aware versions for alternate root fn init_db_rooted(root: string): bool fn indexes_exist_rooted(root: string): bool fn rebuild_indexes_rooted(root: string): bool fn get_index_stats_rooted(root: string): IndexStats fn find_file_owner_rooted(file_path: string, root: string): string // Debug: dump files index contents fn dump_files_index_rooted(root: string, paths: [string], owners: [string], max_count: int): int type IndexStats end export // ============================================================================ // Types // ============================================================================ type PkgIndexEntry = struct name: string version: string release: int description: string installed_date: string file_count: int found: bool end PkgIndexEntry type IndexStats = struct package_count: int file_count: int packages_db_size: int files_db_size: int last_updated: string end IndexStats // Loaded packages index (deserialized from packages.db) type PackagesIndex = struct version: int generated: string package_count: int entries: [PkgIndexEntry] loaded: bool end PackagesIndex // Loaded files index (deserialized from files.db) // Uses parallel arrays for binary search capability type FilesIndex = struct version: int generated: string file_count: int file_paths: [string] // Sorted alphabetically for binary search owners: [string] // Package name at same index as file_paths loaded: bool end FilesIndex // ============================================================================ // Constants // ============================================================================ const INDEX_VERSION: int = 1 const MAX_PACKAGES: int = 2048 const MAX_FILES: int = 131072 // 128K files max // ============================================================================ // Path Constants // ============================================================================ fn db_dir(): string return types.get_index_dir() end db_dir fn packages_db_path(): string return types.get_index_dir() + "/packages.db" end packages_db_path fn files_db_path(): string return types.get_index_dir() + "/files.db" end files_db_path // ============================================================================ // Buffer Size Estimation Helpers // ============================================================================ // Estimate buffer size needed for packages.db fn estimate_packages_buffer_size(pkg_count: int): int // Header overhead: ~100 bytes // Per package: ~200 bytes (name, version, description, dates) return 100 + (pkg_count * 200) end estimate_packages_buffer_size // Estimate buffer size needed for files.db fn estimate_files_buffer_size(file_count: int): int // Header overhead: ~100 bytes // Per file: ~100 bytes (path + package name) return 100 + (file_count * 100) end estimate_files_buffer_size // Get current timestamp as string fn get_timestamp(): string // Simple timestamp - just use a placeholder for now // In production would use sys.time module return "2026-01-29" end get_timestamp // ============================================================================ // Empty/Default Constructors // ============================================================================ fn empty_packages_index(): PackagesIndex return PackagesIndex{ version: 0, generated: "", package_count: 0, entries: new [PkgIndexEntry](MAX_PACKAGES), loaded: false } end empty_packages_index fn empty_files_index(): FilesIndex return FilesIndex{ version: 0, generated: "", file_count: 0, file_paths: new [string](MAX_FILES), owners: new [string](MAX_FILES), loaded: false } end empty_files_index fn empty_pkg_entry(): PkgIndexEntry return PkgIndexEntry{ name: "", version: "", release: 0, description: "", installed_date: "", file_count: 0, found: false } end empty_pkg_entry // Get just the header size for a map (not the entire map contents) fn map_header_size(buf: [int], offset: int): int let b = buf[offset] & 255 // Fixmap (0x80-0x8F): 1 byte header if (b & 0xF0) == 0x80 return 1 // map16 (0xDE): 3 byte header elif b == 0xDE return 3 // map32 (0xDF): 5 byte header elif b == 0xDF return 5 end if return 1 // Default end map_header_size // Get just the header size for an array (not the entire array contents) fn array_header_size(buf: [int], offset: int): int let b = buf[offset] & 255 // Fixarray (0x90-0x9F): 1 byte header if (b & 0xF0) == 0x90 return 1 // array16 (0xDC): 3 byte header elif b == 0xDC return 3 // array32 (0xDD): 5 byte header elif b == 0xDD return 5 end if return 1 // Default end array_header_size // ============================================================================ // MsgPack Deserialization // ============================================================================ // Load packages index from disk fn load_packages_index_rooted(root: string): PackagesIndex mut index = empty_packages_index() // Check if file exists let db_path = packages_db_path_rooted(root) if not file.fileExists(db_path) return index end if // Read base64-encoded content let encoded = res.unwrap_or(file.readFile(db_path), "") if str.length(encoded) == 0 return index end if // Decode base64 to buffer let buf_size = str.length(encoded) // Decoded will be smaller mut buf: [int] = new [int](buf_size) let buf_len = res.unwrap_or(base64.base64_decode_bytes(encoded, buf, buf_size), 0) if buf_len == 0 return index end if // Parse MsgPack structure mut offset = 0 // Expect map header if not msgpack.msgpack_is_map(buf, offset) return index end if let map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) // Iterate through map entries mut entry_idx = 0 while entry_idx < map_len and offset < buf_len // Get key let key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(key, "version") index.version = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "generated") index.generated = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "package_count") index.package_count = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "packages") // Parse packages array let arr_len = res.unwrap_or(msgpack.msgpack_unpack_array_len(buf, offset), 0) offset = offset + array_header_size(buf, offset) mut pkg_idx = 0 while pkg_idx < arr_len and pkg_idx < MAX_PACKAGES and offset < buf_len // Each package is a map let pkg_map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) mut entry = empty_pkg_entry() entry.found = true mut pkg_field = 0 while pkg_field < pkg_map_len and offset < buf_len let field_key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(field_key, "name") entry.name = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") elif str.equals(field_key, "version") entry.version = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") elif str.equals(field_key, "release") entry.release = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) elif str.equals(field_key, "description") entry.description = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") elif str.equals(field_key, "installed_date") entry.installed_date = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") elif str.equals(field_key, "file_count") entry.file_count = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) end if offset = offset + msgpack.msgpack_value_size(buf, offset) pkg_field = pkg_field + 1 end while index.entries[pkg_idx] = entry pkg_idx = pkg_idx + 1 end while else // Skip unknown key's value offset = offset + msgpack.msgpack_value_size(buf, offset) end if entry_idx = entry_idx + 1 end while index.loaded = true return index end load_packages_index_rooted // Load files index from disk fn load_files_index_rooted(root: string): FilesIndex mut index = empty_files_index() // Check if file exists let db_path = files_db_path_rooted(root) if not file.fileExists(db_path) return index end if // Read base64-encoded content let encoded = res.unwrap_or(file.readFile(db_path), "") let encoded_len = str.length(encoded) if encoded_len == 0 return index end if // Decode base64 to buffer let buf_size = encoded_len // Decoded will be smaller mut buf: [int] = new [int](buf_size) let buf_len = res.unwrap_or(base64.base64_decode_bytes(encoded, buf, buf_size), 0) if buf_len <= 0 return index end if // Parse MsgPack structure mut offset = 0 // Expect map header if not msgpack.msgpack_is_map(buf, offset) return index end if let map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) // Iterate through map entries mut entry_idx = 0 while entry_idx < map_len and offset < buf_len // Get key let key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(key, "version") index.version = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "generated") index.generated = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "file_count") index.file_count = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "files") // Parse files map let files_map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) mut file_idx = 0 while file_idx < files_map_len and file_idx < MAX_FILES and offset < buf_len // Key is file path, value is owner let file_path = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) let owner = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) index.file_paths[file_idx] = file_path index.owners[file_idx] = owner file_idx = file_idx + 1 end while index.file_count = file_idx else // Skip unknown key's value offset = offset + msgpack.msgpack_value_size(buf, offset) end if entry_idx = entry_idx + 1 end while index.loaded = true return index end load_files_index_rooted // Binary search for file owner in sorted files index // Returns package name or empty string if not found fn binary_search_file_owner(index: FilesIndex, path: string): string if not index.loaded or index.file_count == 0 return "" end if mut low = 0 mut high = index.file_count - 1 while low <= high let mid = (low + high) / 2 let cmp = str.compare(path, index.file_paths[mid]) if cmp == 0 return index.owners[mid] elif cmp < 0 high = mid - 1 else low = mid + 1 end if end while return "" // Not found end binary_search_file_owner // Normalize file path for lookup (strip leading /) fn normalize_path(path: string): string let len = str.length(path) if len > 0 and path[0] == '/' return str.substring(path, 1, len - 1) end if return path end normalize_path // Linear search fallback for file owner (when binary search fails) fn linear_search_file_owner(index: FilesIndex, path: string): string if not index.loaded or index.file_count == 0 return "" end if mut i = 0 while i < index.file_count if str.equals(path, index.file_paths[i]) return index.owners[i] end if i = i + 1 end while return "" end linear_search_file_owner // ============================================================================ // Initialization // ============================================================================ fn init_db(): bool let db = db_dir() if not dir.dir_exists(db) return res.is_ok(dir.create_dir_all(db)) end if return true end init_db fn indexes_exist(): bool return file.fileExists(packages_db_path()) and file.fileExists(files_db_path()) end indexes_exist // ============================================================================ // Package Index Operations - Uses MsgPack index with fallback // ============================================================================ fn get_package_count(): int // Try index first let index = load_packages_index_rooted("") if index.loaded return index.package_count end if // Fall back to database scan mut names: [string] = new [string](1024) return database.list_installed(names, 1024) end get_package_count fn list_packages(names: [string], versions: [string], max_count: int): int // Try index first let index = load_packages_index_rooted("") if index.loaded mut count = 0 while count < index.package_count and count < max_count names[count] = index.entries[count].name versions[count] = index.entries[count].version count = count + 1 end while return count end if // Fall back to database scan let count = database.list_installed(names, max_count) // Fill in versions mut i = 0 while i < count let pkg = database.get_installed(names[i]) versions[i] = pkg.info.version i = i + 1 end while return count end list_packages fn is_indexed(name: string): bool // Try index first let index = load_packages_index_rooted("") if index.loaded mut i = 0 while i < index.package_count if str.equals(index.entries[i].name, name) return true end if i = i + 1 end while return false end if // Fall back to database check return database.is_installed(name) end is_indexed fn get_indexed_package(name: string): PkgIndexEntry // Try index first let index = load_packages_index_rooted("") if index.loaded mut i = 0 while i < index.package_count if str.equals(index.entries[i].name, name) return index.entries[i] end if i = i + 1 end while return empty_pkg_entry() end if // Fall back to database mut entry = empty_pkg_entry() if not database.is_installed(name) return entry end if let pkg = database.get_installed(name) entry.name = pkg.info.name entry.version = pkg.info.version entry.release = pkg.info.release entry.description = pkg.info.description entry.installed_date = pkg.install_date entry.file_count = pkg.files_count entry.found = true return entry end get_indexed_package // ============================================================================ // File Index Operations - Uses MsgPack index with fallback // ============================================================================ fn find_file_owner(file_path: string): string return find_file_owner_rooted(file_path, "") end find_file_owner fn find_file_owner_rooted(file_path: string, root: string): string // Try index first (O(log N) binary search) let index = load_files_index_rooted(root) if index.loaded let normalized = normalize_path(file_path) let owner = binary_search_file_owner(index, normalized) if str.length(owner) > 0 return owner end if // Binary search failed - try linear search as fallback // This helps diagnose if the index isn't sorted correctly let linear_owner = linear_search_file_owner(index, normalized) if str.length(linear_owner) > 0 return linear_owner end if // File not in index - either not owned or index out of date return "" end if // Fall back to database scan (O(N*M)) if str.length(root) > 0 return database.find_owner_rooted(file_path, root) end if return database.find_owner(file_path) end find_file_owner_rooted // ============================================================================ // Index Modification (stub - no-op for now) // ============================================================================ fn index_add_package(name: string, version: string, release: int, description: string): bool // Stub - no indexing yet return true end index_add_package fn index_remove_package(name: string): bool // Stub - no indexing yet return true end index_remove_package fn index_add_files(pkg_name: string, files: [string], file_count: int): bool // Stub - no indexing yet return true end index_add_files fn index_remove_files(pkg_name: string): bool // Stub - triggers full rebuild return true end index_remove_files // ============================================================================ // MsgPack Serialization // ============================================================================ // Serialize packages index to MsgPack format // Returns buffer and sets length via out parameter simulation (length stored at buf[0]) fn serialize_packages_db_rooted(root: string, buf: [int], max_size: int): int // Get list of installed packages mut names: [string] = new [string](MAX_PACKAGES) let pkg_count = database.list_installed_rooted(names, MAX_PACKAGES, root) mut pos = 0 // Pack map header (4 keys: version, generated, package_count, packages) pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos) // Pack "version" -> INDEX_VERSION pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_int(INDEX_VERSION, buf, pos) // Pack "generated" -> timestamp pos = pos + msgpack.msgpack_pack_string("generated", buf, pos) pos = pos + msgpack.msgpack_pack_string(get_timestamp(), buf, pos) // Pack "package_count" -> count pos = pos + msgpack.msgpack_pack_string("package_count", buf, pos) pos = pos + msgpack.msgpack_pack_int(pkg_count, buf, pos) // Pack "packages" -> array of package entries pos = pos + msgpack.msgpack_pack_string("packages", buf, pos) pos = pos + msgpack.msgpack_pack_array_header(pkg_count, buf, pos) // Pack each package entry mut i = 0 while i < pkg_count and pos < max_size - 1000 let pkg = database.get_installed_rooted(names[i], root) // Each package is a map with 6 keys pos = pos + msgpack.msgpack_pack_map_header(6, buf, pos) // name pos = pos + msgpack.msgpack_pack_string("name", buf, pos) pos = pos + msgpack.msgpack_pack_string(pkg.info.name, buf, pos) // version pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_string(pkg.info.version, buf, pos) // release pos = pos + msgpack.msgpack_pack_string("release", buf, pos) pos = pos + msgpack.msgpack_pack_int(pkg.info.release, buf, pos) // description pos = pos + msgpack.msgpack_pack_string("description", buf, pos) pos = pos + msgpack.msgpack_pack_string(pkg.info.description, buf, pos) // installed_date pos = pos + msgpack.msgpack_pack_string("installed_date", buf, pos) pos = pos + msgpack.msgpack_pack_string(pkg.install_date, buf, pos) // file_count pos = pos + msgpack.msgpack_pack_string("file_count", buf, pos) pos = pos + msgpack.msgpack_pack_int(pkg.files_count, buf, pos) i = i + 1 end while return pos end serialize_packages_db_rooted // Count total installed files across all packages (cheap — no file content read) fn count_total_files_rooted(root: string): int mut pkg_names: [string] = new [string](MAX_PACKAGES) let pkg_count = database.list_installed_rooted(pkg_names, MAX_PACKAGES, root) mut total = 0 mut i = 0 while i < pkg_count mut pkg_files: [string] = new [string](8192) let file_count = database.get_files_rooted(pkg_names[i], pkg_files, 8192, root) total = total + file_count i = i + 1 end while return total end count_total_files_rooted // Serialize files index to MsgPack format // Creates a map of file_path -> package_name for fast ownership lookup fn serialize_files_db_rooted(root: string, buf: [int], max_size: int, alloc_size: int): int // Get all installed packages mut pkg_names: [string] = new [string](MAX_PACKAGES) let pkg_count = database.list_installed_rooted(pkg_names, MAX_PACKAGES, root) // Collect all files with their owners — sized to actual count + margin mut all_files: [string] = new [string](alloc_size) mut all_owners: [string] = new [string](alloc_size) mut total_files = 0 mut i = 0 while i < pkg_count and total_files < alloc_size - 256 mut pkg_files: [string] = new [string](8192) let file_count = database.get_files_rooted(pkg_names[i], pkg_files, 8192, root) mut j = 0 while j < file_count and total_files < alloc_size all_files[total_files] = pkg_files[j] all_owners[total_files] = pkg_names[i] total_files = total_files + 1 j = j + 1 end while i = i + 1 end while // Sort files alphabetically for binary search (simple bubble sort for now) sort_file_index(all_files, all_owners, total_files) mut pos = 0 // Pack map header (4 keys: version, generated, file_count, files) pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos) // Pack "version" -> INDEX_VERSION pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_int(INDEX_VERSION, buf, pos) // Pack "generated" -> timestamp pos = pos + msgpack.msgpack_pack_string("generated", buf, pos) pos = pos + msgpack.msgpack_pack_string(get_timestamp(), buf, pos) // Pack "file_count" -> count pos = pos + msgpack.msgpack_pack_string("file_count", buf, pos) pos = pos + msgpack.msgpack_pack_int(total_files, buf, pos) // Pack "files" -> map of path -> owner pos = pos + msgpack.msgpack_pack_string("files", buf, pos) pos = pos + msgpack.msgpack_pack_map_header(total_files, buf, pos) // Pack each file -> owner pair i = 0 while i < total_files and pos < max_size - 500 pos = pos + msgpack.msgpack_pack_string(all_files[i], buf, pos) pos = pos + msgpack.msgpack_pack_string(all_owners[i], buf, pos) i = i + 1 end while return pos end serialize_files_db_rooted // Sort parallel arrays of files and owners alphabetically by file path // Uses merge sort for O(n log n) performance on large file counts proc sort_file_index(files: [string], owners: [string], count: int) if count <= 1 return end if // Allocate temporary arrays for merge operations mut tmp_files: [string] = new [string](count) mut tmp_owners: [string] = new [string](count) merge_sort(files, owners, tmp_files, tmp_owners, 0, count - 1) end sort_file_index proc merge_sort(files: [string], owners: [string], tmp_files: [string], tmp_owners: [string], left: int, right: int) if left >= right return end if let mid = left + (right - left) / 2 merge_sort(files, owners, tmp_files, tmp_owners, left, mid) merge_sort(files, owners, tmp_files, tmp_owners, mid + 1, right) merge(files, owners, tmp_files, tmp_owners, left, mid, right) end merge_sort proc merge(files: [string], owners: [string], tmp_files: [string], tmp_owners: [string], left: int, mid: int, right: int) // Copy both halves into temp arrays mut i = left while i <= right tmp_files[i] = files[i] tmp_owners[i] = owners[i] i = i + 1 end while // Merge back from temp into original mut l = left mut r = mid + 1 mut k = left while l <= mid and r <= right if str.compare(tmp_files[l], tmp_files[r]) <= 0 files[k] = tmp_files[l] owners[k] = tmp_owners[l] l = l + 1 else files[k] = tmp_files[r] owners[k] = tmp_owners[r] r = r + 1 end if k = k + 1 end while // Copy remaining left half while l <= mid files[k] = tmp_files[l] owners[k] = tmp_owners[l] l = l + 1 k = k + 1 end while // Right half remaining elements are already in place end merge // ============================================================================ // Index Rebuild // ============================================================================ fn rebuild_indexes(): bool return rebuild_indexes_rooted("") end rebuild_indexes // ============================================================================ // Index Statistics // ============================================================================ fn get_index_stats(): IndexStats mut stats = IndexStats{ package_count: 0, file_count: 0, packages_db_size: 0, files_db_size: 0, last_updated: "" } // Get package count from source of truth mut names: [string] = new [string](1024) stats.package_count = database.list_installed(names, 1024) // Count files across all packages mut i = 0 while i < stats.package_count mut files: [string] = new [string](4096) let file_count = database.get_files(names[i], files, 4096) stats.file_count = stats.file_count + file_count i = i + 1 end while return stats end get_index_stats // ============================================================================ // Root-aware versions for alternate root // ============================================================================ fn db_dir_rooted(root: string): string if str.length(root) == 0 return types.get_index_dir() end if return root + types.get_index_dir() end db_dir_rooted fn packages_db_path_rooted(root: string): string return db_dir_rooted(root) + "/packages.db" end packages_db_path_rooted fn files_db_path_rooted(root: string): string return db_dir_rooted(root) + "/files.db" end files_db_path_rooted fn init_db_rooted(root: string): bool let db = db_dir_rooted(root) if not dir.dir_exists(db) return res.is_ok(dir.create_dir_all(db)) end if return true end init_db_rooted fn indexes_exist_rooted(root: string): bool return file.fileExists(packages_db_path_rooted(root)) and file.fileExists(files_db_path_rooted(root)) end indexes_exist_rooted fn rebuild_indexes_rooted(root: string): bool if not init_db_rooted(root) return false end if // Serialize packages.db let pkg_buf_size = estimate_packages_buffer_size(MAX_PACKAGES) mut pkg_buf: [int] = new [int](pkg_buf_size) let pkg_len = serialize_packages_db_rooted(root, pkg_buf, pkg_buf_size) if pkg_len > 0 // Encode to base64 and write let pkg_encoded = base64.base64_encode_bytes(pkg_buf, pkg_len) if not res.is_ok(file.writeFile(packages_db_path_rooted(root), pkg_encoded)) return false end if end if // Serialize files.db — count actual files first to avoid massive over-allocation let actual_file_count = count_total_files_rooted(root) let alloc_size = actual_file_count + 1024 let files_buf_size = estimate_files_buffer_size(alloc_size) mut files_buf: [int] = new [int](files_buf_size) let files_len = serialize_files_db_rooted(root, files_buf, files_buf_size, alloc_size) if files_len > 0 // Encode to base64 and write let files_encoded = base64.base64_encode_bytes(files_buf, files_len) if not res.is_ok(file.writeFile(files_db_path_rooted(root), files_encoded)) return false end if end if return true end rebuild_indexes_rooted fn get_index_stats_rooted(root: string): IndexStats mut stats = IndexStats{ package_count: 0, file_count: 0, packages_db_size: 0, files_db_size: 0, last_updated: "" } // Get package count from source of truth in alternate root mut names: [string] = new [string](1024) stats.package_count = database.list_installed_rooted(names, 1024, root) // Count files across all packages mut i = 0 while i < stats.package_count mut files: [string] = new [string](4096) let file_count = database.get_files_rooted(names[i], files, 4096, root) stats.file_count = stats.file_count + file_count i = i + 1 end while return stats end get_index_stats_rooted // Debug: dump files index contents to arrays fn dump_files_index_rooted(root: string, paths: [string], owners: [string], max_count: int): int let index = load_files_index_rooted(root) if not index.loaded return 0 end if mut count = 0 while count < index.file_count and count < max_count paths[count] = index.file_paths[count] owners[count] = index.owners[count] count = count + 1 end while return count end dump_files_index_rooted end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: port.reef Authors: Chris Tusa License: Description: Port loading and parsing from package.toml ******************************************************************************/ module core.port import types import io.file import io.dir import io.path import core.str import core.config import encoding.toml import core.result as res export // Load a port from a directory path, returns PortResult fn load(port_path: string): types.PortResult // Find a port by name, searches all categories fn find(name: string): types.PortResult // List all ports in a category fn list_category(category: string, names: [string], max_count: int): int // Get all categories fn get_categories(cats: [string], max_count: int): int end export // Dynamically discover categories by scanning the ports directory // Excludes hidden dirs, infrastructure dirs (pkgs/, groups/), and config-aware paths fn get_categories(cats: [string], max_count: int): int let cfg = config.load() let ports_dir = config.get_ports_dir(cfg) if not dir.dir_exists(ports_dir) return 0 end if let packages_path = config.get_packages_dir(cfg) let sources_path = config.get_sources_dir(cfg) let build_path = config.get_build_dir(cfg) // Extract first path component relative to ports_dir for each configured path let skip1 = get_relative_top_dir(ports_dir, packages_path) let skip2 = get_relative_top_dir(ports_dir, sources_path) let skip3 = get_relative_top_dir(ports_dir, build_path) // Also skip the groups directory let groups_dir = types.get_groups_dir() let skip_groups = get_relative_top_dir(ports_dir, groups_dir) let entries = res.unwrap_or(dir.list_dir(ports_dir), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] let elen = str.length(entry) // Skip hidden directories if elen == 0 or entry[0] == '.' i = i + 1 continue end if // Skip infrastructure directories if str.equals(entry, skip1) or str.equals(entry, skip2) or str.equals(entry, skip3) or str.equals(entry, skip_groups) i = i + 1 continue end if // Verify it's actually a directory let entry_path = path.join_path(ports_dir, entry) if dir.dir_exists(entry_path) cats[count] = entry count = count + 1 end if i = i + 1 end while return count end get_categories // Extract the first path component of `child` relative to `parent` // e.g. parent="/usr/zports", child="/usr/zports/pkgs/packages" -> "pkgs" // Returns empty string if child is not under parent fn get_relative_top_dir(parent: string, child: string): string let parent_len = str.length(parent) let child_len = str.length(child) if child_len <= parent_len return "" end if // Check that child starts with parent let prefix = str.substring(child, 0, parent_len) if not str.equals(prefix, parent) return "" end if // Skip the separator mut start = parent_len if child[start] == '/' start = start + 1 end if // Find the next slash or end of string mut end_pos = start while end_pos < child_len if child[end_pos] == '/' break end if end_pos = end_pos + 1 end while if end_pos > start return str.substring(child, start, end_pos - start) end if return "" end get_relative_top_dir // Parse a TOML array value like ["item1", "item2"] into a string array // Returns the number of items parsed fn parse_toml_array(value: string, result: [string], max_items: int): int let len = str.length(value) if len < 2 return 0 end if // Check for array brackets if value[0] != '[' // Single value, not an array if len > 0 result[0] = value return 1 end if return 0 end if // Skip the opening bracket mut pos = 1 mut count = 0 while pos < len and count < max_items // Skip whitespace while pos < len and (value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n') pos = pos + 1 end while if pos >= len or value[pos] == ']' break end if // Check for quoted string if value[pos] == '"' pos = pos + 1 mut item_start = pos // Find closing quote while pos < len and value[pos] != '"' pos = pos + 1 end while if pos > item_start result[count] = str.substring(value, item_start, pos - item_start) count = count + 1 end if pos = pos + 1 // Skip closing quote elif value[pos] != ']' and value[pos] != ',' // Unquoted value mut item_start = pos while pos < len and value[pos] != ',' and value[pos] != ']' and value[pos] != ' ' pos = pos + 1 end while if pos > item_start result[count] = str.substring(value, item_start, pos - item_start) count = count + 1 end if end if // Skip comma and whitespace while pos < len and (value[pos] == ',' or value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n') pos = pos + 1 end while end while return count end parse_toml_array // Create an empty/default Port structure (delegate to types module) fn new_port(): types.Port return types.new_port() end new_port // Helper to create error result fn error_result(msg: string): types.PortResult return types.new_port_result(false, new_port(), msg) end error_result // Helper to create success result fn success_result(p: types.Port): types.PortResult return types.new_port_result(true, p, "") end success_result // Extract filename from URL fn extract_filename_from_url(url: string): string let len = str.length(url) if len == 0 return "" end if // Find last slash mut last_slash = -1 mut i = len - 1 while i >= 0 if url[i] == '/' last_slash = i break end if i = i - 1 end while if last_slash < 0 return url end if let filename = str.substring(url, last_slash + 1, len - last_slash - 1) // Remove query string if present let query_len = str.length(filename) mut query_pos = -1 i = 0 while i < query_len if filename[i] == '?' query_pos = i break end if i = i + 1 end while if query_pos > 0 return str.substring(filename, 0, query_pos) end if return filename end extract_filename_from_url // Parse sources from TOML - supports both new [[source]] and legacy [sources] format fn parse_sources(keys: [string], vals: [string], entry_count: int): types.SourceInfo mut info = types.new_source_info() // Try new [[source]] format first (source.0.file, source.1.file, etc.) mut source_idx = 0 mut found_new_format = false while source_idx < 16 let prefix = "source." + int_to_str(source_idx) let file_key = prefix + ".file" if not toml.toml_has_key(keys, vals, entry_count, file_key) break end if found_new_format = true mut src = types.new_source() // Get file name src.file = toml.toml_get(keys, vals, entry_count, file_key) // Get URLs array let urls_key = prefix + ".urls" if toml.toml_has_key(keys, vals, entry_count, urls_key) let urls_val = toml.toml_get(keys, vals, entry_count, urls_key) mut urls_arr: [string] = new [string](16) let urls_count = parse_toml_array(urls_val, urls_arr, 16) src.urls = urls_arr src.url_count = urls_count end if // Get checksum (single value) let checksum_key = prefix + ".checksum" if toml.toml_has_key(keys, vals, entry_count, checksum_key) src.checksum = toml.toml_get(keys, vals, entry_count, checksum_key) end if // Get extract flag (default: true) let extract_key = prefix + ".extract" if toml.toml_has_key(keys, vals, entry_count, extract_key) src.extract = toml.toml_get_bool(keys, vals, entry_count, extract_key) end if info.sources[source_idx] = src source_idx = source_idx + 1 end while if found_new_format info.count = source_idx return info end if // Fall back to legacy [sources] format if toml.toml_has_key(keys, vals, entry_count, "sources.urls") let urls_val = toml.toml_get(keys, vals, entry_count, "sources.urls") mut urls_arr: [string] = new [string](16) let urls_count = parse_toml_array(urls_val, urls_arr, 16) // Parse checksums mut checksums_arr: [string] = new [string](16) if toml.toml_has_key(keys, vals, entry_count, "sources.checksums") let cksum_val = toml.toml_get(keys, vals, entry_count, "sources.checksums") parse_toml_array(cksum_val, checksums_arr, 16) end if // Convert legacy format to new Source structs mut i = 0 while i < urls_count mut src = types.new_source() src.file = extract_filename_from_url(urls_arr[i]) src.urls[0] = urls_arr[i] src.url_count = 1 if str.length(checksums_arr[i]) > 0 src.checksum = checksums_arr[i] end if src.extract = true info.sources[i] = src i = i + 1 end while info.count = urls_count end if return info end parse_sources // Helper: convert int to string (for source index keys) fn int_to_str(n: int): string if n == 0 return "0" end if mut result = "" mut value = n while value > 0 let digit = value % 10 result = str.concat(str.substring("0123456789", digit, 1), result) value = value / 10 end while return result end int_to_str // Load a port from a directory containing package.toml fn load(port_path: string): types.PortResult // Check if directory exists if not dir.dir_exists(port_path) return error_result("Port directory does not exist: " + port_path) end if // Check for package.toml let toml_path = path.join_path(port_path, "package.toml") if not file.fileExists(toml_path) return error_result("package.toml not found in: " + port_path) end if // Read the TOML file let content = res.unwrap_or(file.readFile(toml_path), "") if str.length(content) == 0 return error_result("Failed to read package.toml or file is empty") end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return error_result("Failed to parse package.toml") end if // Build the Port structure mut port = new_port() port.port_dir = port_path // Extract category from path (e.g., /usr/zports/base/vim -> base) let parent_dir = path.dirname(port_path) port.category = path.basename(parent_dir) // Parse [package] section port.info.name = toml.toml_get(keys, vals, entry_count, "package.name") port.info.version = toml.toml_get(keys, vals, entry_count, "package.version") port.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release") port.info.description = toml.toml_get(keys, vals, entry_count, "package.description") port.info.url = toml.toml_get(keys, vals, entry_count, "package.url") port.info.license = toml.toml_get(keys, vals, entry_count, "package.license") port.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer") // Architecture (default to x86_64 if not specified) let arch_val = toml.toml_get(keys, vals, entry_count, "package.arch") if str.length(arch_val) > 0 port.info.arch = arch_val end if // Parse alternatives array if toml.toml_has_key(keys, vals, entry_count, "package.alternatives") let alt_val = toml.toml_get(keys, vals, entry_count, "package.alternatives") mut alt_arr: [string] = new [string](32) let alt_count = parse_toml_array(alt_val, alt_arr, 32) port.info.alternatives = alt_arr port.info.alternatives_count = alt_count end if // Parse conflicts array if toml.toml_has_key(keys, vals, entry_count, "package.conflicts") let conf_val = toml.toml_get(keys, vals, entry_count, "package.conflicts") mut conf_arr: [string] = new [string](32) let conf_count = parse_toml_array(conf_val, conf_arr, 32) port.info.conflicts = conf_arr port.info.conflicts_count = conf_count end if // Parse [dependencies] section if toml.toml_has_key(keys, vals, entry_count, "dependencies.runtime") let rt_val = toml.toml_get(keys, vals, entry_count, "dependencies.runtime") mut rt_arr: [string] = new [string](64) let rt_count = parse_toml_array(rt_val, rt_arr, 64) port.deps.runtime = rt_arr port.deps.runtime_count = rt_count end if if toml.toml_has_key(keys, vals, entry_count, "dependencies.build") let bd_val = toml.toml_get(keys, vals, entry_count, "dependencies.build") mut bd_arr: [string] = new [string](64) let bd_count = parse_toml_array(bd_val, bd_arr, 64) port.deps.build = bd_arr port.deps.build_count = bd_count end if // Parse sources - try new [[source]] format first, fall back to legacy [sources] port.sources = parse_sources(keys, vals, entry_count) // Parse [patches] section if toml.toml_has_key(keys, vals, entry_count, "patches.files") let patch_val = toml.toml_get(keys, vals, entry_count, "patches.files") mut patch_arr: [string] = new [string](32) let patch_count = parse_toml_array(patch_val, patch_arr, 32) port.patches.files = patch_arr port.patches.count = patch_count end if if toml.toml_has_key(keys, vals, entry_count, "patches.strip") port.patches.strip = toml.toml_get_int(keys, vals, entry_count, "patches.strip") end if // Parse [config] section if toml.toml_has_key(keys, vals, entry_count, "config.files") let cfg_val = toml.toml_get(keys, vals, entry_count, "config.files") mut cfg_arr: [string] = new [string](32) let cfg_count = parse_toml_array(cfg_val, cfg_arr, 32) port.config.files = cfg_arr port.config.count = cfg_count end if // Parse [scripts] section if toml.toml_has_key(keys, vals, entry_count, "scripts.pre_install") port.scripts.pre_install = toml.toml_get(keys, vals, entry_count, "scripts.pre_install") end if if toml.toml_has_key(keys, vals, entry_count, "scripts.post_install") port.scripts.post_install = toml.toml_get(keys, vals, entry_count, "scripts.post_install") end if if toml.toml_has_key(keys, vals, entry_count, "scripts.pre_remove") port.scripts.pre_remove = toml.toml_get(keys, vals, entry_count, "scripts.pre_remove") end if if toml.toml_has_key(keys, vals, entry_count, "scripts.post_remove") port.scripts.post_remove = toml.toml_get(keys, vals, entry_count, "scripts.post_remove") end if // Parse [paths] section (per-package overrides) if toml.toml_has_key(keys, vals, entry_count, "paths.prefix") port.prefix = toml.toml_get(keys, vals, entry_count, "paths.prefix") end if if toml.toml_has_key(keys, vals, entry_count, "paths.sysconfdir") port.sysconfdir = toml.toml_get(keys, vals, entry_count, "paths.sysconfdir") end if // Parse [build] section if toml.toml_has_key(keys, vals, entry_count, "build.parallel") port.build_cfg.parallel = toml.toml_get_bool(keys, vals, entry_count, "build.parallel") end if if toml.toml_has_key(keys, vals, entry_count, "build.jobs") port.build_cfg.jobs = toml.toml_get_int(keys, vals, entry_count, "build.jobs") end if // Validate required fields if str.length(port.info.name) == 0 return types.new_port_result(false, port, "package.name is required") end if if str.length(port.info.version) == 0 return types.new_port_result(false, port, "package.version is required") end if return success_result(port) end load // Find a port by name, searching all categories // Accepts both "kernel" (searches all categories) and "hammerhead/kernel" (direct category/name) fn find(name: string): types.PortResult let cfg = config.load() let ports_dir = config.get_ports_dir(cfg) // Check if ports directory exists if not dir.dir_exists(ports_dir) return error_result("Ports directory not found: " + ports_dir + " (check /etc/coral/coral.conf)") end if // Check if name contains a slash (category/name format) let slash_idx = str.index_of(name, "/") if slash_idx >= 0 let port_path = path.join_path(ports_dir, name) if dir.dir_exists(port_path) return load(port_path) end if return error_result("Port not found: " + name + " (searched " + ports_dir + "/" + name + ")") end if // Search all categories for the name mut cats: [string] = new [string](32) let cat_count = get_categories(cats, 32) mut i = 0 while i < cat_count let port_path = path.join_path(ports_dir, path.join_path(cats[i], name)) if dir.dir_exists(port_path) return load(port_path) end if i = i + 1 end while return error_result("Port not found: " + name + " (searched " + ports_dir + ")") end find // List all ports in a given category fn list_category(category: string, names: [string], max_count: int): int let cfg = config.load() let cat_path = path.join_path(config.get_ports_dir(cfg), category) if not dir.dir_exists(cat_path) return 0 end if let entries = res.unwrap_or(dir.list_dir(cat_path), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Skip hidden files and non-directories if str.length(entry) > 0 and entry[0] != '.' let entry_path = path.join_path(cat_path, entry) // Check if it's a valid port (has package.toml) let toml_path = path.join_path(entry_path, "package.toml") if file.fileExists(toml_path) names[count] = entry count = count + 1 end if end if i = i + 1 end while return count end list_category end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: portindex.reef Authors: Chris Tusa License: Description: Ports index for fast port lookups The ports index provides fast indexed access to port metadata without parsing individual package.toml files from disk. Index file: /var/lib/coral/db/ports.db Format: Base64-encoded MsgPack (same as packages.db, files.db) Rebuild triggers: coral sync, coral db rebuild, coral db init Manual: coral ports cache rebuild ******************************************************************************/ module core.portindex import types import io.file import io.dir import io.path import core.str import core.port import core.config import encoding.msgpack import encoding.base64 import core.result as res export type PortIndexEntry type PortsIndex // Rebuild the ports index from the ports tree fn rebuild_port_index(): bool // Check if ports.db exists fn port_index_exists(): bool // Load the ports index from disk fn load_port_index(): PortsIndex // Find a single port by name or category/name fn find_port(name: string): PortIndexEntry // Search ports by substring match on name + description fn search_ports(term: string, results: [PortIndexEntry], max_count: int): int // List all indexed ports fn list_all_ports(entries: [PortIndexEntry], max_count: int): int // Get count of indexed ports fn get_port_count(): int // Find ports that depend on a given package name fn find_reverse_deps(name: string, results: [PortIndexEntry], max_count: int): int end export // ============================================================================ // Types // ============================================================================ type PortIndexEntry = struct name: string category: string version: string release: int description: string port_dir: string runtime_deps: [string] runtime_dep_count: int found: bool end PortIndexEntry type PortsIndex = struct version: int generated: string port_count: int entries: [PortIndexEntry] loaded: bool end PortsIndex // ============================================================================ // Constants // ============================================================================ const PORT_INDEX_VERSION: int = 1 const MAX_PORTS: int = 4096 // ============================================================================ // Path helpers // ============================================================================ fn ports_db_path(): string return types.get_index_dir() + "/ports.db" end ports_db_path // Get current timestamp as string fn get_timestamp(): string return "2026-02-19" end get_timestamp // ============================================================================ // Empty constructors // ============================================================================ fn empty_port_index(): PortsIndex return PortsIndex{ version: 0, generated: "", port_count: 0, entries: new [PortIndexEntry](MAX_PORTS), loaded: false } end empty_port_index fn empty_port_entry(): PortIndexEntry return PortIndexEntry{ name: "", category: "", version: "", release: 0, description: "", port_dir: "", runtime_deps: new [string](64), runtime_dep_count: 0, found: false } end empty_port_entry // ============================================================================ // Buffer size estimation // ============================================================================ fn estimate_buffer_size(port_count: int): int // Header: ~100 bytes // Per port: ~300 bytes (name, category, version, description, port_dir, deps) return 100 + (port_count * 300) end estimate_buffer_size // ============================================================================ // MsgPack header size helpers (same as pkgdb.reef) // ============================================================================ fn map_header_size(buf: [int], offset: int): int let b = buf[offset] & 255 if (b & 0xF0) == 0x80 return 1 elif b == 0xDE return 3 elif b == 0xDF return 5 end if return 1 end map_header_size fn array_header_size(buf: [int], offset: int): int let b = buf[offset] & 255 if (b & 0xF0) == 0x90 return 1 elif b == 0xDC return 3 elif b == 0xDD return 5 end if return 1 end array_header_size // ============================================================================ // Index existence check // ============================================================================ fn port_index_exists(): bool return file.fileExists(ports_db_path()) end port_index_exists // ============================================================================ // Rebuild: scan all ports, serialize to ports.db // ============================================================================ fn rebuild_port_index(): bool // Ensure db directory exists let db_dir = types.get_index_dir() if not dir.dir_exists(db_dir) dir.create_dir_all(db_dir) end if // Get all categories dynamically mut cats: [string] = new [string](32) let cat_count = port.get_categories(cats, 32) // Collect all port entries mut entries: [PortIndexEntry] = new [PortIndexEntry](MAX_PORTS) mut total = 0 mut i = 0 while i < cat_count and total < MAX_PORTS let category = cats[i] mut port_names: [string] = new [string](256) let port_count = port.list_category(category, port_names, 256) mut j = 0 while j < port_count and total < MAX_PORTS let result = port.load(get_port_path(category, port_names[j])) if result.success let p = result.port mut entry = empty_port_entry() entry.name = p.info.name entry.category = p.category entry.version = p.info.version entry.release = p.info.release entry.description = p.info.description entry.port_dir = p.port_dir entry.found = true // Copy runtime deps mut k = 0 while k < p.deps.runtime_count and k < 64 entry.runtime_deps[k] = p.deps.runtime[k] k = k + 1 end while entry.runtime_dep_count = p.deps.runtime_count entries[total] = entry total = total + 1 end if j = j + 1 end while i = i + 1 end while // Serialize to MsgPack let buf_size = estimate_buffer_size(total) mut buf: [int] = new [int](buf_size) let buf_len = serialize_port_index(entries, total, buf, buf_size) if buf_len == 0 return false end if // Encode to base64 and write let encoded = base64.base64_encode_bytes(buf, buf_len) return res.is_ok(file.writeFile(ports_db_path(), encoded)) end rebuild_port_index // Build a port path from category and name fn get_port_path(category: string, name: string): string let cfg = config.load() let ports_dir = config.get_ports_dir(cfg) return path.join_path(ports_dir, path.join_path(category, name)) end get_port_path // ============================================================================ // Serialization // ============================================================================ fn serialize_port_index(entries: [PortIndexEntry], count: int, buf: [int], max_size: int): int mut pos = 0 // Pack map header (4 keys: version, generated, port_count, ports) pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos) // version pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_int(PORT_INDEX_VERSION, buf, pos) // generated pos = pos + msgpack.msgpack_pack_string("generated", buf, pos) pos = pos + msgpack.msgpack_pack_string(get_timestamp(), buf, pos) // port_count pos = pos + msgpack.msgpack_pack_string("port_count", buf, pos) pos = pos + msgpack.msgpack_pack_int(count, buf, pos) // ports array pos = pos + msgpack.msgpack_pack_string("ports", buf, pos) pos = pos + msgpack.msgpack_pack_array_header(count, buf, pos) mut i = 0 while i < count and pos < max_size - 2000 let entry = entries[i] // Each port is a map with 7 keys pos = pos + msgpack.msgpack_pack_map_header(7, buf, pos) // name pos = pos + msgpack.msgpack_pack_string("name", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.name, buf, pos) // category pos = pos + msgpack.msgpack_pack_string("category", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.category, buf, pos) // version pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.version, buf, pos) // release pos = pos + msgpack.msgpack_pack_string("release", buf, pos) pos = pos + msgpack.msgpack_pack_int(entry.release, buf, pos) // description pos = pos + msgpack.msgpack_pack_string("description", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.description, buf, pos) // port_dir pos = pos + msgpack.msgpack_pack_string("port_dir", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.port_dir, buf, pos) // runtime_deps (array of strings) pos = pos + msgpack.msgpack_pack_string("runtime_deps", buf, pos) pos = pos + msgpack.msgpack_pack_array_header(entry.runtime_dep_count, buf, pos) mut d = 0 while d < entry.runtime_dep_count pos = pos + msgpack.msgpack_pack_string(entry.runtime_deps[d], buf, pos) d = d + 1 end while i = i + 1 end while return pos end serialize_port_index // ============================================================================ // Deserialization // ============================================================================ fn load_port_index(): PortsIndex mut index = empty_port_index() let db_path = ports_db_path() if not file.fileExists(db_path) return index end if let encoded = res.unwrap_or(file.readFile(db_path), "") if str.length(encoded) == 0 return index end if let buf_size = str.length(encoded) mut buf: [int] = new [int](buf_size) let buf_len = res.unwrap_or(base64.base64_decode_bytes(encoded, buf, buf_size), 0) if buf_len == 0 return index end if mut offset = 0 // Expect map header if not msgpack.msgpack_is_map(buf, offset) return index end if let map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) mut entry_idx = 0 while entry_idx < map_len and offset < buf_len let key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(key, "version") index.version = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "generated") index.generated = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "port_count") index.port_count = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "ports") // Parse ports array let arr_len = res.unwrap_or(msgpack.msgpack_unpack_array_len(buf, offset), 0) offset = offset + array_header_size(buf, offset) mut port_idx = 0 while port_idx < arr_len and port_idx < MAX_PORTS and offset < buf_len let port_map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) mut entry = empty_port_entry() entry.found = true mut port_field = 0 while port_field < port_map_len and offset < buf_len let field_key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(field_key, "name") entry.name = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "category") entry.category = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "version") entry.version = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "release") entry.release = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "description") entry.description = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "port_dir") entry.port_dir = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "runtime_deps") // Parse deps array let deps_len = res.unwrap_or(msgpack.msgpack_unpack_array_len(buf, offset), 0) offset = offset + array_header_size(buf, offset) mut dep_idx = 0 while dep_idx < deps_len and dep_idx < 64 and offset < buf_len entry.runtime_deps[dep_idx] = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) dep_idx = dep_idx + 1 end while entry.runtime_dep_count = dep_idx else offset = offset + msgpack.msgpack_value_size(buf, offset) end if port_field = port_field + 1 end while index.entries[port_idx] = entry port_idx = port_idx + 1 end while else offset = offset + msgpack.msgpack_value_size(buf, offset) end if entry_idx = entry_idx + 1 end while index.loaded = true return index end load_port_index // ============================================================================ // Query functions // ============================================================================ // Find a single port by name or category/name fn find_port(name: string): PortIndexEntry let index = load_port_index() if not index.loaded return empty_port_entry() end if // Check if name contains a slash (category/name format) let slash_idx = str.index_of(name, "/") mut i = 0 while i < index.port_count let entry = index.entries[i] if slash_idx >= 0 // Match category/name format let full_name = entry.category + "/" + entry.name // Also check directory-based name (category/dirname) let dir_name = path.basename(entry.port_dir) let full_dir_name = entry.category + "/" + dir_name if str.equals(full_name, name) or str.equals(full_dir_name, name) return entry end if else // Match just the name if str.equals(entry.name, name) return entry end if end if i = i + 1 end while return empty_port_entry() end find_port // Search ports by substring match on name + description fn search_ports(term: string, results: [PortIndexEntry], max_count: int): int let index = load_port_index() if not index.loaded return 0 end if let lower_term = str.to_lower(term) mut count = 0 mut i = 0 while i < index.port_count and count < max_count let entry = index.entries[i] let lower_name = str.to_lower(entry.name) let lower_desc = str.to_lower(entry.description) if str.contains(lower_name, lower_term) or str.contains(lower_desc, lower_term) results[count] = entry count = count + 1 end if i = i + 1 end while return count end search_ports // List all indexed ports fn list_all_ports(entries: [PortIndexEntry], max_count: int): int let index = load_port_index() if not index.loaded return 0 end if mut count = 0 while count < index.port_count and count < max_count entries[count] = index.entries[count] count = count + 1 end while return count end list_all_ports // Get count of indexed ports fn get_port_count(): int let index = load_port_index() if index.loaded return index.port_count end if return 0 end get_port_count // Find ports that have `name` in their runtime dependencies fn find_reverse_deps(name: string, results: [PortIndexEntry], max_count: int): int let index = load_port_index() if not index.loaded return 0 end if mut count = 0 mut i = 0 while i < index.port_count and count < max_count let entry = index.entries[i] mut j = 0 while j < entry.runtime_dep_count if str.equals(entry.runtime_deps[j], name) results[count] = entry count = count + 1 break end if j = j + 1 end while i = i + 1 end while return count end find_reverse_deps end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: repository.reef Authors: Chris Tusa License: Description: Repository manifest handling for binary package distribution ******************************************************************************/ module core.repository import core.str import io.file import io.dir import io.path import encoding.toml import util.http as uhttp import util.color import types import core.result as res export type Repository type RepoResult type RemotePackage type RemotePackageResult fn load_repo(repo_path: string): RepoResult fn list_repos(repos: [Repository], max_count: int): int // Remote package operations fn refresh_repo(repo: Repository): bool fn find_remote_package(name: string): RemotePackageResult fn download_package(pkg: RemotePackage): string fn get_cached_manifest(repo: Repository): string end export // Repository definition type Repository = struct name: string url: string priority: int enabled: bool is_signed: bool keyid: string end Repository // Result type for repository operations type RepoResult = struct success: bool repo: Repository error: string end RepoResult // Remote package from repository manifest type RemotePackage = struct name: string version: string release: int description: string license: string category: string depends: [string] depends_count: int size: int compressed: int filename: string sha256: string signature: string repo_name: string repo_url: string end RemotePackage // Result type for remote package search type RemotePackageResult = struct success: bool pkg: RemotePackage error: string end RemotePackageResult // Create empty repository fn new_repository(): Repository return Repository{ name: "", url: "", priority: 100, enabled: true, is_signed: false, keyid: "" } end new_repository // Create empty remote package fn new_remote_package(): RemotePackage return RemotePackage{ name: "", version: "", release: 0, description: "", license: "", category: "", depends: new [string](64), depends_count: 0, size: 0, compressed: 0, filename: "", sha256: "", signature: "", repo_name: "", repo_url: "" } end new_remote_package // Create error result fn error_result(msg: string): RepoResult return RepoResult{ success: false, repo: new_repository(), error: msg } end error_result // Create package error result fn pkg_error_result(msg: string): RemotePackageResult return RemotePackageResult{ success: false, pkg: new_remote_package(), error: msg } end pkg_error_result // Create package success result fn pkg_success_result(pkg: RemotePackage): RemotePackageResult return RemotePackageResult{ success: true, pkg: pkg, error: "" } end pkg_success_result // Load a repository from a .repo file fn load_repo(repo_path: string): RepoResult if not file.fileExists(repo_path) return error_result("Repository file not found: " + repo_path) end if let content = res.unwrap_or(file.readFile(repo_path), "") if str.length(content) == 0 return error_result("Empty repository file: " + repo_path) end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return error_result("Failed to parse repository file: " + repo_path) end if mut repo = new_repository() // Read repository metadata repo.name = toml.toml_get(keys, vals, entry_count, "repository.name") repo.url = toml.toml_get(keys, vals, entry_count, "repository.url") if toml.toml_has_key(keys, vals, entry_count, "repository.priority") repo.priority = toml.toml_get_int(keys, vals, entry_count, "repository.priority") end if if toml.toml_has_key(keys, vals, entry_count, "repository.enabled") repo.enabled = toml.toml_get_bool(keys, vals, entry_count, "repository.enabled") end if if toml.toml_has_key(keys, vals, entry_count, "repository.signed") repo.is_signed = toml.toml_get_bool(keys, vals, entry_count, "repository.signed") end if if toml.toml_has_key(keys, vals, entry_count, "repository.keyid") repo.keyid = toml.toml_get(keys, vals, entry_count, "repository.keyid") end if return RepoResult{ success: true, repo: repo, error: "" } end load_repo // List all configured repositories fn list_repos(repos: [Repository], max_count: int): int let repos_dir = "/etc/coral/repos.d" if not dir.dir_exists(repos_dir) return 0 end if let entries = res.unwrap_or(dir.list_dir(repos_dir), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Check for .repo extension if str.ends_with(entry, ".repo") let repo_path = path.join_path(repos_dir, entry) let result = load_repo(repo_path) if result.success repos[count] = result.repo count = count + 1 end if end if i = i + 1 end while return count end list_repos // Get the path to cached manifest for a repository fn get_cached_manifest(repo: Repository): string let cache_dir = "/var/lib/coral/repos" return path.join_path(cache_dir, repo.name + ".toml") end get_cached_manifest // Refresh repository metadata from remote fn refresh_repo(repo: Repository): bool if not repo.enabled return false end if if str.length(repo.url) == 0 return false end if // Build URL to repo.toml mut manifest_url = repo.url if not str.ends_with(manifest_url, "/") manifest_url = manifest_url + "/" end if manifest_url = manifest_url + "repo.toml" // Ensure cache directory exists let cache_dir = "/var/lib/coral/repos" if not dir.dir_exists(cache_dir) if not res.is_ok(dir.create_dir_all(cache_dir)) return false end if end if // Download manifest let cache_path = get_cached_manifest(repo) return uhttp.download(manifest_url, cache_path) end refresh_repo // Find a package in all enabled repositories fn find_remote_package(name: string): RemotePackageResult // Get list of repositories mut repos: [Repository] = new [Repository](16) let repo_count = list_repos(repos, 16) if repo_count == 0 return pkg_error_result("No repositories configured") end if // Search in each repository (by priority) // TODO: Sort by priority mut i = 0 while i < repo_count let repo = repos[i] if not repo.enabled i = i + 1 continue end if let result = find_package_in_repo(name, repo) if result.success return result end if i = i + 1 end while return pkg_error_result("Package not found in any repository: " + name) end find_remote_package // Find a package in a specific repository fn find_package_in_repo(name: string, repo: Repository): RemotePackageResult let cache_path = get_cached_manifest(repo) // Check if we have cached manifest if not file.fileExists(cache_path) // Try to refresh if not refresh_repo(repo) return pkg_error_result("Failed to fetch repository manifest") end if end if // Read cached manifest let content = res.unwrap_or(file.readFile(cache_path), "") if str.length(content) == 0 return pkg_error_result("Empty repository manifest") end if // Parse TOML to find the package let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return pkg_error_result("Failed to parse repository manifest") end if // Look for package entry: packages. let pkg_key = "packages." + name if not toml.toml_has_key(keys, vals, entry_count, pkg_key + ".version") return pkg_error_result("Package not found: " + name) end if // Build RemotePackage mut pkg = new_remote_package() pkg.name = name pkg.repo_name = repo.name pkg.repo_url = repo.url pkg.version = toml.toml_get(keys, vals, entry_count, pkg_key + ".version") pkg.release = toml.toml_get_int(keys, vals, entry_count, pkg_key + ".release") pkg.description = toml.toml_get(keys, vals, entry_count, pkg_key + ".description") pkg.license = toml.toml_get(keys, vals, entry_count, pkg_key + ".license") pkg.category = toml.toml_get(keys, vals, entry_count, pkg_key + ".category") pkg.filename = toml.toml_get(keys, vals, entry_count, pkg_key + ".file") pkg.sha256 = toml.toml_get(keys, vals, entry_count, pkg_key + ".sha256") pkg.signature = toml.toml_get(keys, vals, entry_count, pkg_key + ".signature") if toml.toml_has_key(keys, vals, entry_count, pkg_key + ".size") pkg.size = toml.toml_get_int(keys, vals, entry_count, pkg_key + ".size") end if if toml.toml_has_key(keys, vals, entry_count, pkg_key + ".compressed") pkg.compressed = toml.toml_get_int(keys, vals, entry_count, pkg_key + ".compressed") end if // Parse depends array if present if toml.toml_has_key(keys, vals, entry_count, pkg_key + ".depends") let deps_str = toml.toml_get(keys, vals, entry_count, pkg_key + ".depends") pkg.depends_count = parse_toml_array(deps_str, pkg.depends, 64) end if // If filename is empty, construct it if str.length(pkg.filename) == 0 pkg.filename = name + "-" + pkg.version + "-" + int_to_str(pkg.release) + ".x86_64.pkg.tar.xz" end if return pkg_success_result(pkg) end find_package_in_repo // Download a package to local cache fn download_package(pkg: RemotePackage): string if str.length(pkg.filename) == 0 color.print_error("Package has no filename") return "" end if // Build download URL mut base_url = pkg.repo_url if not str.ends_with(base_url, "/") base_url = base_url + "/" end if let pkg_url = base_url + "packages/" + pkg.filename // Ensure packages cache directory exists let cache_dir = types.get_packages_dir() if not dir.dir_exists(cache_dir) if not res.is_ok(dir.create_dir_all(cache_dir)) color.print_error("Failed to create packages cache directory") return "" end if end if // Download to cache let dest_path = path.join_path(cache_dir, pkg.filename) // Check if already cached if file.fileExists(dest_path) color.print_info("Using cached: " + pkg.filename) return dest_path end if // Download color.print_info("Downloading: " + pkg.filename) if not uhttp.download(pkg_url, dest_path) return "" end if return dest_path end download_package // Parse TOML array value into string array fn parse_toml_array(value: string, result: [string], max_items: int): int let len = str.length(value) if len < 2 return 0 end if if value[0] != '[' return 0 end if mut count = 0 mut i = 1 mut in_string = false mut item_start = 0 while i < len and count < max_items let c = value[i] if c == '"' and not in_string in_string = true item_start = i + 1 elif c == '"' and in_string let item_len = i - item_start if item_len > 0 result[count] = str.substring(value, item_start, item_len) count = count + 1 end if in_string = false elif c == ']' and not in_string return count end if i = i + 1 end while return count end parse_toml_array // 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: resolver.reef Authors: Chris Tusa License: Description: Dependency resolution for package installation ******************************************************************************/ module core.resolver import core.str import io.file import io.path import types import core.port import core.database import util.version as ver export type ResolveResult fn resolve(name: string): ResolveResult fn get_install_order(names: [string], name_count: int, result: [string], max_count: int): int fn get_dep_tree(name: string, deps: [string], max_count: int): int // Build dependency resolver (resolves both runtime + build deps recursively from source) fn resolve_build_deps(name: string): ResolveResult // Root-aware version for alternate root installation fn get_install_order_rooted(names: [string], name_count: int, result: [string], max_count: int, root: string): int end export // Result for alternative resolution type AlternativeChoice = struct satisfied: bool // True if an alternative is already installed provider: string // The installed provider (if satisfied) options: [string] // Available options to choose from option_count: int base_dep: string // The base dependency name end AlternativeChoice // Resolution result type ResolveResult = struct success: bool packages: [string] count: int error: string end ResolveResult // Create an empty result fn new_result(): ResolveResult mut packages: [string] = new [string](256) return ResolveResult{ success: false, packages: packages, count: 0, error: "" } end new_result // Resolve dependencies for a package // Returns ordered list of packages to install (dependencies first) fn resolve(name: string): ResolveResult mut result = new_result() // Track visited to detect cycles mut visited: [string] = new [string](256) mut visited_count = 0 // Track resolution order mut order: [string] = new [string](256) mut order_count = 0 // Resolve recursively let ok = resolve_recursive(name, visited, visited_count, order, order_count) if not ok.success result.error = ok.error return result end if result.success = true result.packages = ok.packages result.count = ok.count return result end resolve // Internal result for recursive resolution type RecurseResult = struct success: bool packages: [string] count: int error: string end RecurseResult fn new_recurse_result(): RecurseResult mut packages: [string] = new [string](256) return RecurseResult{ success: true, packages: packages, count: 0, error: "" } end new_recurse_result fn resolve_recursive(name: string, visited: [string], visited_count: int, order: [string], order_count: int): RecurseResult mut result = new_recurse_result() result.packages = order result.count = order_count // Check for cycle if array_contains(visited, visited_count, name) // Already visited - skip result.success = true return result end if // Mark as visited visited[visited_count] = name let new_visited_count = visited_count + 1 // Find the port let port_result = port.find(name) if not port_result.success // Check if already installed if database.is_installed(name) result.success = true return result end if // Check if an alternative provider exists let provider = database.find_provider(name) if str.length(provider) > 0 // Alternative already installed result.success = true return result end if result.success = false result.error = "Port not found: " + name return result end if let p = port_result.port let pkg_name = p.info.name // Resolve runtime dependencies first mut i = 0 while i < p.deps.runtime_count let dep = p.deps.runtime[i] // Check if this is an alternative dependency if is_alternative_dep(dep) let alt_choice = resolve_alternative(dep) if alt_choice.satisfied // An alternative is already installed, skip i = i + 1 continue end if // No alternative installed - pick the first option // In a full implementation, we'd prompt the user here if alt_choice.option_count > 0 let chosen = alt_choice.options[0] if not array_contains(result.packages, result.count, chosen) let dep_result = resolve_recursive(chosen, visited, new_visited_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if else // Regular dependency — may include version constraint (e.g., "libfoo>=1.0") let constraint = ver.parse_dep_constraint(dep) let dep_name = ver.constraint_name(constraint) if database.is_installed(dep_name) // Installed — check version constraint if present if ver.has_constraint(constraint) let installed_pkg = database.get_installed(dep_name) if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint)) // Installed version doesn't satisfy constraint — needs upgrade if not array_contains(result.packages, result.count, dep_name) let dep_result = resolve_recursive(dep_name, visited, new_visited_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if end if // No constraint or constraint satisfied — skip else // Not installed — resolve it if not array_contains(result.packages, result.count, dep_name) let dep_result = resolve_recursive(dep_name, visited, new_visited_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if end if i = i + 1 end while // Add this package to order (after its dependencies) // Use canonical name from package.toml, not the raw search argument if not array_contains(result.packages, result.count, pkg_name) // Skip if already installed if not database.is_installed(pkg_name) result.packages[result.count] = pkg_name result.count = result.count + 1 end if end if result.success = true return result end resolve_recursive // Get install order for multiple packages fn get_install_order(names: [string], name_count: int, result: [string], max_count: int): int mut order_count = 0 mut i = 0 while i < name_count let name = names[i] let resolve_result = resolve(name) if resolve_result.success // Add resolved packages to result mut j = 0 while j < resolve_result.count and order_count < max_count let pkg = resolve_result.packages[j] // Avoid duplicates if not array_contains(result, order_count, pkg) result[order_count] = pkg order_count = order_count + 1 end if j = j + 1 end while end if i = i + 1 end while return order_count end get_install_order // Get dependency tree for display (with indentation info) fn get_dep_tree(name: string, deps: [string], max_count: int): int mut count = 0 collect_deps(name, deps, count, 0, max_count) return count end get_dep_tree // Collect dependencies recursively fn collect_deps(name: string, deps: [string], count: int, depth: int, max_count: int): int if depth > 20 or count >= max_count return count end if let result = port.find(name) if not result.success return count end if let p = result.port mut current_count = count mut i = 0 while i < p.deps.runtime_count and current_count < max_count let dep = p.deps.runtime[i] // Add with depth prefix (number of spaces = depth * 2) let indent = make_indent(depth) deps[current_count] = indent + dep current_count = current_count + 1 // Recurse current_count = collect_deps(dep, deps, current_count, depth + 1, max_count) i = i + 1 end while return current_count end collect_deps // Create indentation string fn make_indent(depth: int): string if depth == 0 return "" end if mut result = "" mut i = 0 while i < depth result = result + " " i = i + 1 end while return result end make_indent // Check if array contains a string fn array_contains(arr: [string], count: int, s: string): bool mut i = 0 while i < count if arr[i] == s return true end if i = i + 1 end while return false end array_contains // Check if a dependency string represents alternatives (format: dep:opt1,opt2,opt3) fn is_alternative_dep(dep: string): bool return str.contains(dep, ":") end is_alternative_dep // Parse alternative options from a dependency string // Format: base_dep:alt1,alt2,alt3 // Returns number of alternatives parsed, fills alternatives array fn parse_alternatives(dep: string, alternatives: [string], max_count: int): int let colon_pos = str.index_of_char(dep, ':') if colon_pos < 0 // Not an alternative, return the single dependency alternatives[0] = dep return 1 end if // Get the alternatives part after the colon let alts_part = str.substring(dep, colon_pos + 1, str.length(dep) - colon_pos - 1) // Split by comma return str.split(alts_part, ',', alternatives, max_count) end parse_alternatives // Get the base dependency name from an alternative string fn get_base_dep(dep: string): string let colon_pos = str.index_of_char(dep, ':') if colon_pos < 0 return dep end if return str.substring(dep, 0, colon_pos) end get_base_dep // Create empty AlternativeChoice fn new_alternative_choice(): AlternativeChoice return AlternativeChoice{ satisfied: false, provider: "", options: new [string](16), option_count: 0, base_dep: "" } end new_alternative_choice // Resolve an alternative dependency // Returns satisfied=true if any option is already installed // Otherwise returns the list of available options fn resolve_alternative(dep: string): AlternativeChoice mut choice = new_alternative_choice() if not is_alternative_dep(dep) // Regular dependency choice.base_dep = dep choice.options[0] = dep choice.option_count = 1 // Check if installed if database.is_installed(dep) choice.satisfied = true choice.provider = dep end if return choice end if // Parse the alternatives choice.base_dep = get_base_dep(dep) choice.option_count = parse_alternatives(dep, choice.options, 16) // Check if any alternative is already installed mut i = 0 while i < choice.option_count let opt = choice.options[i] if database.is_installed(opt) choice.satisfied = true choice.provider = opt return choice end if i = i + 1 end while // Check if any provides this via alternatives let provider = database.find_provider(choice.base_dep) if str.length(provider) > 0 choice.satisfied = true choice.provider = provider return choice end if return choice end resolve_alternative // ============================================================================ // Build dependency resolver (resolves both runtime + build deps from source) // ============================================================================ // Resolve all dependencies needed to build a package from source. // Returns topological order: dependencies first, target package last. // Resolves both runtime and build deps recursively. // Uses proper cycle detection with separate visited/in_stack arrays. fn resolve_build_deps(name: string): ResolveResult mut result = new_result() // Permanent marks: fully resolved packages mut visited: [string] = new [string](256) mut visited_count = 0 // Temporary marks: packages on the current DFS stack (for cycle detection) mut in_stack: [string] = new [string](256) mut stack_count = 0 // Topological build order mut order: [string] = new [string](256) mut order_count = 0 let ok = resolve_build_recursive(name, visited, visited_count, in_stack, stack_count, order, order_count) if not ok.success result.error = ok.error return result end if result.success = true result.packages = ok.packages result.count = ok.count return result end resolve_build_deps fn resolve_build_recursive(name: string, visited: [string], visited_count: int, in_stack: [string], stack_count: int, order: [string], order_count: int): RecurseResult mut result = new_recurse_result() result.packages = order result.count = order_count // Permanent mark: already fully resolved, skip if array_contains(visited, visited_count, name) result.success = true return result end if // Temporary mark: cycle detected if array_contains(in_stack, stack_count, name) result.success = false result.error = "Circular dependency detected: " + name return result end if // If already installed, mark visited and skip (don't need to build) if database.is_installed(name) visited[visited_count] = name result.success = true return result end if // Find the port (must exist to build from source) let port_result = port.find(name) if not port_result.success // Check if a provider is already installed let provider = database.find_provider(name) if str.length(provider) > 0 visited[visited_count] = name result.success = true return result end if result.success = false result.error = "Port not found: " + name return result end if let p = port_result.port let pkg_name = p.info.name // Push onto DFS stack (temporary mark) in_stack[stack_count] = name let new_stack_count = stack_count + 1 // Mark as visited (permanent mark) visited[visited_count] = name let new_visited_count = visited_count + 1 // Recurse into RUNTIME deps mut i = 0 while i < p.deps.runtime_count let dep = p.deps.runtime[i] if is_alternative_dep(dep) let alt_choice = resolve_alternative(dep) if not alt_choice.satisfied and alt_choice.option_count > 0 let chosen = alt_choice.options[0] let dep_result = resolve_build_recursive(chosen, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if else let constraint = ver.parse_dep_constraint(dep) let dep_name = ver.constraint_name(constraint) mut needs_build = false if database.is_installed(dep_name) // Check version constraint if ver.has_constraint(constraint) let installed_pkg = database.get_installed(dep_name) if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint)) needs_build = true end if end if else needs_build = true end if if needs_build let dep_result = resolve_build_recursive(dep_name, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if i = i + 1 end while // Recurse into BUILD deps i = 0 while i < p.deps.build_count let dep = p.deps.build[i] if is_alternative_dep(dep) let alt_choice = resolve_alternative(dep) if not alt_choice.satisfied and alt_choice.option_count > 0 let chosen = alt_choice.options[0] let dep_result = resolve_build_recursive(chosen, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if else let constraint = ver.parse_dep_constraint(dep) let dep_name = ver.constraint_name(constraint) mut needs_build = false if database.is_installed(dep_name) if ver.has_constraint(constraint) let installed_pkg = database.get_installed(dep_name) if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint)) needs_build = true end if end if else needs_build = true end if if needs_build let dep_result = resolve_build_recursive(dep_name, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if i = i + 1 end while // Post-order: add this package AFTER all its deps (topological sort) if not array_contains(result.packages, result.count, pkg_name) result.packages[result.count] = pkg_name result.count = result.count + 1 end if result.success = true return result end resolve_build_recursive // ============================================================================ // Root-aware resolver functions for alternate root installation // ============================================================================ // Internal result for rooted recursive resolution type RootedRecurseResult = struct success: bool packages: [string] count: int error: string end RootedRecurseResult fn new_rooted_recurse_result(): RootedRecurseResult mut packages: [string] = new [string](256) return RootedRecurseResult{ success: true, packages: packages, count: 0, error: "" } end new_rooted_recurse_result // Resolve dependencies checking against a specific root fn resolve_recursive_rooted(name: string, visited: [string], visited_count: int, order: [string], order_count: int, root: string): RootedRecurseResult mut result = new_rooted_recurse_result() result.packages = order result.count = order_count // Check for cycle if array_contains(visited, visited_count, name) result.success = true return result end if // Mark as visited visited[visited_count] = name let new_visited_count = visited_count + 1 // Find the port let port_result = port.find(name) if not port_result.success // Check if already installed in target root if database.is_installed_rooted(name, root) result.success = true return result end if // Check if an alternative provider exists in target root // Note: find_provider doesn't have rooted version yet, skip for now result.success = false result.error = "Port not found: " + name return result end if let p = port_result.port let pkg_name = p.info.name // Resolve runtime dependencies first mut i = 0 while i < p.deps.runtime_count let dep = p.deps.runtime[i] // Skip alternative deps for now (simplification) if not is_alternative_dep(dep) // Parse version constraint if present let constraint = ver.parse_dep_constraint(dep) let dep_name = ver.constraint_name(constraint) if database.is_installed_rooted(dep_name, root) // Check version constraint if present if ver.has_constraint(constraint) let installed_pkg = database.get_installed_rooted(dep_name, root) if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint)) // Needs upgrade let dep_result = resolve_recursive_rooted(dep_name, visited, new_visited_count, result.packages, result.count, root) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if else let dep_result = resolve_recursive_rooted(dep_name, visited, new_visited_count, result.packages, result.count, root) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if i = i + 1 end while // Add this package to order (after its dependencies) // Use canonical name from package.toml, not the raw search argument if not array_contains(result.packages, result.count, pkg_name) // Skip if already installed in target root if not database.is_installed_rooted(pkg_name, root) result.packages[result.count] = pkg_name result.count = result.count + 1 end if end if result.success = true return result end resolve_recursive_rooted // Resolve for a single package with root support fn resolve_rooted(name: string, root: string): ResolveResult mut result = new_result() mut visited: [string] = new [string](256) mut visited_count = 0 mut order: [string] = new [string](256) mut order_count = 0 let ok = resolve_recursive_rooted(name, visited, visited_count, order, order_count, root) if not ok.success result.error = ok.error return result end if result.success = true result.packages = ok.packages result.count = ok.count return result end resolve_rooted // Get install order for multiple packages, checking against target root fn get_install_order_rooted(names: [string], name_count: int, result: [string], max_count: int, root: string): int mut order_count = 0 mut i = 0 while i < name_count let name = names[i] let resolve_result = resolve_rooted(name, root) if resolve_result.success mut j = 0 while j < resolve_result.count and order_count < max_count let pkg = resolve_result.packages[j] if not array_contains(result, order_count, pkg) result[order_count] = pkg order_count = order_count + 1 end if j = j + 1 end while end if i = i + 1 end while return order_count end get_install_order_rooted end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: signing.reef Authors: Chris Tusa License: Description: Ed25519 signing and verification for packages and repositories ******************************************************************************/ module core.signing import core.str import io.file import io.dir import io.path import sys.process import core.result as res export type KeyPair type SignResult type VerifyResult fn generate_keypair(name: string): SignResult fn sign_file(file_path: string, key_path: string): SignResult fn verify_file(file_path: string, sig_path: string, key_path: string): VerifyResult fn import_key(key_data: string, name: string): SignResult fn list_keys(keys: [KeyPair], max_count: int): int fn get_keyring_dir(): string fn get_trusted_keys_dir(): string // High-level verification functions fn verify_package(pkg_path: string, sig_path: string): VerifyResult fn verify_manifest(manifest_path: string): VerifyResult fn is_key_trusted(keyid: string): bool end export // Key pair information type KeyPair = struct name: string keyid: string public_key: string private_key: string created: string trusted: bool end KeyPair // Result for signing operations type SignResult = struct success: bool signature: string error: string end SignResult // Result for verification type VerifyResult = struct success: bool valid: bool keyid: string error: string end VerifyResult // Get the keyring directory fn get_keyring_dir(): string return "/etc/coral/keys" end get_keyring_dir // Create empty keypair fn new_keypair(): KeyPair return KeyPair{ name: "", keyid: "", public_key: "", private_key: "", created: "", trusted: false } end new_keypair // Create error result fn error_sign_result(msg: string): SignResult return SignResult{ success: false, signature: "", error: msg } end error_sign_result fn error_verify_result(msg: string): VerifyResult return VerifyResult{ success: false, valid: false, keyid: "", error: msg } end error_verify_result // Generate a new Ed25519 keypair // Note: This requires an external tool (openssl or signify) fn generate_keypair(name: string): SignResult let keyring = get_keyring_dir() // Ensure keyring directory exists if not dir.dir_exists(keyring) if not res.is_ok(dir.create_dir_all(keyring)) return error_sign_result("Failed to create keyring directory") end if end if let private_path = path.join_path(keyring, name + ".key") let public_path = path.join_path(keyring, name + ".pub") // Check if key already exists if file.fileExists(private_path) return error_sign_result("Key already exists: " + name) end if // Generate key using openssl let cmd = "openssl genpkey -algorithm ed25519 -out \"" + private_path + "\" && " + "openssl pkey -in \"" + private_path + "\" -pubout -out \"" + public_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return error_sign_result("Failed to start key generation") end if let exit_code = process.process_wait(pid) if exit_code != 0 return error_sign_result("Key generation failed") end if return SignResult{ success: true, signature: "", error: "" } end generate_keypair // Sign a file with Ed25519 fn sign_file(file_path: string, key_path: string): SignResult if not file.fileExists(file_path) return error_sign_result("File not found: " + file_path) end if if not file.fileExists(key_path) return error_sign_result("Key not found: " + key_path) end if let sig_path = file_path + ".sig" // Sign using openssl let cmd = "openssl pkeyutl -sign -inkey \"" + key_path + "\" -in \"" + file_path + "\" -out \"" + sig_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return error_sign_result("Failed to start signing") end if let exit_code = process.process_wait(pid) if exit_code != 0 return error_sign_result("Signing failed") end if return SignResult{ success: true, signature: sig_path, error: "" } end sign_file // Verify a file signature fn verify_file(file_path: string, sig_path: string, key_path: string): VerifyResult if not file.fileExists(file_path) return error_verify_result("File not found: " + file_path) end if if not file.fileExists(sig_path) return error_verify_result("Signature not found: " + sig_path) end if if not file.fileExists(key_path) return error_verify_result("Key not found: " + key_path) end if // Verify using openssl let cmd = "openssl pkeyutl -verify -pubin -inkey \"" + key_path + "\" -in \"" + file_path + "\" -sigfile \"" + sig_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return error_verify_result("Failed to start verification") end if let exit_code = process.process_wait(pid) if exit_code == 0 return VerifyResult{ success: true, valid: true, keyid: "", error: "" } else return VerifyResult{ success: true, valid: false, keyid: "", error: "Signature verification failed" } end if end verify_file // Import a public key fn import_key(key_data: string, name: string): SignResult let keyring = get_keyring_dir() if not dir.dir_exists(keyring) if not res.is_ok(dir.create_dir_all(keyring)) return error_sign_result("Failed to create keyring directory") end if end if let key_path = path.join_path(keyring, name + ".pub") if file.fileExists(key_path) return error_sign_result("Key already exists: " + name) end if if not res.is_ok(file.writeFile(key_path, key_data)) return error_sign_result("Failed to write key file") end if return SignResult{ success: true, signature: "", error: "" } end import_key // List all keys in keyring fn list_keys(keys: [KeyPair], max_count: int): int let keyring = get_keyring_dir() if not dir.dir_exists(keyring) return 0 end if let entries = res.unwrap_or(dir.list_dir(keyring), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Look for .pub files if str.ends_with(entry, ".pub") let name_len = str.length(entry) - 4 let name = str.substring(entry, 0, name_len) mut kp = new_keypair() kp.name = name kp.trusted = true // Local keys are trusted // Read public key let pub_path = path.join_path(keyring, entry) kp.public_key = res.unwrap_or(file.readFile(pub_path), "") // Check for private key let priv_path = path.join_path(keyring, name + ".key") if file.fileExists(priv_path) kp.private_key = "(present)" end if keys[count] = kp count = count + 1 end if i = i + 1 end while return count end list_keys // Get the trusted keys directory fn get_trusted_keys_dir(): string return "/var/lib/coral/trusted-keys" end get_trusted_keys_dir // Check if a key is in the trusted keys list fn is_key_trusted(keyid: string): bool let trusted_dir = get_trusted_keys_dir() if not dir.dir_exists(trusted_dir) return false end if // Look for key file by keyid let key_path = path.join_path(trusted_dir, keyid + ".pub") if file.fileExists(key_path) return true end if // Also check the main keyring let keyring_path = path.join_path(get_keyring_dir(), keyid + ".pub") return file.fileExists(keyring_path) end is_key_trusted // Get path to a trusted key by keyid fn get_trusted_key_path(keyid: string): string // Try trusted-keys directory first let trusted_dir = get_trusted_keys_dir() let trusted_path = path.join_path(trusted_dir, keyid + ".pub") if file.fileExists(trusted_path) return trusted_path end if // Fall back to main keyring let keyring_path = path.join_path(get_keyring_dir(), keyid + ".pub") if file.fileExists(keyring_path) return keyring_path end if return "" end get_trusted_key_path // Verify a package with any trusted key fn verify_package(pkg_path: string, sig_path: string): VerifyResult if not file.fileExists(pkg_path) return error_verify_result("Package not found: " + pkg_path) end if if not file.fileExists(sig_path) return error_verify_result("Signature not found: " + sig_path) end if // Try to verify with each trusted key let trusted_dir = get_trusted_keys_dir() let keyring_dir = get_keyring_dir() // Collect all public keys from both directories mut all_keys: [string] = new [string](64) mut key_count = 0 // Get keys from trusted-keys directory if dir.dir_exists(trusted_dir) let entries = res.unwrap_or(dir.list_dir(trusted_dir), new [string](0)) let entry_count = entries.length() mut i = 0 while i < entry_count and key_count < 64 if str.ends_with(entries[i], ".pub") all_keys[key_count] = path.join_path(trusted_dir, entries[i]) key_count = key_count + 1 end if i = i + 1 end while end if // Get keys from keyring directory if dir.dir_exists(keyring_dir) let entries = res.unwrap_or(dir.list_dir(keyring_dir), new [string](0)) let entry_count = entries.length() mut i = 0 while i < entry_count and key_count < 64 if str.ends_with(entries[i], ".pub") let key_path = path.join_path(keyring_dir, entries[i]) // Avoid duplicates mut is_dup = false mut j = 0 while j < key_count if str.equals(all_keys[j], key_path) is_dup = true end if j = j + 1 end while if not is_dup all_keys[key_count] = key_path key_count = key_count + 1 end if end if i = i + 1 end while end if if key_count == 0 return error_verify_result("No trusted keys found") end if // Try each key mut i = 0 while i < key_count let key_path = all_keys[i] let result = verify_file(pkg_path, sig_path, key_path) if result.success and result.valid // Extract keyid from path let filename = path.basename(key_path) let keyid = str.substring(filename, 0, str.length(filename) - 4) return VerifyResult{ success: true, valid: true, keyid: keyid, error: "" } end if i = i + 1 end while return VerifyResult{ success: true, valid: false, keyid: "", error: "Signature does not match any trusted key" } end verify_package // Verify a repository manifest (repo.toml and repo.toml.sig) fn verify_manifest(manifest_path: string): VerifyResult if not file.fileExists(manifest_path) return error_verify_result("Manifest not found: " + manifest_path) end if let sig_path = manifest_path + ".sig" if not file.fileExists(sig_path) return error_verify_result("Manifest signature not found: " + sig_path) end if return verify_package(manifest_path, sig_path) end verify_manifest end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: exitcodes.reef Authors: Chris Tusa License: Description: Exit code constants for Coral package manager ******************************************************************************/ module exitcodes export // Success fn EXIT_SUCCESS(): int // General errors (1-9) fn EXIT_ERROR(): int fn EXIT_USAGE(): int fn EXIT_CANCELLED(): int // Package errors (10-19) fn EXIT_PKG_NOT_FOUND(): int fn EXIT_PKG_ALREADY_INSTALLED(): int fn EXIT_PKG_INVALID(): int fn EXIT_PKG_CHECKSUM_MISMATCH(): int fn EXIT_PKG_SIGNATURE_INVALID(): int fn EXIT_PKG_CONFLICT(): int fn EXIT_INSTALL_FAILED(): int fn EXIT_REMOVE_FAILED(): int fn EXIT_UPGRADE_FAILED(): int // Build errors (20-29) fn EXIT_BUILD_FAILED(): int fn EXIT_PORT_NOT_FOUND(): int fn EXIT_SOURCE_MISSING(): int fn EXIT_PATCH_FAILED(): int fn EXIT_EXTRACT_FAILED(): int fn EXIT_CHECKSUM_FAILED(): int fn EXIT_PACKAGE_FAILED(): int // Network errors (30-39) fn EXIT_DOWNLOAD_FAILED(): int fn EXIT_REPO_UNREACHABLE(): int fn EXIT_TIMEOUT(): int // Database errors (40-49) fn EXIT_DB_ERROR(): int fn EXIT_DB_REGISTER_FAILED(): int fn EXIT_DB_CORRUPT(): int fn EXIT_DB_INDEX_STALE(): int // Permission errors (50-59) fn EXIT_PERMISSION_DENIED(): int fn EXIT_ROOT_REQUIRED(): int // Dependency errors (60-69) fn EXIT_DEPS_UNMET(): int fn EXIT_DEPS_CIRCULAR(): int fn EXIT_DEPS_CONFLICT(): int fn EXIT_DEPS_BLOCKED(): int // Config errors (70-79) fn EXIT_CONFIG_ERROR(): int fn EXIT_CONFIG_INVALID(): int fn EXIT_CONFIG_MISSING(): int // Informational codes for scripting (100+) fn EXIT_UPDATE_AVAILABLE(): int fn EXIT_MANIFEST_STALE(): int fn EXIT_NO_UPDATES(): int end export // ============================================================================ // Success (0) // ============================================================================ fn EXIT_SUCCESS(): int return 0 end EXIT_SUCCESS // ============================================================================ // General Errors (1-9) // ============================================================================ fn EXIT_ERROR(): int return 1 end EXIT_ERROR fn EXIT_USAGE(): int return 2 end EXIT_USAGE fn EXIT_CANCELLED(): int return 3 end EXIT_CANCELLED // ============================================================================ // Package Errors (10-19) // ============================================================================ fn EXIT_PKG_NOT_FOUND(): int return 10 end EXIT_PKG_NOT_FOUND fn EXIT_PKG_ALREADY_INSTALLED(): int return 11 end EXIT_PKG_ALREADY_INSTALLED fn EXIT_PKG_INVALID(): int return 12 end EXIT_PKG_INVALID fn EXIT_PKG_CHECKSUM_MISMATCH(): int return 13 end EXIT_PKG_CHECKSUM_MISMATCH fn EXIT_PKG_SIGNATURE_INVALID(): int return 14 end EXIT_PKG_SIGNATURE_INVALID fn EXIT_PKG_CONFLICT(): int return 15 end EXIT_PKG_CONFLICT fn EXIT_INSTALL_FAILED(): int return 16 end EXIT_INSTALL_FAILED fn EXIT_REMOVE_FAILED(): int return 17 end EXIT_REMOVE_FAILED fn EXIT_UPGRADE_FAILED(): int return 18 end EXIT_UPGRADE_FAILED // ============================================================================ // Build Errors (20-29) // ============================================================================ fn EXIT_BUILD_FAILED(): int return 20 end EXIT_BUILD_FAILED fn EXIT_PORT_NOT_FOUND(): int return 21 end EXIT_PORT_NOT_FOUND fn EXIT_SOURCE_MISSING(): int return 22 end EXIT_SOURCE_MISSING fn EXIT_PATCH_FAILED(): int return 23 end EXIT_PATCH_FAILED fn EXIT_EXTRACT_FAILED(): int return 24 end EXIT_EXTRACT_FAILED fn EXIT_CHECKSUM_FAILED(): int return 25 end EXIT_CHECKSUM_FAILED fn EXIT_PACKAGE_FAILED(): int return 26 end EXIT_PACKAGE_FAILED // ============================================================================ // Network Errors (30-39) // ============================================================================ fn EXIT_DOWNLOAD_FAILED(): int return 30 end EXIT_DOWNLOAD_FAILED fn EXIT_REPO_UNREACHABLE(): int return 31 end EXIT_REPO_UNREACHABLE fn EXIT_TIMEOUT(): int return 32 end EXIT_TIMEOUT // ============================================================================ // Database Errors (40-49) // ============================================================================ fn EXIT_DB_ERROR(): int return 40 end EXIT_DB_ERROR fn EXIT_DB_REGISTER_FAILED(): int return 41 end EXIT_DB_REGISTER_FAILED fn EXIT_DB_CORRUPT(): int return 42 end EXIT_DB_CORRUPT fn EXIT_DB_INDEX_STALE(): int return 43 end EXIT_DB_INDEX_STALE // ============================================================================ // Permission Errors (50-59) // ============================================================================ fn EXIT_PERMISSION_DENIED(): int return 50 end EXIT_PERMISSION_DENIED fn EXIT_ROOT_REQUIRED(): int return 51 end EXIT_ROOT_REQUIRED // ============================================================================ // Dependency Errors (60-69) // ============================================================================ fn EXIT_DEPS_UNMET(): int return 60 end EXIT_DEPS_UNMET fn EXIT_DEPS_CIRCULAR(): int return 61 end EXIT_DEPS_CIRCULAR fn EXIT_DEPS_CONFLICT(): int return 62 end EXIT_DEPS_CONFLICT fn EXIT_DEPS_BLOCKED(): int return 63 end EXIT_DEPS_BLOCKED // ============================================================================ // Config Errors (70-79) // ============================================================================ fn EXIT_CONFIG_ERROR(): int return 70 end EXIT_CONFIG_ERROR fn EXIT_CONFIG_INVALID(): int return 71 end EXIT_CONFIG_INVALID fn EXIT_CONFIG_MISSING(): int return 72 end EXIT_CONFIG_MISSING // ============================================================================ // Informational Codes for Scripting (100+) // ============================================================================ fn EXIT_UPDATE_AVAILABLE(): int return 100 end EXIT_UPDATE_AVAILABLE fn EXIT_MANIFEST_STALE(): int return 101 end EXIT_MANIFEST_STALE fn EXIT_NO_UPDATES(): int return 102 end EXIT_NO_UPDATES end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: main.reef Authors: Chris Tusa License: Description: Entry point for Coral package manager ******************************************************************************/ import sys.args import sys.process import sys.env import io.dir import core.str import types import exitcodes as ec import commands.build as cmd_build import commands.install as cmd_install import commands.remove as cmd_remove import commands.upgrade as cmd_upgrade import commands.info as cmd_info import commands.list as cmd_list import commands.files as cmd_files import commands.owner as cmd_owner import commands.search as cmd_search import commands.depends as cmd_depends import commands.sync as cmd_sync import commands.outdated as cmd_outdated import commands.repo as cmd_repo import commands.key as cmd_key import commands.db as cmd_db import commands.clean as cmd_clean import commands.ports as cmd_ports import commands.verify as cmd_verify import util.color import util.priv fn version(): string return "0.4.5" end version proc print_usage() println("coral " + version() + " - Package manager for Zygaena") println("") println("USAGE:") println(" coral [options] [packages...]") println("") println("COMMANDS:") println(" Package Management:") println(" install Install packages") println(" reinstall Reinstall packages (same as install --force)") println(" remove Remove packages") println(" upgrade Upgrade packages") println("") println(" Query Commands:") println(" info Show package info") println(" list List installed packages") println(" files List package files") println(" owner Find file owner") println(" search Search packages") println(" depends Show dependencies") println(" outdated Show available updates") println(" verify Verify package integrity") println("") println(" Build Commands:") println(" build Build package from port") println(" sync Update ports tree") println(" clean Remove build artifacts (sources|packages|build|all)") println("") println(" System Commands:") println(" db Manage package database (init|rebuild|status)") println(" ports Port development tools (new|cache)") println(" repo Manage repositories") println(" key Manage signing keys") println("") println("OPTIONS:") println(" -h, --help Show this help") println(" --version Show version") println(" -y, --yes Don't ask for confirmation") println(" -f, --force Force operation") println(" -v, --verbose Verbose output") println(" -n, --dry-run Show what would be done") println(" -q, --quiet Minimal output") println(" --no-color Disable colored output") println(" --root Use alternative root directory") println(" --prefix Installation prefix (default: /usr)") println(" --no-chroot-scripts Run maintainer scripts on the host, not chrooted") println("") println("EXIT CODES:") println(" 0 Success") println(" 1 General error") println(" 2 Usage/syntax error") println(" 10-19 Package errors") println(" 20-29 Build errors") println(" 30-39 Network errors") println(" 50-59 Permission errors") println(" 60-69 Dependency errors") println(" 100+ Informational (scripting)") end print_usage // Find the index of the command argument, skipping global flags fn find_command_index(): int let argc = args.count() mut i = 1 while i < argc let arg = args.get(i) // Skip flags that take a value if arg == "--root" or arg == "--prefix" i = i + 2 // Skip flag and its value continue end if // Skip boolean flags if arg == "-y" or arg == "--yes" or arg == "-f" or arg == "--force" or arg == "-v" or arg == "--verbose" or arg == "-n" or arg == "--dry-run" or arg == "-q" or arg == "--quiet" or arg == "--no-color" or arg == "--no-chroot-scripts" i = i + 1 continue end if // Found the command return i end while return -1 end find_command_index // Parse global options from command line arguments fn parse_global_options(): types.GlobalOptions mut opts = types.new_global_options() opts.yes = args.has_flag("y") or args.has_flag("yes") opts.force = args.has_flag("f") or args.has_flag("force") opts.verbose = args.has_flag("v") or args.has_flag("verbose") opts.dry_run = args.has_flag("n") or args.has_flag("dry-run") opts.quiet = args.has_flag("q") or args.has_flag("quiet") // Check for --root option if args.has_flag("root") opts.root = args.get_flag_value("root") end if // Check for --prefix option if args.has_flag("prefix") opts.prefix = args.get_flag_value("prefix") end if // Check for --no-chroot-scripts option (valueless) opts.no_chroot = args.has_flag("no-chroot-scripts") return opts end parse_global_options // Check if a command is a query command (doesn't require root) fn is_query_command(cmd: string): bool // Query commands that don't modify the system if cmd == "info" return true elif cmd == "list" return true elif cmd == "files" return true elif cmd == "owner" return true elif cmd == "search" return true elif cmd == "depends" return true elif cmd == "outdated" return true elif cmd == "verify" return true elif cmd == "build" // Building packages doesn't require root - only writes to build directories return true elif cmd == "--help" or cmd == "-h" return true elif cmd == "--version" or cmd == "version" return true end if return false end is_query_command fn main(): int let argc = args.count() if argc < 2 print_usage() return ec.EXIT_USAGE() end if // Check for --help and --version before finding command // (these can appear anywhere) if args.has_flag("help") or args.has_flag("h") print_usage() return ec.EXIT_SUCCESS() end if if args.has_flag("version") println("coral " + version()) return ec.EXIT_SUCCESS() end if // Find the actual command, skipping global flags let cmd_index = find_command_index() if cmd_index < 0 print_usage() return ec.EXIT_USAGE() end if let cmd = args.get(cmd_index) // Parse global options and set command index mut opts = parse_global_options() opts.cmd_index = cmd_index // Check for root privileges if required if not is_query_command(cmd) if not priv.is_root() color.print_error("This command requires root privileges") println("Run 'coral " + cmd + "' as root or with sudo") return ec.EXIT_ROOT_REQUIRED() end if end if // Dispatch to command handlers // Commands return exit codes if cmd == "build" return cmd_build.execute(opts) elif cmd == "install" return cmd_install.execute(opts) elif cmd == "reinstall" // Reinstall is install with force flag enabled mut reinstall_opts = opts reinstall_opts.force = true return cmd_install.execute(reinstall_opts) elif cmd == "remove" return cmd_remove.execute(opts) elif cmd == "upgrade" return cmd_upgrade.execute(opts) elif cmd == "info" return cmd_info.execute(opts) elif cmd == "list" return cmd_list.execute(opts) elif cmd == "files" return cmd_files.execute(opts) elif cmd == "owner" return cmd_owner.execute(opts) elif cmd == "search" return cmd_search.execute() elif cmd == "depends" return cmd_depends.execute() elif cmd == "sync" return cmd_sync.execute() elif cmd == "outdated" return cmd_outdated.execute() elif cmd == "repo" return cmd_repo.execute() elif cmd == "key" return cmd_key.execute() elif cmd == "db" return cmd_db.execute(opts) elif cmd == "clean" return cmd_clean.execute() elif cmd == "ports" return cmd_ports.execute(opts) elif cmd == "verify" return cmd_verify.execute(opts) elif cmd == "version" println(version()) return ec.EXIT_SUCCESS() else color.print_error("unknown command '" + cmd + "'") println("Run 'coral --help' for usage.") return ec.EXIT_USAGE() end if end main /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: types.reef Authors: Chris Tusa License: Description: Shared type definitions for Coral package manager ******************************************************************************/ module types export // Path getter functions (constants can't be exported across modules) fn get_ports_dir(): string fn get_sources_dir(): string fn get_packages_dir(): string fn get_build_dir(): string fn get_db_dir(): string fn get_index_dir(): string fn get_repos_dir(): string fn get_config_file(): string fn get_groups_dir(): string // Legacy (for compatibility) fn get_work_dir(): string fn get_pkg_dir(): string // Types type PackageInfo type Dependencies type Source type SourceInfo type PatchInfo type ConfigInfo type ScriptsInfo type BuildConfig type Port type InstalledPackage type BuildResult type GlobalOptions type PackageGroup type PortResult // Factory functions fn new_package_info(): PackageInfo fn new_dependencies(): Dependencies fn new_global_options(): GlobalOptions fn new_source(): Source fn new_source_info(): SourceInfo fn new_patch_info(): PatchInfo fn new_config_info(): ConfigInfo fn new_scripts_info(): ScriptsInfo fn new_build_config(): BuildConfig fn new_port(): Port fn new_port_result(ok: bool, p: Port, err: string): PortResult fn new_installed_package(): InstalledPackage fn new_package_group(): PackageGroup end export // Zygaena/Coral filesystem paths // Ports tree and build artifacts const PORTS_DIR: string = "/usr/zports" const GROUPS_DIR: string = "/usr/zports/groups" const SOURCES_DIR: string = "/usr/zports/pkgs/sources" const PACKAGES_DIR: string = "/usr/zports/pkgs/packages" const BUILD_DIR: string = "/usr/zports/pkgs/build" // System state (in /var/lib) const DB_DIR: string = "/var/lib/coral/installed" const INDEX_DIR: string = "/var/lib/coral/db" const REPOS_DIR: string = "/var/lib/coral/repos" // Configuration const CONFIG_FILE: string = "/etc/coral/coral.conf" // Legacy aliases (for compatibility during transition) const WORK_DIR: string = "/usr/zports/pkgs/build" const PKG_DIR: string = "/usr/zports/pkgs/build" // Package metadata from package.toml type PackageInfo = struct name: string version: string release: int description: string url: string license: string maintainer: string arch: string alternatives: [string] alternatives_count: int conflicts: [string] conflicts_count: int runtime_deps: [string] build_deps: [string] runtime_deps_count: int build_deps_count: int end PackageInfo // Dependencies type Dependencies = struct runtime: [string] build: [string] runtime_count: int build_count: int end Dependencies // Individual source file with mirror support type Source = struct file: string // Output filename (what to save as) urls: [string] // Mirror URLs (tried in order) url_count: int // Number of URLs checksum: string // SHA256 checksum (or "SKIP") extract: bool // Whether to extract archive (default: true) end Source // Collection of source files to download type SourceInfo = struct sources: [Source] // Array of Source structs count: int // Number of sources end SourceInfo // Patch configuration type PatchInfo = struct files: [string] strip: int count: int end PatchInfo // Config file protection type ConfigInfo = struct files: [string] count: int end ConfigInfo // Install/remove scripts type ScriptsInfo = struct pre_install: string post_install: string pre_remove: string post_remove: string end ScriptsInfo // Build configuration type BuildConfig = struct parallel: bool jobs: int end BuildConfig // Complete port definition type Port = struct info: PackageInfo deps: Dependencies sources: SourceInfo patches: PatchInfo config: ConfigInfo scripts: ScriptsInfo build_cfg: BuildConfig port_dir: string category: string // Per-package path overrides (from package.toml [paths] section) prefix: string sysconfdir: string end Port // Installed package record type InstalledPackage = struct info: PackageInfo install_date: string install_reason: string files: [string] files_count: int end InstalledPackage // Build result type BuildResult = struct success: bool package_path: string error_message: string end BuildResult // Global CLI options type GlobalOptions = struct yes: bool force: bool verbose: bool dry_run: bool quiet: bool root: string prefix: string no_chroot: bool // --no-chroot-scripts: run maintainer scripts on the host, not chrooted cmd_index: int // Index of the command in args (for subcommand parsing) end GlobalOptions // Package group (meta-package) type PackageGroup = struct name: string description: string members: [string] members_count: int end PackageGroup // Parse result wrapper type PortResult = struct success: bool port: Port error: string end PortResult // Initialize empty PackageInfo fn new_package_info(): PackageInfo return PackageInfo{ name: "", version: "", release: 1, description: "", url: "", license: "", maintainer: "", arch: "x86_64", alternatives: new [string](0), alternatives_count: 0, conflicts: new [string](0), conflicts_count: 0, runtime_deps: new [string](64), build_deps: new [string](64), runtime_deps_count: 0, build_deps_count: 0 } end new_package_info // Initialize empty Dependencies fn new_dependencies(): Dependencies return Dependencies{ runtime: new [string](0), build: new [string](0), runtime_count: 0, build_count: 0 } end new_dependencies // Initialize empty GlobalOptions with defaults fn new_global_options(): GlobalOptions return GlobalOptions{ yes: false, force: false, verbose: false, dry_run: false, quiet: false, root: "", prefix: "", no_chroot: false, cmd_index: 1 } end new_global_options // Initialize empty Source fn new_source(): Source return Source{ file: "", urls: new [string](16), url_count: 0, checksum: "SKIP", extract: true } end new_source // Initialize empty SourceInfo fn new_source_info(): SourceInfo return SourceInfo{ sources: new [Source](16), count: 0 } end new_source_info // Initialize empty PatchInfo fn new_patch_info(): PatchInfo return PatchInfo{ files: new [string](0), strip: 1, count: 0 } end new_patch_info // Initialize empty ConfigInfo fn new_config_info(): ConfigInfo return ConfigInfo{ files: new [string](0), count: 0 } end new_config_info // Initialize empty ScriptsInfo fn new_scripts_info(): ScriptsInfo return ScriptsInfo{ pre_install: "", post_install: "", pre_remove: "", post_remove: "" } end new_scripts_info // Initialize default BuildConfig fn new_build_config(): BuildConfig return BuildConfig{ parallel: true, jobs: 0 } end new_build_config // Initialize empty Port fn new_port(): Port return Port{ info: new_package_info(), deps: new_dependencies(), sources: new_source_info(), patches: new_patch_info(), config: new_config_info(), scripts: new_scripts_info(), build_cfg: new_build_config(), port_dir: "", category: "", prefix: "", sysconfdir: "" } end new_port // Create a PortResult fn new_port_result(ok: bool, p: Port, err: string): PortResult return PortResult{ success: ok, port: p, error: err } end new_port_result // Path getter functions fn get_ports_dir(): string return PORTS_DIR end get_ports_dir fn get_sources_dir(): string return SOURCES_DIR end get_sources_dir fn get_packages_dir(): string return PACKAGES_DIR end get_packages_dir fn get_build_dir(): string return BUILD_DIR end get_build_dir fn get_db_dir(): string return DB_DIR end get_db_dir fn get_index_dir(): string return INDEX_DIR end get_index_dir fn get_repos_dir(): string return REPOS_DIR end get_repos_dir fn get_config_file(): string return CONFIG_FILE end get_config_file fn get_groups_dir(): string return GROUPS_DIR end get_groups_dir // Legacy aliases (for compatibility during transition) fn get_work_dir(): string return BUILD_DIR end get_work_dir fn get_pkg_dir(): string return BUILD_DIR end get_pkg_dir // Initialize empty InstalledPackage fn new_installed_package(): InstalledPackage return InstalledPackage{ info: new_package_info(), install_date: "", install_reason: "", files: new [string](0), files_count: 0 } end new_installed_package fn new_package_group(): PackageGroup return PackageGroup{ name: "", description: "", members: new [string](256), members_count: 0 } end new_package_group end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: archive.reef Authors: Chris Tusa License: Description: Tar/xz archive operations using shell commands ******************************************************************************/ module util.archive import sys.process import io.file import io.dir import io.path import core.str import core.result as res export // Archive type enumeration (as strings for simplicity) const ARCHIVE_TAR_GZ: string const ARCHIVE_TAR_XZ: string const ARCHIVE_TAR_BZ2: string const ARCHIVE_ZIP: string const ARCHIVE_UNKNOWN: string // Detect archive type from filename extension fn detect_type(archive_path: string): string // Extract an archive to destination directory fn extract(archive_path: string, dest_dir: string): bool // Create a .pkg.tar.xz package from a directory fn create_package(source_dir: string, output_path: string): bool // List contents of an archive (returns count, fills files array) fn list_contents(archive_path: string, files: [string], max_count: int): int end export // Archive type constants const ARCHIVE_TAR_GZ: string = "tar.gz" const ARCHIVE_TAR_XZ: string = "tar.xz" const ARCHIVE_TAR_BZ2: string = "tar.bz2" const ARCHIVE_ZIP: string = "zip" const ARCHIVE_UNKNOWN: string = "unknown" // Get the tar command (use gtar on illumos/OmniOS, tar on Linux) fn get_tar_cmd(): string // On illumos, we need GNU tar for full compatibility // Check if gtar exists, otherwise fall back to tar (for Linux) if file.fileExists("/usr/bin/gtar") return "/usr/bin/gtar" elif file.fileExists("/usr/gnu/bin/tar") return "/usr/gnu/bin/tar" end if // Fall back to system tar (Linux) return "tar" end get_tar_cmd // Detect archive type from filename fn detect_type(archive_path: string): string let lower = str.to_lower(archive_path) if str.ends_with(lower, ".tar.xz") or str.ends_with(lower, ".txz") return ARCHIVE_TAR_XZ elif str.ends_with(lower, ".tar.gz") or str.ends_with(lower, ".tgz") return ARCHIVE_TAR_GZ elif str.ends_with(lower, ".tar.bz2") or str.ends_with(lower, ".tbz2") or str.ends_with(lower, ".tbz") return ARCHIVE_TAR_BZ2 elif str.ends_with(lower, ".zip") return ARCHIVE_ZIP elif str.ends_with(lower, ".tar") // Plain tar (no compression) return "tar" end if return ARCHIVE_UNKNOWN end detect_type // Build the tar extraction flags based on archive type fn get_extract_flags(archive_type: string): string if archive_type == ARCHIVE_TAR_XZ return "-Jxf" elif archive_type == ARCHIVE_TAR_GZ return "-zxf" elif archive_type == ARCHIVE_TAR_BZ2 return "-jxf" elif archive_type == "tar" return "-xf" end if return "" end get_extract_flags // Build the tar list flags based on archive type fn get_list_flags(archive_type: string): string if archive_type == ARCHIVE_TAR_XZ return "-Jtf" elif archive_type == ARCHIVE_TAR_GZ return "-ztf" elif archive_type == ARCHIVE_TAR_BZ2 return "-jtf" elif archive_type == "tar" return "-tf" end if return "" end get_list_flags // Extract an archive to the destination directory fn extract(archive_path: string, dest_dir: string): bool // Verify archive exists if not file.fileExists(archive_path) return false end if // Ensure destination directory exists if not dir.dir_exists(dest_dir) if not res.is_ok(dir.create_dir_all(dest_dir)) return false end if end if let archive_type = detect_type(archive_path) if archive_type == ARCHIVE_ZIP // Use unzip for zip files let cmd = "unzip -q -o \"" + archive_path + "\" -d \"" + dest_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if let exit_code = process.process_wait(pid) return exit_code == 0 end if let flags = get_extract_flags(archive_type) if str.length(flags) == 0 return false end if // Build tar command let tar = get_tar_cmd() let cmd = tar + " " + flags + " \"" + archive_path + "\" -C \"" + dest_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if let exit_code = process.process_wait(pid) return exit_code == 0 end extract // Create a .pkg.tar.xz package from a source directory fn create_package(source_dir: string, output_path: string): bool // Verify source directory exists if not dir.dir_exists(source_dir) return false end if // Ensure parent directory of output exists let output_dir = path.dirname(output_path) if str.length(output_dir) > 0 and not dir.dir_exists(output_dir) if not res.is_ok(dir.create_dir_all(output_dir)) return false end if end if // Create tar.xz archive // Command: cd source_dir && tar -Jcf output_path . let tar = get_tar_cmd() let cmd = "cd \"" + source_dir + "\" && " + tar + " -Jcf \"" + output_path + "\" ." let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if let exit_code = process.process_wait(pid) return exit_code == 0 end create_package // List contents of an archive fn list_contents(archive_path: string, files: [string], max_count: int): int if not file.fileExists(archive_path) return 0 end if let archive_type = detect_type(archive_path) if archive_type == ARCHIVE_ZIP // For zip files, use unzip -l and parse output // This is more complex, so for now return 0 // TODO: Implement zip listing if needed return 0 end if let flags = get_list_flags(archive_type) if str.length(flags) == 0 return 0 end if // Build tar command with output to temp file let tar = get_tar_cmd() let tmp_file = "/tmp/coral_archive_list_" + int_to_string(process.getpid()) + ".txt" let cmd = tar + " " + flags + " \"" + archive_path + "\" > \"" + tmp_file + "\" 2>/dev/null" let pid = process.process_spawn_shell(cmd) if pid < 0 return 0 end if let exit_code = process.process_wait(pid) if exit_code != 0 return 0 end if // Read the temp file if not file.fileExists(tmp_file) return 0 end if let content = res.unwrap_or(file.readFile(tmp_file), "") // Clean up temp file cleanup_temp_file(tmp_file) if str.length(content) == 0 return 0 end if // Split by newlines mut lines: [string] = new [string](max_count) let line_count = str.split(content, '\n', lines, max_count) // Copy non-empty lines to output mut count = 0 mut i = 0 while i < line_count and count < max_count let line = str.trim_ws(lines[i]) if str.length(line) > 0 files[count] = line count = count + 1 end if i = i + 1 end while return count end list_contents // Helper: remove temp file fn cleanup_temp_file(path: string): bool let cmd = "rm -f \"" + path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if process.process_wait(pid) return true end cleanup_temp_file // Helper: convert int to string (local copy to avoid circular import) fn int_to_string(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_string end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: checksum.reef Authors: Chris Tusa License: Description: SHA256 checksum verification utilities ******************************************************************************/ module util.checksum import io.file import core.str import sys.process import crypto.sha256 as sha import core.result as res export // Compute SHA256 hash of a file, returns hex string (lowercase) fn sha256_file(path: string): string // Compute SHA256 hash of a string, returns hex string (lowercase) fn sha256_string(data: string): string // Verify a file matches expected checksum fn verify_checksum(path: string, expected: string): bool // Parse checksum format "algo:hash" and verify fn verify_checksum_with_algo(path: string, checksum_str: string): bool end export // Compute SHA256 of a file // Uses Reef's native streaming sha256_file() (Reef 0.1.14+) // Falls back to sha256sum command if native fails fn sha256_file(path: string): string if not file.fileExists(path) return "" end if // Use native streaming API (memory efficient, works on all file sizes) // Reef 0.1.14 fixed the large file hang/segfault issue let hash = sha.sha256_file(path) if str.length(hash) == 64 return str.to_lower(hash) end if // Fallback to sha256sum command if native fails return sha256_file_cmd(path) end sha256_file // Compute SHA256 using sha256sum command (for large files) fn sha256_file_cmd(path: string): string let output_file = "/tmp/coral_checksum_" + extract_basename(path) let cmd = "sha256sum \"" + path + "\" | cut -d' ' -f1 > \"" + output_file + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return "" end if process.process_wait(pid) // Read the hash from output file if not file.fileExists(output_file) return "" end if let hash = str.trim_ws(res.unwrap_or(file.readFile(output_file), "")) // Clean up let rm_pid = process.process_spawn_shell("rm -f \"" + output_file + "\"") if rm_pid > 0 process.process_wait(rm_pid) end if return hash end sha256_file_cmd // Extract basename from path for temp file naming fn extract_basename(path: string): string let last_slash = str.last_index_of_char(path, '/') if last_slash < 0 return path end if return str.substring(path, last_slash + 1, str.length(path) - last_slash - 1) end extract_basename // Compute SHA256 of a string using native crypto fn sha256_string(data: string): string let hash = sha.sha256(data) return str.to_lower(hash) end sha256_string // Verify that a file's SHA256 matches the expected value fn verify_checksum(path: string, expected: string): bool let computed = sha256_file(path) if str.length(computed) == 0 return false end if // Compare case-insensitively (normalize to lowercase) let computed_lower = str.to_lower(computed) let expected_lower = str.to_lower(expected) return computed_lower == expected_lower end verify_checksum // Parse and verify checksum in format "algo:hash" or just "hash" // Supported algorithms: sha256, sha256sum // If no algorithm prefix, assumes sha256 fn verify_checksum_with_algo(path: string, checksum_str: string): bool // Check for algorithm prefix let colon_idx = str.index_of_char(checksum_str, ':') if colon_idx < 0 // No prefix, assume SHA256 return verify_checksum(path, checksum_str) end if // Extract algorithm and hash let algo = str.to_lower(str.substring(checksum_str, 0, colon_idx)) let hash = str.substring(checksum_str, colon_idx + 1, str.length(checksum_str) - colon_idx - 1) // Verify based on algorithm if algo == "sha256" or algo == "sha256sum" return verify_checksum(path, hash) end if // Unsupported algorithm return false end verify_checksum_with_algo end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: color.reef Authors: Chris Tusa License: Description: Terminal color utilities for Coral ******************************************************************************/ module util.color import sys.args import sys.env import sys.platform.runtime as runtime export // Color functions - wrap text in ANSI color codes fn bold(s: string): string fn dim(s: string): string fn red(s: string): string fn green(s: string): string fn yellow(s: string): string fn blue(s: string): string fn cyan(s: string): string fn magenta(s: string): string // Formatted output helpers proc print_error(msg: string) proc print_success(msg: string) proc print_warning(msg: string) proc print_info(msg: string) proc print_action(msg: string) end export // ANSI escape code prefix: ESC[ (ASCII 27) // Reset code: ESC[0m // Helper to convert int to char (for ESC character) fn int_to_char(n: int): char unsafe let result: char = n return result end unsafe end int_to_char // Get ESC character (ASCII 27) as a single-character string fn get_esc(): string unsafe let result: string = runtime.reef_alloc_string_buffer(2) result[0] = int_to_char(27) return result end unsafe end get_esc // Check if color output should be disabled // Respects --no-color flag and NO_COLOR environment variable fn no_color(): bool if args.has_flag("no-color") return true end if if env.has_env("NO_COLOR") return true end if return false end no_color // Helper to wrap text with ANSI codes fn wrap_color(s: string, code: string): string if no_color() return s end if // ESC[ + code + m + text + ESC[0m let esc = get_esc() return esc + "[" + code + "m" + s + esc + "[0m" end wrap_color // Bold text (code 1) fn bold(s: string): string return wrap_color(s, "1") end bold // Dim text (code 2) fn dim(s: string): string return wrap_color(s, "2") end dim // Red text (code 31) fn red(s: string): string return wrap_color(s, "31") end red // Green text (code 32) fn green(s: string): string return wrap_color(s, "32") end green // Yellow text (code 33) fn yellow(s: string): string return wrap_color(s, "33") end yellow // Blue text (code 34) fn blue(s: string): string return wrap_color(s, "34") end blue // Magenta text (code 35) fn magenta(s: string): string return wrap_color(s, "35") end magenta // Cyan text (code 36) fn cyan(s: string): string return wrap_color(s, "36") end cyan // Print error message with red X prefix proc print_error(msg: string) if no_color() println("ERROR: " + msg) else println(red("error:") + " " + msg) end if end print_error // Print success message with green checkmark prefix proc print_success(msg: string) if no_color() println("OK: " + msg) else println(green("ok:") + " " + msg) end if end print_success // Print warning message with yellow prefix proc print_warning(msg: string) if no_color() println("WARNING: " + msg) else println(yellow("warning:") + " " + msg) end if end print_warning // Print info message with blue arrow prefix proc print_info(msg: string) if no_color() println("-> " + msg) else println(blue("->") + " " + msg) end if end print_info // Print action message with bold arrow prefix proc print_action(msg: string) if no_color() println("=> " + msg) else println(bold("=>") + " " + msg) end if end print_action end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: http.reef Authors: Chris Tusa License: Description: HTTP download utilities using native Reef net.http Requires Reef 0.1.16+ for progress bar support ******************************************************************************/ module util.http import net.http import net.extras import io.file import core.str import core.result as res import sys.process import util.color export // Download a file from URL to destination path fn download(url: string, dest: string): bool // Download with verbose progress output fn download_verbose(url: string, dest: string): bool // Fetch URL content as text fn fetch_text(url: string): string // Fetch URL content with timeout fn fetch_text_timeout(url: string, timeout_ms: int): string // Check if a URL is reachable (HEAD request) fn check_url(url: string): bool // Get response status code for a URL fn get_status(url: string): int // Configure proxy usage (call before downloads) proc enable_proxy(use_proxy: bool) end export // Module-level proxy setting mut proxy_enabled = false // Default timeout in milliseconds (30 seconds) fn default_timeout(): int return 30000 end default_timeout // Internal: download with progress bar (Style C: solid block) // Returns true on success, false on failure fn download_with_progress(url: string, dest: string): bool // Configure progress bar - Style C: solid block character extras.progress_bar_set_char("█") extras.progress_bar_set_width(30) extras.progress_bar_init(url) // Download with progress callback let cb = fn(p: http.HttpDownloadProgress): bool => extras.progress_bar(p) return res.is_ok(http.http_download_file_callback(url, dest, cb)) end download_with_progress // Check if URL will redirect (3xx status) - native HTTP download crashes on redirects fn url_will_redirect(url: string): bool let status = get_status(url) return status >= 300 and status < 400 end url_will_redirect // Download a file from URL to destination with progress bar // Uses native Reef http_download_file_callback() (requires Reef 0.1.16+) // Falls back to curl if native download fails or URL redirects fn download(url: string, dest: string): bool let filename = extract_filename(url) print(" " + filename + " ") // Check for redirects first - native HTTP crashes on 302s if not url_will_redirect(url) // Try native Reef download with progress bar if download_with_progress(url, dest) println("") // Newline after progress bar completes return true end if println("") // Newline after failed progress end if // Fallback to curl (handles redirects with -L flag) print(" (using curl) ... ") if download_with_curl(url, dest) println("done") return true end if println("failed") return false end download // Download with verbose progress output // Uses native Reef http_download_file_callback() with progress bar (requires Reef 0.1.16+) fn download_verbose(url: string, dest: string): bool let filename = extract_filename(url) println(" " + url) print(" " + filename + " ") // Check for redirects first - native HTTP crashes on 302s if not url_will_redirect(url) // Try native Reef download with progress bar if download_with_progress(url, dest) println("") // Newline after progress bar completes color.print_success("Download complete: " + filename) return true end if println("") // Newline after failed progress end if // Fallback to curl (handles redirects with -L flag) print(" (using curl) ... ") if download_with_curl(url, dest) println("done") color.print_success("Download complete: " + filename) return true end if println("failed") color.print_error("Download failed: " + url) return false end download_verbose // Fetch URL content as text fn fetch_text(url: string): string return fetch_text_timeout(url, default_timeout()) end fetch_text // Fetch URL content with timeout // Uses http_get_auto() which handles redirects and is safe for text/binary fn fetch_text_timeout(url: string, timeout_ms: int): string // Create request with timeout let req = http.http_request_new("GET", url) http.http_request_set_header(req, "User-Agent", "Coral/1.0") let resp_r = http.http_send_timeout(req, timeout_ms) // Check for errors (transport failure is now Err; HttpResponse no longer has an error field) if res.is_err(resp_r) return "" end if let resp = res.unwrap_ok(resp_r) // Check for success if not http.http_response_is_ok(resp) return "" end if return http.http_response_body(resp) end fetch_text_timeout // Check if a URL is reachable fn check_url(url: string): bool let req = http.http_request_new("HEAD", url) http.http_request_set_header(req, "User-Agent", "Coral/1.0") let resp_r = http.http_send_timeout(req, 10000) if res.is_err(resp_r) return false end if let resp = res.unwrap_ok(resp_r) // Accept success codes and redirects as "reachable" return resp.status_code >= 200 and resp.status_code < 400 end check_url // Get response status code for a URL fn get_status(url: string): int let req = http.http_request_new("HEAD", url) http.http_request_set_header(req, "User-Agent", "Coral/1.0") let resp_r = http.http_send_timeout(req, 10000) if res.is_err(resp_r) return 0 end if let resp = res.unwrap_ok(resp_r) return resp.status_code end get_status // Extract filename from URL fn extract_filename(url: string): string // Find last slash 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 proc enable_proxy(use_proxy: bool) proxy_enabled = use_proxy end enable_proxy // Download using curl command // Handles HTTP redirects, chunked encoding, and SSL/TLS properly fn download_with_curl(url: string, dest: string): bool // Use curl with: // -L: follow redirects (required for GitHub, etc.) // -f: fail silently on HTTP errors (returns non-zero exit code) // -s: silent mode (no progress meter) // -S: show errors even in silent mode // -o: output file mut proxy_flag = " --noproxy \"*\"" if proxy_enabled proxy_flag = "" end if let cmd = "curl -L -f -s -S" + proxy_flag + " -o \"" + dest + "\" \"" + url + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to spawn curl") return false end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Download failed: " + url) return false end if return true end download_with_curl end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: mtree.reef Authors: Chris Tusa License: Description: mtree-format manifest for package file inventory ******************************************************************************/ module util.mtree import io.file import io.dir import io.path import core.str import fs.stat import fs.link import crypto.sha256 as sha import util.checksum import text.stringbuilder as sb import core.result as res import core.option as opt export // Manifest entry representing a single file, directory, or symlink type ManifestEntry // Parse an mtree manifest string into an array of entries // Returns the number of entries parsed fn parse_manifest(content: string, entries: [ManifestEntry], max_entries: int): int // Generate an mtree manifest string from a package staging directory fn generate_manifest(pkg_dir: string): string // Accessors fn entry_path(entry: ManifestEntry): string fn entry_type(entry: ManifestEntry): string fn entry_mode(entry: ManifestEntry): int fn entry_uname(entry: ManifestEntry): string fn entry_gname(entry: ManifestEntry): string fn entry_sha256(entry: ManifestEntry): string fn entry_link(entry: ManifestEntry): string // Format a ManifestEntry as an mtree line fn format_entry(entry: ManifestEntry): string // Uid/gid name resolution helpers fn uid_to_name(uid: int): string fn gid_to_name(gid: int): string fn name_to_uid(name: string): int fn name_to_gid(name: string): int end export // ============================================================================ // Types // ============================================================================ type ManifestEntry = struct epath: string // Relative path (e.g., ./usr/bin/foo) etype: string // "file", "dir", or "link" mode: int // Permission bits as integer (e.g., 0755 = 493) uname: string // Owner user name gname: string // Owner group name sha256: string // SHA256 checksum (files only) link_target: string // Symlink target (links only) end ManifestEntry // ============================================================================ // Accessors // ============================================================================ fn entry_path(entry: ManifestEntry): string return entry.epath end entry_path fn entry_type(entry: ManifestEntry): string return entry.etype end entry_type fn entry_mode(entry: ManifestEntry): int return entry.mode end entry_mode fn entry_uname(entry: ManifestEntry): string return entry.uname end entry_uname fn entry_gname(entry: ManifestEntry): string return entry.gname end entry_gname fn entry_sha256(entry: ManifestEntry): string return entry.sha256 end entry_sha256 fn entry_link(entry: ManifestEntry): string return entry.link_target end entry_link // ============================================================================ // Parser // ============================================================================ // Parse an mtree manifest string into entries. // Format per line: ./path type=T mode=OOOO uname=U gname=G [sha256=H] [link=T] // Lines starting with # are comments. Empty lines are skipped. fn parse_manifest(content: string, entries: [ManifestEntry], max_entries: int): int if str.length(content) == 0 return 0 end if // Split into lines mut lines: [string] = new [string](max_entries + 64) let line_count = str.split(content, '\n', lines, max_entries + 64) mut count = 0 mut i = 0 while i < line_count and count < max_entries let line = str.trim_ws(lines[i]) let line_len = str.length(line) // Skip empty lines and comments if line_len == 0 i = i + 1 continue end if if line[0] == '#' i = i + 1 continue end if // Parse the line: first token is path, rest are key=value let entry = parse_entry_line(line) if str.length(entry.epath) > 0 entries[count] = entry count = count + 1 end if i = i + 1 end while return count end parse_manifest // Parse a single mtree line into a ManifestEntry fn parse_entry_line(line: string): ManifestEntry mut entry = ManifestEntry{ epath: "", etype: "", mode: 0, uname: "root", gname: "root", sha256: "", link_target: "" } // Split line by spaces mut tokens: [string] = new [string](16) let token_count = str.split(line, ' ', tokens, 16) if token_count == 0 return entry end if // First token is always the path entry.epath = tokens[0] // Remaining tokens are key=value pairs mut t = 1 while t < token_count let token = tokens[t] let eq_pos = str.index_of_char(token, '=') if eq_pos > 0 let key = str.substring(token, 0, eq_pos) let val = str.substring(token, eq_pos + 1, str.length(token) - eq_pos - 1) if str.equals(key, "type") entry.etype = val elif str.equals(key, "mode") entry.mode = octal_to_int(val) elif str.equals(key, "uname") entry.uname = val elif str.equals(key, "gname") entry.gname = val elif str.equals(key, "sha256") entry.sha256 = val elif str.equals(key, "link") entry.link_target = val end if end if t = t + 1 end while return entry end parse_entry_line // ============================================================================ // Generator // ============================================================================ // Generate an mtree manifest for all files in a package staging directory. // Walks the directory tree, collects entries, sorts them, and returns the // formatted manifest string. fn generate_manifest(pkg_dir: string): string // Collect entries by walking the directory tree mut entries: [ManifestEntry] = new [ManifestEntry](8192) let count = walk_directory(pkg_dir, pkg_dir, entries, 0, 8192) if count == 0 return "" end if // Sort entries by path (simple insertion sort — fine for package file counts) sort_entries(entries, count) // Build the output string using StringBuilder to avoid O(n²) heap // fragmentation from repeated string concatenation (Reef BUG-027) let builder = sb.sb_new() sb.sb_append(builder, "#mtree\n") mut i = 0 while i < count sb.sb_append(builder, format_entry(entries[i])) sb.sb_append(builder, "\n") i = i + 1 end while return sb.sb_build(builder) end generate_manifest // Recursively walk a directory, collecting ManifestEntry records. // base_dir is the package root (for computing relative paths). // current_dir is the directory being walked. // Returns total number of entries added (starting from offset). fn walk_directory(base_dir: string, current_dir: string, entries: [ManifestEntry], offset: int, max: int): int mut count = offset // Add the current directory itself (unless it's the base) if not str.equals(current_dir, base_dir) and count < max let rel = get_relative_path(base_dir, current_dir) if not is_metadata_path(rel) let mode = res.unwrap_or(stat.file_mode(current_dir), 0) let uid = res.unwrap_or(stat.file_uid(current_dir), 0) let gid = res.unwrap_or(stat.file_gid(current_dir), 0) entries[count] = ManifestEntry{ epath: "./" + rel, etype: "dir", mode: mode, uname: uid_to_name(uid), gname: gid_to_name(gid), sha256: "", link_target: "" } count = count + 1 end if end if // List directory contents let dir_entries = res.unwrap_or(dir.list_dir(current_dir), new [string](0)) let entry_count = dir_entries.length() mut i = 0 while i < entry_count and count < max let name = dir_entries[i] // Skip . and .. to prevent infinite recursion if str.equals(name, ".") or str.equals(name, "..") i = i + 1 continue end if let full_path = path.join_path(current_dir, name) let rel = get_relative_path(base_dir, full_path) // Skip metadata files and directories if is_metadata_path(rel) i = i + 1 continue end if // Check type: symlink first (symlinks to dirs would match is_directory) if stat.is_symlink(full_path) let target = res.unwrap_or(link.readlink(full_path), "") let uid = res.unwrap_or(stat.file_uid(current_dir), 0) let gid = res.unwrap_or(stat.file_gid(current_dir), 0) entries[count] = ManifestEntry{ epath: "./" + rel, etype: "link", mode: 0, uname: uid_to_name(uid), gname: gid_to_name(gid), sha256: "", link_target: target } count = count + 1 elif stat.is_directory(full_path) // Recurse into subdirectory count = walk_directory(base_dir, full_path, entries, count, max) elif stat.is_file(full_path) let mode = res.unwrap_or(stat.file_mode(full_path), 0) let uid = res.unwrap_or(stat.file_uid(full_path), 0) let gid = res.unwrap_or(stat.file_gid(full_path), 0) let hash = checksum.sha256_file(full_path) entries[count] = ManifestEntry{ epath: "./" + rel, etype: "file", mode: mode, uname: uid_to_name(uid), gname: gid_to_name(gid), sha256: hash, link_target: "" } count = count + 1 end if i = i + 1 end while return count end walk_directory // Check if a relative path is a package metadata file/directory fn is_metadata_path(rel: string): bool if str.equals(rel, ".PKGINFO") or str.equals(rel, ".MANIFEST") return true end if if str.equals(rel, ".FOOTPRINT") or str.equals(rel, ".CONFIG") return true end if if str.equals(rel, ".SCRIPTS") return true end if if str.starts_with(rel, ".SCRIPTS/") return true end if return false end is_metadata_path // Get relative path by stripping the base directory prefix fn get_relative_path(base: string, full: string): string let base_len = str.length(base) let full_len = str.length(full) if full_len <= base_len return "" end if // Skip the base prefix and the trailing / mut start = base_len if start < full_len and full[start] == '/' start = start + 1 end if return str.substring(full, start, full_len - start) end get_relative_path // ============================================================================ // Formatting // ============================================================================ // Format a ManifestEntry as a single mtree line fn format_entry(entry: ManifestEntry): string mut line = entry.epath + " type=" + entry.etype if str.equals(entry.etype, "file") or str.equals(entry.etype, "dir") line = line + " mode=" + int_to_octal(entry.mode) end if line = line + " uname=" + entry.uname line = line + " gname=" + entry.gname if str.equals(entry.etype, "file") and str.length(entry.sha256) > 0 line = line + " sha256=" + entry.sha256 end if if str.equals(entry.etype, "link") and str.length(entry.link_target) > 0 line = line + " link=" + entry.link_target end if return line end format_entry // ============================================================================ // Sorting // ============================================================================ // Insertion sort entries by path (lexicographic). // Package file counts are typically <1000, so insertion sort is adequate. proc sort_entries(entries: [ManifestEntry], count: int) mut i = 1 while i < count let key_entry = entries[i] let key_path = entries[i].epath mut j = i - 1 while j >= 0 and str.compare(entries[j].epath, key_path) > 0 entries[j + 1] = entries[j] j = j - 1 end while entries[j + 1] = key_entry i = i + 1 end while end sort_entries // ============================================================================ // Numeric Conversion Helpers // ============================================================================ // Convert an octal string (e.g., "0755", "4755") to an integer fn octal_to_int(s: string): int let slen = str.length(s) if slen == 0 return 0 end if mut result = 0 mut i = 0 while i < slen let c = s[i] if c >= '0' and c <= '7' result = result * 8 + (c - '0') end if i = i + 1 end while return result end octal_to_int // Convert an integer to a 4-digit octal string (e.g., 493 -> "0755") fn int_to_octal(n: int): string if n == 0 return "0000" end if mut value = n mut digits: [int] = new [int](16) mut count = 0 while value > 0 and count < 16 digits[count] = value - (value / 8 * 8) value = value / 8 count = count + 1 end while // Pad to 4 digits minimum while count < 4 digits[count] = 0 count = count + 1 end while // Build string in reverse (most significant first) mut result = "" mut i = count - 1 while i >= 0 result = result + digit_char(digits[i]) i = i - 1 end while return result end int_to_octal // Single digit to string character fn digit_char(d: int): string if d == 0 return "0" elif d == 1 return "1" elif d == 2 return "2" elif d == 3 return "3" elif d == 4 return "4" elif d == 5 return "5" elif d == 6 return "6" elif d == 7 return "7" end if return "0" end digit_char // Convert int to decimal string (local helper to avoid circular imports) 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 - (value / 10 * 10) result = str.concat(digit_char(digit), result) value = value / 10 end while if negative result = str.concat("-", result) end if return result end int_to_str // ============================================================================ // UID/GID Name Resolution // ============================================================================ // Resolve a numeric uid to a username. // Uses POSIX getpwuid() via Reef's fs.stat.uid_name() — zero heap allocation. fn uid_to_name(uid: int): string return opt.unwrap_or(stat.uid_name(uid), "") end uid_to_name // Resolve a numeric gid to a group name. // Uses POSIX getgrgid() via Reef's fs.stat.gid_name() — zero heap allocation. fn gid_to_name(gid: int): string return opt.unwrap_or(stat.gid_name(gid), "") end gid_to_name // Resolve a username to numeric uid by reading /etc/passwd. // Only used during verify/install (low call count), not the hot manifest walk. fn name_to_uid(name: string): int if str.equals(name, "root") return 0 end if let content = res.unwrap_or(file.readFile("/etc/passwd"), "") if str.length(content) == 0 return 0 end if mut lines: [string] = new [string](256) let line_count = str.split(content, '\n', lines, 256) mut i = 0 while i < line_count let line = lines[i] if str.length(line) > 0 mut fields: [string] = new [string](8) let field_count = str.split(line, ':', fields, 8) if field_count >= 3 and str.equals(fields[0], name) return str_to_int(fields[2]) end if end if i = i + 1 end while return 0 end name_to_uid // Resolve a group name to numeric gid by reading /etc/group. // Only used during verify/install (low call count), not the hot manifest walk. fn name_to_gid(name: string): int if str.equals(name, "root") return 0 end if let content = res.unwrap_or(file.readFile("/etc/group"), "") if str.length(content) == 0 return 0 end if mut lines: [string] = new [string](256) let line_count = str.split(content, '\n', lines, 256) mut i = 0 while i < line_count let line = lines[i] if str.length(line) > 0 mut fields: [string] = new [string](8) let field_count = str.split(line, ':', fields, 8) if field_count >= 3 and str.equals(fields[0], name) return str_to_int(fields[2]) end if end if i = i + 1 end while return 0 end name_to_gid // Simple string-to-int conversion fn str_to_int(s: string): int let slen = str.length(s) if slen == 0 return 0 end if mut result = 0 mut i = 0 while i < slen let c = s[i] if c >= '0' and c <= '9' result = result * 10 + (c - '0') end if i = i + 1 end while return result end str_to_int end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: priv.reef Authors: Chris Tusa License: Description: Privilege detection helpers ******************************************************************************/ module util.priv import io.file import core.result as res export // Check if we are running as root (UID 0) fn is_root(): bool end export // Check if we are running as root (UID 0) // Uses file test to check write permission on system directory fn is_root(): bool // Try to create a temp file in /var/lib/coral - only root can write there let test_file = "/var/lib/coral/.root_check" // writeFile now returns Result[bool, Error] (Reef 0.7.5); Ok means we wrote it if res.is_ok(file.writeFile(test_file, "1")) // Successfully wrote - we have root-level access // File will be overwritten next time, no need to delete return true end if return false end is_root end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: prompt.reef Authors: Chris Tusa License: Description: User prompt utilities for Coral ******************************************************************************/ module util.prompt import io.console import core.option as opt import core.str import core.convert import util.color import types export fn has_yes_flag(opts: types.GlobalOptions): bool fn confirm(message: string, opts: types.GlobalOptions): bool fn confirm_packages(action: string, packages: [string], count: int, opts: types.GlobalOptions): bool fn confirm_install(to_install: [string], install_count: int, to_upgrade: [string], upgrade_count: int, opts: types.GlobalOptions): bool fn confirm_remove(to_remove: [string], count: int, dependents: [string], dep_count: int, opts: types.GlobalOptions): bool fn confirm_build_plan(packages: [string], versions: [string], count: int, target: string, opts: types.GlobalOptions): bool end export // Check if -y or --yes flag is present fn has_yes_flag(opts: types.GlobalOptions): bool return opts.yes end has_yes_flag // Ask for confirmation, returns true if user confirms fn confirm(message: string, opts: types.GlobalOptions): bool // Skip if -y flag if has_yes_flag(opts) return true end if print(message + " [Y/n] ") let response = opt.unwrap_or(console.readLine(), "") let r = str.trim_ws(response) // Empty or Y/y means yes if str.length(r) == 0 return true end if if r == "Y" or r == "y" or r == "yes" or r == "Yes" or r == "YES" return true end if return false end confirm // Display list of packages and ask for confirmation fn confirm_packages(action: string, packages: [string], count: int, opts: types.GlobalOptions): bool if count == 0 return false end if println("") println(color.bold("Packages to " + action + ":")) println("") // Display packages in columns mut i = 0 while i < count println(" " + packages[i]) i = i + 1 end while println("") println(color.dim("Total: " + convert.to_string(count) + " package(s)")) println("") return confirm("Proceed?", opts) end confirm_packages // Display package info and ask for install confirmation fn confirm_install(to_install: [string], install_count: int, to_upgrade: [string], upgrade_count: int, opts: types.GlobalOptions): bool if install_count == 0 and upgrade_count == 0 return false end if println("") if install_count > 0 println(color.bold("New packages to install:")) mut i = 0 while i < install_count println(" " + color.green("+") + " " + to_install[i]) i = i + 1 end while println("") end if if upgrade_count > 0 println(color.bold("Packages to upgrade:")) mut i = 0 while i < upgrade_count println(" " + color.cyan("^") + " " + to_upgrade[i]) i = i + 1 end while println("") end if let total = install_count + upgrade_count println(color.dim("Total: " + convert.to_string(total) + " package(s)")) println("") return confirm("Proceed with installation?", opts) end confirm_install // Display packages to remove and ask for confirmation fn confirm_remove(to_remove: [string], count: int, dependents: [string], dep_count: int, opts: types.GlobalOptions): bool if count == 0 return false end if println("") println(color.bold("Packages to remove:")) mut i = 0 while i < count println(" " + color.red("-") + " " + to_remove[i]) i = i + 1 end while if dep_count > 0 println("") println(color.yellow("The following packages depend on these and will also be removed:")) i = 0 while i < dep_count println(" " + color.yellow("-") + " " + dependents[i]) i = i + 1 end while end if println("") let total = count + dep_count println(color.dim("Total: " + convert.to_string(total) + " package(s)")) println("") return confirm("Proceed with removal?", opts) end confirm_remove // Display build plan and ask for confirmation fn confirm_build_plan(packages: [string], versions: [string], count: int, target: string, opts: types.GlobalOptions): bool if count == 0 return true end if println("") println(color.bold("Build plan for '" + target + "':")) println("") println("Packages to build from source:") mut i = 0 while i < count mut line = " " + color.green("+") + " " + packages[i] if str.length(versions[i]) > 0 line = line + " " + color.dim(versions[i]) end if println(line) i = i + 1 end while println("") println(color.dim("Total: " + convert.to_string(count) + " package(s) to build")) println("") return confirm("Proceed with build?", opts) end confirm_build_plan end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: version.reef Authors: Chris Tusa License: Description: Semantic version parsing and comparison utilities ******************************************************************************/ module util.version import core.str export // Version constraint parsing type DepConstraint // Compare two version strings semantically // Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2 fn compare_versions(v1: string, v2: string): int // Check if v1 is newer than v2 fn is_newer(v1: string, v2: string): bool // Parse a dependency string with optional version constraint // e.g., "libfoo>=1.0" -> { name: "libfoo", op: ">=", version: "1.0" } fn parse_dep_constraint(dep: string): DepConstraint // Check if an installed version satisfies a version constraint fn check_version_constraint(installed_version: string, op: string, required_version: string): bool // Accessor functions for DepConstraint fn constraint_name(c: DepConstraint): string fn constraint_op(c: DepConstraint): string fn constraint_version(c: DepConstraint): string fn has_constraint(c: DepConstraint): bool // Extract version from package filename // e.g., "reef-0.1.10-1.pkg.tar.xz" -> "0.1.10" fn extract_version_from_filename(filename: string, pkg_name: string): string // Extract release number from package filename // e.g., "reef-0.1.10-1.pkg.tar.xz" -> 1 fn extract_release_from_filename(filename: string): int end export // Parsed dependency constraint type DepConstraint = struct name: string op: string version: string end DepConstraint // Compare two version strings semantically // Handles versions like "0.1.10" vs "0.1.7" correctly (0.1.10 > 0.1.7) fn compare_versions(v1: string, v2: string): int // Parse version components mut v1_parts: [int] = new [int](10) mut v2_parts: [int] = new [int](10) let v1_count = parse_version(v1, v1_parts, 10) let v2_count = parse_version(v2, v2_parts, 10) // Compare each component let max_parts = max_int(v1_count, v2_count) mut i = 0 while i < max_parts let p1 = get_part(v1_parts, v1_count, i) let p2 = get_part(v2_parts, v2_count, i) if p1 > p2 return 1 elif p1 < p2 return -1 end if i = i + 1 end while return 0 end compare_versions // Check if v1 is newer than v2 fn is_newer(v1: string, v2: string): bool return compare_versions(v1, v2) > 0 end is_newer // Parse version string into integer components // "0.1.10" -> [0, 1, 10] fn parse_version(version: string, parts: [int], max_parts: int): int mut count = 0 mut current = 0 mut in_number = false mut i = 0 while i < str.length(version) and count < max_parts let c = str.char_at(version, i) if is_digit(c) current = current * 10 + char_to_digit(c) in_number = true elif c == '.' or c == '-' if in_number parts[count] = current count = count + 1 current = 0 in_number = false end if else // Skip non-numeric characters (like 'alpha', 'beta', 'rc') if in_number parts[count] = current count = count + 1 current = 0 in_number = false end if end if i = i + 1 end while // Don't forget the last number if in_number and count < max_parts parts[count] = current count = count + 1 end if return count end parse_version // Extract version from package filename // e.g., "reef-0.1.10-1.pkg.tar.xz" with pkg_name "reef" -> "0.1.10" fn extract_version_from_filename(filename: string, pkg_name: string): string // Remove package name prefix and dash let prefix_len = str.length(pkg_name) + 1 if str.length(filename) <= prefix_len return "" end if // Get the part after "pkgname-" let rest = str.substring(filename, prefix_len, str.length(filename) - prefix_len) // Find the last dash before .pkg.tar.xz (that's the release separator) // e.g., "0.1.10-1.pkg.tar.xz" -> version is "0.1.10" let suffix = ".pkg.tar.xz" let suffix_len = str.length(suffix) if not str.ends_with(rest, suffix) return "" end if // Remove suffix: "0.1.10-1.pkg.tar.xz" -> "0.1.10-1" let ver_rel = str.substring(rest, 0, str.length(rest) - suffix_len) // Find last dash (release separator) let last_dash = str.last_index_of_char(ver_rel, '-') if last_dash < 0 return ver_rel end if // Return version part: "0.1.10-1" -> "0.1.10" return str.substring(ver_rel, 0, last_dash) end extract_version_from_filename // Extract release number from package filename // e.g., "reef-0.1.10-1.pkg.tar.xz" -> 1 fn extract_release_from_filename(filename: string): int let suffix = ".pkg.tar.xz" if not str.ends_with(filename, suffix) return 1 end if // Remove suffix let without_suffix = str.substring(filename, 0, str.length(filename) - str.length(suffix)) // Find last dash let last_dash = str.last_index_of_char(without_suffix, '-') if last_dash < 0 return 1 end if // Extract release number let release_str = str.substring(without_suffix, last_dash + 1, str.length(without_suffix) - last_dash - 1) return parse_int(release_str) end extract_release_from_filename // Helper: check if character is a digit fn is_digit(c: char): bool return c >= '0' and c <= '9' end is_digit // Helper: convert digit character to integer fn char_to_digit(c: char): int return c - '0' end char_to_digit // Helper: get part at index, or 0 if out of bounds fn get_part(parts: [int], count: int, idx: int): int if idx < count return parts[idx] end if return 0 end get_part // Helper: max of two integers fn max_int(a: int, b: int): int if a > b return a end if return b end max_int // Helper: parse string to int fn parse_int(s: string): int mut result = 0 mut i = 0 while i < str.length(s) let c = str.char_at(s, i) if is_digit(c) result = result * 10 + char_to_digit(c) else break end if i = i + 1 end while return result end parse_int // Parse a dependency string into name, operator, and version // Handles: "libfoo", "libfoo>=1.0", "libfoo<=2.0", "libfoo>1.0", "libfoo<2.0", "libfoo=1.0" fn parse_dep_constraint(dep: string): DepConstraint let len = str.length(dep) mut i = 0 while i < len let c = str.char_at(dep, i) if c == '>' or c == '<' or c == '=' let name = str.substring(dep, 0, i) // Check for two-char operators (>=, <=) if i + 1 < len let next = str.char_at(dep, i + 1) if next == '=' let op = str.substring(dep, i, 2) let ver = str.substring(dep, i + 2, len - i - 2) return DepConstraint{ name: name, op: op, version: ver } end if end if // Single char operator (>, <, =) let op = str.substring(dep, i, 1) let ver = str.substring(dep, i + 1, len - i - 1) return DepConstraint{ name: name, op: op, version: ver } end if i = i + 1 end while // No constraint return DepConstraint{ name: dep, op: "", version: "" } end parse_dep_constraint // Check if an installed version satisfies a version constraint fn check_version_constraint(installed_version: string, op: string, required_version: string): bool if str.length(op) == 0 return true end if let cmp = compare_versions(installed_version, required_version) if str.equals(op, ">=") return cmp >= 0 elif str.equals(op, "<=") return cmp <= 0 elif str.equals(op, ">") return cmp > 0 elif str.equals(op, "<") return cmp < 0 elif str.equals(op, "=") return cmp == 0 end if return true end check_version_constraint // Accessor functions fn constraint_name(c: DepConstraint): string return c.name end constraint_name fn constraint_op(c: DepConstraint): string return c.op end constraint_op fn constraint_version(c: DepConstraint): string return c.version end constraint_version fn has_constraint(c: DepConstraint): bool return str.length(c.op) > 0 end has_constraint end module