/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (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 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 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 = 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 = 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 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 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 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 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 = 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 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 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