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