Initial mercurial commit for Isurus
Author:
Chris Tusa <chris.tusa@leafscale.com>
Date:
Jun 09, 2026 17:28
Changeset:
d0a96d73e9e1ec091d9837ce8b30f767464e7c06
Branch:
default
Tags:
v0.4.2
Changed files:
Diff
diff -r 000000000000 -r d0a96d73e9e1 .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,32 @@ +syntax: glob + +# Build output +build/ +*.o +*.a +coral + +# Generated C code (use --keep-c to debug) +*.c.reef.c + +# Editor files +*~ +*.swp +*.swo +.vscode/ +.idea/ +.claude/ + +# OS files +.DS_Store +Thumbs.db + +# Release +releases/ + +# Local project files +resume +bugs/ + +# Claude superpowers scratch (plans, etc.) +docs/superpowers/ diff -r 000000000000 -r d0a96d73e9e1 .isurus-ci.yml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.isurus-ci.yml Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,34 @@ +container-registry: + - url: registry.sharkos.one + secret: SHARKOS_REGISTRY_CREDS + +when: + event: [push, pull_request, tag] # build on pushes, PRs, and tags; + # package + release are tag-gated below. + +steps: + - name: build + docker: { image: registry.sharkos.one/reef/reef-builder:latest } + commands: + - reefc doctor + - reefc build + - ./build/coral --version + timeout: 30m + + - name: package + docker: { image: registry.sharkos.one/reef/reef-builder:latest } + when: { event: [tag] } + commands: + - ./scripts/make-release.sh "${CI_TAG#v}" + - ls -la releases/ + timeout: 30m + +release: + tag: "$CI_TAG" + title: "Coral $CI_TAG" + assets: + - releases/*.tar.xz + - releases/*.sha256 + secret: ISURUS_RELEASE_TOKEN + when: + event: [tag] diff -r 000000000000 -r d0a96d73e9e1 .remember/.gitignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.remember/.gitignore Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,1 @@ +* diff -r 000000000000 -r d0a96d73e9e1 .remember/logs/memory-2026-06-09.log --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.remember/logs/memory-2026-06-09.log Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,67 @@ +16:14:20 [hook] session-start: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:16:19 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:16:22 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:16:41 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:16:48 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:23:36 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:24:03 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:24:28 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:24:28 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:24:28 [extract] session 53cf3104-6785-4f13-8a98-9048e7c078b4 +16:24:28 [extract] 17 exchanges (2 human) +16:24:28 [extract] 2 human msgs < 3, skip +16:24:36 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:24:36 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:24:36 [cooldown] 8s < 120s, skip +16:24:53 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:24:53 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:24:53 [cooldown] 25s < 120s, skip +16:45:19 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:45:19 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:45:19 [extract] session 53cf3104-6785-4f13-8a98-9048e7c078b4 +16:45:19 [extract] 23 exchanges (4 human) +16:45:19 [haiku] calling (branch: main) +16:45:43 [tokens] tokens: 9+17847cache→2861out ($0.031154) +16:45:43 [write] appended: ## 21:45 | main +16:45:43 [write] position → 69 +16:45:43 [ndc] now.md → today-2026-06-09.md +16:45:43 [ndc] running (PID 2647879) +16:45:58 [ndc] tokens: 9+17847cache→1609out ($0.020934) +16:45:58 [ndc] 218→162b (-25%) +16:47:22 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:47:31 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:47:37 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:51:38 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:54:48 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:54:55 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:55:05 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:55:36 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:56:41 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:56:49 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:56:49 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:56:49 [extract] session 53cf3104-6785-4f13-8a98-9048e7c078b4 +16:56:49 [extract] 18 exchanges (1 human) +16:56:49 [extract] 1 human msgs < 3, skip +16:56:54 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:56:54 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:56:54 [cooldown] 5s < 120s, skip +16:56:58 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:56:58 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:56:58 [cooldown] 9s < 120s, skip +16:57:05 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:57:05 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:57:05 [cooldown] 16s < 120s, skip +16:57:08 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:57:08 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:57:08 [cooldown] 19s < 120s, skip +16:57:51 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:57:52 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +16:57:52 [cooldown] 63s < 120s, skip +17:25:30 [hook] post-tool: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +17:25:30 [hook] save-session: PROJECT_DIR=/home/ctusa/repos/zygaena-project/coral PIPELINE_DIR=/home/ctusa/.claude/plugins/cache/claude-plugins-official/remember/0.7.3 PYTHON=python3 REMEMBER_DIR=/home/ctusa/repos/zygaena-project/coral/.remember +17:25:30 [extract] session 53cf3104-6785-4f13-8a98-9048e7c078b4 +17:25:30 [extract] 33 exchanges (3 human) +17:25:30 [haiku] calling (branch: main) +17:25:48 [tokens] tokens: 9+17847cache→1799out ($0.025559) +17:25:48 [write] appended: ## 22:25 | main +17:25:48 [write] position → 160 diff -r 000000000000 -r d0a96d73e9e1 .remember/now.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.remember/now.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,3 @@ + +## 22:25 | main +Excluded superpowers/ from release tarball (make-release.sh + .gitignore); verified Coral 0.4.2 build against Reef 0.6.4; archived 120-commit hist to docs/git-history.{md,csv}. \ No newline at end of file diff -r 000000000000 -r d0a96d73e9e1 .remember/tmp/last-ndc.ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.remember/tmp/last-ndc.ts Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,1 @@ +1781041543 diff -r 000000000000 -r d0a96d73e9e1 .remember/tmp/last-save-ts --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.remember/tmp/last-save-ts Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,1 @@ +1781043930 diff -r 000000000000 -r d0a96d73e9e1 .remember/tmp/last-save.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.remember/tmp/last-save.json Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,1 @@ +{"session": "53cf3104-6785-4f13-8a98-9048e7c078b4", "line": 160} \ No newline at end of file diff -r 000000000000 -r d0a96d73e9e1 .remember/tmp/save-session.pid --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.remember/tmp/save-session.pid Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,1 @@ +2654712 diff -r 000000000000 -r d0a96d73e9e1 .remember/today-2026-06-09.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.remember/today-2026-06-09.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,2 @@ +## 21:45 | main +Migr Coral CI Woodpecker→Isurus: .isurus-ci.yml (reef-builder+native release); archived 120 commits MD+CSV; Reef 0.5.20→0.6.4 bump (hardcoded) \ No newline at end of file diff -r 000000000000 -r d0a96d73e9e1 README.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,119 @@ +# Coral Package Manager + +Coral is the package manager for Zygaena, written in [Reef](https://github.com/reef-lang/reef). + +## Building + +### Prerequisites + +- Reef compiler (`reefc`) version 0.1.6+ +- On illumos: source the development environment first + +### Quick Start + +```bash +# On illumos - source the environment +source /opt/zygaena-env.sh + +# Build coral +cd ~/repos/zygaena/coral +reefc build -l socket -l nsl + +# The binary is created at build/coral +./build/coral --help +``` + +### Build Options + +```bash +# Basic build (Linux/macOS) +reefc build + +# illumos/Solaris (requires socket libraries) +reefc build -l socket -l nsl + +# Type-check only (no compilation) +reefc build --check + +# Verbose output +reefc build -v + +# Keep generated C code for debugging +reefc build --keep-c +``` + +### Installation + +After building, install to your PATH: + +```bash +# Option 1: Copy to /usr/local/bin (requires root) +sudo cp build/coral /usr/local/bin/ + +# Option 2: Copy to user bin +mkdir -p ~/bin +cp build/coral ~/bin/ +export PATH="$HOME/bin:$PATH" +``` + +## Usage + +```bash +coral --help # Show help +coral build <port> # Build a package from port +coral install <pkg> # Install package(s) +coral remove <pkg> # Remove package(s) +coral upgrade [pkg] # Upgrade package(s) +coral info <pkg> # Show package info +coral list # List installed packages +coral files <pkg> # List files owned by package +coral owner <file> # Find which package owns a file +coral search <term> # Search for packages +coral depends <pkg> # Show dependency tree +coral sync # Update ports tree +coral outdated # Show outdated packages +coral db status # Show database status +coral clean status # Show build artifact status +coral ports new cat/name # Scaffold a new port +coral ports cache status # Show ports index status +``` + +## Project Structure + +``` +coral/ +├── reef.toml # Project manifest +├── src/ +│ ├── main.reef # Entry point, CLI dispatch +│ ├── types.reef # Shared type definitions +│ ├── commands/ # Command implementations +│ │ ├── build.reef +│ │ ├── install.reef +│ │ ├── remove.reef +│ │ └── ... +│ ├── core/ # Core functionality +│ │ ├── port.reef # Port file parsing +│ │ ├── portindex.reef # Ports index (MsgPack cache) +│ │ ├── package.reef # Package operations +│ │ ├── database.reef # Package database +│ │ ├── resolver.reef # Dependency resolution +│ │ └── ... +│ └── util/ # Utilities +│ ├── http.reef # HTTP downloads +│ ├── archive.reef # Archive handling +│ └── ... +├── test/ # Tests +├── build/ # Build output (generated) +└── docs/ # Documentation +``` + +## Configuration + +Coral uses TOML for configuration. See `docs/` for details on: +- Port file format (`package.toml`) +- Repository configuration +- Build scripts (`build.reef`) + +## License + +BSD-2-Clause diff -r 000000000000 -r d0a96d73e9e1 docs/GUIDE.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/GUIDE.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,1223 @@ +# Coral Package Manager Guide + +Coral is the package manager for Zygaena, a ports-based operating system built on illumos. This guide covers all aspects of using Coral, from basic package management to creating repositories and signing packages. + +## Table of Contents + +1. [Installation and Setup](#installation-and-setup) +2. [Basic Usage](#basic-usage) +3. [Creating and Building Packages](#creating-and-building-packages) +4. [Package Groups](#package-groups) +5. [Alternative Dependencies](#alternative-dependencies) +6. [Creating Repositories](#creating-repositories) +7. [Package Signing](#package-signing) +8. [Database Management](#database-management) +9. [Port Development Tools](#port-development-tools) +10. [Configuration Reference](#configuration-reference) + +--- + +## Installation and Setup + +### Prerequisites + +Coral requires: +- Zygaena or illumos-based system +- Reef compiler 0.5.0 or later +- Root privileges for package installation +- GCC or compatible C compiler (for building from source) + +### Directory Structure + +Coral uses the following directories: + +| Path | Purpose | +|------|---------| +| `/usr/zports/` | Ports tree (package build definitions) | +| `/usr/zports/groups/` | Package group definitions | +| `/etc/coral/coral.conf` | Main configuration file | +| `/etc/coral/repos.d/` | Repository configurations | +| `/etc/coral/keys/` | Signing keys | +| `/var/lib/coral/installed/` | Installed package database (source of truth) | +| `/var/lib/coral/db/` | Package database indexes (MsgPack) | +| `/var/lib/coral/packages/` | Binary package cache | +| `/var/lib/coral/sources/` | Source archive cache | +| `/var/lib/coral/repos/` | Repository metadata cache | +| `/var/lib/coral/trusted-keys/` | Trusted public keys | +| `/var/tmp/coral/work/` | Build work directory | +| `/var/tmp/coral/pkg/` | Package staging directory | + +### Initial Setup + +1. Create required directories: +```bash +sudo mkdir -p /usr/zports/{core,base,devel,desktop,extra} +sudo mkdir -p /usr/zports/groups +sudo mkdir -p /etc/coral/repos.d +sudo mkdir -p /etc/coral/keys +sudo mkdir -p /var/lib/coral/{installed,packages,sources,repos,trusted-keys,db} +sudo mkdir -p /var/tmp/coral/{work,pkg} +``` + +2. Create initial configuration: +```bash +sudo tee /etc/coral/coral.conf << 'EOF' +[general] +arch = "x86_64" +jobs = 0 +confirm = true +color = true + +[build] +logging = true +quiet = false + +[security] +require_signed_repos = false +require_signed_packages = false +EOF +``` + +3. Initialize the package database: +```bash +sudo coral db init +``` + +--- + +## Basic Usage + +### Installing Packages + +Install a single package: +```bash +coral install vim +``` + +Install multiple packages: +```bash +coral install vim nano htop +``` + +Install from a local package file: +```bash +coral install /path/to/package-1.0.0-1.pkg.tar.xz +``` + +Install without confirmation: +```bash +coral install -y vim +``` + +Force reinstall (also overrides conflict checks): +```bash +coral install --force vim +``` + +Dry run (show what would be done without making changes): +```bash +coral install -n vim +``` + +When a binary package is not found in any repository and `fallback_to_source = true` (default), Coral will attempt to build the package from the ports tree automatically. + +### Removing Packages + +Remove a package: +```bash +coral remove vim +``` + +Remove multiple packages: +```bash +coral remove vim nano htop +``` + +### Upgrading Packages + +Upgrade all packages: +```bash +coral upgrade +``` + +Upgrade specific packages: +```bash +coral upgrade vim nano +``` + +Check for available updates: +```bash +coral outdated +``` + +### Querying Packages + +List installed packages: +```bash +coral list +``` + +Show package information: +```bash +coral info vim +``` + +List files owned by a package: +```bash +coral files vim +``` + +Find which package owns a file: +```bash +coral owner /usr/bin/vim +``` + +Search for packages: +```bash +coral search editor +``` + +Show dependency tree: +```bash +coral depends vim +``` + +Verify package integrity: +```bash +coral verify vim +``` + +### Building Packages + +Build a package from source: +```bash +coral build vim +``` + +The built package is stored in `/var/lib/coral/packages/`. + +### Syncing Ports + +Update the ports tree: +```bash +coral sync +``` + +--- + +## Creating and Building Packages + +### Port Structure + +A port consists of a directory under `/usr/zports/<category>/<name>/` containing: + +``` +/usr/zports/base/mypackage/ +├── package.toml # Package metadata +├── build.sh # Build script (zsh) +├── fix-build.patch # Optional patch files +└── hammerhead-solaris-compat.patch # (referenced in package.toml [patches]) +``` + +### package.toml + +The `package.toml` file defines package metadata: + +```toml +[package] +name = "mypackage" +version = "1.2.3" +release = 1 +description = "A useful package" +url = "https://example.com/mypackage" +license = "MIT" +maintainer = "Your Name <you@example.com>" +arch = "x86_64" + +# Optional: alternatives this package provides +alternatives = ["editor", "pager"] + +# Optional: packages this conflicts with (install aborts if any are installed) +conflicts = ["otherpackage"] + +[dependencies] +# Packages required at runtime +runtime = ["libfoo", "libbar>=1.2"] + +# Packages required only for building +build = ["gcc", "gmake"] + +### Per-Package Path Overrides + +Packages can override the default installation prefix and sysconfdir. This is useful for packages that install to `/opt`, `/srv`, or other non-standard locations: + +```toml +[paths] +prefix = "/opt/myapp" +sysconfdir = "/opt/myapp/etc" +``` + +These override the system defaults from `coral.conf` and are exported as `$PREFIX` and `$SYSCONFDIR` to the build script. If omitted, the system-wide values are used. + +### Source Files + +Source files can be specified using TOML table arrays (`[[source]]`), which supports +mirror URLs for automatic failover: + +```toml +[[source]] +file = "mypackage-1.2.3.tar.gz" +urls = [ + "https://example.com/mypackage-1.2.3.tar.gz", + "https://mirror.example.org/mypackage-1.2.3.tar.gz" +] +checksum = "abc123def456..." + +[[source]] +file = "extra-data.tar.gz" +urls = ["https://example.com/extra-data.tar.gz"] +checksum = "789xyz..." +extract = false # Don't extract this archive +``` + +Each `[[source]]` entry supports: + +| Field | Required | Description | +|-------|----------|-------------| +| `file` | Yes | Output filename to save as | +| `urls` | Yes | Array of mirror URLs (tried in order until success) | +| `checksum` | Yes | SHA256 checksum, or `"SKIP"` to skip verification | +| `extract` | No | Whether to extract the archive (default: `true`) | + +**Mirror Failover**: When downloading, Coral tries each URL in the `urls` array +in order. If a download fails or checksum verification fails, the next mirror +is tried automatically. + +#### Local Source Files + +For packages where sources aren't available on public HTTP servers (private repos, +bundled tarballs), you can place source files directly in the port directory and +reference them by filename: + +```toml +[[source]] +file = "reef-lang-0.1.10.tar.gz" +urls = ["reef-lang-0.1.10.tar.gz"] # Local file in port directory +checksum = "abc123..." +``` + +URLs that don't start with `http://` or `https://` are treated as local paths: +- **Relative paths** (no leading `/`) are resolved relative to the port directory +- **Absolute paths** are used as-is + +Local files are copied to the sources cache (`/var/lib/coral/sources/`) for +consistency with downloaded files. + +You can also mix local and remote sources for fallback: +```toml +[[source]] +file = "mypackage-1.0.tar.gz" +urls = [ + "mypackage-1.0.tar.gz", # Try local first + "https://example.com/mypackage-1.0.tar.gz" # Fall back to remote +] +checksum = "abc123..." +``` + +#### Legacy Format (Deprecated) + +The older `[sources]` format is still supported for backward compatibility: + +```toml +[sources] +urls = [ + "https://example.com/mypackage-1.2.3.tar.gz" +] +checksums = [ + "abc123def456..." +] +``` + +**Note**: The legacy format does not support mirrors or the `extract` flag. + +### Patches + +Coral can apply patches to source code before running `build.sh`. Patch files +are placed in the port directory alongside `package.toml` and declared in a +`[patches]` section: + +```toml +[patches] +files = ["hammerhead-solaris-compat.patch", "fix-build.patch"] +strip = 0 +``` + +| Field | Required | Default | Description | +|-------|----------|---------|-------------| +| `files` | Yes | — | Array of patch filenames, applied in order | +| `strip` | No | `0` | Strip level passed to `patch -p<N>` | + +Patches are applied from within the extracted source directory using `patch -p<strip>`. +The `strip` value controls how many leading path components are removed from filenames +in the diff header: +- `strip = 0` — paths used as-is (e.g., diff against `configure` / `configure.orig`) +- `strip = 1` — strips `a/` and `b/` prefixes (standard `git diff` output) + +**Example**: Adding Hammerhead OS recognition to ncurses' configure script: +```diff +--- configure.orig ++++ configure +@@ -7066,7 +7066,7 @@ + test "$cf_cv_shlib_version" = auto && cf_cv_shlib_version=rel + ;; +- (solaris2*) ++ (solaris2*|hammerhead*) + # tested with SunOS 5.5.1 (solaris 2.5.1) and gcc 2.7.2 +``` + +[build] +# Enable parallel builds +parallel = true + +# Number of parallel jobs (0 = auto) +jobs = 0 +``` + +### Build Script (build.sh) + +The build script is executed in the source directory. Environment variables available: + +| Variable | Description | +|----------|-------------| +| `$DESTDIR` | Installation staging directory | +| `$PREFIX` | Installation prefix (default: `/usr/local`, configurable in `coral.conf` or per-package) | +| `$SYSCONFDIR` | System configuration directory (default: `/etc`, configurable in `coral.conf` or per-package) | +| `$PKG_ROOT` | Alternate root directory (empty when installing to `/`) | +| `$SRCDIR` | Extracted source directory | +| `$PKGDIR` | Package staging directory (alias for `$DESTDIR`) | +| `$CFLAGS` | C compiler flags | +| `$CXXFLAGS` | C++ compiler flags | +| `$LDFLAGS` | Linker flags (see [Linker Flags](#linker-flags-and-library-resolution) below) | +| `$PKG_CONFIG_PATH` | pkg-config search path (set to `$PREFIX/lib/pkgconfig`) | +| `$MAKEFLAGS` | Make flags (includes `-j` for parallel builds) | +| `$JOBS` | Number of parallel jobs | +| `$PKG_NAME` | Package name | +| `$PKG_VERSION` | Package version | +| `$BUILD_TRIPLE` | Canonical build triplet (e.g., `x86_64-zygaea-hammerhead`) | +| `$MAKE` | Make command (`gmake`) | +| `$TAR` | Tar command (`gtar`) | + +Example build.sh: +```bash +#!/usr/bin/zsh +set -e + +# Configure using $PREFIX and $SYSCONFDIR (don't hardcode paths) +./configure --prefix=$PREFIX --sysconfdir=$SYSCONFDIR + +# Build +$MAKE ${MAKEFLAGS} + +# Install to staging directory +$MAKE DESTDIR="${DESTDIR}" install +``` + +Build scripts are executed with `zsh` (the Zygaena standard shell). + +#### Linker Flags and Library Resolution + +Coral sets `LDFLAGS="-R$PREFIX/lib"` globally. This embeds an RPATH in every +built binary so the runtime linker can find shared libraries installed under +`$PREFIX` (e.g., `/usr/local/lib`) without requiring system-wide `crle` +configuration. + +**Why only `-R` and not `-L`?** + +Coral intentionally does **not** set `-L$PREFIX/lib` globally. The `-L` flag +changes the **build-time** linker search order, which could cause Coral-installed +libraries to shadow base OS libraries in `/lib` and `/usr/lib`. For example, if +both the base OS and a Coral port ship `libz.so`, a global `-L/usr/local/lib` +would cause all ports to link against the Coral version instead of the base +version, potentially introducing ABI mismatches or version conflicts. + +The `-R` flag only affects **runtime** resolution and does not change build-time +search order, making it safe to set globally. + +**When a build script needs `-L`:** + +If a port's `configure` script cannot find a dependency's library on its own, +the build script should add `-L` explicitly: + +```bash +export LDFLAGS="-L$PREFIX/lib $LDFLAGS" +``` + +This appends to Coral's RPATH setting rather than replacing it. Only add `-L` +when `configure` fails to locate the library — most autotools-based projects +find libraries via `pkg-config` (which Coral also configures via +`$PKG_CONFIG_PATH`) or their own `--with-*` flags. + +#### pkg-config + +Coral sets `PKG_CONFIG_PATH` to `$PREFIX/lib/pkgconfig` so that `configure` +scripts using `pkg-config` can find `.pc` files from Coral-installed packages. +This is the preferred mechanism for library discovery — most modern autotools +and meson projects use `pkg-config` and will automatically pick up the correct +`-I`, `-L`, and `-l` flags from `.pc` files. + +### Install Scripts + +Packages can include scripts that run during installation, removal, and upgrade. Create a `.SCRIPTS/` directory in the package: + +``` +.SCRIPTS/ +├── pre-install.sh # Runs before files are installed ($VERSION) +├── post-install.sh # Runs after files are installed ($VERSION) +├── pre-remove.sh # Runs before files are removed ($VERSION) +├── post-remove.sh # Runs after files are removed ($VERSION) +├── pre-upgrade.sh # Runs before upgrade ($OLD_VERSION $NEW_VERSION) +└── post-upgrade.sh # Runs after upgrade ($OLD_VERSION $NEW_VERSION) +``` + +Scripts are executed with `zsh`. Version arguments are passed as positional parameters (`$1`, `$2`). + +**Hook behavior:** +- Pre-hooks (`pre-install`, `pre-remove`, `pre-upgrade`) abort the operation if they exit non-zero +- Post-hooks (`post-install`, `post-remove`, `post-upgrade`) log a warning but don't abort on failure + +**Upgrade flow:** When installing a newer version of an already-installed package, Coral runs: `pre-upgrade` → remove old files → install new files → `post-upgrade`. + +Example post-install.sh: +```bash +#!/usr/bin/zsh +# Update font cache after installing fonts +fc-cache -f +``` + +Example pre-upgrade.sh: +```bash +#!/usr/bin/zsh +OLD_VERSION=$1 +NEW_VERSION=$2 +# Stop service before upgrade +svcadm disable myservice +``` + +### Config Files + +To mark files as configuration files (preserved during upgrades), create a `.CONFIG` file listing them: + +``` +/etc/mypackage/config.conf +/etc/mypackage/settings.toml +``` + +Config files use checksum-based protection during upgrades: +- **Fresh install**: config file is installed normally +- **Upgrade, user has not modified config**: new version overwrites cleanly +- **Upgrade, user has modified config**: new version is installed as `.pkg-new`, user's file is preserved + +On removal, user-modified config files are preserved (not deleted). + +### Building the Package + +```bash +coral build mypackage +``` + +This will: +1. Resolve dependencies recursively (build uninstalled deps from source first) +2. Download sources (with automatic mirror failover) +3. Verify checksums +4. Extract sources +5. Apply patches +6. Run build script +7. Compress man pages (if `compress_man = true`) +8. Create package archive +9. Clean up build directories + +If a dependency is not installed, Coral builds it from source automatically using a topological sort of the full dependency tree. Circular dependencies are detected and reported. + +#### Build Command Options + +| Option | Description | +|--------|-------------| +| `--noclean` / `-n` | Skip cleanup of work and staging directories after build | +| `--clean` / `-c` | Force cleanup even if config says to keep directories | +| `--quiet` / `-q` | Hide subprocess output (still logged to file) | +| `--verbose` / `-v` | Show all output (overrides quiet mode) | +| `-y` | Skip confirmation prompts | +| `--force` | Force rebuild even if package exists | + +Example: Keep build artifacts for debugging: +```bash +coral build --noclean mypackage +``` + +#### Build Cleanup Behavior + +After a successful build, Coral cleans up: +- **Work directory** (`/var/tmp/coral/work/<package>`): Extracted sources and build files +- **Staging directory** (`/var/tmp/coral/pkg/<package>`): Package staging area + +Cleanup behavior can be controlled via: +1. Command-line flags (`--noclean`, `--clean`) +2. Configuration file settings (`clean_work`, `clean_pkg`) + +Downloaded source archives are preserved by default in `/var/lib/coral/sources/`. + +#### Build Logging + +By default, Coral logs all build output to a file in the work directory: + +``` +/var/tmp/coral/work/<package>/coral-build-<package>-<timestamp>.log +``` + +The log captures: +- All Coral status messages (download, extract, patch, build, package) +- Full subprocess output (build script, patch commands) +- Commands executed + +Log files are cleaned up with the work directory unless `--noclean` is specified. + +Use `--quiet` to hide subprocess output from the terminal while still logging everything: +```bash +coral build --quiet vim # Clean output, full log file +coral build --verbose vim # Show all output (overrides quiet) +``` + +--- + +## Package Groups + +Groups allow installing multiple related packages with a single command. + +### Creating a Group + +Create a TOML file in `/usr/zports/groups/`: + +```toml +# /usr/zports/groups/base-devel.toml + +[group] +name = "base-devel" +description = "Base development tools" +packages = [ + "gcc", + "gmake", + "binutils", + "autoconf", + "automake", + "libtool", + "pkg-config" +] +``` + +### Installing a Group + +Use the `@` prefix: +```bash +coral install @base-devel +``` + +This expands to all packages in the group and installs them with dependency resolution. + +### Listing Groups + +Groups are stored as `.toml` files in `/usr/zports/groups/`. List them with: +```bash +ls /usr/zports/groups/ +``` + +--- + +## Alternative Dependencies + +Alternatives allow a package to depend on one of several options. This is useful when multiple packages can satisfy a dependency. + +### Version Constraints + +Dependencies can include version constraints using operators: + +```toml +[dependencies] +runtime = [ + "libfoo>=1.0", # Version 1.0 or newer + "libbar<=2.0", # Version 2.0 or older + "libqux=3.1.0", # Exactly version 3.1.0 + "libbaz>0.9", # Strictly newer than 0.9 + "libold<2.0", # Strictly older than 2.0 +] +``` + +If an installed package doesn't satisfy the version constraint, Coral includes it in the install order for upgrade. + +### Specifying Alternatives + +In `package.toml`, use the colon syntax: + +```toml +[dependencies] +runtime = [ + "libfoo", + "editor:vim,nano,emacs", # Any of these editors + "database:postgresql,mysql" # Any of these databases +] +``` + +Format: `base_name:option1,option2,option3` + +### How Alternatives Work + +1. During dependency resolution, Coral checks if any alternative is installed +2. If one is installed, the dependency is satisfied +3. If none are installed, the first option is automatically selected +4. Users can override by pre-installing their preferred option + +### Providing Alternatives + +A package can declare that it provides certain alternatives: + +```toml +[package] +name = "vim" +alternatives = ["editor", "vi"] +``` + +When another package depends on "editor", vim will satisfy that dependency. + +--- + +## Creating Repositories + +Repositories distribute pre-built binary packages. + +### Repository Structure + +``` +/path/to/repo/ +├── repo.toml # Repository manifest +├── repo.toml.sig # Manifest signature (optional) +└── packages/ # Binary packages + ├── vim-9.0-1.x86_64.pkg.tar.xz + ├── vim-9.0-1.x86_64.pkg.tar.xz.sig + └── ... +``` + +### Initializing a Repository + +```bash +coral repo init /path/to/repo +``` + +This creates the directory structure and an empty manifest. + +### Adding Packages + +Copy built packages to the `packages/` directory: +```bash +cp /var/lib/coral/packages/*.pkg.tar.xz /path/to/repo/packages/ +``` + +### Rebuilding the Manifest + +After adding packages, rebuild the manifest: +```bash +coral repo rebuild /path/to/repo +``` + +This scans all packages and generates `repo.toml`: + +```toml +[repository] +name = "myrepo" +description = "My Package Repository" +url = "https://repo.example.com" + +[packages.vim] +version = "9.0" +release = 1 +description = "Vi IMproved" +license = "Vim" +category = "base" +file = "vim-9.0-1.x86_64.pkg.tar.xz" +sha256 = "abc123..." +size = 12345678 +compressed = 2345678 +depends = ["ncurses", "libfoo"] + +[packages.nano] +# ... more packages +``` + +### Signing the Repository + +```bash +coral repo sign /path/to/repo +``` + +This signs `repo.toml` with your private key, creating `repo.toml.sig`. + +### Configuring Clients + +Create a repository configuration file on client systems: + +```toml +# /etc/coral/repos.d/myrepo.repo + +[repository] +name = "myrepo" +url = "https://repo.example.com" +priority = 100 +enabled = true +signed = true +keyid = "myrepo-key" +``` + +### Refreshing Repository Data + +```bash +coral repo refresh +``` + +### Mirroring + +Export a repository for offline use: +```bash +coral repo export /path/to/repo /path/to/output +``` + +--- + +## Package Signing + +Coral uses Ed25519 signatures for package and repository verification. + +### Key Management + +#### Initialize Keyring + +```bash +coral key init +``` + +Creates `/etc/coral/keys/` directory. + +#### Generate a Signing Key + +```bash +coral key generate mykey +``` + +Creates: +- `/etc/coral/keys/mykey.key` (private key - keep secure!) +- `/etc/coral/keys/mykey.pub` (public key - distribute this) + +#### List Keys + +```bash +coral key list +``` + +#### Export Public Key + +```bash +coral key export mykey > mykey.pub +``` + +#### Import a Public Key + +```bash +coral key import mykey.pub repokey +``` + +#### Trust a Key + +```bash +coral key trust repokey +``` + +Adds the key to `/var/lib/coral/trusted-keys/`. + +#### Revoke Trust + +```bash +coral key revoke repokey +``` + +### Signing Packages + +When building a package, sign it: +```bash +coral build mypackage +coral key sign /var/lib/coral/packages/mypackage-1.0-1.pkg.tar.xz mykey +``` + +### Signing Repositories + +```bash +coral repo sign /path/to/repo +``` + +Uses the first available private key, or specify one: +```bash +coral repo sign /path/to/repo --key mykey +``` + +### Verification + +Enable mandatory verification in `/etc/coral/coral.conf`: + +```toml +[security] +require_signed_repos = true +require_signed_packages = true +``` + +When enabled: +- Repository manifests must have valid signatures +- Packages must have valid signatures +- Signatures must be from trusted keys + +### Verification Process + +1. Download package and signature +2. Find matching public key in trusted keys +3. Verify signature using Ed25519 +4. Reject if verification fails (when required) + +--- + +## Database Management + +Coral maintains MsgPack-based indexes for fast package and file lookups. The source of truth is always the installed package database (`/var/lib/coral/installed/`); indexes can be safely rebuilt at any time. + +### Database Files + +| File | Purpose | +|------|---------| +| `/var/lib/coral/db/packages.db` | Indexed package metadata (name, version, description) | +| `/var/lib/coral/db/files.db` | File-to-package mapping for O(log N) ownership lookups | +| `/var/lib/coral/db/ports.db` | Ports index for fast search/list without parsing TOML from disk | + +### Initializing the Database + +After initial system setup, initialize the database: +```bash +coral db init +``` + +This creates the index files based on currently installed packages. + +### Checking Database Status + +View the current database status: +```bash +coral db status +``` + +Output shows: +- Index location +- Number of packages indexed +- Number of files indexed +- Number of ports indexed + +### Rebuilding Indexes + +If indexes become corrupted or out of sync (e.g., after manual edits to `/var/lib/coral/installed/`), rebuild them: +```bash +coral db rebuild +``` + +This scans all installed packages and regenerates both indexes from scratch. + +### How Indexes Work + +**packages.db**: Stores package metadata in MsgPack format, encoded as Base64. Contains name, version, release, description, install date, and file count for each installed package. + +**files.db**: Maps file paths to owning packages. Files are sorted alphabetically, enabling O(log N) binary search lookups. This dramatically speeds up `coral owner` queries. + +### Automatic Index Updates + +Indexes are automatically updated when: +- A package is installed (`coral install`) +- A package is removed (`coral remove`) +- The ports tree is synced (`coral sync` — rebuilds ports.db) + +You do not need to manually rebuild indexes during normal operation. + +### Performance + +| Operation | Without Index | With Index | +|-----------|---------------|------------| +| `coral list` (100 packages) | ~500ms | ~10ms | +| `coral owner /usr/bin/vim` (50K files) | ~2000ms | <10ms | + +### Using with Alternate Root + +All database commands support the `--root` flag for chroot environments: +```bash +coral --root /mnt/system db init +coral --root /mnt/system db status +coral --root /mnt/system db rebuild +``` + +--- + +## Port Development Tools + +The `coral ports` command provides tooling for port developers. + +### Scaffolding a New Port + +Create a new port with boilerplate files: +```bash +coral ports new hammerhead/my-port +``` + +This creates: +- `{ports_dir}/hammerhead/my-port/package.toml` — package metadata template +- `{ports_dir}/hammerhead/my-port/build.sh` — build script stub + +The generated `package.toml` includes all standard sections with placeholder values. Edit it to fill in your package's details, then test with: +```bash +coral build hammerhead/my-port +``` + +### Ports Index Cache + +The ports index (`ports.db`) caches metadata from all ports so that commands like `coral search`, `coral list --available`, and `coral depends --reverse` don't need to parse every `package.toml` from disk. + +Check index status: +```bash +coral ports cache status +``` + +Manually rebuild the index: +```bash +coral ports cache rebuild +``` + +The index is automatically rebuilt by `coral sync`, `coral db init`, and `coral db rebuild`. + +### Dynamic Categories + +Categories are discovered dynamically by scanning the ports directory. Any subdirectory under the ports tree (excluding infrastructure directories like `pkgs/` and `groups/`) is treated as a category. Adding a new category directory is all that's needed — no code changes required. + +--- + +## Configuration Reference + +### /etc/coral/coral.conf + +```toml +[general] +# Target architecture +arch = "x86_64" + +# Parallel build jobs (0 = auto-detect CPU count) +jobs = 0 + +# Ask for confirmation before operations +confirm = true + +# Enable colored output +color = true + +[paths] +# Installation prefix (exported as $PREFIX to build scripts) +prefix = "/usr/local" + +# System configuration directory (exported as $SYSCONFDIR to build scripts) +sysconfdir = "/etc" + +# Ports tree location +ports = "/usr/ports" + +# Built packages cache +packages = "/var/lib/coral/packages" + +# Downloaded sources cache +sources = "/var/lib/coral/sources" + +# Installed package database +database = "/var/lib/coral/installed" + +# Build work directory +work = "/var/tmp/coral/work" + +# Package staging directory +pkg = "/var/tmp/coral/pkg" + +[build] +# Default C compiler flags +cflags = "-O2 -pipe" + +# Default C++ compiler flags +cxxflags = "-O2 -pipe" + +# Default make flags +makeflags = "" + +# Strip binaries +strip = true + +# Compress man pages +compress_man = true + +# Fall back to building from source if binary not available +fallback_to_source = true + +# Log build output to file (in work directory) +logging = true + +# Hide subprocess output from terminal (still logged) +quiet = false + +[network] +# Download timeout in seconds +timeout = 30 + +# Number of retry attempts +retries = 3 + +# Use proxy (reads http_proxy/https_proxy env vars) +use_proxy = false + +[sync] +# Auto-refresh repository metadata +auto_refresh = false + +[cache] +# Keep downloaded packages after install +keep_packages = true + +# Keep downloaded sources after build +keep_sources = true + +# Clean work directory after successful build (part of [build] section) +clean_work = true + +# Clean package staging directory after successful build +clean_pkg = true + +[security] +# Require signed repository manifests +require_signed_repos = false + +# Require signed packages +require_signed_packages = false +``` + +### Repository Configuration + +Repository files in `/etc/coral/repos.d/*.repo`: + +```toml +[repository] +# Repository name (used in messages) +name = "zygaena-main" + +# Base URL for the repository +url = "https://repo.zygaena.org/main" + +# Priority (lower = higher priority) +priority = 100 + +# Enable/disable this repository +enabled = true + +# Repository is signed +signed = true + +# Key ID for signature verification +keyid = "zygaena-main" +``` + +--- + +## Troubleshooting + +### Common Issues + +**"Package not found"** +- Check if the port exists: `ls /usr/zports/*/packagename` +- Refresh repository metadata: `coral repo refresh` +- Check repository configuration in `/etc/coral/repos.d/` + +**"Signature verification failed"** +- Import the repository's public key: `coral key import` +- Trust the key: `coral key trust keyname` +- Check key hasn't been revoked + +**"Dependency resolution failed"** +- Check for circular dependencies +- Verify all dependencies are available +- Try installing dependencies manually + +**"Build failed"** +- Check build.sh for errors +- Verify build dependencies are installed +- Check logs in `/var/tmp/coral/work/` + +### Debug Mode + +Enable verbose output: +```bash +coral -v install vim +``` + +Dry run (show what would be done): +```bash +coral -n install vim +``` + +--- + +## Quick Reference + +| Command | Description | +|---------|-------------| +| `coral install pkg` | Install package | +| `coral install @group` | Install package group | +| `coral install -n pkg` | Dry run (show what would happen) | +| `coral remove pkg` | Remove package | +| `coral upgrade` | Upgrade all packages | +| `coral list` | List installed packages | +| `coral info pkg` | Show package info | +| `coral files pkg` | List package files | +| `coral owner /path` | Find file owner | +| `coral search term` | Search packages | +| `coral depends pkg` | Show dependencies | +| `coral verify pkg` | Verify package integrity | +| `coral build pkg` | Build from port (no install) | +| `coral build --install pkg` | Build and install | +| `coral build --noclean pkg` | Build, keep work directories | +| `coral build --quiet pkg` | Build with quiet output | +| `coral sync` | Update ports tree | +| `coral outdated` | Show available updates | +| `coral repo list` | List repositories | +| `coral repo refresh` | Refresh repo metadata | +| `coral key list` | List signing keys | +| `coral key generate name` | Generate new key | +| `coral clean status` | Show build artifact status | +| `coral clean all` | Clean all build artifacts | +| `coral clean --force all` | Clean all, override cache protection | +| `coral db init` | Initialize database indexes | +| `coral db status` | Show database status | +| `coral db rebuild` | Rebuild indexes from installed packages | +| `coral ports new cat/name` | Scaffold a new port | +| `coral ports cache status` | Show ports index status | +| `coral ports cache rebuild` | Rebuild ports index | + +--- + +## License + +Coral is part of the Zygaena project. + +Copyright (C) 2025 Leafscale, LLC +Licensed under BSD-2-Clause + +https://www.leafscale.com diff -r 000000000000 -r d0a96d73e9e1 docs/MSGPACK_DATABASE_PLAN.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/MSGPACK_DATABASE_PLAN.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,309 @@ +# Implementation Plan: MsgPack Package Database Indexes + +**Date:** 2026-01-29 +**Status:** Planned +**Target:** Coral v0.2 + +## Overview + +Implement the MsgPack-based package database indexes that were planned as the key feature of Coral v0.2. Currently `src/core/pkgdb.reef` is entirely stubs that fall back to slow file scanning. This causes: + +- `coral list` - O(N) TOML file reads +- `coral owner` - O(N*M) full scan of all packages' file lists (CRITICAL bottleneck) +- `coral info` - O(1) but requires TOML parsing + +## Target Performance + +| Command | Current | With MsgPack | +|---------|---------|--------------| +| `coral list` (100 pkgs) | ~500ms | ~10ms | +| `coral owner /usr/bin/vim` (50K files) | ~2000ms | ~1ms | +| `coral info vim` | ~50ms | ~5ms | + +--- + +## File Format Specifications + +### packages.db (MsgPack + Base64) + +``` +Structure: +{ + "version": 1, + "generated": "2026-01-29T...", + "package_count": N, + "packages": [ + { "name": "vim", "version": "9.1", "release": 1, + "description": "...", "installed_date": "...", "file_count": 423 }, + ... + ] +} +``` + +### files.db (MsgPack + Base64) + +``` +Structure: +{ + "version": 1, + "generated": "2026-01-29T...", + "file_count": M, + "files": { + "usr/bin/vim": "vim", + "usr/lib/libz.so": "zlib", + ... + } +} +``` + +**Note:** Base64 encoding required because Reef's `file.readFile()`/`file.writeFile()` work with strings. + +--- + +## Implementation Phases + +### Phase 1: Foundation (in pkgdb.reef) + +Add new types: + +```reef +type PackagesIndex = struct + version: int + generated: string + package_count: int + entries: [PkgIndexEntry] + loaded: bool +end PackagesIndex + +type FilesIndex = struct + version: int + generated: string + file_count: int + file_paths: [string] // Sorted for binary search + owners: [string] // Parallel array - owner at same index + loaded: bool +end FilesIndex +``` + +Add helper functions: +- `base64_encode(buf: [int], length: int): string` +- `base64_decode(encoded: string, buf: [int]): int` +- `estimate_packages_buffer_size(pkg_count: int): int` +- `estimate_files_buffer_size(file_count: int): int` + +### Phase 2: Serialization + +Implement MsgPack serialization: + +```reef +fn serialize_packages_db(root: string): [int] + // 1. Get list of installed packages from database module + // 2. Allocate buffer based on count estimate + // 3. Pack map header (4 keys: version, generated, package_count, packages) + // 4. Pack array of package entries + // Returns packed buffer +end serialize_packages_db + +fn serialize_files_db(root: string): [int] + // 1. Get all packages, collect all files with owners + // 2. Sort file paths for binary search later + // 3. Allocate buffer + // 4. Pack map header + files map + // Returns packed buffer +end serialize_files_db +``` + +Implement `rebuild_indexes_rooted()`: +```reef +fn rebuild_indexes_rooted(root: string): bool + // 1. Serialize packages.db, base64 encode, write file + // 2. Serialize files.db, base64 encode, write file + // 3. Return success +end rebuild_indexes_rooted +``` + +### Phase 3: Deserialization + +Implement loading functions: + +```reef +fn load_packages_index(root: string): PackagesIndex + // 1. Read base64 string from packages.db file + // 2. Decode to binary buffer + // 3. Unpack MsgPack structure + // 4. Return populated PackagesIndex +end load_packages_index + +fn load_files_index(root: string): FilesIndex + // 1. Read base64 string from files.db file + // 2. Decode to binary buffer + // 3. Unpack MsgPack map into parallel arrays + // 4. Return populated FilesIndex (already sorted from serialize) +end load_files_index + +fn binary_search_file(index: FilesIndex, path: string): string + // O(log N) binary search in sorted file_paths array + // Return owner or empty string +end binary_search_file +``` + +### Phase 4: Update Public API + +Replace stub implementations: + +```reef +fn list_packages(names: [string], versions: [string], max_count: int): int + let index = load_packages_index("") + if not index.loaded + return fallback_list_packages(names, versions, max_count) + end if + // Copy from index to arrays +end list_packages + +fn find_file_owner(file_path: string): string + let index = load_files_index("") + if not index.loaded + return database.find_owner(file_path) // Fallback + end if + return binary_search_file(index, normalize_path(file_path)) +end find_file_owner + +fn get_indexed_package(name: string): PkgIndexEntry + let index = load_packages_index("") + if not index.loaded + return fallback_get_indexed_package(name) + end if + // Search index for package by name +end get_indexed_package +``` + +Add rooted variants: +- `find_file_owner_rooted(file_path: string, root: string): string` + +### Phase 5: Command Integration + +**install.reef** - After registering package: +```reef +// Rebuild indexes after install +pkgdb.rebuild_indexes_rooted(root_path) +``` + +**remove.reef** - After unregistering package: +```reef +// Rebuild indexes after remove +pkgdb.rebuild_indexes_rooted(root_path) +``` + +**owner.reef** - Use indexed lookup: +```reef +// Change from database.find_owner() to pkgdb.find_file_owner() +``` + +--- + +## Files to Modify + +| File | Changes | +|------|---------| +| `src/core/pkgdb.reef` | Main implementation - add types, serialization, deserialization, binary search | +| `src/commands/install.reef` | Call `rebuild_indexes_rooted()` after registration | +| `src/commands/remove.reef` | Call `rebuild_indexes_rooted()` after unregistration | +| `src/commands/owner.reef` | Use `pkgdb.find_file_owner()` instead of `database.find_owner()` | + +--- + +## Key Implementation Details + +### Base64 Encoding (required for file I/O) + +Reef's `encoding.base64` module exists in reef-stdlib. Use: +```reef +import encoding.base64 +let encoded = base64.base64_encode(binary_string) +let decoded = base64.base64_decode(encoded) +``` + +**Issue:** base64 module works with strings, but MsgPack produces `[int]` buffer. Need conversion helpers. + +### MsgPack Usage (from reef-stdlib/encoding/msgpack.reef) + +```reef +import encoding.msgpack + +// Packing +let buf = msgpack.msgpack_alloc_buffer(size) +let pos = 0 +pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos) +pos = pos + msgpack.msgpack_pack_string("version", buf, pos) +pos = pos + msgpack.msgpack_pack_int(1, buf, pos) +// ... etc + +// Unpacking +let map_len = msgpack.msgpack_unpack_map_len(buf, offset) +offset = offset + msgpack.msgpack_value_size(buf, offset) +// ... iterate through map entries +``` + +### Buffer to String Conversion + +Need helper to convert `[int]` buffer to string for base64: +```reef +fn buffer_to_string(buf: [int], length: int): string + mut result = "" + mut i = 0 + while i < length + result = result + chr(buf[i]) + i = i + 1 + end while + return result +end buffer_to_string +``` + +--- + +## Verification + +### Test Commands + +```bash +# Initialize database (should create packages.db and files.db) +sudo coral --root /tmp/coral-chroot db init +ls -la /tmp/coral-chroot/var/lib/coral/db/ + +# Verify files exist +file /tmp/coral-chroot/var/lib/coral/db/packages.db +file /tmp/coral-chroot/var/lib/coral/db/files.db + +# Build and install a test package +coral build core/zlib +sudo coral --root /tmp/coral-chroot install zlib + +# Verify index updated +sudo coral --root /tmp/coral-chroot db status + +# Test fast list +time sudo coral --root /tmp/coral-chroot list + +# Test fast owner lookup +time sudo coral --root /tmp/coral-chroot owner /usr/lib/libz.so + +# Test rebuild from scratch +sudo rm /tmp/coral-chroot/var/lib/coral/db/*.db +sudo coral --root /tmp/coral-chroot db rebuild +``` + +### Performance Validation + +- `coral list` with 100 packages should complete in <50ms +- `coral owner` should complete in <10ms regardless of file count +- Index files should be created after install/remove + +--- + +## Design Decisions + +1. **Full Rebuild on Changes:** Each install/remove triggers a complete index rebuild. Simpler, always consistent, acceptable performance for typical package counts (<500 packages). + +2. **Trust the Index:** Commands trust the index is correct. If indexes get out of sync (e.g., manual edits to installed/ directory), user runs `coral db rebuild` to fix. + +3. **No Caching:** Reload index from disk on each command invocation. Keeps implementation simple and avoids stale memory issues. diff -r 000000000000 -r d0a96d73e9e1 docs/PACKAGE_FORMAT_V2_DESIGN.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/PACKAGE_FORMAT_V2_DESIGN.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,408 @@ +# Design: Package Format v2 + +**Date:** 2026-03-07 +**Status:** Draft +**Target:** Coral v0.3 +**Authors:** Zygaena Team + +## Overview + +Redesign of the Coral package internal format to eliminate the rsync dependency, +add a rich file manifest for install/remove/verify operations, embed dependency +metadata in packages, fix config file handling, and clean up the scripting model. + +These changes are possible now because Reef v0.5.0 provides native filesystem +operations (`fs.stat`, `fs.perm`, `fs.link`, `fs.ops`) that replace the need for +shell-outs to rsync, cp, chmod, chown, and ln. + +No package has shipped to production yet, so there is no backward-compatibility +constraint. + +--- + +## 1. Package Archive Structure + +The archive format remains `.pkg.tar.xz` (gtar + xz). The internal layout changes: + +``` +name#version-release.pkg.tar.xz + .PKGINFO # TOML — package metadata + dependencies + .MANIFEST # mtree-format file inventory (replaces .FOOTPRINT) + .CONFIG # list of config-protected paths (unchanged) + .SCRIPTS/ # optional hook scripts (zsh only) + pre-install.sh + post-install.sh + pre-remove.sh + post-remove.sh + pre-upgrade.sh + post-upgrade.sh + usr/ + etc/ + ... # payload files at their install paths +``` + +### Changes from v1 + +| Item | v1 | v2 | +|------|----|----| +| File manifest | `.FOOTPRINT` (flat path list) | `.MANIFEST` (mtree with metadata) | +| Dependencies | not in package | `[dependencies]` in `.PKGINFO` | +| Script hooks | 4 (install/remove) | 6 (+ upgrade pair) | +| Script runners | `.reef`, `.sh`, bare | `.sh` (zsh) and bare only | +| Install method | `rsync -a` | `fs.ops` driven by manifest | + +--- + +## 2. .PKGINFO — Package Metadata + +TOML format. Adds a `[dependencies]` section with optional version constraints. + +```toml +[package] +name = "openssl" +version = "3.2.1" +release = 1 +description = "TLS/SSL and crypto library" +url = "https://www.openssl.org" +license = "Apache-2.0" +maintainer = "admin@zygaena.org" +arch = "x86_64" + +[dependencies] +runtime = ["zlib", "libc"] +build = ["perl", "makedepend"] +``` + +### Version constraints + +Runtime and build dependencies support optional version operators: + +```toml +runtime = ["zlib>=1.3", "libc"] +``` + +Supported operators: `>=`, `<=`, `>`, `<`, `=`. No operator means "any version". + +Most dependencies will be unversioned. Version constraints are added only when a +specific ABI or feature is required (e.g., a soname bump, a new API entry point). +Exact version pinning (`=`) is discouraged — it creates rebuild cascades. + +The port's `package.toml` remains the authoritative source. The `[dependencies]` +section in `.PKGINFO` is a copy embedded at build time so standalone packages are +self-describing without the ports tree. + +### Generation + +`generate_pkginfo()` in `build.reef` writes the `[dependencies]` section by +reading `ctx.port.deps.runtime` and `ctx.port.deps.build`. + +--- + +## 3. .MANIFEST — mtree File Inventory + +Replaces `.FOOTPRINT`. Uses mtree-style format: one line per filesystem entry, +path first, then space-separated `key=value` attributes. + +### Format + +``` +#mtree +./usr/bin type=dir mode=0755 uname=root gname=root +./usr/bin/passwd type=file mode=4755 uname=root gname=sys sha256=a1b2c3d4e5f6... +./usr/bin/zlib-flate type=file mode=0755 uname=root gname=root sha256=d4e5f6a7... +./usr/include type=dir mode=0755 uname=root gname=root +./usr/include/zconf.h type=file mode=0644 uname=root gname=root sha256=e5f6a7b8... +./usr/lib type=dir mode=0755 uname=root gname=root +./usr/lib/libz.a type=file mode=0644 uname=root gname=root sha256=9c0d1e2f... +./usr/lib/libz.so type=link uname=root gname=root link=libz.so.1 +./usr/lib/libz.so.1 type=link uname=root gname=root link=libz.so.1.3.1 +./usr/lib/libz.so.1.3.1 type=file mode=0755 uname=root gname=root sha256=f0a1b2... +``` + +### Fields + +| Key | Required | Values | Notes | +|-----|----------|--------|-------| +| `type` | always | `file`, `dir`, `link` | First attribute after path | +| `mode` | files, dirs | 4-digit octal | Includes setuid/setgid/sticky | +| `uname` | always | user name | String, not numeric uid | +| `gname` | always | group name | String, not numeric gid | +| `sha256` | files only | 64-char hex | For install verification and `coral verify` | +| `link` | symlinks only | target path | Relative or absolute link target | + +### Sort order + +Entries are sorted lexicographically by path. This ensures: +- Directories appear before their contents (install order: top-down) +- Removal in reverse order processes files before their parent directories + +### Extensibility + +Unknown `key=value` pairs are ignored by the parser. Future attributes (e.g., +`xattr=...`, `acl=...`, `flags=...`) can be added without breaking existing +Coral versions. However, ACLs and extended attributes are better handled in +post-install scripts where policy decisions belong. + +### Metadata exclusion + +`.PKGINFO`, `.MANIFEST`, `.CONFIG`, and `.SCRIPTS/` are not listed in the +manifest. They are package metadata, not payload files. + +### Generation + +`generate_manifest()` in `build.reef` replaces `generate_footprint()`. It walks +the staging directory using `fs.stat` and `fs.perm` to read type, mode, owner, +and group for each entry. File checksums are computed with `checksum.sha256_file()`. +Symlink targets are read with `fs.link.readlink()`. + +### Parser module + +New module: `util/mtree.reef` + +``` +export + type ManifestEntry + fn parse_manifest(content: string, entries: [ManifestEntry], max: int): int + fn generate_manifest(pkg_dir: string): string + fn entry_type(entry: ManifestEntry): string + fn entry_path(entry: ManifestEntry): string + fn entry_mode(entry: ManifestEntry): int + fn entry_sha256(entry: ManifestEntry): string + fn entry_link(entry: ManifestEntry): string + fn entry_uname(entry: ManifestEntry): string + fn entry_gname(entry: ManifestEntry): string +end export +``` + +--- + +## 4. Install Method — Replacing rsync + +### Current flow (v1) + +``` +extract .pkg.tar.xz to temp dir + → find symlinks, remove conflicts in root + → rsync -a --exclude=metadata temp_dir/ root/ +``` + +### New flow (v2) + +``` +extract .pkg.tar.xz to temp dir + → parse .MANIFEST into entry array + → for each entry (in manifest order): + if type=dir: + fs.ops.create_dir_all(root + path) -- create if missing + fs.perm.chmod(root + path, mode) -- set permissions + fs.perm.chown(root + path, uname, gname) -- set ownership + if type=file: + check if config-protected (see section 6) + if config file with user modifications → install as .pkg + else: + remove any existing conflicting path (file, symlink, or dir) + fs.ops.copy_file_preserve(src, root + path) + fs.perm.chmod(root + path, mode) + fs.perm.chown(root + path, uname, gname) + verify sha256 matches manifest + if type=link: + remove any existing conflicting path + fs.link.symlink(link_target, root + path) +``` + +### Advantages over rsync + +- **Zero external dependencies** — no rsync required during bootstrap +- **Manifest-driven** — every file placed is accounted for, no silent extras +- **Per-file verification** — sha256 checked on install, not just blindly copied +- **Config protection integrated** — checked inline during install, not as a + separate pass +- **Explicit permissions** — set from manifest, not inherited from build host + filesystem quirks + +### Error handling + +If any file operation fails, installation stops and returns an error. Partial +installs are recorded so `coral remove` can clean up. This is safer than rsync's +`exit code 23` (partial transfer accepted). + +--- + +## 5. Scripting Model + +### Hooks + +Six hooks, all executed with `zsh`: + +| Hook | Arguments | When | Abort on failure | +|------|-----------|------|------------------| +| `pre-install.sh` | `$VERSION` | Before files installed (fresh install) | Yes | +| `post-install.sh` | `$VERSION` | After files installed (fresh install) | No* | +| `pre-remove.sh` | `$VERSION` | Before files removed (full remove) | Yes | +| `post-remove.sh` | `$VERSION` | After files removed (full remove) | No* | +| `pre-upgrade.sh` | `$OLD_VERSION $NEW_VERSION` | Before files replaced (upgrade) | Yes | +| `post-upgrade.sh` | `$OLD_VERSION $NEW_VERSION` | After new files installed (upgrade) | No* | + +*Post-scripts log failures as warnings but do not roll back, since the file +operation has already completed. + +### Execution + +All scripts are invoked as: + +``` +zsh "/path/to/.SCRIPTS/hook-name.sh" [args...] +``` + +The `.reef` script type is removed. Requiring the Reef compiler at install time +creates a bootstrap dependency that cannot be satisfied when building the base OS. + +Bare executables (no extension) are still supported — Coral executes them +directly, relying on a shebang line or ELF header. + +### Script search order + +For each hook, Coral looks for (in order): +1. `hook-name.sh` — executed with `zsh` +2. `hook-name` — executed directly (must be executable with valid shebang or ELF) + +If neither exists, the hook is silently skipped (not an error). + +### Upgrade flow + +``` +pre-upgrade.sh $OLD_VERSION $NEW_VERSION # abort if non-zero + → remove old files (driven by installed manifest) + → install new files (driven by new manifest) +post-upgrade.sh $OLD_VERSION $NEW_VERSION # warn if non-zero +``` + +If no `pre-upgrade.sh` or `post-upgrade.sh` exists, Coral performs the file +swap silently. It does **not** fall back to running pre-remove + post-install, +as those hooks may have side effects inappropriate for an upgrade (e.g., +post-install creating a fresh database vs. preserving an existing one). + +### Stored scripts + +Remove scripts (`pre-remove.sh`, `post-remove.sh`) must be available when a +package is removed, even if the original `.pkg.tar.xz` is gone. Coral stores +a copy of `.SCRIPTS/` in the package database at install time +(`/var/lib/coral/installed/<name>/scripts/`). Upgrade scripts are stored there +as well, since they're needed when a future version replaces the current one. + +--- + +## 6. Config File Protection — Checksum-Based + +### Current behavior (v1, broken) + +Reads both files fully into memory and compares content with `str.equals()`. +Both branches of the comparison (equal / not equal) execute the same code path — +always installs as `.pkg`. The "files are identical" case should overwrite in +place. + +### New behavior (v2) + +Uses the sha256 checksum from the manifest to detect user modifications without +reading the full installed file into memory twice. + +**On fresh install:** +1. Install config file normally to `dest_path` +2. Store the package-provided sha256 in the database for this file + +**On upgrade (file already exists):** +1. Compute sha256 of the currently installed file +2. Compare against the stored checksum from the *previously installed* version +3. Decision: + +| Installed matches old pkg checksum? | Action | +|-------------------------------------|--------| +| Yes (user has not modified) | Overwrite with new version | +| No (user has modified) | Install new version as `dest_path.pkg-new`, keep user's file | + +4. Update stored checksum to the new package's sha256 + +**On remove:** +- Config files listed in `.CONFIG` are **not** removed if they differ from the + package-provided checksum (user has modified them) +- Config files that match the package checksum are removed normally + +This matches the proven dpkg conffile model. + +### Implementation + +``` +fn install_config_file(src: string, dest: string, manifest_sha256: string, stored_sha256: string): bool + if not fs.stat.exists(dest) + // Fresh install — copy file + fs.ops.copy_file_preserve(src, dest) + return true + end if + + // File exists — check if user modified it + let installed_sha256 = checksum.sha256_file(dest) + + if installed_sha256 == stored_sha256 + // User has not modified — safe to overwrite + fs.ops.copy_file_preserve(src, dest) + return true + else + // User has modified — preserve their version + let new_path = dest + ".pkg-new" + fs.ops.copy_file_preserve(src, new_path) + // Log: "Config file preserved, new version at dest.pkg-new" + return true + end if +end install_config_file +``` + +--- + +## 7. Implementation Plan + +### Phase 1: mtree module and manifest generation +- New module `util/mtree.reef` — parser and generator +- Update `build.reef`: replace `generate_footprint()` with `generate_manifest()` +- Use `fs.stat`, `fs.perm`, `fs.link.readlink()`, `checksum.sha256_file()` + +### Phase 2: fs.ops-based installer +- Rewrite `install_files()` in `package.reef` to walk the manifest +- Use `fs.ops.copy_file_preserve`, `fs.perm.chmod`, `fs.perm.chown`, `fs.link.symlink` +- Remove rsync dependency from install path +- Integrate config file checksum logic + +### Phase 3: Enhanced .PKGINFO +- Update `generate_pkginfo()` to emit `[dependencies]` section +- Update `read_pkginfo()` to parse dependencies +- Add `deps` field to `PackageInfo` type or extend existing `Dependencies` + +### Phase 4: Script model cleanup +- Add `pre-upgrade.sh` / `post-upgrade.sh` hook support +- Remove `.reef` script type from `run_script()` and `run_stored_script()` +- Change `.sh` invocation from `sh` to `zsh` +- Pass version arguments to all hooks +- Abort on non-zero exit from pre-* hooks +- Update upgrade command to call upgrade hooks + +### Phase 5: Removal and verify +- Rewrite `remove_files()` to use manifest (directories tracked explicitly) +- Use `fs.ops.remove_file`, `fs.ops.remove_tree`, `dir.remove_dir` +- Implement `coral verify <pkg>` — walk stored manifest, check sha256 and + permissions against live filesystem + +### Dependencies + +- Reef >= 0.5.0 (for `fs.stat`, `fs.perm`, `fs.link`, `fs.ops`) +- rsync is no longer required for package installation (remains used by + `coral repo mirror` only) + +--- + +## 8. Risks and Mitigations + +| Risk | Mitigation | +|------|------------| +| fs.ops.copy_file_preserve slower than rsync for large packages | Benchmark during Phase 2; rsync does buffered I/O and copy_file_preserve uses 8KB buffer — comparable. Manifest-driven install skips metadata files without needing exclude flags. | +| mtree parser bugs cause install failures | Comprehensive test coverage in Phase 1; mtree is a simple line-oriented format with no nesting or escaping edge cases beyond spaces in paths. | +| Packages with >8192 files overflow entry array | Use manifest file_count pre-scan or dynamic sizing. Current .FOOTPRINT has same limit. | +| Upgrade hook ordering with dependency chains | Out of scope for this design. Coral currently upgrades packages individually. Dependency-ordered upgrades (topological sort) are a future enhancement. | diff -r 000000000000 -r d0a96d73e9e1 docs/PACKAGE_FORMAT_V2_TODO.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/PACKAGE_FORMAT_V2_TODO.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,115 @@ +# Package Format v2 — Task List + +**Design:** `docs/PACKAGE_FORMAT_V2_DESIGN.md` +**Target:** Coral v0.3 +**Date:** 2026-03-07 +**Requires:** Reef >= 0.5.0 + +--- + +## Phase 1: mtree module and manifest generation — COMPLETE + +- [x] Create `src/util/mtree.reef` module with `ManifestEntry` type +- [x] Implement `parse_manifest()` — line-oriented parser, split on space, extract key=value pairs +- [x] Implement `generate_manifest()` — walk directory tree using `fs.stat`, `fs.perm`, `fs.link.readlink()`, `checksum.sha256_file()` +- [x] Implement accessor functions: `entry_type`, `entry_path`, `entry_mode`, `entry_sha256`, `entry_link`, `entry_uname`, `entry_gname` +- [x] Handle sort order: lexicographic by path so dirs precede their contents +- [x] Skip metadata files (`.PKGINFO`, `.MANIFEST`, `.CONFIG`, `.SCRIPTS/`) during generation +- [x] Update `build.reef`: replace `generate_footprint()` with `generate_manifest()` using new mtree module +- [x] Rename output file from `.FOOTPRINT` to `.MANIFEST` in build output +- [ ] Test: build a package, verify `.MANIFEST` output contains correct types, modes, ownership, checksums +- [ ] Test: parse a generated manifest back and verify round-trip correctness + +## Phase 2: fs.ops-based installer — COMPLETE + +- [x] Rewrite `install_files()` in `package.reef` to parse `.MANIFEST` instead of `.FOOTPRINT` +- [x] Implement dir install: `dir.create_dir_all()`, `fs.perm.chmod()`, `fs.perm.chown()` +- [x] Implement file install: `fs.ops.copy_file_preserve()`, `fs.perm.chmod()`, `fs.perm.chown()` +- [x] Implement symlink install: remove conflicting target, `fs.link.symlink()` +- [x] Add post-copy sha256 verification against manifest checksum +- [x] Integrate config file checksum logic (see Phase 2b below) +- [x] Remove rsync call from `install_files()` +- [x] Remove symlink conflict prep_cmd shell-out (handle inline during manifest walk) +- [x] Error handling: stop on any failed file operation +- [x] Update `read_footprint()` to read `.MANIFEST` first, fall back to `.FOOTPRINT` +- [x] Update `register_from_extract_dir()` to store `manifest.mtree` in database +- [x] Update `remove_files()` to use `fs.ops.remove_file` instead of shell-outs +- [x] Add `remove_files_manifest()` for manifest-driven removal with explicit directory tracking +- [x] Clean up stale rsync comments in `install.reef` +- [ ] Test: install a package to a test root, verify all files/dirs/symlinks match manifest +- [ ] Test: install package with setuid binary (mode=4755), verify mode preserved +- [ ] Test: install package with symlinks, verify targets correct + +### Phase 2b: Config file checksum logic — COMPLETE + +- [x] Update `install_config_file()` to accept manifest sha256 and stored sha256 parameters +- [x] On fresh install: copy file normally +- [x] On upgrade (file exists, matches old checksum): overwrite with new version +- [x] On upgrade (file exists, differs from old checksum): install as `.pkg-new`, preserve user's file +- [x] Remove old content-comparison logic (`str.equals(src_content, dest_content)`) +- [x] On remove: skip config files where installed sha256 differs from package sha256 +- [ ] Test: fresh install places config file and records checksum +- [ ] Test: upgrade with unmodified config overwrites cleanly +- [ ] Test: upgrade with user-modified config installs `.pkg-new` and preserves original + +## Phase 3: Enhanced .PKGINFO — COMPLETE + +- [x] Add `runtime_deps`, `build_deps`, `runtime_deps_count`, `build_deps_count` fields to `PackageInfo` type in `types.reef` +- [x] Update `new_package_info()` factory to initialize new fields +- [x] Update `generate_pkginfo()` in `build.reef` to emit `[dependencies]` section +- [x] Write `runtime` array from `ctx.port.deps.runtime` +- [x] Write `build` array from `ctx.port.deps.build` +- [x] Skip `[dependencies]` section if both arrays are empty +- [x] Update `read_pkginfo()` in `package.reef` to parse `[dependencies]` section +- [x] Handle legacy packages without `[dependencies]` gracefully +- [x] Add `parse_toml_array()` helper to `package.reef` +- [x] Handle version constraint syntax in dependency strings (`>=`, `<=`, `>`, `<`, `=`) +- [x] Add version comparison utility for constraint checking +- [ ] Test: build a package with dependencies, verify `.PKGINFO` contains correct `[dependencies]` +- [ ] Test: parse `.PKGINFO` with dependencies and verify fields populated + +## Phase 4: Script model cleanup — COMPLETE + +- [x] Remove `.reef` script type from `run_script()` in `package.reef` +- [x] Remove `.reef` script type from `run_stored_script()` in `package.reef` +- [x] Change `.sh` invocation from `sh` to `zsh` in `run_script()` +- [x] Change `.sh` invocation from `sh` to `zsh` in `run_stored_script()` +- [x] Update script search order: try `.sh` first, then bare executable +- [x] Add `pre-upgrade.sh` / `post-upgrade.sh` hook support +- [x] Pass `$VERSION` argument to pre-install, post-install, pre-remove, post-remove +- [x] Pass `$OLD_VERSION $NEW_VERSION` arguments to pre-upgrade, post-upgrade +- [x] Abort install/remove/upgrade if pre-* hook exits non-zero +- [x] Log warning (don't abort) if post-* hook exits non-zero +- [x] Store upgrade scripts in database alongside remove scripts at install time +- [x] Implement upgrade flow: pre-upgrade → remove old files → install new files → post-upgrade +- [ ] Test: package with pre-install that exits 0 — install proceeds +- [ ] Test: package with pre-install that exits 1 — install aborts +- [ ] Test: package with post-install that exits 1 — install completes with warning +- [ ] Test: upgrade with pre-upgrade/post-upgrade scripts, verify version args passed + +## Phase 5: Removal and verify — COMPLETE + +- [x] Rewrite `remove_files()` to use `fs.ops` instead of shell-outs (done in Phase 2) +- [x] Add `remove_files_manifest()` with explicit directory tracking (done in Phase 2) +- [x] Respect config file protection on remove (skip user-modified configs) (done in Phase 2b) +- [x] Implement `coral verify <pkg>` command + - [x] Read stored manifest from database + - [x] For each file entry: check exists, compare sha256, compare mode/ownership + - [x] For each symlink entry: check exists, verify link target + - [x] For each dir entry: check exists + - [x] Report: missing files, checksum mismatches, permission changes, broken symlinks +- [ ] Test: remove a package, verify all files deleted and empty dirs cleaned up +- [ ] Test: remove a package with user-modified config, verify config preserved +- [ ] Test: verify a clean install — all files pass +- [ ] Test: verify after modifying a file — checksum mismatch reported + +--- + +## Cleanup + +- [x] Remove `generate_footprint()` from `build.reef` (replaced by `generate_manifest()`) +- [x] Update `read_footprint()` to read `.MANIFEST` via mtree parser (with `.FOOTPRINT` fallback) +- [x] Update `verify_package()` to check for `.MANIFEST` or `.FOOTPRINT` +- [x] Remove rsync from install path (only remains in `repo.reef` mirror command) +- [x] Clean up rsync comments in `install.reef` +- [x] Update `docs/GUIDE.md` — fix .reef references, sh→zsh, .pkg→.pkg-new, add verify command, add version constraints, add upgrade scripts diff -r 000000000000 -r d0a96d73e9e1 docs/REFACTOR_SOURCES.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/REFACTOR_SOURCES.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,215 @@ +# Refactor: Source Mirrors Support + +**Status:** Planning +**Version:** 0.1.3 +**Date:** January 2026 + +--- + +## Overview + +Refactor the source download system to support multiple mirror URLs per source file, with automatic fallback when primary sources are unavailable. + +--- + +## Current Design + +```toml +[sources] +urls = ["https://example.com/file.tar.gz"] +checksums = ["sha256:abc123..."] +``` + +**Problems:** +- Multiple URLs treated as separate files, not mirrors +- No fallback on download failure +- Checksum tied to array index, fragile + +--- + +## New Design + +### Package.toml Format + +```toml +[[source]] +file = "vim-9.1.0.tar.gz" +urls = [ + "https://github.com/vim/vim/archive/v9.1.0.tar.gz", + "https://mirror.example.com/vim-9.1.0.tar.gz", + "https://backup.example.org/vim-9.1.0.tar.gz" +] +checksum = "sha256:abc123..." + +[[source]] +file = "illumos-compat.patch" +urls = ["https://patches.zygaena.org/vim/illumos.patch"] +checksum = "sha256:def456..." +extract = false +``` + +### New Fields + +| Field | Required | Default | Description | +|-------|----------|---------|-------------| +| `file` | Yes | - | Output filename (what to save as) | +| `urls` | Yes | - | Array of mirror URLs (tried in order) | +| `checksum` | No | "SKIP" | SHA256 checksum for verification | +| `extract` | No | true | Whether to extract archive | + +--- + +## Implementation Plan + +### Phase 1: Type Definitions (types.reef) + +**New struct:** +```reef +type Source = struct + file: string // Output filename + urls: [string] // Mirror URLs (tried in order) + url_count: int // Number of URLs + checksum: string // SHA256 checksum + extract: bool // Whether to extract (default: true) +end Source +``` + +**Update SourceInfo:** +```reef +type SourceInfo = struct + sources: [Source] // Array of Source structs + count: int // Number of sources +end SourceInfo +``` + +**Add factory function:** +```reef +fn new_source(): Source +``` + +### Phase 2: TOML Parsing (port.reef) + +Add support for parsing `[[source]]` table arrays: + +1. Detect `source.` prefixed keys +2. Group by index (source.0.file, source.0.urls, etc.) +3. Parse each source into Source struct +4. Handle both old format (backward compat) and new format + +**Backward Compatibility:** +- If `sources.urls` exists (old format), convert to new format +- Each URL becomes a separate Source with auto-detected filename +- Checksums mapped by index + +### Phase 3: Download Logic (build.reef) + +Update `download_sources()`: + +``` +for each source in sources: + if file exists in cache and checksum matches: + skip download + + for each url in source.urls: + try download to source.file + if success: + verify checksum + if checksum ok: + break to next source + else: + delete file, try next mirror + else: + try next mirror + + if all mirrors failed: + return error +``` + +### Phase 4: Extract Logic (build.reef) + +Update `extract_sources()`: + +``` +for each source in sources: + if source.extract: + extract archive + else: + copy file to work_dir (for patches, etc.) +``` + +### Phase 5: Update Test Ports + +Convert test ports to new format: +- test/ports/base/vim/package.toml +- test/ports/base/tree/package.toml +- test/ports/core/coral/package.toml +- etc. + +--- + +## File Changes Summary + +| File | Changes | +|------|---------| +| `src/types.reef` | Add Source struct, update SourceInfo, add new_source() | +| `src/core/port.reef` | Parse [[source]] tables, backward compat for old format | +| `src/commands/build.reef` | Mirror fallback in download, respect extract flag | +| `src/util/http.reef` | Add download_with_mirrors() helper (optional) | +| `test/ports/*/package.toml` | Update to new format | + +--- + +## Example Migration + +### Before (old format) +```toml +[sources] +urls = ["https://github.com/vim/vim/archive/v9.1.0.tar.gz"] +checksums = ["SKIP"] +``` + +### After (new format) +```toml +[[source]] +file = "vim-9.1.0.tar.gz" +urls = [ + "https://github.com/vim/vim/archive/v9.1.0.tar.gz" +] +checksum = "SKIP" +``` + +### With mirrors +```toml +[[source]] +file = "vim-9.1.0.tar.gz" +urls = [ + "https://github.com/vim/vim/archive/v9.1.0.tar.gz", + "https://ftp.nluug.nl/pub/vim/unix/vim-9.1.tar.bz2", + "https://mirror.freedif.org/vim/unix/vim-9.1.tar.bz2" +] +checksum = "sha256:1234567890abcdef..." +``` + +--- + +## Testing Checklist + +- [ ] Single source, single URL (basic case) +- [ ] Single source, multiple URLs (mirror fallback) +- [ ] Multiple sources +- [ ] Primary URL fails, mirror succeeds +- [ ] All mirrors fail (error handling) +- [ ] Checksum verification after download +- [ ] Checksum mismatch triggers next mirror +- [ ] `extract = false` for non-archive files +- [ ] Backward compatibility with old format +- [ ] Cached file with valid checksum skips download + +--- + +## Future Enhancements + +- **Priority/weight** for mirrors (prefer faster/closer) +- **Parallel downloads** from multiple mirrors +- **Resume partial downloads** +- **Mirror health tracking** (remember failed mirrors) diff -r 000000000000 -r d0a96d73e9e1 docs/ROADMAP.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/ROADMAP.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,59 @@ +# Coral Roadmap + +**Target:** Coral v0.4.x +**Date:** 2026-03-08 +**Tracks:** Unwired features, cleanup, and new functionality + +--- + +## Phase 1: Cleanup + +- [x] **1. Remove redundant `opts.no_color` field** + - Removed `GlobalOptions.no_color` from `types.reef` and `main.reef`; color module reads flag directly +- [x] **2. Remove legacy `SourceInfo.urls` / `SourceInfo.checksums` fields** + - Removed unused backward-compat fields from `types.reef` and dead writes from `port.reef` +- [x] **3. Clean up resolver exports** + - Removed `resolve_alternative()`, `is_alternative_dep()`, `parse_alternatives()` from export block + +--- + +## Phase 2: High Priority — Features users expect to work + +- [x] **4. Implement `--dry-run` / `-n` flag** + - Added dry-run support to install, remove, build, and upgrade commands +- [x] **5. Wire up `fallback_to_source` config option** + - Install falls back to `coral build -y` when no binary package found and `fallback_to_source = true` +- [x] **6. Implement package conflict detection** + - Install checks `conflicts` field against installed packages; `--force` overrides +- [x] **7. Implement signature verification on install** + - When `require_signed_packages = true`, install verifies `.sig` file before proceeding +- [x] **8. Implement `compress_man` build step** + - After build, gzips uncompressed man pages in staging dir when `compress_man = true` + +--- + +## Phase 3: Medium Priority — Partially wired features + +- [x] **9. Wire up `keep_packages` / `keep_sources` config** + - `clean` command respects `keep_packages`/`keep_sources`; `--force` overrides protection +- [x] **10. Wire up `use_proxy` config** + - HTTP module accepts proxy config; curl disables proxy by default, enables when `use_proxy = true` +- [x] **11. Wire up `auto_refresh` config** + - Install and upgrade rebuild port index before execution when `auto_refresh = true` +- [x] **12. Fix `coral diff` documentation** + - Updated GUIDE.md to reference `coral outdated` instead of `coral diff` +- [x] **13. Use specific exit codes throughout codebase** + - Replaced generic `EXIT_ERROR()` with specific codes across all commands + +--- + +## Completed + +- [x] Package Format v2 (mtree manifest, fs.ops installer, config protection) — v0.3.0 +- [x] Script model rewrite (zsh, upgrade hooks, version args) — v0.3.0 +- [x] Verify command — v0.3.0 +- [x] CI update to Reef 0.5.0 — v0.3.0 +- [x] Auto-install build dependencies — v0.3.1 +- [x] Export $PREFIX, $SYSCONFDIR, $PKG_ROOT to build scripts — v0.3.2 +- [x] Recursive build-from-source dependency resolver — v0.3.3 +- [x] All 13 unwired features (cleanup, high-priority, medium-priority) — v0.3.5 diff -r 000000000000 -r d0a96d73e9e1 docs/STAGE1_CORAL_PLAN.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/STAGE1_CORAL_PLAN.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,1893 @@ +# Stage 1: Coral Package Manager Implementation Plan + +**Document Version:** 2.5 +**Created:** January 2026 +**Last Updated:** January 2026 +**Status:** In Progress (Stub Implementation Complete) + +## Overview + +Stage 1 builds **Coral**, the package manager for Zygaena, written entirely in Reef. Coral is a single unified command with subcommands (similar to pacman/cargo). + +**Prerequisites:** Stage 0 Complete (Reef 0.1.3 installed on OmniOS at 192.168.168.148) + +--- + +## Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| **Name** | `coral` (ocean/reef theme) | Consistent with Zygaena (shark) and Reef (language) naming | +| **Architecture** | Single binary with subcommands | Simpler for users, like pacman/cargo/git | +| **Source Location** | `/home/ctusa/repos/zygaena/coral/` | Part of main zygaena repo for coordinated development | +| **Install Location** | `/usr/local/bin/coral` (OmniOS), `/usr/bin/coral` (Zygaena) | Standard Unix paths | +| **Compression** | xz (`.pkg.tar.xz`) | Good compression, widely available on illumos | +| **HTTP** | Native Reef `net.http` (report issues to Reef team) | Avoid shell exec where possible | +| **Privileges** | Root required for all operations | Multi-user system security | +| **Confirmation** | Ask by default, `-y` to skip | Safe default, scriptable override | +| **Colors** | Yes, when terminal supports it | Better UX, respects NO_COLOR standard | +| **Port Categories** | core, base, devel, desktop, extra | Clear separation of concerns | +| **Distribution** | Binary + Source packages | Binary for speed, source as fallback | +| **Repository Access** | Local filesystem + HTTP | Both equally supported (ISO/USB and mirrors) | +| **Package Signing** | Ed25519/Signify | Simple, minimal dependencies, proven security | +| **Config Format** | TOML | Human-readable, matches port definitions | +| **Third-party Repos** | Supported from day one | Enables community packages | + +--- + +## Command Structure + +``` +coral <subcommand> [options] [packages...] + +BUILD/CREATE: + coral build <port> Build package from port directory + coral build . Build from current directory + +INSTALL/REMOVE: + coral install <pkg...> Install packages (resolves dependencies) + coral remove <pkg...> Remove packages + coral upgrade [pkg...] Upgrade packages (all if none specified) + +QUERY: + coral info <pkg> Show package details + coral list List installed packages + coral list -a List all available packages (from repos) + coral files <pkg> List files owned by package + coral owner <file> Find which package owns file + coral search <term> Search available packages + coral depends <pkg> Show dependency tree + +PORTS: + coral sync Update ports collection and repo metadata (git/hg) + coral diff Show packages with available updates + +REPOSITORY MANAGEMENT: + coral repo init <path> Create new repository structure + coral repo add <path> <pkg> Add packages to repository + coral repo remove <path> <pkg> Remove package from repository + coral repo list <path> List packages in repository + coral repo rebuild <path> Regenerate repo.toml manifest + coral repo sign <path> Sign repository manifest + coral repo verify <path> Verify repository signatures + coral repo sync <path> <dest> Rsync repository to mirror + coral repo export <path> <out> Export repository for ISO/USB + +KEY MANAGEMENT: + coral key init Initialize keyring directory + coral key generate <name> Generate new Ed25519 signing keypair + coral key list List available keys + coral key import <file> Import public key + coral key export <name> Export public key + coral key trust <keyid> Mark key as trusted + coral key revoke <keyid> Revoke trust for key + +CACHE MANAGEMENT: + coral cache clean Remove cached packages + coral cache clean --sources Remove cached source tarballs + coral cache clean --all Remove all cached files + coral cache list Show cache contents and sizes + +GLOBAL OPTIONS: + -y, --yes Don't ask for confirmation + -f, --force Force operation (overwrite conflicts) + -v, --verbose Verbose output + -n, --dry-run Show what would be done + -q, --quiet Minimal output + --no-color Disable colored output + --root <path> Use alternative root (for building ISOs) + +EXAMPLES: + coral install vim # Install vim and dependencies + coral install vim nano -y # Install multiple, no confirmation + coral build /usr/ports/core/hello + coral search editor + coral upgrade # Upgrade all packages + coral remove firefox -y + coral repo init /srv/packages + coral key generate official +``` + +--- + +## Configuration + +### Main Configuration File + +Location: `/etc/coral/coral.conf` + +```toml +# Coral Package Manager Configuration + +[general] +# System architecture +arch = "x86_64" + +# Number of parallel build jobs (0 = auto-detect CPU count) +jobs = 0 + +# Ask for confirmation before operations (override with -y) +confirm = true + +# Enable colored output (respects NO_COLOR env var) +color = true + +[paths] +# Local ports collection +ports = "/usr/ports" + +# Built package cache +packages = "/var/lib/coral/packages" + +# Downloaded source tarballs +sources = "/var/lib/coral/sources" + +# Installed package database +database = "/var/lib/coral/installed" + +# Repository metadata cache +repos = "/var/lib/coral/repos" + +# Build working directory (cleaned after build) +work = "/var/tmp/coral/work" + +# Package staging directory +pkg = "/var/tmp/coral/pkg" + +# Signing keys directory +keys = "/etc/coral/keys" + +[build] +# Default CFLAGS for building packages +cflags = "-O2 -pipe -march=x86-64" + +# Default CXXFLAGS (defaults to cflags if empty) +cxxflags = "${cflags}" + +# Make flags +makeflags = "-j${jobs}" + +# Strip binaries after build +strip = true + +# Compress man pages with gzip +compress_man = true + +# What to do when binary package not available: +# "ask" - prompt user to build from source +# "yes" - automatically build from source +# "no" - fail if binary not available +fallback_to_source = "ask" + +[network] +# Connection timeout in seconds +timeout = 30 + +# Number of retry attempts +retries = 3 + +# Use system proxy settings +use_proxy = true + +[sync] +# Auto-refresh repository metadata (hours, 0 = manual only) +auto_refresh = 24 + +[cache] +# Keep downloaded packages after installation +keep_packages = true + +# Keep downloaded source tarballs +keep_sources = true + +[security] +# Require repositories to have valid signatures +require_signed_repos = true + +# Require packages to have valid signatures +require_signed_packages = true +``` + +### Repository Configuration Directory + +Location: `/etc/coral/repos.d/` + +Each repository is configured in a separate TOML file. + +#### Official Repository Example + +File: `/etc/coral/repos.d/official.toml` + +```toml +[repository] +# Unique identifier +name = "official" + +# Human-readable description +description = "Official Zygaena packages" + +# Enable/disable this repository +enabled = true + +# Priority (higher = preferred when same package in multiple repos) +priority = 100 + +# Mirrors to try (in order) +[[mirrors]] +url = "https://pkg.zygaena.org/stable/${arch}/" + +[[mirrors]] +url = "https://mirror.example.com/zygaena/${arch}/" + +# Local fallback (e.g., installation media) +[[mirrors]] +url = "file:///mnt/cdrom/packages/" + +[security] +# Require signed packages from this repo +require_signed = true + +# Public key file for signature verification +keyfile = "/etc/coral/keys/official.pub" +``` + +#### Third-Party Repository Example + +File: `/etc/coral/repos.d/community.toml` + +```toml +[repository] +name = "community" +description = "Community-maintained packages" +enabled = true +priority = 50 + +[[mirrors]] +url = "https://community.example.org/zygaena/${arch}/" + +[security] +require_signed = true +keyfile = "/etc/coral/keys/community.pub" +``` + +--- + +## Repository Manifest Format + +Each repository publishes a `repo.toml` manifest that coral downloads to understand available packages. + +### Manifest Structure + +File: `repo.toml` (at repository root) + +```toml +[repository] +# Repository identifier (must match repos.d config) +name = "official" + +# Human-readable description +description = "Official Zygaena packages" + +# Maintainer contact +maintainer = "[email protected]" + +# Target architecture +arch = "x86_64" + +# When this manifest was generated (ISO 8601) +generated = 2026-01-23T12:00:00Z + +# Total number of packages +package_count = 150 + +# Base URL for package downloads (relative to manifest location) +base_url = "packages/" + +# Signing key identifier +signing_key = "zygaena-official-2026" + +# Package entries +[[package]] +name = "bash" +version = "5.2.21" +release = 1 +description = "The GNU Bourne Again shell" +license = "GPL-3.0" +category = "core" +depends = ["glibc", "readline", "ncurses"] +optdepends = [] +provides = ["sh"] +conflicts = [] +size = 7834521 # Installed size in bytes +compressed = 1823456 # Package file size in bytes +filename = "bash-5.2.21-1.pkg.tar.xz" +sha256 = "a1b2c3d4e5f6789..." +signature = "sig:ed25519:..." # Optional inline signature + +[[package]] +name = "vim" +version = "9.1.0" +release = 1 +description = "Vi IMproved - enhanced vi editor" +license = "Vim" +category = "base" +depends = ["ncurses"] +optdepends = ["python:Python scripting support", "lua:Lua scripting support"] +provides = ["vi"] +conflicts = [] +size = 12456789 +compressed = 3456789 +filename = "vim-9.1.0-1.pkg.tar.xz" +sha256 = "fedcba987654321..." +signature = "sig:ed25519:..." + +# ... more packages ... +``` + +### Manifest Signature + +The manifest itself is signed. Signature stored in: `repo.toml.sig` + +``` +-----BEGIN ED25519 SIGNATURE----- +<base64 encoded signature> +-----END ED25519 SIGNATURE----- +``` + +--- + +## Package Signing + +### Key Format + +Coral uses Ed25519 keys (same as OpenBSD signify). + +**Private key:** `<name>.sec` +``` +untrusted comment: coral secret key +<base64 encoded private key> +``` + +**Public key:** `<name>.pub` +``` +untrusted comment: coral public key +<base64 encoded public key> +``` + +### Key Storage + +- System keys: `/etc/coral/keys/` +- User keys: `~/.coral/keys/` +- Trusted keys database: `/var/lib/coral/trusted-keys` + +### Signing Workflow + +```bash +# Generate a keypair for repository signing +coral key generate official + +# Sign the repository manifest +coral repo sign /srv/packages + +# Verify a repository +coral repo verify /srv/packages + +# Import a third-party public key +coral key import community.pub + +# Mark the key as trusted +coral key trust community +``` + +--- + +## Source Code Structure + +``` +/home/ctusa/repos/zygaena/ +├── coral/ # Package manager source +│ ├── reef.toml # Reef project config +│ └── src/ +│ ├── main.reef # Entry point, CLI dispatch +│ ├── commands/ +│ │ ├── build.reef # coral build +│ │ ├── install.reef # coral install +│ │ ├── remove.reef # coral remove +│ │ ├── upgrade.reef # coral upgrade +│ │ ├── info.reef # coral info +│ │ ├── list.reef # coral list +│ │ ├── files.reef # coral files +│ │ ├── owner.reef # coral owner +│ │ ├── search.reef # coral search +│ │ ├── depends.reef # coral depends +│ │ ├── sync.reef # coral sync +│ │ ├── diff.reef # coral diff +│ │ ├── repo.reef # coral repo (subcommands) +│ │ ├── key.reef # coral key (subcommands) +│ │ └── cache.reef # coral cache (subcommands) +│ ├── core/ +│ │ ├── port.reef # Port parsing (package.toml) +│ │ ├── package.reef # Package archive handling +│ │ ├── database.reef # Installed package database +│ │ ├── resolver.reef # Dependency resolution +│ │ ├── config.reef # Configuration loading +│ │ ├── repository.reef # Repository manifest handling +│ │ └── signing.reef # Ed25519 signing/verification +│ ├── util/ +│ │ ├── http.reef # HTTP downloads (wraps net.http) +│ │ ├── archive.reef # Tar/xz operations +│ │ ├── checksum.reef # SHA256 verification +│ │ ├── color.reef # Terminal colors +│ │ └── prompt.reef # User prompts +│ └── types.reef # Shared type definitions +├── ports/ # Port collection (Stage 2) +│ ├── core/ +│ ├── base/ +│ ├── devel/ +│ ├── desktop/ +│ └── extra/ +├── docs/ +│ ├── STAGE0_BOOTSTRAP.md +│ ├── STAGE1_CORAL_PLAN.md # This document +│ └── ... +└── bootstrap/ +``` + +--- + +## Port Categories + +| Category | Description | Examples | +|----------|-------------|----------| +| **core** | Essential system packages | kernel, libc, coreutils, bash | +| **base** | Standard Unix utilities | vim, git, curl, openssh | +| **devel** | Development tools & libraries | gcc, cmake, python, rust, ncurses-dev | +| **desktop** | X11, window managers, GUI apps | xlibre, windowmaker, firefox | +| **extra** | Everything else | games, themes, misc utilities | + +--- + +## Reef Language Notes + +### Reserved Keywords +**IMPORTANT:** `run` is a reserved keyword for Active Objects in Reef. Command handlers must use `execute()` instead of `run()`. + +### Key Syntax Rules +- Blocks end with `end` keyword: `end if`, `end for`, `end while`, `end FunctionName`, `end module` +- `fn` returns a value, `proc` does not return a value +- String interpolation: `"Hello, ${name}"` (simple variables only, not expressions) +- Mutable variables: `mut count = 0` +- Immutable variables: `let value = 42` +- Logical NOT: `not` (not `!`) +- Array allocation: `new [type](size)` +- Module declaration: `module path.to.module` ... `end module` +- Export block: `export` ... `end export` +- Entry point (`main.reef`) does NOT use a module declaration + +### Module Structure Pattern +```reef +module module.name + +export + // Public interface declarations + fn public_function(): type + proc public_procedure() +end export + +// Implementation +fn public_function(): type + // ... +end public_function + +// Private helpers (not in export block) +fn private_helper(): type + // ... +end private_helper + +end module +``` + +### TOML Parsing Pattern +The Reef stdlib uses parallel arrays for TOML parsing: +```reef +import encoding.toml + +let keys = toml.toml_alloc_keys() // Allocate keys array (max 256) +let vals = toml.toml_alloc_values() // Allocate values array +let count = toml.toml_parse(content, keys, vals) +let name = toml.toml_get(keys, vals, count, "package.name") +let release = toml.toml_get_int(keys, vals, count, "package.release") +``` + +--- + +## Reef Standard Library Modules + +```reef +// Core imports for Coral +import io.file // readFile, writeFile, fileExists, readBinaryFile +import io.dir // list_dir, dir_exists, create_dir_all, current_dir +import io.path // join_path, dirname, basename, extension +import core.str // split, join, contains, starts_with, ends_with, trim_ws +import sys.args // count, get, has_flag (simple CLI parsing) +import sys.flag // FlagParser for rich CLI parsing with help generation +import sys.process // process_spawn_shell, process_wait +import sys.env // get_env, set_env, has_env +import io.console // readLine, confirm, printErr +import encoding.toml // toml_parse, toml_get, toml_get_int, toml_has_key +import net.http // HTTP client (HTTPS support coming from Reef team) +import time.clock // Timestamps +import collections.list // Dynamic lists +import collections.map // Hash maps +import crypto.sha256 // SHA256 checksums (native, pure Reef implementation) +``` + +### Native vs Shell-out + +| Functionality | Implementation | Notes | +|---------------|----------------|-------| +| **SHA256** | Native `crypto.sha256` | Pure Reef, supports string and binary data | +| **TOML parsing** | Native `encoding.toml` | Uses parallel arrays pattern | +| **HTTP** | Native `net.http` | HTTP now, HTTPS coming from Reef team | +| **File I/O** | Native `io.file`, `io.dir` | Full support | +| **Tar/xz** | Shell-out to `tar`, `xz` | System tools, acceptable | +| **Patch** | Shell-out to `patch` | Apply source patches (-p<strip>) | +| **Ed25519 signing** | Shell-out to `signify` | Until Reef adds crypto.ed25519 | +| **Git** | Shell-out to `git` | For `coral sync` ports update | +| **Mercurial** | Shell-out to `hg` | Alternative VCS for ports update | + +--- + +## Core Data Types + +```reef +// types.reef - Shared type definitions + +// Package metadata from package.toml +type PackageInfo = struct + name: string + version: string + release: int + description: string + url: string + license: string + maintainer: string + arch: string + category: string + alternatives: [string] // Alternative sets this package belongs to + alternatives_count: int + conflicts: [string] // Packages that conflict with this + conflicts_count: int +end PackageInfo + +type Dependencies = struct + runtime: [string] + build: [string] + optional: [string] // Optional dependencies with descriptions + runtime_count: int + build_count: int + optional_count: int +end Dependencies + +type SourceInfo = struct + urls: [string] + checksums: [string] + count: int +end SourceInfo + +type PatchInfo = struct + files: [string] // Patch filenames in order + strip: int // Strip level for patch -p (default: 1) + count: int // Number of patches +end PatchInfo + +type ConfigInfo = struct + files: [string] // Config files to protect (installed as .pkg) + count: int +end ConfigInfo + +type ScriptsInfo = struct + pre_install: string // Script filename or empty + post_install: string + pre_remove: string + post_remove: string +end ScriptsInfo + +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 +end Port + +// Installed package record +type InstalledPackage = struct + info: PackageInfo + install_date: string + install_reason: string // "explicit" or "dependency" + files: [string] // All files owned by this package + files_count: int + config_files: [string] // Config files (for upgrade handling) + config_count: int + repository: string // Which repo it came from +end InstalledPackage + +// Remote package from repository manifest +type RemotePackage = struct + info: PackageInfo + depends: [string] + optdepends: [string] + provides: [string] + conflicts: [string] + size: int // Installed size + compressed: int // Download size + filename: string + sha256: string + signature: string + depends_count: int +end RemotePackage + +// Repository definition +type Repository = struct + name: string + description: string + enabled: bool + priority: int + mirrors: [string] + mirror_count: int + keyfile: string + require_signed: bool +end Repository + +// Build result +type BuildResult = struct + success: bool + package_path: string + error_message: string +end BuildResult + +// Signing key +type SigningKey = struct + name: string + public_key: string + fingerprint: string + trusted: bool + created: string +end SigningKey +``` + +--- + +## Key Implementation Details + +### 1. Main Entry Point (main.reef) + +```reef +// main.reef - Entry point (no module declaration) +// Entry point files do NOT use module declarations in Reef + +import sys.args +import commands.build +import commands.install +import commands.remove +import commands.repo +import commands.key +import commands.cache +// ... other imports + +proc main() + // Check for root (except for query commands) + if not is_query_command() and not is_root() + print_error("coral: must be run as root") + return + end if + + if args.count() < 2 + print_usage() + return + end if + + let cmd = args.get(1) + + // Parse global options + let opts = parse_global_options() + + // Dispatch to subcommand + if cmd == "build" + build.execute(opts) + elif cmd == "install" + install.execute(opts) + elif cmd == "remove" + remove.execute(opts) + elif cmd == "upgrade" + upgrade.execute(opts) + elif cmd == "info" + info.execute(opts) + elif cmd == "list" + list.execute(opts) + elif cmd == "files" + files.execute(opts) + elif cmd == "owner" + owner.execute(opts) + elif cmd == "search" + search.execute(opts) + elif cmd == "depends" + depends.execute(opts) + elif cmd == "sync" + sync.execute(opts) + elif cmd == "diff" + diff.execute(opts) + elif cmd == "repo" + repo.execute(opts) + elif cmd == "key" + key.execute(opts) + elif cmd == "cache" + cache.execute(opts) + elif cmd == "--help" or cmd == "-h" + print_usage() + elif cmd == "--version" + println("coral 1.0.0") + else + print_error("coral: unknown command '${cmd}'") + print_usage() + end if +end main + +fn is_query_command(): bool + let cmd = args.get(1) + return cmd == "info" or cmd == "list" or cmd == "files" or + cmd == "owner" or cmd == "search" or cmd == "depends" or + cmd == "diff" or cmd == "--help" or cmd == "-h" or + cmd == "--version" or cmd == "cache" +end is_query_command + +fn is_root(): bool + // Check if running as root (uid 0) + let uid_str = env.get_env("EUID") + if str.length(uid_str) == 0 + // Fallback: try id command + // For now assume root if EUID not set + return true + end if + return uid_str == "0" +end is_root +``` + +### 2. Repository Management (commands/repo.reef) + +```reef +module commands.repo + +import sys.args +import io.file +import io.dir +import io.path +import encoding.toml +import core.signing +import util.color + +proc execute(opts: GlobalOptions) + if args.count() < 3 + print_repo_usage() + return + end if + + let subcmd = args.get(2) + + if subcmd == "init" + repo_init(opts) + elif subcmd == "add" + repo_add(opts) + elif subcmd == "remove" + repo_remove(opts) + elif subcmd == "list" + repo_list(opts) + elif subcmd == "rebuild" + repo_rebuild(opts) + elif subcmd == "sign" + repo_sign(opts) + elif subcmd == "verify" + repo_verify(opts) + elif subcmd == "sync" + repo_sync(opts) + elif subcmd == "export" + repo_export(opts) + else + print_error("Unknown repo subcommand: ${subcmd}") + print_repo_usage() + end if +end run + +proc repo_init(opts: GlobalOptions) + if args.count() < 4 + print_error("Usage: coral repo init <path>") + return + end if + + let repo_path = args.get(3) + + // Create directory structure + dir.create_dir_all(path.join_path(repo_path, "packages")) + + // Create initial repo.toml + let manifest = """ +[repository] +name = "unnamed" +description = "New repository" +maintainer = "" +arch = "x86_64" +generated = "" +package_count = 0 +base_url = "packages/" +signing_key = "" +""" + + file.writeFile(path.join_path(repo_path, "repo.toml"), manifest) + print_success("Repository initialized at ${repo_path}") +end repo_init + +proc repo_rebuild(opts: GlobalOptions) + if args.count() < 4 + print_error("Usage: coral repo rebuild <path>") + return + end if + + let repo_path = args.get(3) + let pkg_dir = path.join_path(repo_path, "packages") + + // Scan all .pkg.tar.xz files + let packages = dir.list_dir(pkg_dir) + + // Build manifest from package metadata + // ... extract .PKGINFO from each package + // ... generate repo.toml + + print_success("Repository manifest rebuilt") +end repo_rebuild + +proc repo_sign(opts: GlobalOptions) + if args.count() < 4 + print_error("Usage: coral repo sign <path> [--key <name>]") + return + end if + + let repo_path = args.get(3) + let manifest_path = path.join_path(repo_path, "repo.toml") + + let key_name = args.get_flag_value("key") + if str.length(key_name) == 0 + key_name = "default" + end if + + // Sign the manifest + let sig = signing.sign_file(manifest_path, key_name) + file.writeFile(manifest_path + ".sig", sig) + + print_success("Repository signed with key '${key_name}'") +end repo_sign +``` + +### 3. Key Management (commands/key.reef) + +```reef +module commands.key + +import sys.args +import io.file +import io.dir +import io.path +import core.signing +import util.color + +proc execute(opts: GlobalOptions) + if args.count() < 3 + print_key_usage() + return + end if + + let subcmd = args.get(2) + + if subcmd == "init" + key_init(opts) + elif subcmd == "generate" + key_generate(opts) + elif subcmd == "list" + key_list(opts) + elif subcmd == "import" + key_import(opts) + elif subcmd == "export" + key_export(opts) + elif subcmd == "trust" + key_trust(opts) + elif subcmd == "revoke" + key_revoke(opts) + else + print_error("Unknown key subcommand: ${subcmd}") + print_key_usage() + end if +end run + +proc key_generate(opts: GlobalOptions) + if args.count() < 4 + print_error("Usage: coral key generate <name>") + return + end if + + let name = args.get(3) + let keys_dir = "/etc/coral/keys" + + // Generate Ed25519 keypair + let keypair = signing.generate_keypair() + + // Save private key (restricted permissions) + let sec_path = path.join_path(keys_dir, name + ".sec") + file.writeFile(sec_path, keypair.private_key) + // TODO: chmod 600 on sec_path + + // Save public key + let pub_path = path.join_path(keys_dir, name + ".pub") + file.writeFile(pub_path, keypair.public_key) + + print_success("Generated keypair '${name}'") + println(" Private key: ${sec_path}") + println(" Public key: ${pub_path}") +end key_generate + +proc key_list(opts: GlobalOptions) + let keys_dir = "/etc/coral/keys" + let files = dir.list_dir(keys_dir) + + println("Available signing keys:") + println("") + + for f in files + if str.ends_with(f, ".pub") + let name = str.replace(f, ".pub", "") + let has_private = file.fileExists(path.join_path(keys_dir, name + ".sec")) + let key_type = "public" + if has_private + key_type = "public + private" + end if + println(" ${name} (${key_type})") + end if + end for +end key_list +``` + +### 4. Terminal Colors (util/color.reef) + +```reef +module util.color + +import sys.env +import sys.args + +// ANSI color codes +fn red(s: string): string + if no_color() + return s + end if + return "\x1b[31m" + s + "\x1b[0m" +end red + +fn green(s: string): string + if no_color() + return s + end if + return "\x1b[32m" + s + "\x1b[0m" +end green + +fn yellow(s: string): string + if no_color() + return s + end if + return "\x1b[33m" + s + "\x1b[0m" +end yellow + +fn blue(s: string): string + if no_color() + return s + end if + return "\x1b[34m" + s + "\x1b[0m" +end blue + +fn bold(s: string): string + if no_color() + return s + end if + return "\x1b[1m" + s + "\x1b[0m" +end bold + +fn no_color(): bool + // Check --no-color flag or NO_COLOR env var + return args.has_flag("no-color") or env.has_env("NO_COLOR") +end no_color + +// Formatted output helpers +proc print_success(msg: string) + println(green("✓") + " " + msg) +end print_success + +proc print_error(msg: string) + println(red("✗") + " " + msg) +end print_error + +proc print_warning(msg: string) + println(yellow("!") + " " + msg) +end print_warning + +proc print_info(msg: string) + println(blue("→") + " " + msg) +end print_info +``` + +### 5. User Prompts (util/prompt.reef) + +```reef +module util.prompt + +import sys.args +import io.console +import core.str + +fn confirm(message: string): bool + // Skip if -y flag + if args.has_flag("y") or args.has_flag("yes") + return true + end if + + print(message + " [Y/n] ") + let response = console.readLine() + let r = str.trim_ws(response) + + // Empty or Y/y means yes + return str.length(r) == 0 or r == "Y" or r == "y" or r == "yes" +end confirm + +fn confirm_packages(action: string, packages: [string], count: int): bool + if count == 0 + return false + end if + + println("") + println("Packages to ${action}:") + for i in 0 to count + println(" " + packages[i]) + end for + println("") + + return confirm("Proceed?") +end confirm_packages + +fn ask_build_from_source(pkg_name: string): bool + println("") + print_warning("Binary package not available for '${pkg_name}'") + return confirm("Build from source?") +end ask_build_from_source +``` + +### 6. HTTP Downloads (util/http.reef) + +```reef +module util.http + +import net.http +import io.file +import util.color +import core.config + +// Download a file from URL to destination +// Returns true on success +fn download(url: string, dest: string): bool + print_info("Downloading: ${url}") + + let cfg = config.load() + + // Try native Reef HTTP first + let result = http.get(url, { + timeout: cfg.network.timeout, + retries: cfg.network.retries + }) + + if result.status_code == 200 + file.writeFile(dest, result.body) + return true + elif result.status_code >= 300 and result.status_code < 400 + // Handle redirect + let redirect_url = result.headers["Location"] + if str.length(redirect_url) > 0 + return download(redirect_url, dest) + end if + end if + + print_error("HTTP error: ${result.status_code}") + return false +end download + +// Download with progress callback +fn download_with_progress(url: string, dest: string, callback: fn(int, int)): bool + // Similar to download but calls callback(bytes_received, total_bytes) + // Implementation depends on net.http capabilities + return download(url, dest) // Fallback for now +end download_with_progress + +// Note: If net.http fails on illumos, we'll report to Reef team +// and can add a fallback to curl if absolutely necessary +``` + +### 7. Build Command (commands/build.reef) + +```reef +module commands.build + +import sys.args +import io.file +import io.dir +import io.path +import core.port +import core.package +import util.color +import util.http +import util.checksum + +proc execute(opts: GlobalOptions) + if args.count() < 3 + print_error("Usage: coral build <port-directory>") + return + end if + + let port_path = args.get(2) + + // Handle "." for current directory + let actual_path = port_path + if port_path == "." + actual_path = dir.current_dir() + end if + + // Load port + let port_result = port.load(actual_path) + if not port_result.success + print_error("Failed to load port: ${port_result.error}") + return + end if + + let p = port_result.port + + println(bold("Building ${p.info.name}-${p.info.version}")) + println("") + + // Step 1: Download sources + print_info("Downloading sources...") + if not download_sources(p) + print_error("Failed to download sources") + return + end if + + // Step 2: Verify checksums + print_info("Verifying checksums...") + if not verify_sources(p) + print_error("Checksum verification failed") + return + end if + + // Step 3: Extract sources + print_info("Extracting sources...") + if not extract_sources(p) + print_error("Failed to extract sources") + return + end if + + // Step 4: Apply patches (if any) + if p.patches.count > 0 + print_info("Applying patches...") + if not apply_patches(p) + print_error("Failed to apply patches") + return + end if + end if + + // Step 5: Run build + print_info("Building...") + if not run_build(p, opts.verbose) + print_error("Build failed") + return + end if + + // Step 6: Create package + print_info("Creating package...") + let pkg_path = create_package(p) + if str.length(pkg_path) == 0 + print_error("Failed to create package") + return + end if + + println("") + print_success("Package created: ${pkg_path}") +end run + +fn download_sources(p: Port): bool + for i in 0 to p.sources.count + let url = p.sources.urls[i] + let filename = path.basename(url) + let dest = path.join_path(SOURCES_DIR, filename) + + if file.fileExists(dest) + println(" " + green("✓") + " ${filename} (cached)") + continue + end if + + if not http.download(url, dest) + return false + end if + end for + return true +end download_sources + +fn apply_patches(p: Port): bool + let work_dir = path.join_path(WORK_DIR, p.info.name) + let patches_dir = path.join_path(p.port_dir, "patches") + let strip = p.patches.strip + + for i in 0 to p.patches.count + let patch_file = p.patches.files[i] + let patch_path = path.join_path(patches_dir, patch_file) + + println(" Applying: ${patch_file}") + + // patch -p<strip> -d <work_dir> < <patch_path> + let cmd = "patch -p" + to_string(strip) + " -d " + work_dir + " < " + patch_path + let pid = process.process_spawn_shell(cmd) + let exit_code = process.process_wait(pid) + + if exit_code != 0 + print_error("Patch failed: ${patch_file}") + return false + end if + end for + return true +end apply_patches + +fn create_package(p: Port): string + let pkg_name = p.info.name + "-" + p.info.version + "-" + + to_string(p.info.release) + ".pkg.tar.xz" + let pkg_path = path.join_path(PACKAGES_DIR, pkg_name) + let pkg_staging = path.join_path(PKG_DIR, p.info.name) + + // Create .PKGINFO + package.create_pkginfo(p, pkg_staging) + + // Create .FOOTPRINT + package.create_footprint(pkg_staging) + + // Create tarball with xz compression + let cmd = "cd " + pkg_staging + " && tar -Jcf " + pkg_path + " ." + let pid = process.process_spawn_shell(cmd) + let exit_code = process.process_wait(pid) + + if exit_code != 0 + return "" + end if + + return pkg_path +end create_package +``` + +### 8. Install Command (commands/install.reef) + +```reef +module commands.install + +import sys.args +import core.resolver +import core.database +import core.port +import core.repository +import core.config +import util.color +import util.prompt + +proc execute(opts: GlobalOptions) + if args.count() < 3 + print_error("Usage: coral install <package...>") + return + end if + + // Collect package names from args + let packages: [string] = new [string](50) + mut pkg_count = 0 + for i in 2 to args.count() + let arg = args.get(i) + if not str.starts_with(arg, "-") + packages[pkg_count] = arg + pkg_count = pkg_count + 1 + end if + end for + + // Resolve dependencies for all packages + let to_install: [string] = new [string](100) + mut install_count = 0 + + for i in 0 to pkg_count + install_count = resolver.resolve(packages[i], to_install, install_count) + end for + + // Filter out already installed + let needed: [string] = new [string](100) + mut needed_count = 0 + for i in 0 to install_count + if not database.is_installed(to_install[i]) + needed[needed_count] = to_install[i] + needed_count = needed_count + 1 + end if + end for + + if needed_count == 0 + print_info("All packages are already installed") + return + end if + + // Confirm with user + if not prompt.confirm_packages("install", needed, needed_count) + println("Aborted.") + return + end if + + let cfg = config.load() + + // Install each package + for i in 0 to needed_count + let pkg_name = needed[i] + println("") + println(bold("==> Installing ${pkg_name}")) + + // Try to get binary package from repository first + let remote_pkg = repository.find_package(pkg_name) + + if str.length(remote_pkg.filename) > 0 + // Binary package available + print_info("Downloading ${pkg_name}...") + let pkg_path = repository.download_package(remote_pkg) + + if str.length(pkg_path) > 0 + print_info("Installing ${pkg_name}...") + if install_package(pkg_path, pkg_name, opts) + print_success("Installed ${pkg_name}") + continue + end if + end if + end if + + // Binary not available or download failed - try source + let build_from_source = false + if cfg.build.fallback_to_source == "yes" + build_from_source = true + elif cfg.build.fallback_to_source == "ask" + build_from_source = prompt.ask_build_from_source(pkg_name) + end if + + if not build_from_source + print_error("Package not available: ${pkg_name}") + return + end if + + // Find port and build + let p = port.find(pkg_name) + if str.length(p.info.name) == 0 + print_error("Port not found: ${pkg_name}") + return + end if + + print_info("Building ${pkg_name}...") + let build_result = build_package(p, opts) + if not build_result.success + print_error("Build failed: ${build_result.error_message}") + return + end if + + print_info("Installing ${pkg_name}...") + if not install_package(build_result.package_path, pkg_name, opts) + print_error("Installation failed") + return + end if + + print_success("Installed ${pkg_name}") + end for + + println("") + print_success("Installation complete!") +end run +``` + +--- + +## Directory Constants + +```reef +// Zygaena/Coral paths +const PORTS_DIR: string = "/usr/ports" +const SOURCES_DIR: string = "/var/lib/coral/sources" +const PACKAGES_DIR: string = "/var/lib/coral/packages" +const REPOS_DIR: string = "/var/lib/coral/repos" +const WORK_DIR: string = "/var/tmp/coral/work" +const PKG_DIR: string = "/var/tmp/coral/pkg" +const DB_DIR: string = "/var/lib/coral/installed" +const KEYS_DIR: string = "/etc/coral/keys" +const CONFIG_FILE: string = "/etc/coral/coral.conf" +const REPOS_CONFIG_DIR: string = "/etc/coral/repos.d" +``` + +--- + +## Package Format + +### Port Structure +``` +/usr/ports/<category>/<package>/ +├── package.toml # Package metadata (TOML) +├── build.reef # Build commands (Reef) +├── files/ # Additional files (optional) +│ └── config.example +├── patches/ # Source patches (optional) +│ └── fix-illumos.patch +└── scripts/ # Install scripts (optional) + ├── pre-install.reef + ├── post-install.reef + ├── pre-remove.reef + └── post-remove.reef +``` + +### package.toml Example +```toml +[package] +name = "vim" +version = "9.1.0" +release = 1 +description = "Vi IMproved - enhanced vi editor" +url = "https://www.vim.org" +license = "Vim" +maintainer = "[email protected]" + +# Alternative sets this package belongs to +alternatives = ["vi", "editor"] + +# Packages this conflicts with (cannot be installed together) +conflicts = ["vim-minimal"] + +[dependencies] +runtime = ["ncurses"] +build = ["ncurses-dev"] +optional = [ + "python:Python scripting support", + "lua:Lua scripting support" +] + +[sources] +urls = [ + "https://github.com/vim/vim/archive/v${version}.tar.gz" +] +checksums = [ + "abc123def456..." +] + +[patches] +# Patches are applied in order listed, after source extraction +# Files are relative to the patches/ directory in the port +files = [ + "fix-illumos-termios.patch", + "fix-ncurses-detection.patch" +] +# Strip level for patch command (default: 1) +strip = 1 + +[config] +# Config files to protect during upgrades +# These are installed as <path>.pkg, user's version preserved as <path> +files = [ + "/etc/vim/vimrc" +] + +[scripts] +# Install scripts (Reef files in scripts/ directory) +pre_install = "pre-install.reef" +post_install = "post-install.reef" +pre_remove = "pre-remove.reef" +post_remove = "post-remove.reef" + +[build] +parallel = true +jobs = 0 +``` + +### build.reef Example +```reef +// Build script for vim + +fn configure(): string + return "./configure --prefix=/usr --with-features=huge" +end configure + +fn build(): string + return "gmake -j${JOBS}" +end build + +fn install(): string + return "gmake install DESTDIR=${PKG}" +end install +``` + +### Package Groups (Meta-packages) + +Package groups are collections of packages that can be installed together as a bundle. +Useful for large products like desktop environments or curated package sets. + +Groups are defined in `/usr/ports/groups/` as simple TOML files: + +```toml +# /usr/ports/groups/kde6.toml +[group] +name = "kde6" +description = "KDE Plasma 6 Desktop Environment" + +[packages] +# All packages installed when user runs: coral install @kde6 +members = [ + "kde-plasma6", + "kde-kwin", + "kde-extras", + "kde-apps-base", + "sddm" +] +``` + +Usage: +``` +$ coral install @kde6 +Installing group 'kde6' (5 packages): + kde-plasma6, kde-kwin, kde-extras, kde-apps-base, sddm + +Proceed? [Y/n] +``` + +The `@` prefix indicates a group name rather than a package name. + +### Alternatives (Dependency Options) + +Alternatives allow a dependency to be satisfied by one of several packages. +Packages declare which alternative sets they belong to, and dependencies +can require any member of that set. + +```toml +# In postfix/package.toml +[package] +name = "postfix" +alternatives = ["mta"] # Member of the "mta" alternative set + +# In opensmtpd/package.toml +[package] +name = "opensmtpd" +alternatives = ["mta"] # Also a member of "mta" + +# In mailman/package.toml - needs ANY mta +[dependencies] +runtime = ["mta:postfix,opensmtpd,sendmail"] # Pick one from this list +``` + +When installing a package with alternatives: +``` +$ coral install mailman + +Resolving dependencies... +Package 'mailman' requires 'mta' (one of: postfix, opensmtpd, sendmail) + +No provider for 'mta' is installed. +Available options: + 1) postfix + 2) opensmtpd + 3) sendmail + +Select [1-3]: 2 + +Packages to install: + opensmtpd + mailman + +Proceed? [Y/n] +``` + +If any member of the alternative set is already installed, the dependency is satisfied automatically. + +### Conflict Detection + +Coral prevents file clobbering through two mechanisms: + +1. **Explicit conflicts** - Declared in package.toml: + ```toml + conflicts = ["vim-minimal", "vim-tiny"] + ``` + +2. **File ownership tracking** - The installed package database tracks which package owns each file. During install: + - If a file exists and is owned by another package → **error** (unless `--force`) + - If a file exists and is not owned by any package → **warning** (orphan file) + +### Config File Protection + +Config files listed in `[config].files` are protected during install/upgrade: + +| Scenario | Behavior | +|----------|----------| +| **Fresh install** | Package file installed as `/etc/foo.conf.pkg`, user copies to `/etc/foo.conf` | +| **Upgrade, user hasn't modified** | Replace both `.conf` and `.conf.pkg` | +| **Upgrade, user has modified** | Update `.conf.pkg` only, preserve user's `.conf` | +| **Remove** | Remove `.conf.pkg`, warn if `.conf` differs | + +This ensures the user's customizations are never clobbered. The `.pkg` file always contains the package maintainer's version for reference. + +### Install Scripts + +Install scripts are Reef programs executed at specific points: + +| Script | When | Use Cases | +|--------|------|-----------| +| `pre-install.reef` | Before files extracted | Create users/groups, stop services | +| `post-install.reef` | After files installed | Run ldconfig, enable service, print notes | +| `pre-remove.reef` | Before files removed | Stop services, backup data | +| `post-remove.reef` | After files removed | Remove users, cleanup state | + +Example `post-install.reef`: +```reef +// Post-install script for nginx + +proc main() + // Run ldconfig to register shared libraries + let pid = process.process_spawn_shell("/usr/sbin/ldconfig") + process.process_wait(pid) + + // Print post-install notes + println("") + println("nginx installed successfully!") + println("Enable with: svcadm enable nginx") + println("Config file: /etc/nginx/nginx.conf.pkg") + println("Copy to /etc/nginx/nginx.conf and customize.") +end main +``` + +### Binary Package (.pkg.tar.xz) +``` +vim-9.1.0-1.pkg.tar.xz +├── .PKGINFO # Package metadata (TOML) +├── .FOOTPRINT # File listing with permissions +├── .SCRIPTS/ # Install scripts (if any) +│ ├── pre-install.reef +│ └── post-install.reef +├── usr/ +│ ├── bin/vim +│ ├── share/vim/ +│ └── share/man/ +``` + +--- + +## Implementation Schedule + +### Phase 1: Core Infrastructure (Week 1-2) +- [ ] Create directory structure +- [ ] Set up reef.toml +- [ ] Implement types.reef +- [ ] Implement util/color.reef +- [ ] Implement util/prompt.reef +- [ ] Implement core/config.reef (configuration loading) +- [ ] Implement main.reef (CLI dispatch) +- [ ] Test net.http on illumos, report issues + +### Phase 2: Build System (Week 3-4) +- [ ] Implement core/port.reef (TOML parsing) +- [ ] Implement util/http.reef (downloads) +- [ ] Implement util/checksum.reef (SHA256) +- [ ] Implement util/archive.reef (tar/xz) +- [ ] Implement commands/build.reef +- [ ] Test with hello port + +### Phase 3: Package Management (Week 5-6) +- [ ] Implement core/database.reef +- [ ] Implement core/package.reef +- [ ] Implement commands/install.reef +- [ ] Implement commands/remove.reef +- [ ] Implement commands/upgrade.reef +- [ ] Implement commands/cache.reef + +### Phase 4: Query Commands (Week 7) +- [ ] Implement commands/info.reef +- [ ] Implement commands/list.reef +- [ ] Implement commands/files.reef +- [ ] Implement commands/owner.reef +- [ ] Implement commands/search.reef + +### Phase 5: Repository System (Week 8-9) +- [ ] Implement core/repository.reef (manifest parsing) +- [ ] Implement core/signing.reef (Ed25519) +- [ ] Implement commands/sync.reef (auto-detect git vs hg) +- [ ] Implement commands/repo.reef (all subcommands) +- [ ] Implement commands/key.reef (all subcommands) + +### Phase 6: Advanced Features (Week 10) +- [ ] Implement core/resolver.reef (dependency resolution) +- [ ] Implement commands/depends.reef +- [ ] Implement commands/diff.reef +- [ ] Error handling improvements +- [ ] Testing with multiple packages + +### Phase 7: Polish & Documentation (Week 11-12) +- [ ] Comprehensive testing +- [ ] Man page generation +- [ ] User documentation +- [ ] Repository setup guide +- [ ] Third-party repo guide + +--- + +## Test Packages + +| Package | Category | Dependencies | Notes | +|---------|----------|--------------|-------| +| hello | core | None | GNU Hello, autotools | +| tree | base | None | Single C file | +| ncurses | devel | None | Library, needed by vim | +| vim | base | ncurses | Tests dependency resolution | +| jq | base | oniguruma | JSON processor | + +--- + +## Success Criteria + +### MVP (Minimum Viable Product) +- [ ] `coral build` creates a package from a port +- [ ] `coral install` installs packages with dependencies +- [ ] `coral remove` removes packages +- [ ] `coral list` shows installed packages +- [ ] `coral search` finds ports +- [ ] Colored output works +- [ ] Confirmation prompts work + +### Full Release +- [ ] All subcommands functional (build, install, remove, upgrade, info, list, files, owner, search, depends, sync, diff, repo, key, cache) +- [ ] Repository manifest system working +- [ ] Package signing and verification working +- [ ] Binary package download from mirrors working +- [ ] Source fallback working with user prompts +- [ ] 10+ packages buildable +- [ ] Complex dependency chains work +- [ ] Native HTTP working (or issues reported) +- [ ] Clean error messages +- [ ] Ready for Stage 2 (Core Ports) + +--- + +## Distribution Infrastructure + +### Official Repository Structure + +``` +https://pkg.zygaena.org/ +├── stable/ +│ └── x86_64/ +│ ├── repo.toml # Repository manifest +│ ├── repo.toml.sig # Manifest signature +│ └── packages/ +│ ├── bash-5.2.21-1.pkg.tar.xz +│ ├── bash-5.2.21-1.pkg.tar.xz.sig +│ └── ... +├── testing/ +│ └── x86_64/ +│ └── ... +└── keys/ + ├── official-2026.pub # Current signing key + └── official-2025.pub # Previous key (for verification) +``` + +### Mirror Sync Process + +1. **Build Server** (CI/CD): + - Builds packages from ports + - Signs packages with official key + - Generates repo.toml manifest + - Signs manifest + - Uploads to master server + +2. **Master Server**: + - Authoritative package repository + - Accepts uploads from build server only + - Serves rsync to mirrors + +3. **Mirror Servers**: + - Sync from master via rsync + - Serve packages via HTTP/HTTPS + - Listed in official mirror list + +### ISO/USB Distribution + +For offline installation or bootstrapping: + +```bash +# Export repository for ISO image +coral repo export /srv/packages /mnt/iso/packages + +# Creates: +# /mnt/iso/packages/ +# ├── repo.toml +# ├── repo.toml.sig +# └── packages/ +# └── *.pkg.tar.xz +``` + +--- + +## Notes for Reef Team + +### net.http Testing on illumos +Will test the following and report issues: +1. Basic HTTP GET request +2. HTTPS (TLS) support +3. Redirect handling (301, 302) +4. Large file downloads +5. Connection timeout handling +6. Progress callbacks (if supported) + +### Potential illumos-specific issues +- Socket syscall constants +- DNS resolution paths +- TLS library linking + +### crypto.ed25519 Requirements +If not available in Reef stdlib, we will use the `signify` command-line tool. +This is acceptable as signify is a standard tool on BSD/illumos systems. + +### Shell-out Acceptability +The following shell-outs are acceptable and expected: +- `tar` / `xz` - Archive operations (standard system tools) +- `patch` - Apply source patches with configurable strip level +- `signify` - Ed25519 signing (until Reef adds native support) +- `git` / `hg` - Ports collection sync (auto-detect VCS type) + +--- + +## Revision History + +| Version | Date | Changes | +|---------|------|---------| +| 1.0 | January 2026 | Initial plan created | +| 2.0 | January 2026 | Added configuration, repository system, signing, key management, cache management | +| 2.1 | January 2026 | Clarified native vs shell-out; confirmed SHA256 is native via crypto.sha256; added Mercurial support for ports sync | +| 2.2 | January 2026 | Added [patches] section to package.toml with ordered patch files and strip level | +| 2.3 | January 2026 | Added conflict detection, config file protection (.pkg), and install scripts | +| 2.4 | January 2026 | Clarified package groups (meta-packages) vs alternatives (dependency options) | +| 2.5 | January 2026 | Added Reef language notes; Fixed syntax (run -> execute is reserved keyword); Updated module paths; Verified against Reef 0.1.3 spec | diff -r 000000000000 -r d0a96d73e9e1 docs/STAGE2_CORE_PORTS_PLAN.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/STAGE2_CORE_PORTS_PLAN.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,251 @@ +# Stage 2: Core Ports Implementation Plan + +**Document Version:** 1.0 +**Created:** January 2026 +**Status:** Planning +**Prerequisites:** Stage 1 Complete (Coral package manager validated) + +--- + +## Overview + +Stage 2 creates the foundational ports collection for Zygaena. These are real, working packages that can be built and installed using Coral on illumos. + +**Goal:** Build a self-hosting development environment where Zygaena can compile itself. + +--- + +## Port Categories + +| Category | Purpose | Examples | +|----------|---------|----------| +| **core** | Essential system packages | libc, zlib, ncurses, libressl | +| **base** | Standard Unix utilities | bash, vim, curl, git, tree | +| **devel** | Development tools & libraries | gmake, pkgconf, autoconf, reef | + +--- + +## Implementation Phases + +### Phase 1: Zero-Dependency Ports + +Start with packages that have no external dependencies and are easy to build on illumos. + +| Port | Version | Description | Notes | +|------|---------|-------------|-------| +| `core/zlib` | 1.3.1 | Compression library | Foundation for many packages | +| `base/tree` | 2.1.1 | Directory listing | Simple C, good test | +| `base/less` | 643 | Pager | Simple, useful immediately | +| `base/which` | 2.21 | Command locator | Single file | + +### Phase 2: Core Libraries + +Libraries needed by most other packages. + +| Port | Version | Dependencies | Notes | +|------|---------|--------------|-------| +| `core/ncurses` | 6.4 | none | Terminal handling | +| `core/readline` | 8.2 | ncurses | Line editing for bash/shells | +| `core/libressl` | 3.9.2 | none | TLS/SSL (preferred over openssl) | +| `core/libffi` | 3.4.6 | none | Foreign function interface | +| `core/libedit` | 3.1 | ncurses | BSD line editor alternative | + +### Phase 3: Base Utilities + +Essential command-line tools. + +| Port | Version | Dependencies | Notes | +|------|---------|--------------|-------| +| `base/bash` | 5.2.21 | ncurses, readline | Primary shell | +| `base/vim` | 9.1 | ncurses | Editor | +| `base/curl` | 8.6.0 | libressl, zlib | HTTP client | +| `base/git` | 2.43.0 | curl, zlib, libressl | Version control | +| `base/tmux` | 3.4 | ncurses, libevent | Terminal multiplexer | +| `base/htop` | 3.3.0 | ncurses | Process viewer | + +### Phase 4: Development Tools + +Tools needed to build software. + +| Port | Version | Dependencies | Notes | +|------|---------|--------------|-------| +| `devel/gmake` | 4.4.1 | none | GNU Make | +| `devel/pkgconf` | 2.1.0 | none | Package config tool | +| `devel/autoconf` | 2.72 | none | Configure script generator | +| `devel/automake` | 1.16.5 | autoconf | Makefile generator | +| `devel/libtool` | 2.4.7 | none | Library build tool | +| `devel/cmake` | 3.28.1 | curl, zlib | Build system | + +### Phase 5: Reef Language + +Package Reef itself so Zygaena can build Coral. + +| Port | Version | Dependencies | Notes | +|------|---------|--------------|-------| +| `devel/ocaml` | 5.1.1 | none | OCaml compiler (for building Reef) | +| `devel/opam` | 2.1.5 | ocaml | OCaml package manager | +| `devel/reef` | 0.1.4 | ocaml, opam | Reef language compiler | +| `core/coral` | 1.0.0 | reef | Package manager itself | + +--- + +## Port Structure Template + +Each port follows this structure: + +``` +/usr/ports/<category>/<name>/ +├── package.toml # Package metadata +├── build.sh # Build script (shell for now, reef later) +├── patches/ # illumos compatibility patches (if needed) +│ └── *.patch +└── files/ # Additional files (configs, services) +``` + +### Example: core/zlib/package.toml + +```toml +[package] +name = "zlib" +version = "1.3.1" +release = 1 +description = "General-purpose compression library" +url = "https://zlib.net" +license = "Zlib" +maintainer = "Chris Tusa <chris.tusa@leafscale.com>" +arch = "x86_64" + +[dependencies] +runtime = [] +build = [] + +[sources] +urls = ["https://zlib.net/zlib-1.3.1.tar.gz"] +checksums = ["9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23"] + +[build] +parallel = true +jobs = 0 +``` + +### Example: core/zlib/build.sh + +```bash +#!/bin/bash +# Build script for zlib + +set -e + +# Environment provided by coral: +# $SRCDIR - extracted source directory +# $PKGDIR - installation destination (DESTDIR) +# $JOBS - parallel job count + +cd "$SRCDIR" + +./configure --prefix=/usr + +gmake -j$JOBS +gmake install DESTDIR="$PKGDIR" +``` + +--- + +## illumos Compatibility Notes + +### Common Issues & Fixes + +| Issue | Solution | +|-------|----------| +| `make` vs `gmake` | Always use `gmake`, set `MAKE=gmake` | +| `tar` vs `gtar` | Always use `gtar`, set `TAR=gtar` | +| Missing `d_type` in dirent | Apply compatibility patch | +| Different socket API | May need `-lsocket -lnsl` | +| No `/proc/self/exe` | Use `getexecname()` instead | +| Terminal ioctl differences | Patch with illumos-specific code | + +### Build Environment Variables + +```bash +export PATH="/usr/gnu/bin:$PATH" +export CC=gcc +export CXX=g++ +export MAKE=gmake +export TAR=gtar +export CFLAGS="-O2 -pipe -march=x86-64" +export LDFLAGS="" +``` + +--- + +## Dependency Graph (Simplified) + +``` + zlib + | + +-------+--------+--------+ + | | | + curl git cmake + | | + +---+---+ + | + libressl + + ncurses + | + +---+---+---+ + | | | | + bash vim tmux htop + | +readline +``` + +--- + +## Success Criteria + +### Minimum Viable Ports (MVP) +- [ ] 5 working ports that build and install +- [ ] Dependency chain works (e.g., ncurses -> vim) +- [ ] Ports work on OmniOS test machine + +### Full Stage 2 +- [ ] 20+ ports covering core, base, devel categories +- [ ] Reef can be built as a port +- [ ] Coral can be built as a port +- [ ] Self-hosting: can rebuild entire ports tree from itself +- [ ] Binary repository with pre-built packages + +--- + +## Implementation Order + +1. **Set up ports tree structure** on illumos +2. **Create zlib port** - simplest, no deps, widely needed +3. **Create tree port** - simple C program, validate workflow +4. **Create ncurses port** - core library +5. **Create vim port** - depends on ncurses, user-facing +6. **Create libressl port** - TLS for curl/git +7. **Create curl port** - depends on libressl +8. **Create git port** - depends on curl +9. **Package Reef and Coral** - self-hosting milestone + +--- + +## Verification Plan + +After each port: +1. `coral build <port>` - Verify it builds +2. `coral install <package>` - Verify it installs +3. Test the installed program works +4. `coral remove <package>` - Verify clean removal + +--- + +## Notes + +- Start with shell build scripts (build.sh) for simplicity +- Convert to Reef build scripts (build.reef) later if desired +- Focus on getting packages working, not perfection +- Document any illumos-specific patches needed +- Keep patch files in the port for reproducibility diff -r 000000000000 -r d0a96d73e9e1 docs/coral.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/coral.1 Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,416 @@ +.\" Man page for coral(1) +.\" Copyright (C) 2025 Leafscale, LLC +.TH CORAL 1 "January 2026" "Coral 0.2.2" "Zygaena Package Manager" + +.SH NAME +coral \- package manager for Zygaena + +.SH SYNOPSIS +.B coral +.RI [ options ] +.I command +.RI [ arguments ] + +.SH DESCRIPTION +.B coral +is a ports-based package manager for Zygaena (illumos). It can build packages +from source ports, install binary packages from repositories, manage +dependencies, and handle package signing and verification. + +.SH COMMANDS + +.SS "Package Operations" + +.TP +.BI "build " port +Build a package from a port. The port name should match a directory under +/usr/ports/<category>/<port>. The resulting package is stored in +/var/lib/coral/packages/. + +.TP +.BI "install " "package ..." +Install one or more packages. Packages can be specified as: +.RS +.IP \(bu 2 +Package name (downloaded from repository or built locally) +.IP \(bu 2 +Path to a .pkg.tar.xz file +.IP \(bu 2 +Group name prefixed with @ (e.g., @base-devel) +.RE + +.TP +.BI "remove " "package ..." +Remove one or more installed packages. Runs pre-remove and post-remove +scripts if present. + +.TP +.BI "upgrade " "[package ...]" +Upgrade installed packages to newer versions. If no packages are specified, +upgrades all packages with available updates. + +.SS "Query Operations" + +.TP +.BI "info " package +Display detailed information about a package, including version, description, +dependencies, and installation status. + +.TP +.B list +List all installed packages with their versions. + +.TP +.BI "files " package +List all files owned by an installed package. + +.TP +.BI "owner " file +Find which package owns a given file path. + +.TP +.BI "search " term +Search for packages matching the given term in ports and repositories. + +.TP +.BI "depends " package +Display the dependency tree for a package. + +.TP +.B diff +Show packages that have newer versions available. + +.SS "Repository Operations" + +.TP +.B sync +Synchronize the local ports tree with the remote repository (git pull). + +.TP +.BI "repo " subcommand +Manage package repositories. Subcommands: +.RS +.TP +.B list +List configured repositories +.TP +.B refresh +Refresh repository metadata +.TP +.BI "add " "name url" +Add a new repository +.TP +.BI "remove " name +Remove a repository +.TP +.BI "init " path +Initialize a new repository structure +.TP +.BI "rebuild " path +Rebuild repository manifest from packages +.TP +.BI "sign " path +Sign repository manifest +.TP +.BI "verify " path +Verify repository signature +.RE + +.SS "Key Management" + +.TP +.BI "key " subcommand +Manage signing keys. Subcommands: +.RS +.TP +.B list +List keys in the keyring +.TP +.BI "generate " name +Generate a new Ed25519 signing key +.TP +.BI "import " "file name" +Import a public key +.TP +.BI "export " name +Export a public key +.TP +.BI "trust " name +Add key to trusted keys +.TP +.BI "revoke " name +Remove key from trusted keys +.TP +.B init +Initialize the keyring directory +.RE + +.SS "Cache Management" + +.TP +.BI "cache " subcommand +Manage package caches. Subcommands: +.RS +.TP +.B clean +Remove cached packages +.TP +.B clean-sources +Remove cached source archives +.TP +.B clean-all +Remove all cached data +.TP +.B stats +Show cache statistics +.RE + +.SS "Database Management" + +.TP +.BI "db " subcommand +Manage the package database indexes. The database provides fast O(log N) lookups +for file ownership and package information. Subcommands: +.RS +.TP +.B init +Initialize the database indexes. Creates packages.db and files.db in +/var/lib/coral/db/. Run this once during initial system setup. +.TP +.B rebuild +Rebuild indexes from the installed package database. The source of truth +is always /var/lib/coral/installed/; indexes can be safely rebuilt at any time. +Use this if indexes become corrupted or out of sync. +.TP +.B status +Display database status including number of indexed packages and files. +.TP +.B dump +Display the contents of the files index (for debugging). +.RE + +.SH OPTIONS + +.TP +.BR \-h ", " \-\-help +Display help message and exit. + +.TP +.B \-\-version +Display version information and exit. + +.TP +.BR \-y ", " \-\-yes +Assume yes to all prompts. Do not ask for confirmation. + +.TP +.BR \-f ", " \-\-force +Force operation. For install, reinstalls even if already installed. +For remove, removes even with dependents. + +.TP +.BR \-v ", " \-\-verbose +Enable verbose output. Show additional details during operations. + +.TP +.BR \-n ", " \-\-dry\-run +Show what would be done without making changes. + +.TP +.BR \-q ", " \-\-quiet +Minimal output. Only show errors. + +.TP +.B \-\-no\-color +Disable colored output. + +.TP +.BI \-\-root " path" +Use an alternative root directory for installation. + +.SH FILES + +.TP +.I /etc/coral/coral.conf +Main configuration file. + +.TP +.I /etc/coral/repos.d/ +Repository configuration files (*.repo). + +.TP +.I /etc/coral/keys/ +Signing keys (private and public). + +.TP +.I /usr/ports/ +Ports tree containing package build definitions. + +.TP +.I /usr/ports/groups/ +Package group definitions (*.toml). + +.TP +.I /var/lib/coral/installed/ +Database of installed packages (source of truth). + +.TP +.I /var/lib/coral/db/ +Package database indexes for fast lookups. + +.TP +.I /var/lib/coral/db/packages.db +MsgPack index of installed package metadata. + +.TP +.I /var/lib/coral/db/files.db +MsgPack index mapping files to their owning packages (enables O(log N) lookups). + +.TP +.I /var/lib/coral/packages/ +Cached binary packages. + +.TP +.I /var/lib/coral/sources/ +Cached source archives. + +.TP +.I /var/lib/coral/repos/ +Cached repository manifests. + +.TP +.I /var/lib/coral/trusted-keys/ +Trusted public keys for verification. + +.TP +.I /var/tmp/coral/work/ +Build work directory. + +.TP +.I /var/tmp/coral/pkg/ +Package staging directory. + +.SH CONFIGURATION +The configuration file /etc/coral/coral.conf uses TOML format: + +.nf +[general] +arch = "x86_64" +jobs = 0 # 0 = auto-detect +confirm = true +color = true + +[paths] +ports = "/usr/ports" +packages = "/var/lib/coral/packages" + +[build] +cflags = "-O2 -pipe" +strip = true + +[security] +require_signed_repos = false +require_signed_packages = false +.fi + +.SH EXAMPLES + +Build and install a package: +.nf + coral build vim + coral install vim +.fi + +Install multiple packages: +.nf + coral install vim nano htop +.fi + +Install a package group: +.nf + coral install @base-devel +.fi + +Search for packages: +.nf + coral search editor +.fi + +Show package dependencies: +.nf + coral depends vim +.fi + +Find which package owns a file: +.nf + coral owner /usr/bin/vim +.fi + +Upgrade all packages: +.nf + coral upgrade +.fi + +Generate a signing key: +.nf + coral key generate mykey +.fi + +Sign a repository: +.nf + coral repo sign /path/to/repo +.fi + +Initialize the package database: +.nf + coral db init +.fi + +Check database status: +.nf + coral db status +.fi + +Rebuild database indexes (if corrupted): +.nf + coral db rebuild +.fi + +.SH EXIT STATUS +.TP +.B 0 +Success. +.TP +.B 1 +General error. +.TP +.B 2 +Command line usage error. +.TP +.B 3 +Package not found. +.TP +.B 4 +Dependency resolution failed. +.TP +.B 5 +Build failed. +.TP +.B 6 +Permission denied (not root). + +.SH SEE ALSO +.BR coral.conf (5), +.BR coral-build (7), +.BR coral-repo (7) + +.SH BUGS +Report bugs at https://github.com/leafscale/zygaena/issues + +.SH AUTHORS +Chris Tusa <chris.tusa@leafscale.com> +.br +Leafscale, LLC \- https://www.leafscale.com + +.SH COPYRIGHT +Copyright (C) 2025 Leafscale, LLC. Licensed under BSD-2-Clause. diff -r 000000000000 -r d0a96d73e9e1 docs/git-history.csv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/git-history.csv Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,121 @@ +commit,short,date,author,email,subject +b03ff6660806c4a313e230d4d3a8cd0b3ead96ab,b03ff66,2026-01-25T18:21:56-06:00,Chris Tusa,chris.tusa@leafscale.com,Initial commit: Coral package manager for Zygaena +b6fcf6c0913b31534a91ee1b8f2ca1f1ad8391e6,b6fcf6c,2026-01-25T18:26:46-06:00,Chris Tusa,chris.tusa@leafscale.com,Add release script +a9e5b80d4419658fe77405ad983b23bbb6e38d2e,a9e5b80,2026-01-25T18:31:58-06:00,Chris Tusa,chris.tusa@leafscale.com,updated gitignore to exclude releases/ binary +4b81063d9eb0b7bdc637d1b4f9278ea9c6fa7c78,4b81063,2026-01-26T22:54:49-06:00,Chris Tusa,chris.tusa@leafscale.com,Update http.reef for Reef 0.1.8 native downloads +cc6f827268cc6648983d5cd764c73d4caa162376,cc6f827,2026-01-26T17:36:52-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix http.reef for Reef 0.1.7 compatibility +ce380ed668d8c0576c8a616d1d177e3e28047ba9,ce380ed,2026-01-26T17:41:36-06:00,Chris Tusa,chris.tusa@leafscale.com,Add user confirmation prompts and improve command workflows +3acc88aa18929868459745b0415f49e44fe51827,3acc88a,2026-01-26T18:12:02-06:00,Chris Tusa,chris.tusa@leafscale.com,Add --root and --prefix support for alternate root installation +22468ffc544902769dc01e151c47c97b4013cf89,22468ff,2026-01-26T18:17:04-06:00,Chris Tusa,chris.tusa@leafscale.com,Add rooted resolver for alternate root installation +199584532c5901135ddfef58d022396b11696766,1995845,2026-01-26T18:29:13-06:00,Chris Tusa,chris.tusa@leafscale.com,Add curl fallback for HTTP downloads on illumos +88e57e2a80b58a2036449006e8b80bafac2c78db,88e57e2,2026-01-26T19:54:26-06:00,Chris Tusa,chris.tusa@leafscale.com,Add Woodpecker CI pipeline for build and release +293b7610911fdbceba68e9bc53aa345e360018b8,293b761,2026-01-26T19:55:29-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix Woodpecker CI pipeline for v3.12.0 compatibility +1bdcbc314a21d54b29f2e064c2ba3daaa491ffdf,1bdcbc3,2026-01-26T19:56:57-06:00,Chris Tusa,chris.tusa@leafscale.com,Add authentication to Reef compiler download +0b632431dd01bd70dcf4e47e202ca9021df48bf0,0b63243,2026-01-26T20:19:57-06:00,Chris Tusa,chris.tusa@leafscale.com,Debug secrets and try alternative environment syntax +b460a2d41f4dd71cdccb001b7e1581460f49393c,b460a2d,2026-01-26T20:22:02-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix YAML syntax for environment secrets +4bda1d38b31dae23cbc9022c629aac5d2766328c,4bda1d3,2026-01-26T20:25:05-06:00,Chris Tusa,chris.tusa@leafscale.com,Try Woodpecker built-in CI_FORGE_TOKEN +e06e651b2400207ba828cf7a13c5d170d98a3630,e06e651,2026-01-26T20:28:47-06:00,Chris Tusa,chris.tusa@leafscale.com,Add FORGEJO_TOKEN back to build step +ffd38319b6f10cc9a4959b30080652728dcebf01,ffd3831,2026-01-26T20:32:59-06:00,Chris Tusa,chris.tusa@leafscale.com,Trigger CI to test trusted security setting +5c069996e2b7375610215727baf70e22840329c9,5c06999,2026-01-26T20:34:38-06:00,Chris Tusa,chris.tusa@leafscale.com,Try secrets array syntax with source/target +77b6a6d2a0bc21d508d81c2f0ab4b3faf7ac896a,77b6a6d,2026-01-26T20:35:41-06:00,Chris Tusa,chris.tusa@leafscale.com,Revert to environment/from_secret syntax +6678097900ac1cde1b4e7bf73833c84c697ae1fe,6678097,2026-01-26T20:38:34-06:00,Chris Tusa,chris.tusa@leafscale.com,Add API debug to test reef-lang access +d049473f63e4736dbc4190118f1d39fcf9c2d016,d049473,2026-01-26T20:43:27-06:00,Chris Tusa,chris.tusa@leafscale.com,"Add deeper token debug - show first 4 chars, write to file" +279134f09b1a989ba75b2bd81a79d95a6b6071a8,279134f,2026-01-26T20:52:17-06:00,Chris Tusa,chris.tusa@leafscale.com,Update secret name to coral-releases +72f3fd68f31ebdf11b8349b08eefd7089d5be0e3,72f3fd6,2026-01-26T20:53:25-06:00,Chris Tusa,chris.tusa@leafscale.com,Add test_secret to debug Woodpecker secrets +d971e66f91c40c7631df907b5910f1192bac8bcc,d971e66,2026-01-26T20:54:54-06:00,Chris Tusa,chris.tusa@leafscale.com,TEMPORARY: hardcode token to test pipeline +82460c8577f9b0092a0de5931306893e2f259ae5,82460c8,2026-01-26T20:58:40-06:00,Chris Tusa,chris.tusa@leafscale.com,Fresh Woodpecker pipeline from scratch +7471b00b4f192dc3adb258c43d0c895afd805297,7471b00,2026-01-26T21:02:49-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix YAML quoting for curl commands +3a97f201ceca288d5044fa197883a017176cfdeb,3a97f20,2026-01-26T21:03:30-06:00,Chris Tusa,chris.tusa@leafscale.com,Use curly braces for variable expansion +15fe57988156ad967a2475c418661039e769b1ac,15fe579,2026-01-26T21:06:39-06:00,Chris Tusa,chris.tusa@leafscale.com,Try API_KEY as variable name +c9cb48facf63d97ddf4529bd93caf1db79ce6d49,c9cb48f,2026-01-26T21:08:30-06:00,Chris Tusa,chris.tusa@leafscale.com,"Revert to reef_token secret, clean up pipeline" +aaa7a4f57c971ef4103996e85669db8c8a354e8e,aaa7a4f,2026-01-26T21:09:23-06:00,Chris Tusa,chris.tusa@leafscale.com,Add test_secret to debug secret injection +3a1cbd831fdbf3b695d93d661b8c61a8a49c5e9b,3a1cbd8,2026-01-26T21:27:00-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix variable escaping for Woodpecker preprocessing +6e928a411ca49868f940fd307f068b6d17d75bf5,6e928a4,2026-01-26T21:27:44-06:00,Chris Tusa,chris.tusa@leafscale.com,Quote curl command to fix YAML parsing +2486d042722a126716c75dff08eb6b79a957f0ad,2486d04,2026-01-26T21:28:44-06:00,Chris Tusa,chris.tusa@leafscale.com,Debug: show tarball structure +1c191e93d4984e8bfc99c0ba30d3a6a7f3e565d2,1c191e9,2026-01-26T21:30:02-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix reefc path and clean up debug statements +6bf7480082b3938723293cdcc958bdef16769cfd,6bf7480,2026-01-26T21:31:13-06:00,Chris Tusa,chris.tusa@leafscale.com,Switch to Ubuntu 24.04 for GLIBC 2.38+ compatibility +f2ef479898aab95e3a478f4eb17e6e2c3ab4d654,f2ef479,2026-01-26T21:32:20-06:00,Chris Tusa,chris.tusa@leafscale.com,Set REEF_STDLIB environment variable for reefc +ffbb7793c9819eea7efd4fbd3aafb85a8abfb210,ffbb779,2026-01-26T21:33:39-06:00,Chris Tusa,chris.tusa@leafscale.com,Use REEF_HOME instead of REEF_STDLIB +c646c2f45e235c2713daac859a509faa244d4fcc,c646c2f,2026-01-26T21:34:25-06:00,Chris Tusa,chris.tusa@leafscale.com,Set both REEF_HOME and REEF_STDLIB +618ab8c7ed35cd0fe7d6ebab76f5829712ab4a38,618ab8c,2026-01-26T21:35:36-06:00,Chris Tusa,chris.tusa@leafscale.com,"Debug: extract without strip, show structure" +3a465aa5f2690e5dfdc02ed55e19f199a52952aa,3a465aa,2026-01-26T21:37:48-06:00,Chris Tusa,chris.tusa@leafscale.com,Set both REEF_HOME and REEF_STDLIB for reefc +537795eff98516a07b184c9d3f0d21a08f3115e0,537795e,2026-01-26T21:39:03-06:00,Chris Tusa,chris.tusa@leafscale.com,Debug: check stdlib contents in binary distribution +0bf8a2060e3b1af4f59433623dd3291add69aaab,0bf8a20,2026-01-26T21:40:04-06:00,Chris Tusa,chris.tusa@leafscale.com,Workaround: symlink stdlib to reef-stdlib (Reef packaging bug) +79d691addc6e79d4364bd8d6b289593e516d8a93,79d691a,2026-01-26T21:41:02-06:00,Chris Tusa,chris.tusa@leafscale.com,"Set REEF_HOME in environment block, add reef-runtime symlink and gcc" +5b95ae40fa223e8b196aa20b589dc7deeee2d9a2,5b95ae4,2026-01-26T22:48:54-06:00,Chris Tusa,chris.tusa@leafscale.com,Simplify Reef setup - just add bin to PATH +52d81e297fdf5f522faaa533f56523f6fee51a18,52d81e2,2026-01-26T22:49:56-06:00,Chris Tusa,chris.tusa@leafscale.com,Install build-essential for C headers +22599cf95715bb9b59b44d158ed3535ca341fe74,22599cf,2026-01-26T22:54:13-06:00,Chris Tusa,chris.tusa@leafscale.com,Bump version to 0.1.2 +017c4012259061c3d101faf25007ba3a7837cb19,017c401,2026-01-26T23:04:38-06:00,Chris Tusa,chris.tusa@leafscale.com,Add gnu and illumos categories to port search +0318423949888750d43d046f420c1b4a284ac13f,0318423,2026-01-27T10:51:34-06:00,Chris Tusa,chris.tusa@leafscale.com,Add build cleanup options and implementation +f3b3e306109d1528bc52d1499abcc45f2248654c,f3b3e30,2026-01-27T11:03:36-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix dependency bug +457b3cb798fbb197bae46b57be4608227c447d70,457b3cb,2026-01-27T11:30:35-06:00,Chris Tusa,chris.tusa@leafscale.com,Add source mirrors refactor plan (Option B) +1af9101522207a7d3e431bc128a2327e9bb75d9e,1af9101,2026-01-27T12:14:54-06:00,Chris Tusa,chris.tusa@leafscale.com,Implement source mirrors support (Option B) +2d49d527619b5aeccb01cec1fb59b23b9d14346d,2d49d52,2026-01-27T13:15:05-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.1.3: Documentation updates and source mirrors +295065f656a9d12f975cfd721bb1bbd76be37996,295065f,2026-01-27T15:45:15-06:00,Chris Tusa,chris.tusa@leafscale.com,Update Reef compiler to 0.1.10 +ba09e0184c81971064c76ff9ae19069c2ea01f4f,ba09e01,2026-01-27T15:48:45-06:00,Chris Tusa,chris.tusa@leafscale.com,Handle existing releases in CI pipeline +2f44050a6abaa9493f2b862a9cb7f55b9c1b3b53,2f44050,2026-01-27T17:28:16-06:00,Chris Tusa,chris.tusa@leafscale.com,Add local source support and empty package validation +e062adc983f4c2616dd30d29365b522ba55bf4e5,e062adc,2026-01-27T17:41:23-06:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.1.4: Local sources, empty package validation" +de2a6793c6d2c1df8054db0237ce5b0853002e94,de2a679,2026-01-27T22:00:43-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.1.5: Build logging and output ordering fix +39a6f92bca85838fe24528be9e02d2137708e817,39a6f92,2026-01-28T00:27:36-06:00,Chris Tusa,chris.tusa@leafscale.com,Add bug report for HTTP redirect and version selection issues +e08240846c675b3904d67211370f7a36aa5e34c9,e082408,2026-01-28T11:33:02-06:00,Chris Tusa,chris.tusa@leafscale.com,Add bug report for source extraction hang +c2a68d0ca690312b7eb1b7681f6c641dd8c227af,c2a68d0,2026-01-28T14:05:47-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.1.7: Bug fixes and new features +998a2cc975a783420807c20bd0ef5f07e6cb0cdd,998a2cc,2026-01-28T15:38:39-06:00,Chris Tusa,chris.tusa@leafscale.com,Update extraction bug: XZ files still broken in 0.1.7 +e4efc01cb74e3e519596d36bb4fb276359f41b9e,e4efc01,2026-01-28T16:28:26-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.1.9: Fix XZ extraction hang with Reef 0.1.14 +d418f8e8da7482cecf355ac30dea445b9dc8f5ba,d418f8e,2026-01-28T17:41:59-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.1.10: illumos compatibility and file tracking fixes +11a24e2ba15cb76159278e4fd8a174e222f21550,11a24e2,2026-01-29T20:22:08-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.1: Add --root support for alternate filesystem operations +6456fce16d606342244f913a1551f148995eeec8,6456fce,2026-01-29T20:23:20-06:00,Chris Tusa,chris.tusa@leafscale.com,Remove resolved bug reports and deprecated cache command +aac35d47153677ba767e1811fc765b9f2a584721,aac35d4,2026-01-29T20:34:49-06:00,Chris Tusa,chris.tusa@leafscale.com,Move chroot scripts to tools/ and add coral binary to gitignore +12dcf11386c26692093183acbcc4408750d60541,12dcf11,2026-01-29T20:38:25-06:00,Chris Tusa,chris.tusa@leafscale.com,mkchroot: Generate scripts alongside chroot directory +9478e6215aa6435f0209f6222dfd4bcc311aaebd,9478e62,2026-01-29T21:01:07-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix --root/--prefix flag parsing to work in any position +2e8a15f2a8c502adf6f009ea5a334cb210498a35,2e8a15f,2026-01-29T21:48:55-06:00,Chris Tusa,chris.tusa@leafscale.com,Implement MsgPack-based package database indexes +a668688fbf217971cd9499bd44f80615a655a552,a668688,2026-01-29T22:20:13-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.2: Fix MsgPack parsing and add database documentation +8d1f81dfd4649ae7cab7090d3e7671d07ce21594,8d1f81d,2026-01-30T09:19:28-06:00,Chris Tusa,chris.tusa@leafscale.com,updated woodpecker to use reef-lang-0.1.15 +9f1c0251f55dbc9bb3b1638fe45c3152d7db6e4e,9f1c025,2026-01-30T10:21:47-06:00,Chris Tusa,chris.tusa@leafscale.com,updated gitignore +826d764a8328b043a12c4f20ddf03b3c0e98bc7e,826d764,2026-01-30T17:31:25-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.3: Add download progress bar and allow non-root builds +623ec00c0079403ac97a77e244c7f0bdcb8b6257,623ec00,2026-01-30T18:43:31-06:00,Chris Tusa,chris.tusa@leafscale.com,Update woodpecker CI to use reef-lang-0.1.16 +606628b29f8081499a217f3cda105514ecf56261,606628b,2026-02-19T20:38:07-06:00,Chris Tusa,chris.tusa@leafscale.com,Add hammerhead category to port search list +2c66115aab82b148e7c3839b4c075f2bfc7c2521,2c66115,2026-02-19T20:40:02-06:00,Chris Tusa,chris.tusa@leafscale.com,Support category/name format in port find +b416c5a9e7f826f2e17b93f773a5ea8ce6691ec3,b416c5a,2026-02-19T22:45:31-06:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.2.4: Fix resolver bug, dynamic categories, ports index, ports command" +75d402d5455b0427e99d5b55b5f3962fc7e9f2e6,75d402d,2026-02-19T22:50:33-06:00,Chris Tusa,chris.tusa@leafscale.com,Update documentation for v0.2.4 features +3ae110affb94f89ea99887141ecdf87e5a22441b,3ae110a,2026-02-19T23:04:47-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.5: Fix files.db sort hang on large packages +fae09e1490c6bb57a5b36fbfb6107064790347f1,fae09e1,2026-02-19T23:17:55-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.6: Fix files.db rebuild hang from massive over-allocation +38023ac9014277fccab175462941dce1a688f0e0,38023ac,2026-02-19T23:33:19-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.7: Fix install hang on large packages (161K+ files) +6d0400d05640a3b2e597718e94d11ac0b435a954,6d0400d,2026-02-19T23:47:07-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.8: Fix register hang by replacing string building with shell pipeline +373f7f46b378f7cdb32e933a120233069e913ca0,373f7f4,2026-02-19T23:56:38-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.9: Replace shell workaround with str.join() for footprint processing +601da8e5ef047189eec94f5b620a0c9b6cb49020,601da8e,2026-03-02T23:16:32-06:00,Chris Tusa,chris.tusa@leafscale.com,Fix example config and default arch to match Zygaena defaults +8c6d1a8dafa3b346e92bb9efac7a669c285012fb,8c6d1a8,2026-03-03T11:55:01-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.10 +c0d1b543bcbea7f0791cecd0c017a07023c919df,c0d1b54,2026-03-03T12:12:18-06:00,Chris Tusa,chris.tusa@leafscale.com,Update CI to use reef-lang v0.3.4 +f220c00e69ffcba8b5d1f89865504d32dd6d52d0,f220c00,2026-03-03T19:45:55-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.2.11: Fix build script shell invocation and CPU detection +66624612dfb38f4b6f46644ad3486fafa363b247,6662461,2026-03-07T19:47:23-06:00,Chris Tusa,chris.tusa@leafscale.com,Add mtree manifest module and replace .FOOTPRINT with .MANIFEST in build +5289616d94fb3a9b2d72b984be505528b572b1ba,5289616,2026-03-07T19:58:29-06:00,Chris Tusa,chris.tusa@leafscale.com,Replace rsync with manifest-driven fs.ops installer +373d4996b402e89933b1520032a4e954b225033c,373d499,2026-03-07T20:02:14-06:00,Chris Tusa,chris.tusa@leafscale.com,Add [dependencies] to .PKGINFO and update TODO status +261fd8e618ec70bdd848d2ac69e70742d0969452,261fd8e,2026-03-07T20:06:41-06:00,Chris Tusa,chris.tusa@leafscale.com,Add config file protection on package removal +d7c9548c735d16ec08632cb05b648d4d8fb54190,d7c9548,2026-03-07T20:40:41-06:00,Chris Tusa,chris.tusa@leafscale.com,"Rewrite script model: remove .reef type, use zsh, add upgrade flow" +78422089d8bf212b3dec6d5a63654d6153e0618d,7842208,2026-03-07T21:06:29-06:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.3.0: Package Format v2 complete +3f74bcde5e57e13a31ca28c94f064978483e6cb2,3f74bcd,2026-03-07T22:15:22-06:00,Chris Tusa,chris.tusa@leafscale.com,Auto-install missing build dependencies before compilation +cc4f8e31c68efe5092bae574414ed63f19e84871,cc4f8e3,2026-03-07T23:10:59-06:00,Chris Tusa,chris.tusa@leafscale.com,"Export $PREFIX, $SYSCONFDIR, $PKG_ROOT to build scripts" +02417f9862b4236f7ac76b54181c195328d205c8,02417f9,2026-03-08T00:14:11-06:00,Chris Tusa,chris.tusa@leafscale.com,Recursive build-from-source dependency resolver +fbe0637b2dc158e1afc77202d5bd24687a2152f3,fbe0637,2026-03-08T12:09:49-05:00,Chris Tusa,chris.tusa@leafscale.com,Release v0.3.5: Wire up all unwired features and cleanup +b88a15a3a11cdc6401038fe3d663e2fd1c68cf5c,b88a15a,2026-03-08T15:37:57-05:00,Chris Tusa,chris.tusa@leafscale.com,Fix infinite CPU loop in packaging step +604db08ff4f7276ebf76b4095640f76a67c05ffa,604db08,2026-03-08T20:05:35-05:00,Chris Tusa,chris.tusa@leafscale.com,Fix O(n²) manifest generation using StringBuilder (BUG-027 workaround) +2f040691a486473343c7b2ef8a1eab5bf31032f6,2f04069,2026-03-08T23:38:23-05:00,Chris Tusa,chris.tusa@leafscale.com,Cache config paths to reduce TOML parsing allocation pressure +c388de7ff8f4f92a33896b965fac57541cb593d7,c388de7,2026-03-08T23:40:56-05:00,Chris Tusa,chris.tusa@leafscale.com,Update CI to use Reef 0.5.2 +242ff329a523c8547db03a486611a774c589c022,242ff32,2026-03-09T16:12:57-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.3.9: Fix GC-related crashes, use self for install subprocess" +2c7e72fcd76d8974b724825140a7fe85e769af10,2c7e72f,2026-03-09T16:44:19-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.3.10: Fix install from file path, fix conflicts array crash" +4723f14f9809bc746a0c110284a7e91ac2a885a4,4723f14,2026-03-09T20:33:14-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.3.11: Fix reinstall/force install, add version subcommand" +65244f3fa976aa9ee3614cf01e0a1a1ccc8b3668,65244f3,2026-03-10T16:34:48-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.3.12: Install package after build, update CI to Reef 0.5.5" +7d9ff3efc23b38f310cd53b24e5a217abcc70ed4,7d9ff3e,2026-03-10T22:24:23-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.3.13: Fix redirect crash, illumos build compat, zero-alloc uid/gid" +507ca32e163e73594cc22535c8b72dac6bdee890,507ca32,2026-03-11T14:49:42-05:00,Chris Tusa,chris.tusa@leafscale.com,"Release v0.4.0: Align build system with Hammerhead porting guide, default prefix /usr/local" +c5956c1b204ba2a39370ec9431f208c760f7d602,c5956c1,2026-03-11T20:52:10-05:00,Chris Tusa,chris.tusa@leafscale.com,"docs: add patches section, BUILD_TRIPLE, update prefix to /usr/local" +dcc8974f3fb937f7a5f1d1b0c9f05b44e4ddd652,dcc8974,2026-03-11T22:29:39-05:00,Chris Tusa,chris.tusa@leafscale.com,Fix package removal failing to unregister from database +4e82c0f0cccfa2cecfebf54071392f418542775c,4e82c0f,2026-03-12T15:34:07-05:00,Chris Tusa,chris.tusa@leafscale.com,Add LDFLAGS and PKG_CONFIG_PATH to build environment +4ff188f470ff970c73ba8bdc422425e2d72213ec,4ff188f,2026-03-12T15:35:54-05:00,Chris Tusa,chris.tusa@leafscale.com,Simplify PKG_CONFIG_PATH (avoid ${} in string literal - reefc bug) +9b1904eb8b0ecb1d5e0c10feda09d36f2db98038,9b1904e,2026-03-12T15:39:49-05:00,Chris Tusa,chris.tusa@leafscale.com,"LDFLAGS: only set -R (RPATH), drop -L to avoid shadowing base libs" +1f1b687238b9b16d7819d7916f295a79ba8c0377,1f1b687,2026-03-12T15:40:59-05:00,Chris Tusa,chris.tusa@leafscale.com,"docs: explain LDFLAGS, PKG_CONFIG_PATH, and library resolution design" +ec67d5dfb3effec2ea7ff826fc8537a62314e5e1,ec67d5d,2026-04-08T18:56:43-05:00,Chris Tusa,chris.tusa@leafscale.com,fix(build): add --install/--no-install flag parsing (default: no-install) +aefab7046c5022e9647aa94158c7940c9a6213cd,aefab70,2026-04-08T18:57:34-05:00,Chris Tusa,chris.tusa@leafscale.com,fix(build): gate target install behind --install flag (fixes bug 007) +4ace9941b38dff3ecc442b19717df5a54ba2d7ee,4ace994,2026-04-08T18:58:22-05:00,Chris Tusa,chris.tusa@leafscale.com,docs: update build command usage for --install/--no-install flags +b85d2815eb145523dfcaf26b131f396d6b418d03,b85d281,2026-04-08T19:29:22-05:00,Chris Tusa,chris.tusa@leafscale.com,bump version to 0.4.1 +9ffd39abc2c212b57454651a84172a9a42809c67,9ffd39a,2026-05-04T20:15:04-05:00,Chris Tusal,chris.tusa@leafscale.com,fix: compile against Reef 0.5.20 strict type checking (fixes bug 009) +c528299ec33440d40f1ee76448cdd2c9271c8469,c528299,2026-05-04T20:15:07-05:00,Chris Tusal,chris.tusa@leafscale.com,bump version to 0.4.2 +a578ffb3457529c059f204edd65cb7364a259548,a578ffb,2026-05-04T20:24:57-05:00,Chris Tusal,chris.tusa@leafscale.com,ci: pin Reef toolchain to 0.5.20 diff -r 000000000000 -r d0a96d73e9e1 docs/git-history.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/git-history.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,127 @@ +# Coral Git History + +Historical record of the Coral git repository prior to the Mercurial + Isurus migration. +Captured at migration time. Total commits: **120**. + +| Date | Commit | Author | Subject | +|------|--------|--------|---------| +| 2026-05-04T20:24:57-05:00 | `a578ffb` | Chris Tusal | ci: pin Reef toolchain to 0.5.20 | +| 2026-05-04T20:15:07-05:00 | `c528299` | Chris Tusal | bump version to 0.4.2 | +| 2026-05-04T20:15:04-05:00 | `9ffd39a` | Chris Tusal | fix: compile against Reef 0.5.20 strict type checking (fixes bug 009) | +| 2026-04-08T19:29:22-05:00 | `b85d281` | Chris Tusa | bump version to 0.4.1 | +| 2026-04-08T18:58:22-05:00 | `4ace994` | Chris Tusa | docs: update build command usage for --install/--no-install flags | +| 2026-04-08T18:57:34-05:00 | `aefab70` | Chris Tusa | fix(build): gate target install behind --install flag (fixes bug 007) | +| 2026-04-08T18:56:43-05:00 | `ec67d5d` | Chris Tusa | fix(build): add --install/--no-install flag parsing (default: no-install) | +| 2026-03-12T15:40:59-05:00 | `1f1b687` | Chris Tusa | docs: explain LDFLAGS, PKG_CONFIG_PATH, and library resolution design | +| 2026-03-12T15:39:49-05:00 | `9b1904e` | Chris Tusa | LDFLAGS: only set -R (RPATH), drop -L to avoid shadowing base libs | +| 2026-03-12T15:35:54-05:00 | `4ff188f` | Chris Tusa | Simplify PKG_CONFIG_PATH (avoid ${} in string literal - reefc bug) | +| 2026-03-12T15:34:07-05:00 | `4e82c0f` | Chris Tusa | Add LDFLAGS and PKG_CONFIG_PATH to build environment | +| 2026-03-11T22:29:39-05:00 | `dcc8974` | Chris Tusa | Fix package removal failing to unregister from database | +| 2026-03-11T20:52:10-05:00 | `c5956c1` | Chris Tusa | docs: add patches section, BUILD_TRIPLE, update prefix to /usr/local | +| 2026-03-11T14:49:42-05:00 | `507ca32` | Chris Tusa | Release v0.4.0: Align build system with Hammerhead porting guide, default prefix /usr/local | +| 2026-03-10T22:24:23-05:00 | `7d9ff3e` | Chris Tusa | Release v0.3.13: Fix redirect crash, illumos build compat, zero-alloc uid/gid | +| 2026-03-10T16:34:48-05:00 | `65244f3` | Chris Tusa | Release v0.3.12: Install package after build, update CI to Reef 0.5.5 | +| 2026-03-09T20:33:14-05:00 | `4723f14` | Chris Tusa | Release v0.3.11: Fix reinstall/force install, add version subcommand | +| 2026-03-09T16:44:19-05:00 | `2c7e72f` | Chris Tusa | Release v0.3.10: Fix install from file path, fix conflicts array crash | +| 2026-03-09T16:12:57-05:00 | `242ff32` | Chris Tusa | Release v0.3.9: Fix GC-related crashes, use self for install subprocess | +| 2026-03-08T23:40:56-05:00 | `c388de7` | Chris Tusa | Update CI to use Reef 0.5.2 | +| 2026-03-08T23:38:23-05:00 | `2f04069` | Chris Tusa | Cache config paths to reduce TOML parsing allocation pressure | +| 2026-03-08T20:05:35-05:00 | `604db08` | Chris Tusa | Fix O(n²) manifest generation using StringBuilder (BUG-027 workaround) | +| 2026-03-08T15:37:57-05:00 | `b88a15a` | Chris Tusa | Fix infinite CPU loop in packaging step | +| 2026-03-08T12:09:49-05:00 | `fbe0637` | Chris Tusa | Release v0.3.5: Wire up all unwired features and cleanup | +| 2026-03-08T00:14:11-06:00 | `02417f9` | Chris Tusa | Recursive build-from-source dependency resolver | +| 2026-03-07T23:10:59-06:00 | `cc4f8e3` | Chris Tusa | Export $PREFIX, $SYSCONFDIR, $PKG_ROOT to build scripts | +| 2026-03-07T22:15:22-06:00 | `3f74bcd` | Chris Tusa | Auto-install missing build dependencies before compilation | +| 2026-03-07T21:06:29-06:00 | `7842208` | Chris Tusa | Release v0.3.0: Package Format v2 complete | +| 2026-03-07T20:40:41-06:00 | `d7c9548` | Chris Tusa | Rewrite script model: remove .reef type, use zsh, add upgrade flow | +| 2026-03-07T20:06:41-06:00 | `261fd8e` | Chris Tusa | Add config file protection on package removal | +| 2026-03-07T20:02:14-06:00 | `373d499` | Chris Tusa | Add [dependencies] to .PKGINFO and update TODO status | +| 2026-03-07T19:58:29-06:00 | `5289616` | Chris Tusa | Replace rsync with manifest-driven fs.ops installer | +| 2026-03-07T19:47:23-06:00 | `6662461` | Chris Tusa | Add mtree manifest module and replace .FOOTPRINT with .MANIFEST in build | +| 2026-03-03T19:45:55-06:00 | `f220c00` | Chris Tusa | Release v0.2.11: Fix build script shell invocation and CPU detection | +| 2026-03-03T12:12:18-06:00 | `c0d1b54` | Chris Tusa | Update CI to use reef-lang v0.3.4 | +| 2026-03-03T11:55:01-06:00 | `8c6d1a8` | Chris Tusa | Release v0.2.10 | +| 2026-03-02T23:16:32-06:00 | `601da8e` | Chris Tusa | Fix example config and default arch to match Zygaena defaults | +| 2026-02-19T23:56:38-06:00 | `373f7f4` | Chris Tusa | Release v0.2.9: Replace shell workaround with str.join() for footprint processing | +| 2026-02-19T23:47:07-06:00 | `6d0400d` | Chris Tusa | Release v0.2.8: Fix register hang by replacing string building with shell pipeline | +| 2026-02-19T23:33:19-06:00 | `38023ac` | Chris Tusa | Release v0.2.7: Fix install hang on large packages (161K+ files) | +| 2026-02-19T23:17:55-06:00 | `fae09e1` | Chris Tusa | Release v0.2.6: Fix files.db rebuild hang from massive over-allocation | +| 2026-02-19T23:04:47-06:00 | `3ae110a` | Chris Tusa | Release v0.2.5: Fix files.db sort hang on large packages | +| 2026-02-19T22:50:33-06:00 | `75d402d` | Chris Tusa | Update documentation for v0.2.4 features | +| 2026-02-19T22:45:31-06:00 | `b416c5a` | Chris Tusa | Release v0.2.4: Fix resolver bug, dynamic categories, ports index, ports command | +| 2026-02-19T20:40:02-06:00 | `2c66115` | Chris Tusa | Support category/name format in port find | +| 2026-02-19T20:38:07-06:00 | `606628b` | Chris Tusa | Add hammerhead category to port search list | +| 2026-01-30T18:43:31-06:00 | `623ec00` | Chris Tusa | Update woodpecker CI to use reef-lang-0.1.16 | +| 2026-01-30T17:31:25-06:00 | `826d764` | Chris Tusa | Release v0.2.3: Add download progress bar and allow non-root builds | +| 2026-01-30T10:21:47-06:00 | `9f1c025` | Chris Tusa | updated gitignore | +| 2026-01-30T09:19:28-06:00 | `8d1f81d` | Chris Tusa | updated woodpecker to use reef-lang-0.1.15 | +| 2026-01-29T22:20:13-06:00 | `a668688` | Chris Tusa | Release v0.2.2: Fix MsgPack parsing and add database documentation | +| 2026-01-29T21:48:55-06:00 | `2e8a15f` | Chris Tusa | Implement MsgPack-based package database indexes | +| 2026-01-29T21:01:07-06:00 | `9478e62` | Chris Tusa | Fix --root/--prefix flag parsing to work in any position | +| 2026-01-29T20:38:25-06:00 | `12dcf11` | Chris Tusa | mkchroot: Generate scripts alongside chroot directory | +| 2026-01-29T20:34:49-06:00 | `aac35d4` | Chris Tusa | Move chroot scripts to tools/ and add coral binary to gitignore | +| 2026-01-29T20:23:20-06:00 | `6456fce` | Chris Tusa | Remove resolved bug reports and deprecated cache command | +| 2026-01-29T20:22:08-06:00 | `11a24e2` | Chris Tusa | Release v0.2.1: Add --root support for alternate filesystem operations | +| 2026-01-28T17:41:59-06:00 | `d418f8e` | Chris Tusa | Release v0.1.10: illumos compatibility and file tracking fixes | +| 2026-01-28T16:28:26-06:00 | `e4efc01` | Chris Tusa | Release v0.1.9: Fix XZ extraction hang with Reef 0.1.14 | +| 2026-01-28T15:38:39-06:00 | `998a2cc` | Chris Tusa | Update extraction bug: XZ files still broken in 0.1.7 | +| 2026-01-28T14:05:47-06:00 | `c2a68d0` | Chris Tusa | Release v0.1.7: Bug fixes and new features | +| 2026-01-28T11:33:02-06:00 | `e082408` | Chris Tusa | Add bug report for source extraction hang | +| 2026-01-28T00:27:36-06:00 | `39a6f92` | Chris Tusa | Add bug report for HTTP redirect and version selection issues | +| 2026-01-27T22:00:43-06:00 | `de2a679` | Chris Tusa | Release v0.1.5: Build logging and output ordering fix | +| 2026-01-27T17:41:23-06:00 | `e062adc` | Chris Tusa | Release v0.1.4: Local sources, empty package validation | +| 2026-01-27T17:28:16-06:00 | `2f44050` | Chris Tusa | Add local source support and empty package validation | +| 2026-01-27T15:48:45-06:00 | `ba09e01` | Chris Tusa | Handle existing releases in CI pipeline | +| 2026-01-27T15:45:15-06:00 | `295065f` | Chris Tusa | Update Reef compiler to 0.1.10 | +| 2026-01-27T13:15:05-06:00 | `2d49d52` | Chris Tusa | Release v0.1.3: Documentation updates and source mirrors | +| 2026-01-27T12:14:54-06:00 | `1af9101` | Chris Tusa | Implement source mirrors support (Option B) | +| 2026-01-27T11:30:35-06:00 | `457b3cb` | Chris Tusa | Add source mirrors refactor plan (Option B) | +| 2026-01-27T11:03:36-06:00 | `f3b3e30` | Chris Tusa | Fix dependency bug | +| 2026-01-27T10:51:34-06:00 | `0318423` | Chris Tusa | Add build cleanup options and implementation | +| 2026-01-26T23:04:38-06:00 | `017c401` | Chris Tusa | Add gnu and illumos categories to port search | +| 2026-01-26T22:54:13-06:00 | `22599cf` | Chris Tusa | Bump version to 0.1.2 | +| 2026-01-26T22:49:56-06:00 | `52d81e2` | Chris Tusa | Install build-essential for C headers | +| 2026-01-26T22:48:54-06:00 | `5b95ae4` | Chris Tusa | Simplify Reef setup - just add bin to PATH | +| 2026-01-26T21:41:02-06:00 | `79d691a` | Chris Tusa | Set REEF_HOME in environment block, add reef-runtime symlink and gcc | +| 2026-01-26T21:40:04-06:00 | `0bf8a20` | Chris Tusa | Workaround: symlink stdlib to reef-stdlib (Reef packaging bug) | +| 2026-01-26T21:39:03-06:00 | `537795e` | Chris Tusa | Debug: check stdlib contents in binary distribution | +| 2026-01-26T21:37:48-06:00 | `3a465aa` | Chris Tusa | Set both REEF_HOME and REEF_STDLIB for reefc | +| 2026-01-26T21:35:36-06:00 | `618ab8c` | Chris Tusa | Debug: extract without strip, show structure | +| 2026-01-26T21:34:25-06:00 | `c646c2f` | Chris Tusa | Set both REEF_HOME and REEF_STDLIB | +| 2026-01-26T21:33:39-06:00 | `ffbb779` | Chris Tusa | Use REEF_HOME instead of REEF_STDLIB | +| 2026-01-26T21:32:20-06:00 | `f2ef479` | Chris Tusa | Set REEF_STDLIB environment variable for reefc | +| 2026-01-26T21:31:13-06:00 | `6bf7480` | Chris Tusa | Switch to Ubuntu 24.04 for GLIBC 2.38+ compatibility | +| 2026-01-26T21:30:02-06:00 | `1c191e9` | Chris Tusa | Fix reefc path and clean up debug statements | +| 2026-01-26T21:28:44-06:00 | `2486d04` | Chris Tusa | Debug: show tarball structure | +| 2026-01-26T21:27:44-06:00 | `6e928a4` | Chris Tusa | Quote curl command to fix YAML parsing | +| 2026-01-26T21:27:00-06:00 | `3a1cbd8` | Chris Tusa | Fix variable escaping for Woodpecker preprocessing | +| 2026-01-26T21:09:23-06:00 | `aaa7a4f` | Chris Tusa | Add test_secret to debug secret injection | +| 2026-01-26T21:08:30-06:00 | `c9cb48f` | Chris Tusa | Revert to reef_token secret, clean up pipeline | +| 2026-01-26T21:06:39-06:00 | `15fe579` | Chris Tusa | Try API_KEY as variable name | +| 2026-01-26T21:03:30-06:00 | `3a97f20` | Chris Tusa | Use curly braces for variable expansion | +| 2026-01-26T21:02:49-06:00 | `7471b00` | Chris Tusa | Fix YAML quoting for curl commands | +| 2026-01-26T20:58:40-06:00 | `82460c8` | Chris Tusa | Fresh Woodpecker pipeline from scratch | +| 2026-01-26T20:54:54-06:00 | `d971e66` | Chris Tusa | TEMPORARY: hardcode token to test pipeline | +| 2026-01-26T20:53:25-06:00 | `72f3fd6` | Chris Tusa | Add test_secret to debug Woodpecker secrets | +| 2026-01-26T20:52:17-06:00 | `279134f` | Chris Tusa | Update secret name to coral-releases | +| 2026-01-26T20:43:27-06:00 | `d049473` | Chris Tusa | Add deeper token debug - show first 4 chars, write to file | +| 2026-01-26T20:38:34-06:00 | `6678097` | Chris Tusa | Add API debug to test reef-lang access | +| 2026-01-26T20:35:41-06:00 | `77b6a6d` | Chris Tusa | Revert to environment/from_secret syntax | +| 2026-01-26T20:34:38-06:00 | `5c06999` | Chris Tusa | Try secrets array syntax with source/target | +| 2026-01-26T20:32:59-06:00 | `ffd3831` | Chris Tusa | Trigger CI to test trusted security setting | +| 2026-01-26T20:28:47-06:00 | `e06e651` | Chris Tusa | Add FORGEJO_TOKEN back to build step | +| 2026-01-26T20:25:05-06:00 | `4bda1d3` | Chris Tusa | Try Woodpecker built-in CI_FORGE_TOKEN | +| 2026-01-26T20:22:02-06:00 | `b460a2d` | Chris Tusa | Fix YAML syntax for environment secrets | +| 2026-01-26T20:19:57-06:00 | `0b63243` | Chris Tusa | Debug secrets and try alternative environment syntax | +| 2026-01-26T19:56:57-06:00 | `1bdcbc3` | Chris Tusa | Add authentication to Reef compiler download | +| 2026-01-26T19:55:29-06:00 | `293b761` | Chris Tusa | Fix Woodpecker CI pipeline for v3.12.0 compatibility | +| 2026-01-26T19:54:26-06:00 | `88e57e2` | Chris Tusa | Add Woodpecker CI pipeline for build and release | +| 2026-01-26T18:29:13-06:00 | `1995845` | Chris Tusa | Add curl fallback for HTTP downloads on illumos | +| 2026-01-26T18:17:04-06:00 | `22468ff` | Chris Tusa | Add rooted resolver for alternate root installation | +| 2026-01-26T18:12:02-06:00 | `3acc88a` | Chris Tusa | Add --root and --prefix support for alternate root installation | +| 2026-01-26T17:41:36-06:00 | `ce380ed` | Chris Tusa | Add user confirmation prompts and improve command workflows | +| 2026-01-26T17:36:52-06:00 | `cc6f827` | Chris Tusa | Fix http.reef for Reef 0.1.7 compatibility | +| 2026-01-26T22:54:49-06:00 | `4b81063` | Chris Tusa | Update http.reef for Reef 0.1.8 native downloads | +| 2026-01-25T18:31:58-06:00 | `a9e5b80` | Chris Tusa | updated gitignore to exclude releases/ binary | +| 2026-01-25T18:26:46-06:00 | `b6fcf6c` | Chris Tusa | Add release script | +| 2026-01-25T18:21:56-06:00 | `b03ff66` | Chris Tusa | Initial commit: Coral package manager for Zygaena | diff -r 000000000000 -r d0a96d73e9e1 examples/coral.conf --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/examples/coral.conf Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,102 @@ +# Coral Package Manager Configuration +# /etc/coral/coral.conf +# +# This is a sample configuration file showing all available options. +# Copy to /etc/coral/coral.conf and modify as needed. + +[general] +# Target architecture +arch = "amd64" + +# Parallel build jobs (0 = auto-detect CPU count) +jobs = 0 + +# Ask for confirmation before operations +confirm = true + +# Enable colored output +color = true + +[paths] +# Alternate root directory (empty = /, used for cross-building) +root = "" + +# Installation prefix +prefix = "/usr/local" + +# Ports tree location +ports = "/usr/zports" + +# Built packages cache +packages = "/usr/zports/pkgs/packages" + +# Downloaded sources cache +sources = "/usr/zports/pkgs/sources" + +# Installed package database +database = "/var/lib/coral/installed" + +# Ports index directory +index = "/var/lib/coral/db" + +# Build work directory +build = "/usr/zports/pkgs/build" + +[build] +# Default C compiler flags +cflags = "-O2 -pipe" + +# Default C++ compiler flags +cxxflags = "-O2 -pipe" + +# Default make flags +makeflags = "" + +# Strip binaries +strip = true + +# Compress man pages +compress_man = true + +# Fall back to building from source if binary not available +fallback_to_source = true + +# Clean work directory after successful build +clean_work = true + +# Clean package staging directory after successful build +clean_pkg = true + +# Log build output to file (in work directory) +logging = true + +# Hide subprocess output from terminal (still logged to file) +quiet = false + +[network] +# Download timeout in seconds +timeout = 30 + +# Number of retry attempts +retries = 3 + +# Use proxy (reads http_proxy/https_proxy env vars) +use_proxy = false + +[sync] +# Auto-refresh repository metadata +auto_refresh = false + +[cache] +# Keep downloaded packages after install +keep_packages = true + +# Keep downloaded sources after build +keep_sources = true + +[security] +# Require signed repository manifests +require_signed_repos = false + +# Require signed packages +require_signed_packages = false diff -r 000000000000 -r d0a96d73e9e1 examples/package.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/examples/package.toml Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,105 @@ +# Coral Package Definition +# /usr/ports/<category>/<name>/package.toml +# +# This is a sample package.toml showing all available options. +# Only [package] section with name and version is required. + +[package] +# Required: Package name (lowercase, alphanumeric, hyphens allowed) +name = "example-package" + +# Required: Package version (semantic versioning recommended) +version = "1.2.3" + +# Package release number (increment when rebuilding same version) +release = 1 + +# Short description of the package +description = "An example package demonstrating all package.toml options" + +# Project homepage URL +url = "https://example.com/example-package" + +# License identifier (SPDX recommended: MIT, BSD-2-Clause, GPL-3.0, etc.) +license = "MIT" + +# Package maintainer +maintainer = "Your Name <you@example.com>" + +# Target architecture (x86_64, aarch64, or "any" for arch-independent) +arch = "x86_64" + +# Alternative package names this provides (for dependency resolution) +# If another package depends on "editor", this package can satisfy it +alternatives = ["editor", "text-editor"] + +# Packages this conflicts with (cannot be installed together) +conflicts = ["other-editor"] + +[dependencies] +# Packages required at runtime +runtime = ["libfoo", "libbar"] + +# Packages required only for building +build = ["gcc", "gmake", "pkg-config"] + +# Alternative dependencies: any one of the listed packages satisfies the dep +# Format: "base_name:option1,option2,option3" +# Example: depends on any database +# runtime = ["database:postgresql,mysql,sqlite"] + +# ============================================================================= +# Source Files - New format with mirror support (recommended) +# ============================================================================= + +# Each [[source]] block defines a source file to download +[[source]] +# Output filename (what to save the file as) +file = "example-package-1.2.3.tar.gz" + +# URLs to download from (tried in order until success) +# Can mix remote URLs and local files (relative to port directory) +urls = [ + "https://example.com/releases/example-package-1.2.3.tar.gz", + "https://mirror.example.org/example-package-1.2.3.tar.gz", + "example-package-1.2.3.tar.gz" +] + +# SHA256 checksum for verification +# Use "SKIP" to skip verification (not recommended for production) +checksum = "abc123def456789..." + +# Whether to extract the archive (default: true) +# Set to false for files that shouldn't be extracted (patches, data files) +extract = true + +# Multiple sources example: +# [[source]] +# file = "extra-data.tar.gz" +# urls = ["https://example.com/extra-data.tar.gz"] +# checksum = "789xyz..." +# extract = false + +# ============================================================================= +# Legacy Source Format (deprecated, still supported) +# ============================================================================= +# [sources] +# urls = ["https://example.com/example-package-1.2.3.tar.gz"] +# checksums = ["abc123def456789..."] + +[patches] +# Patch files to apply (in order), relative to port directory +files = [ + "patches/fix-build.patch", + "patches/add-feature.patch" +] + +# Strip level for patch command (-p flag) +strip = 1 + +[build] +# Enable parallel builds +parallel = true + +# Number of parallel jobs (0 = auto-detect, uses JOBS env var) +jobs = 0 diff -r 000000000000 -r d0a96d73e9e1 reef.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/reef.toml Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,18 @@ +[package] +name = "coral" +version = "0.4.2" +author = "Zygaena Team" +description = "Package manager for Zygaena" +license = "BSD-2-Clause" +url = "https://github.com/zygaena/coral" + +[build] +entry = "src/main.reef" +output = "coral" +output_dir = "build" +source_dirs = ["src"] +# Note: On illumos/Solaris, build with: reefc build -l socket -l nsl + +[docs] +output = "docs" +include_private = false diff -r 000000000000 -r d0a96d73e9e1 scripts/make-release.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/make-release.sh Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,55 @@ +#!/bin/bash +# +# Create a source release tarball for Coral +# +# Usage: ./scripts/make-release.sh [version] +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CORAL_DIR="$(dirname "$SCRIPT_DIR")" +cd "$CORAL_DIR" + +# Get version from reef.toml or argument +if [[ -n "$1" ]]; then + VERSION="$1" +else + VERSION=$(grep '^version' reef.toml | head -1 | sed 's/.*"\([^"]*\)".*/\1/') +fi + +if [[ -z "$VERSION" ]]; then + echo "Error: Could not determine version" + exit 1 +fi + +RELEASE_NAME="coral-${VERSION}-source" +RELEASE_DIR="releases" +TARBALL="${RELEASE_DIR}/${RELEASE_NAME}.tar.xz" + +echo "Creating release: $RELEASE_NAME" + +# Create releases directory +mkdir -p "$RELEASE_DIR" + +# Create tarball excluding build artifacts and git +tar --exclude='build/*' \ + --exclude='.git' \ + --exclude='releases' \ + --exclude='*.o' \ + --exclude='*.a' \ + --exclude='docs/superpowers' \ + --exclude='.DS_Store' \ + --exclude='*.swp' \ + --exclude='*~' \ + --transform "s,^,${RELEASE_NAME}/," \ + -cJf "$TARBALL" \ + reef.toml README.md src/ docs/ test/ + +echo "Created: $TARBALL" +ls -la "$TARBALL" + +# Generate checksum +sha256sum "$TARBALL" > "${TARBALL}.sha256" +echo "Checksum: ${TARBALL}.sha256" +cat "${TARBALL}.sha256" diff -r 000000000000 -r d0a96d73e9e1 src/commands/build.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/build.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,1291 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: build.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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] <port-path>") + println("") + println("Build a package from a port directory.") + println("") + println("Arguments:") + println(" <port-path> 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/<pkgname>/src/ - extracted sources, compilation + // /usr/zports/pkgs/build/<pkgname>/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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/clean.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/clean.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,290 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: clean.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 + + mut entries: [string] = new [string](512) + let count = dir.list_dir(dir_path, entries, 512) + + 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 + + mut entries: [string] = new [string](256) + let count = dir.list_dir(dir_path, entries, 256) + + // 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 <target>") + 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/db.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/db.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,308 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: db.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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) + 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 <command>") + 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/depends.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/depends.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,276 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: depends.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 <package> [options]") + println("") + println("Show dependency tree for a package.") + println("") + println("Arguments:") + println(" <package> 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/files.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/files.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,137 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: files.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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) + 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 <package>") + println("") + println("List all files owned by an installed package.") + println("") + println("Arguments:") + println(" <package> 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/info.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/info.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,198 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: info.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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) + 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 <package>") + println("") + println("Display detailed information about a package.") + println("") + println("Arguments:") + println(" <package> 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/install.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/install.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,836 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: install.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 exitcodes as ec + +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) + 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 + 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 <package...>") + println("") + println("Install one or more packages.") + println("") + println("Arguments:") + println(" <package> 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) + + 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(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(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(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(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 + mut entries: [string] = new [string](256) + let entry_count = dir.list_dir(packages_dir, entries, 256) + + // 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 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 = 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/key.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/key.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,483 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: key.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 <name>") + 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 <file> [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 <name>") + 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 <name>") + 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 <name>") + 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 <name>") + 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 <name>") + 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 = 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 = 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 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 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 <name>") +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 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 + + mut entries: [string] = new [string](64) + let count = dir.list_dir(trusted_dir, entries, 64) + + 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 <command> [options]") + println("") + println("Manage signing keys for package verification.") + println("") + println("Commands:") + println(" list List all keys in keyring") + println(" generate <name> Generate a new Ed25519 keypair") + println(" import <file> [name] Import a public key") + println(" export <name> Export a public key") + println(" delete <name> Delete a key (use --force for private keys)") + println("") + println("Trust management:") + println(" init Initialize keyring directories") + println(" trust <name> Add key to trusted list") + println(" revoke <name> 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/list.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/list.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,206 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: list.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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) + 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/outdated.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/outdated.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,203 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: outdated.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 <package>' 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/owner.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/owner.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,97 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: owner.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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) + 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 <file>") + println("") + println("Find which package owns a given file.") + println("") + println("Arguments:") + println(" <file> 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/ports.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/ports.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,241 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: ports.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 <category/name> +// ============================================================================ + +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 <category/name>") + 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 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 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 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 <command> [args]") + println("") + println("Port development and cache management.") + println("") + println("Commands:") + println(" new <category/name> 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/remove.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/remove.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,468 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: remove.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 exitcodes as ec + +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) + 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 + 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) + 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 <package...>") + println("") + println("Remove one or more installed packages.") + println("") + println("Arguments:") + println(" <package> 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): 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 + "...") + + // 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(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 = 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(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 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/repo.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/repo.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,670 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: repo.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 <url> [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 <name>") + 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 <path>") + 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 <path>") + 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 <path> [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 <path>") + 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 <source> <dest>") + 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 <path> <output>") + 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 <url>") + 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 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 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 = 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 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 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 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 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 + mut entries: [string] = new [string](1024) + let entry_count = dir.list_dir(packages_dir, entries, 1024) + + 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 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 = 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 + mut entries: [string] = new [string](64) + let count = dir.list_dir(keyring, entries, 64) + + 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 <command> [options]") + println("") + println("Manage package repositories.") + println("") + println("Commands:") + println(" list List configured repositories") + println(" add <url> [name] Add a new repository") + println(" remove <name> Remove a repository") + println(" enable <name> Enable a repository") + println(" disable <name> Disable a repository") + println("") + println("Repository maintenance:") + println(" init <path> Create a new repository structure") + println(" rebuild <path> Scan packages and rebuild manifest") + println(" sign <path> [key] Sign repository with Ed25519 key") + println(" verify <path> Verify repository signature") + println(" mirror <src> <dst> Sync repository to mirror (rsync)") + println(" export <path> <out> 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/search.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/search.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,192 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: search.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 <term>") + println("") + println("Search for packages by name or description.") + println("") + println("Arguments:") + println(" <term> 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/sync.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/sync.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,158 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: sync.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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-repo-url> " + 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) + mut entries: [string] = new [string](256) + let count = dir.list_dir(cat_path, entries, 256) + + // 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/upgrade.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/upgrade.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,465 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: upgrade.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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) + 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 + 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 + 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 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 diff -r 000000000000 -r d0a96d73e9e1 src/commands/verify.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/commands/verify.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,315 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: verify.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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) + 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 = 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 = stat.file_mode(full_path) + // 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 = stat.file_mode(full_path) + 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 = stat.file_uid(full_path) + let actual_gid = stat.file_gid(full_path) + 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 = 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 <package>") + 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(" <package> 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 diff -r 000000000000 -r d0a96d73e9e1 src/core/config.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/core/config.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,503 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: config.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +Description: Configuration file loading and access + +******************************************************************************/ + +module core.config + +import types +import io.file +import core.str +import encoding.toml + +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): 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 +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 +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 + } +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 = 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 = toml.toml_parse(content, keys, vals) + + 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 + + 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 + +// 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): Config + mut result = cfg + if str.length(root) > 0 + result.root = root + end if + if str.length(prefix) > 0 + result.prefix = prefix + end if + return result +end apply_overrides + +end module diff -r 000000000000 -r d0a96d73e9e1 src/core/database.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/core/database.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,1061 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: database.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 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 + + mut entries: [string] = new [string](256) + let entry_count = dir.list_dir(types.get_db_dir(), entries, 256) + + 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 = 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 = toml.toml_parse(content, keys, vals) + + 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 = 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 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 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 = 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 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 = file.readFile(footprint_path) + if str.length(content) == 0 + file.writeFile(files_path, "") + return true + end if + + let result = process_footprint(content) + return 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 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 = 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 = file.readFile(toml_path) + if str.length(content) > 0 + let keys = toml.toml_alloc_keys() + let vals = toml.toml_alloc_values() + let entry_count = toml.toml_parse(content, keys, vals) + + 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 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 = file.readFile(src_config) + if str.length(content) > 0 + return 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 = 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 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 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 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 = 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 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 = file.readFile(footprint_path) + if str.length(content) == 0 + file.writeFile(files_path, "") + return true + end if + + let result = process_footprint(content) + return 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 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 = file.readFile(src_config) + if str.length(content) > 0 + return 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 + + mut entries: [string] = new [string](256) + let entry_count = dir.list_dir(db_dir, entries, 256) + + 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 = 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 = toml.toml_parse(content, keys, vals) + + 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 = 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 = 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 dir.remove_dir(pkg_dir) +end unregister_package_rooted + +end module diff -r 000000000000 -r d0a96d73e9e1 src/core/groups.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/core/groups.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,211 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: groups.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 = 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 = toml.toml_parse(content, keys, vals) + + 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 + + mut entries: [string] = new [string](64) + let entry_count = dir.list_dir(groups_dir, entries, 64) + + 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 diff -r 000000000000 -r d0a96d73e9e1 src/core/package.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/core/package.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,957 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: package.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 + + // Install scripts support (version passed as $VERSION to scripts) + fn has_scripts(pkg_dir: string): bool + fn run_pre_install(pkg_dir: string, version: string): bool + fn run_post_install(pkg_dir: string, version: string): bool + fn run_pre_remove(pkg_dir: string, version: string): bool + fn run_post_remove(pkg_dir: string, version: string): bool + + // Upgrade scripts ($OLD_VERSION $NEW_VERSION passed to scripts) + fn run_pre_upgrade(pkg_dir: string, old_version: string, new_version: string): bool + fn run_post_upgrade(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(scripts_dir: string, version: string): bool + fn run_stored_post_remove(scripts_dir: string, version: string): bool + fn run_stored_pre_upgrade(scripts_dir: string, old_version: string, new_version: string): bool + fn run_stored_post_upgrade(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 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 = 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 = toml.toml_parse(content, keys, vals) + + 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 = 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 = 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 = 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 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 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 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 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 + +// Run a script from the .SCRIPTS directory +fn run_script(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 + + mut cmd = "" + + // Shell scripts (.sh) - run with zsh + if str.ends_with(script_name, ".sh") + cmd = "zsh \"" + script_path + "\"" + else + // Default: try to execute directly (binary or has shebang) + cmd = "\"" + script_path + "\"" + end if + + if str.length(script_args) > 0 + cmd = cmd + " " + script_args + end if + + 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 run_script + +// Run pre-install script (passes $VERSION) +fn run_pre_install(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(pkg_dir, "pre-install.sh", args) + end if + return run_script(pkg_dir, "pre-install", args) +end run_pre_install + +// Run post-install script (passes $VERSION) +fn run_post_install(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(pkg_dir, "post-install.sh", args) + end if + return run_script(pkg_dir, "post-install", args) +end run_post_install + +// Run pre-remove script (passes $VERSION) +fn run_pre_remove(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(pkg_dir, "pre-remove.sh", args) + end if + return run_script(pkg_dir, "pre-remove", args) +end run_pre_remove + +// Run post-remove script (passes $VERSION) +fn run_post_remove(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(pkg_dir, "post-remove.sh", args) + end if + return run_script(pkg_dir, "post-remove", args) +end run_post_remove + +// Run pre-upgrade script (passes $OLD_VERSION $NEW_VERSION) +fn run_pre_upgrade(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(pkg_dir, "pre-upgrade.sh", args) + end if + return run_script(pkg_dir, "pre-upgrade", args) +end run_pre_upgrade + +// Run post-upgrade script (passes $OLD_VERSION $NEW_VERSION) +fn run_post_upgrade(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(pkg_dir, "post-upgrade.sh", args) + end if + return run_script(pkg_dir, "post-upgrade", args) +end run_post_upgrade + +// Run a script directly from a stored scripts directory +fn run_stored_script(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 + + mut cmd = "" + + // Shell scripts (.sh) - run with zsh + if str.ends_with(script_name, ".sh") + cmd = "zsh \"" + script_path + "\"" + else + // Default: try to execute directly + cmd = "\"" + script_path + "\"" + end if + + if str.length(script_args) > 0 + cmd = cmd + " " + script_args + end if + + 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 run_stored_script + +// Run pre-remove script from stored scripts directory (passes $VERSION) +fn run_stored_pre_remove(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(scripts_dir, "pre-remove.sh", args) + end if + return run_stored_script(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(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(scripts_dir, "post-remove.sh", args) + end if + return run_stored_script(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(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(scripts_dir, "pre-upgrade.sh", args) + end if + return run_stored_script(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(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(scripts_dir, "post-upgrade.sh", args) + end if + return run_stored_script(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 = 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 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 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 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 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 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 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 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 diff -r 000000000000 -r d0a96d73e9e1 src/core/pkgdb.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/core/pkgdb.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,1027 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: pkgdb.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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>/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 + +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 = 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 = base64.base64_decode_bytes(encoded, buf, buf_size) + + 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 = msgpack.msgpack_unpack_map_len(buf, offset) + 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 = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + + if str.equals(key, "version") + index.version = msgpack.msgpack_unpack_int(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(key, "generated") + index.generated = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(key, "package_count") + index.package_count = msgpack.msgpack_unpack_int(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(key, "packages") + // Parse packages array + let arr_len = msgpack.msgpack_unpack_array_len(buf, offset) + 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 = msgpack.msgpack_unpack_map_len(buf, offset) + 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 = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + + if str.equals(field_key, "name") + entry.name = msgpack.msgpack_unpack_string(buf, offset) + elif str.equals(field_key, "version") + entry.version = msgpack.msgpack_unpack_string(buf, offset) + elif str.equals(field_key, "release") + entry.release = msgpack.msgpack_unpack_int(buf, offset) + elif str.equals(field_key, "description") + entry.description = msgpack.msgpack_unpack_string(buf, offset) + elif str.equals(field_key, "installed_date") + entry.installed_date = msgpack.msgpack_unpack_string(buf, offset) + elif str.equals(field_key, "file_count") + entry.file_count = msgpack.msgpack_unpack_int(buf, offset) + 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 = 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 = base64.base64_decode_bytes(encoded, buf, buf_size) + + 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 = msgpack.msgpack_unpack_map_len(buf, offset) + 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 = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + + if str.equals(key, "version") + index.version = msgpack.msgpack_unpack_int(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(key, "generated") + index.generated = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(key, "file_count") + index.file_count = msgpack.msgpack_unpack_int(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(key, "files") + // Parse files map + let files_map_len = msgpack.msgpack_unpack_map_len(buf, offset) + 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 = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + + let owner = 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 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 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 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 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 diff -r 000000000000 -r d0a96d73e9e1 src/core/port.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/core/port.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,588 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: port.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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) + + mut entries: [string] = new [string](256) + let entry_count = dir.list_dir(ports_dir, entries, 256) + + 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 = 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 = toml.toml_parse(content, keys, vals) + + 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 + + mut entries: [string] = new [string](256) + let entry_count = dir.list_dir(cat_path, entries, 256) + + 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 diff -r 000000000000 -r d0a96d73e9e1 src/core/portindex.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/core/portindex.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,575 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: portindex.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 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 = 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 = base64.base64_decode_bytes(encoded, buf, buf_size) + + 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 = msgpack.msgpack_unpack_map_len(buf, offset) + offset = offset + map_header_size(buf, offset) + + mut entry_idx = 0 + while entry_idx < map_len and offset < buf_len + let key = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + + if str.equals(key, "version") + index.version = msgpack.msgpack_unpack_int(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(key, "generated") + index.generated = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(key, "port_count") + index.port_count = msgpack.msgpack_unpack_int(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(key, "ports") + // Parse ports array + let arr_len = msgpack.msgpack_unpack_array_len(buf, offset) + 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 = msgpack.msgpack_unpack_map_len(buf, offset) + 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 = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + + if str.equals(field_key, "name") + entry.name = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(field_key, "category") + entry.category = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(field_key, "version") + entry.version = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(field_key, "release") + entry.release = msgpack.msgpack_unpack_int(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(field_key, "description") + entry.description = msgpack.msgpack_unpack_string(buf, offset) + offset = offset + msgpack.msgpack_value_size(buf, offset) + elif str.equals(field_key, "port_dir") + entry.port_dir = 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 = msgpack.msgpack_unpack_array_len(buf, offset) + 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] = 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 diff -r 000000000000 -r d0a96d73e9e1 src/core/repository.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/core/repository.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,451 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: repository.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 = 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 = toml.toml_parse(content, keys, vals) + + 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 + + mut entries: [string] = new [string](64) + let entry_count = dir.list_dir(repos_dir, entries, 64) + + 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 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 = 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 = toml.toml_parse(content, keys, vals) + + if entry_count == 0 + return pkg_error_result("Failed to parse repository manifest") + end if + + // Look for package entry: packages.<name> + 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 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 diff -r 000000000000 -r d0a96d73e9e1 src/core/resolver.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/core/resolver.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,756 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: resolver.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 diff -r 000000000000 -r d0a96d73e9e1 src/core/signing.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/core/signing.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,392 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: signing.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 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 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 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 + + mut entries: [string] = new [string](64) + let entry_count = dir.list_dir(keyring, entries, 64) + + 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 = 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) + mut entries: [string] = new [string](32) + let entry_count = dir.list_dir(trusted_dir, entries, 32) + + 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) + mut entries: [string] = new [string](32) + let entry_count = dir.list_dir(keyring_dir, entries, 32) + + 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 diff -r 000000000000 -r d0a96d73e9e1 src/exitcodes.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/exitcodes.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,277 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: exitcodes.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 diff -r 000000000000 -r d0a96d73e9e1 src/main.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/main.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,294 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: main.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +Description: Entry point for Coral package manager + +******************************************************************************/ + +import sys.args +import sys.process +import sys.env +import io.file +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 + +fn version(): string + return "0.4.2" +end version + +proc print_usage() + println("coral " + version() + " - Package manager for Zygaena") + println("") + println("USAGE:") + println(" coral <command> [options] [packages...]") + println("") + println("COMMANDS:") + println(" Package Management:") + println(" install <pkg> Install packages") + println(" reinstall <pkg> Reinstall packages (same as install --force)") + println(" remove <pkg> Remove packages") + println(" upgrade Upgrade packages") + println("") + println(" Query Commands:") + println(" info <pkg> Show package info") + println(" list List installed packages") + println(" files <pkg> List package files") + println(" owner <file> Find file owner") + println(" search <term> Search packages") + println(" depends <pkg> Show dependencies") + println(" outdated Show available updates") + println(" verify <pkg> Verify package integrity") + println("") + println(" Build Commands:") + println(" build <port> Build package from port") + println(" sync Update ports tree") + println(" clean <target> Remove build artifacts (sources|packages|build|all)") + println("") + println(" System Commands:") + println(" db <cmd> Manage package database (init|rebuild|status)") + println(" ports <cmd> 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 <path> Use alternative root directory") + println(" --prefix <path> Installation prefix (default: /usr)") + 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" + 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 + + return opts +end parse_global_options + +// 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" + if 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 + + +// 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 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 diff -r 000000000000 -r d0a96d73e9e1 src/types.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/types.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,397 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: types.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + 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: "", + 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 diff -r 000000000000 -r d0a96d73e9e1 src/util/archive.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/util/archive.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,295 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: archive.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 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 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 = 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 diff -r 000000000000 -r d0a96d73e9e1 src/util/checksum.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/util/checksum.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,138 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: checksum.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +Description: SHA256 checksum verification utilities + +******************************************************************************/ + +module util.checksum + +import io.file +import core.str +import sys.process +import crypto.sha256 as sha + +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(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 diff -r 000000000000 -r d0a96d73e9e1 src/util/color.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/util/color.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,170 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: color.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 diff -r 000000000000 -r d0a96d73e9e1 src/util/http.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/util/http.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,273 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: http.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 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 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 = http.http_send_timeout(req, timeout_ms) + + // Check for errors + if str.length(resp.error) > 0 + return "" + end if + + // 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 = http.http_send_timeout(req, 10000) + + if str.length(resp.error) > 0 + return false + end if + + // 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 = http.http_send_timeout(req, 10000) + + if str.length(resp.error) > 0 + return 0 + end if + + 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 diff -r 000000000000 -r d0a96d73e9e1 src/util/mtree.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/util/mtree.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,609 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: mtree.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 + +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 = stat.file_mode(current_dir) + let uid = stat.file_uid(current_dir) + let gid = stat.file_gid(current_dir) + 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 + mut dir_entries: [string] = new [string](4096) + let entry_count = dir.list_dir(current_dir, dir_entries, 4096) + + 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 = link.readlink(full_path) + let uid = stat.file_uid(current_dir) + let gid = stat.file_gid(current_dir) + 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 = stat.file_mode(full_path) + let uid = stat.file_uid(full_path) + let gid = stat.file_gid(full_path) + 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 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 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 = 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 = 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 diff -r 000000000000 -r d0a96d73e9e1 src/util/prompt.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/util/prompt.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,186 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: prompt.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +Description: User prompt utilities for Coral + +******************************************************************************/ + +module util.prompt + +import io.console +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 = 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 diff -r 000000000000 -r d0a96d73e9e1 src/util/version.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/util/version.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,316 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: version.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +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 diff -r 000000000000 -r d0a96d73e9e1 test/http_test.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/http_test.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,53 @@ +// Minimal HTTP download test for debugging +// Tests net.http.http_download_file() which appears to hang on illumos + +import net.http +import io.file + +proc main() + println("=== HTTP Download Test ===") + println("") + + // Test 1: Simple HTTPS URL (zlib - small file) + let url1 = "https://zlib.net/zlib-1.3.1.tar.gz" + let dest1 = "/tmp/http_test_zlib.tar.gz" + + println("Test 1: zlib download (1.5MB)") + println("URL: " + url1) + println("Calling http_download_file()...") + + let result1 = http.http_download_file(url1, dest1) + + if result1 + println("SUCCESS: Downloaded to " + dest1) + if file.fileExists(dest1) + println("File exists: true") + end if + else + println("FAILED: http_download_file returned false") + end if + + println("") + + // Test 2: GitHub URL (requires redirect handling) + let url2 = "https://github.com/vim/vim/archive/refs/tags/v9.1.0.tar.gz" + let dest2 = "/tmp/http_test_vim.tar.gz" + + println("Test 2: GitHub download with redirects (17MB)") + println("URL: " + url2) + println("Calling http_download_file()...") + + let result2 = http.http_download_file(url2, dest2) + + if result2 + println("SUCCESS: Downloaded to " + dest2) + if file.fileExists(dest2) + println("File exists: true") + end if + else + println("FAILED: http_download_file returned false") + end if + + println("") + println("=== Test Complete ===") +end main diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/apptest/build.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/base/apptest/build.sh Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,34 @@ +#!/bin/sh +# Build script for apptest +# A test application that uses libtest + +set -e + +echo "Building apptest..." + +# Create source file +mkdir -p src + +cat > src/apptest.c << 'EOF' +#include <stdio.h> +#include <libtest.h> + +int main(void) { + printf("apptest v1.0.0\n"); + printf("Using libtest: %s\n", libtest_version()); + printf("Message: %s\n", libtest_greet()); + printf("\nDependency chain validated successfully!\n"); + return 0; +} +EOF + +# Compile - link against libtest +echo "Compiling apptest..." +gcc ${CFLAGS} -I/usr/include -o apptest src/apptest.c -L/usr/lib -ltest + +# Install to DESTDIR +echo "Installing to ${DESTDIR}..." +mkdir -p "${DESTDIR}/usr/bin" +install -m 755 apptest "${DESTDIR}/usr/bin/apptest" + +echo "Build complete!" diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/apptest/package.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/base/apptest/package.toml Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,26 @@ +# apptest - Simple Test Application +# Depends on libtest - for testing dependency resolution + +[package] +name = "apptest" +version = "1.0.0" +release = 1 +description = "A test application that depends on libtest" +url = "https://github.com/leafscale/zygaena" +license = "BSD-2-Clause" +maintainer = "Chris Tusa <chris.tusa@leafscale.com>" +arch = "x86_64" + +[dependencies] +# This is the key test - apptest depends on libtest +runtime = ["libtest"] +build = ["libtest"] + +[sources] +# No external sources - we create inline +urls = [] +checksums = [] + +[build] +parallel = true +jobs = 1 diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/hello/build.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/base/hello/build.sh Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,46 @@ +#!/bin/sh +# Build script for hello package +# This is a test package that creates a simple hello world program + +set -e + +echo "Building hello world test package..." + +# Create source directory +mkdir -p src + +# Create simple hello.c +cat > src/hello.c << 'EOF' +#include <stdio.h> + +int main(void) { + printf("Hello from Coral package manager!\n"); + printf("If you see this, the build system is working.\n"); + return 0; +} +EOF + +# Compile +echo "Compiling hello.c..." +gcc ${CFLAGS} -o hello src/hello.c + +# Install to DESTDIR +echo "Installing to ${DESTDIR}..." +mkdir -p "${DESTDIR}/usr/bin" +install -m 755 hello "${DESTDIR}/usr/bin/hello" + +# Create a simple man page +mkdir -p "${DESTDIR}/usr/share/man/man1" +cat > "${DESTDIR}/usr/share/man/man1/hello.1" << 'EOF' +.TH HELLO 1 "2025" "Coral" "User Commands" +.SH NAME +hello \- a test program for coral package manager +.SH SYNOPSIS +.B hello +.SH DESCRIPTION +A simple hello world program used to test the Coral package build system. +.SH AUTHOR +Chris Tusa <chris.tusa@leafscale.com> +EOF + +echo "Build complete!" diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/hello/package.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/base/hello/package.toml Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,25 @@ +# Hello World Package - Test Port +# This is a simple test package to verify coral build works + +[package] +name = "hello" +version = "1.0.1" +release = 1 +description = "A simple hello world program for testing coral" +url = "https://github.com/leafscale/zygaena" +license = "MIT" +maintainer = "Chris Tusa <chris.tusa@leafscale.com>" +arch = "x86_64" + +[dependencies] +runtime = [] +build = [] + +[sources] +# No external sources - we include the source directly +urls = [] +checksums = [] + +[build] +parallel = true +jobs = 1 diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/tree/build.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/base/tree/build.sh Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,37 @@ +#!/bin/sh +# Build script for tree +# A directory listing utility + +set -e + +echo "Building tree..." + +# Enter source directory (GitHub archives extract with different name) +cd tree-2.1.1 + +# On illumos, use GNU make +MAKE=gmake +if [ "$(uname -s)" != "SunOS" ]; then + MAKE=make +fi + +# Build with appropriate flags for illumos +case "$(uname -s)" in + SunOS|illumos) + # illumos needs specific flags + $MAKE CC=gcc CFLAGS="${CFLAGS} -D_XOPEN_SOURCE=600" prefix=/usr + ;; + *) + $MAKE CC=gcc CFLAGS="${CFLAGS}" prefix=/usr + ;; +esac + +# Install to DESTDIR +echo "Installing to ${DESTDIR}..." +mkdir -p "${DESTDIR}/usr/bin" +mkdir -p "${DESTDIR}/usr/share/man/man1" + +install -m 755 tree "${DESTDIR}/usr/bin/tree" +install -m 644 doc/tree.1 "${DESTDIR}/usr/share/man/man1/tree.1" + +echo "Build complete!" diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/tree/package.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/base/tree/package.toml Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,25 @@ +# Tree Package - Directory Listing Utility +# A simple C program for testing coral with real source + +[package] +name = "tree" +version = "2.1.1" +release = 1 +description = "List directory contents in a tree-like format" +url = "http://mama.indstate.edu/users/ice/tree/" +license = "GPL-2.0" +maintainer = "Chris Tusa <chris.tusa@leafscale.com>" +arch = "x86_64" + +[dependencies] +runtime = [] +build = [] + +[sources] +# Real source from upstream (single-line format for TOML parser compatibility) +urls = ["https://github.com/Old-Man-Programmer/tree/archive/refs/tags/2.1.1.tar.gz"] +checksums = ["SKIP"] + +[build] +parallel = true +jobs = 1 diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/vim/build.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/base/vim/build.sh Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,39 @@ +#!/bin/sh +# Build script for vim +# Vi IMproved text editor + +set -e + +echo "Building vim..." + +cd vim-9.1.0000 + +# On illumos, use GNU make +MAKE=gmake +if [ "$(uname -s)" != "SunOS" ]; then + MAKE=make +fi + +# Configure with minimal features for testing +# (Full vim would have more options) +./configure \ + --prefix=/usr \ + --with-features=normal \ + --enable-multibyte \ + --disable-gui \ + --without-x \ + --disable-netbeans \ + --enable-cscope + +# Build +$MAKE ${MAKEFLAGS} + +# Install to DESTDIR +echo "Installing to ${DESTDIR}..." +$MAKE DESTDIR="${DESTDIR}" install + +# Create vi symlink +cd "${DESTDIR}/usr/bin" +ln -sf vim vi 2>/dev/null || true + +echo "Build complete!" diff -r 000000000000 -r d0a96d73e9e1 test/ports/base/vim/package.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/base/vim/package.toml Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,32 @@ +# Vim - Vi IMproved +# The classic text editor + +[package] +name = "vim" +version = "9.1.0" +release = 1 +description = "Vi IMproved - enhanced vi text editor" +url = "https://www.vim.org/" +license = "Vim" +maintainer = "Chris Tusa <chris.tusa@leafscale.com>" +arch = "x86_64" + +# vim provides the editor and vi alternatives +alternatives = ["editor", "vi"] + +[dependencies] +# Depends on ncurses for terminal handling +runtime = ["ncurses"] +build = ["ncurses"] + +[[source]] +file = "vim-9.1.0.tar.gz" +urls = [ + "https://github.com/vim/vim/archive/refs/tags/v9.1.0000.tar.gz", + "https://ftp.nluug.nl/pub/vim/unix/vim-9.1.tar.bz2" +] +checksum = "SKIP" + +[build] +parallel = true +jobs = 0 diff -r 000000000000 -r d0a96d73e9e1 test/ports/devel/libtest/build.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/devel/libtest/build.sh Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,62 @@ +#!/bin/sh +# Build script for libtest +# A simple test library for validating coral dependency resolution + +set -e + +echo "Building libtest..." + +# Create source files +mkdir -p src + +cat > src/libtest.h << 'EOF' +#ifndef LIBTEST_H +#define LIBTEST_H + +/* Return a greeting message */ +const char* libtest_greet(void); + +/* Return the library version */ +const char* libtest_version(void); + +#endif /* LIBTEST_H */ +EOF + +cat > src/libtest.c << 'EOF' +#include "libtest.h" + +const char* libtest_greet(void) { + return "Hello from libtest!"; +} + +const char* libtest_version(void) { + return "1.0.0"; +} +EOF + +# Compile shared library +echo "Compiling libtest..." +gcc ${CFLAGS} -fPIC -c src/libtest.c -o libtest.o +gcc -shared -o libtest.so.1.0.0 libtest.o + +# Create version symlinks +ln -sf libtest.so.1.0.0 libtest.so.1 +ln -sf libtest.so.1 libtest.so + +# Install to DESTDIR +echo "Installing to ${DESTDIR}..." +mkdir -p "${DESTDIR}/usr/lib" +mkdir -p "${DESTDIR}/usr/include" + +# Save current directory (build dir) +BUILD_DIR=$(pwd) + +install -m 755 libtest.so.1.0.0 "${DESTDIR}/usr/lib/" +cd "${DESTDIR}/usr/lib" +ln -sf libtest.so.1.0.0 libtest.so.1 +ln -sf libtest.so.1 libtest.so + +# Install header from saved build directory +install -m 644 "${BUILD_DIR}/src/libtest.h" "${DESTDIR}/usr/include/" + +echo "Build complete!" diff -r 000000000000 -r d0a96d73e9e1 test/ports/devel/libtest/package.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/devel/libtest/package.toml Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,28 @@ +# libtest - Simple Test Library +# For testing dependency resolution + +[package] +name = "libtest" +version = "1.0.1" +release = 1 +description = "A simple test library for validating coral dependencies" +url = "https://github.com/leafscale/zygaena" +license = "BSD-2-Clause" +maintainer = "Chris Tusa <chris.tusa@leafscale.com>" +arch = "x86_64" + +# Provides the testlib alternative +alternatives = ["testlib"] + +[dependencies] +runtime = [] +build = [] + +[sources] +# No external sources - we create inline +urls = [] +checksums = [] + +[build] +parallel = true +jobs = 1 diff -r 000000000000 -r d0a96d73e9e1 test/ports/devel/ncurses/build.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/devel/ncurses/build.sh Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,48 @@ +#!/bin/sh +# Build script for ncurses +# Terminal UI library + +set -e + +echo "Building ncurses..." + +cd ncurses-6.4 + +# On illumos, use GNU make +MAKE=gmake +if [ "$(uname -s)" != "SunOS" ]; then + MAKE=make +fi + +# Configure with common options +./configure \ + --prefix=/usr \ + --with-shared \ + --with-cxx-shared \ + --without-debug \ + --without-normal \ + --enable-widec \ + --enable-pc-files \ + --with-pkg-config-libdir=/usr/lib/pkgconfig + +# Build +$MAKE ${MAKEFLAGS} + +# Install to DESTDIR +echo "Installing to ${DESTDIR}..." +$MAKE DESTDIR="${DESTDIR}" install + +# Create compatibility symlinks for non-wide-char programs +# Many programs expect -lncurses, but we built -lncursesw +cd "${DESTDIR}/usr/lib" +for lib in ncurses form panel menu; do + ln -sf lib${lib}w.so lib${lib}.so 2>/dev/null || true + ln -sf ${lib}w.pc /usr/lib/pkgconfig/${lib}.pc 2>/dev/null || true +done + +# Also provide curses.h symlink +cd "${DESTDIR}/usr/include" +ln -sf ncursesw/curses.h curses.h 2>/dev/null || true +ln -sf ncursesw/ncurses.h ncurses.h 2>/dev/null || true + +echo "Build complete!" diff -r 000000000000 -r d0a96d73e9e1 test/ports/devel/ncurses/package.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/devel/ncurses/package.toml Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,31 @@ +# ncurses - Terminal UI Library +# Foundation library for terminal applications + +[package] +name = "ncurses" +version = "6.4" +release = 1 +description = "Terminal UI library for Unix-like systems" +url = "https://invisible-island.net/ncurses/" +license = "MIT" +maintainer = "Chris Tusa <chris.tusa@leafscale.com>" +arch = "x86_64" + +# ncurses provides the curses alternative +alternatives = ["curses"] + +[dependencies] +runtime = [] +build = [] + +[sources] +urls = [ + "https://ftp.gnu.org/gnu/ncurses/ncurses-6.4.tar.gz" +] +checksums = [ + "SKIP" +] + +[build] +parallel = true +jobs = 0 diff -r 000000000000 -r d0a96d73e9e1 test/ports/groups/base-devel.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/ports/groups/base-devel.toml Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,18 @@ +# Base development tools package group +# Install with: coral install @base-devel + +[group] +name = "base-devel" +description = "Base development tools for building packages" +packages = [ + "gcc", + "gmake", + "binutils", + "autoconf", + "automake", + "libtool", + "pkg-config", + "m4", + "bison", + "flex" +] diff -r 000000000000 -r d0a96d73e9e1 tools/mkchroot/.gitignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/mkchroot/.gitignore Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,16 @@ +# Build artifacts +build/ +*.c +*.o +*.so +*.exe + +# Editor +.vscode/ +.idea/ +*.swp +*~ + +# OS +.DS_Store +Thumbs.db diff -r 000000000000 -r d0a96d73e9e1 tools/mkchroot/README.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/mkchroot/README.md Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,20 @@ +# mkchroot + +A Reef project. + +## Building + +```bash +cd mkchroot +reefc build +``` + +## Running + +```bash +reefc run +``` + +## About + +Created with Reef Compiler 0.1.15 diff -r 000000000000 -r d0a96d73e9e1 tools/mkchroot/reef.toml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/mkchroot/reef.toml Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,17 @@ +[package] +name = "mkchroot" +version = "0.1.0" +author = "Chris Tusa <chris.tusa@leafscale.com>" +description = "Create chrooted test environment for Coral package manager" +license = "MIT" +url = "https://github.com/leafscale/coral" + +[build] +entry = "src/main.reef" +output = "mkchroot" +output_dir = "build" +source_dirs = ["src"] + +[docs] +output = "docs" +include_private = false diff -r 000000000000 -r d0a96d73e9e1 tools/mkchroot/src/main.reef --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/mkchroot/src/main.reef Tue Jun 09 17:28:44 2026 -0500 @@ -0,0 +1,626 @@ +/****************************************************************************** + __ ____ __ + / / ___ ____ _/ __/_____________ _/ /__ + / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ + / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ + /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ + + (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com + + Project: Zygaena + Filename: main.reef + Authors: Chris Tusa <chris.tusa@leafscale.com> + License: <see LICENSE file included with this source code> +Description: mkchroot - Create chrooted test environment for Coral + +******************************************************************************/ + +import sys.args +import sys.process +import io.file +import io.dir +import io.path +import core.str + +// Default chroot base path +const DEFAULT_CHROOT: string = "/tmp/coral-chroot" + +// Ports source location +const PORTS_SOURCE: string = "/home/ctusa/repos/zygaena-ports" + +fn main(): int + println("mkchroot - Coral Test Environment Builder") + println("") + + // Parse command line for custom path and coral-bin option + let chroot_path = get_chroot_path() + let coral_bin = get_coral_bin() + + // Handle help + if chroot_path == "--help" or chroot_path == "-h" + print_usage() + return 0 + end if + + println("Creating chroot at: " + chroot_path) + println("") + + // Check if chroot already exists + if dir.dir_exists(chroot_path) + println("WARNING: Chroot directory already exists") + println("Removing existing chroot...") + let rm_cmd = "rm -rf \"" + chroot_path + "\"" + let rm_pid = process.process_spawn_shell(rm_cmd) + if rm_pid > 0 + process.process_wait(rm_pid) + end if + end if + + // Create base filesystem structure + if not create_filesystem(chroot_path) + println("ERROR: Failed to create filesystem structure") + return 1 + end if + + // Create Coral-specific directories + if not create_coral_dirs(chroot_path) + println("ERROR: Failed to create Coral directories") + return 1 + end if + + // Create default configuration + if not create_config(chroot_path) + println("ERROR: Failed to create configuration") + return 1 + end if + + // Copy ports tree + if not copy_ports(chroot_path) + println("ERROR: Failed to copy ports tree") + return 1 + end if + + // Copy coral binary if specified + if str.length(coral_bin) > 0 + if not install_coral(chroot_path, coral_bin) + println("WARNING: Failed to copy coral binary") + end if + end if + + // Generate setup and teardown scripts + if not create_setup_script(chroot_path) + println("WARNING: Failed to create setup script") + end if + + if not create_teardown_script(chroot_path) + println("WARNING: Failed to create teardown script") + end if + + println("") + println("Chroot environment created successfully!") + println("") + println("=== Usage ===") + println("") + println("Option 1: Use --root flag (no mounts needed):") + println(" coral --root " + chroot_path + " db init") + println(" coral --root " + chroot_path + " install zlib") + println(" coral --root " + chroot_path + " list") + println("") + println("Option 2: Enter chroot (requires setup):") + println(" 1. Setup mounts: sudo " + chroot_path + "-setup.sh " + chroot_path) + println(" 2. Enter chroot: sudo chroot " + chroot_path + " /bin/bash") + println(" 3. Init database: coral db init") + println(" 4. Build/install: coral build core/zlib") + println(" 5. Exit: exit") + println(" 6. Cleanup: sudo " + chroot_path + "-teardown.sh " + chroot_path) + println("") + println("=== Directory layout ===") + println(" " + chroot_path + "/etc/coral/ - Configuration") + println(" " + chroot_path + "/var/lib/coral/ - Package database") + println(" " + chroot_path + "/usr/zports/ - Ports tree") + println("") + println("=== Generated scripts ===") + println(" " + chroot_path + "-setup.sh - Mount /dev, /proc, /sys, and host binaries") + println(" " + chroot_path + "-teardown.sh - Unmount all chroot mounts") + println("") + + return 0 +end main + +fn get_chroot_path(): string + let argc = args.count() + + // Check for --path argument + if argc >= 3 + if args.get(1) == "--path" + return args.get(2) + end if + end if + + // Check for positional argument (skip if it's a flag) + if argc >= 2 + let arg = args.get(1) + if not str.starts_with(arg, "-") + return arg + end if + end if + + // Check for positional after flags + mut i = 1 + while i < argc + let arg = args.get(i) + if arg == "--coral-bin" and i + 1 < argc + i = i + 2 // Skip flag and value + continue + end if + if not str.starts_with(arg, "-") + return arg + end if + i = i + 1 + end while + + return DEFAULT_CHROOT +end get_chroot_path + +fn get_coral_bin(): string + let argc = args.count() + + mut i = 1 + while i < argc + if args.get(i) == "--coral-bin" and i + 1 < argc + return args.get(i + 1) + end if + i = i + 1 + end while + + return "" +end get_coral_bin + +fn install_coral(base: string, coral_path: string): bool + println("Installing coral binary from " + coral_path + "...") + + // Check if source exists + if not file.fileExists(coral_path) + println(" ERROR: Coral binary not found at " + coral_path) + return false + end if + + let dst = base + "/usr/local/bin/coral" + let cmd = "cp \"" + coral_path + "\" \"" + dst + "\" && chmod +x \"" + dst + "\"" + let pid = process.process_spawn_shell(cmd) + if pid > 0 + let exit_code = process.process_wait(pid) + if exit_code == 0 + println(" Installed coral to " + dst) + return true + end if + end if + + return false +end install_coral + +fn create_setup_script(base: string): bool + let script_path = base + "-setup.sh" + println("Creating " + script_path + "...") + + let script_content = "#!/bin/sh\n" + + "# Setup script for coral chroot environment\n" + + "# Generated by mkchroot\n" + + "\n" + + "CHROOT=\"$1\"\n" + + "\n" + + "if [ -z \"$CHROOT\" ]; then\n" + + " echo \"Usage: $0 <chroot-path>\"\n" + + " exit 1\n" + + "fi\n" + + "\n" + + "if [ ! -d \"$CHROOT\" ]; then\n" + + " echo \"Error: Chroot directory does not exist: $CHROOT\"\n" + + " exit 1\n" + + "fi\n" + + "\n" + + "echo \"Setting up mounts for chroot: $CHROOT\"\n" + + "\n" + + "# Mount essential filesystems\n" + + "mount --bind /dev \"$CHROOT/dev\" 2>/dev/null && echo \" Mounted /dev\"\n" + + "mount --bind /proc \"$CHROOT/proc\" 2>/dev/null && echo \" Mounted /proc\"\n" + + "mount -t sysfs sysfs \"$CHROOT/sys\" 2>/dev/null && echo \" Mounted /sys\"\n" + + "\n" + + "# Mount host binaries and libraries for building\n" + + "mount --bind /usr/bin \"$CHROOT/usr/bin\" 2>/dev/null && echo \" Mounted /usr/bin\"\n" + + "mount --bind /usr/lib \"$CHROOT/usr/lib\" 2>/dev/null && echo \" Mounted /usr/lib\"\n" + + "mount --bind /lib \"$CHROOT/lib\" 2>/dev/null && echo \" Mounted /lib\"\n" + + "\n" + + "# illumos-specific mounts\n" + + "[ -d /lib/64 ] && mount --bind /lib/64 \"$CHROOT/lib/64\" 2>/dev/null && echo \" Mounted /lib/64\"\n" + + "[ -d /usr/lib/64 ] && mount --bind /usr/lib/64 \"$CHROOT/usr/lib/64\" 2>/dev/null && echo \" Mounted /usr/lib/64\"\n" + + "\n" + + "echo \"\"\n" + + "echo \"Chroot is ready. Enter with:\"\n" + + "echo \" sudo chroot $CHROOT /bin/bash\"\n" + + if not file.writeFile(script_path, script_content) + return false + end if + + // Make executable + let chmod_cmd = "chmod +x \"" + script_path + "\"" + let pid = process.process_spawn_shell(chmod_cmd) + if pid > 0 + process.process_wait(pid) + end if + + println(" Created " + script_path) + return true +end create_setup_script + +fn create_teardown_script(base: string): bool + let script_path = base + "-teardown.sh" + println("Creating " + script_path + "...") + + let script_content = "#!/bin/sh\n" + + "# Teardown script for coral chroot environment\n" + + "# Generated by mkchroot\n" + + "\n" + + "CHROOT=\"$1\"\n" + + "\n" + + "if [ -z \"$CHROOT\" ]; then\n" + + " echo \"Usage: $0 <chroot-path>\"\n" + + " exit 1\n" + + "fi\n" + + "\n" + + "echo \"Unmounting chroot: $CHROOT\"\n" + + "\n" + + "# Unmount in reverse order\n" + + "umount \"$CHROOT/usr/lib/64\" 2>/dev/null && echo \" Unmounted /usr/lib/64\"\n" + + "umount \"$CHROOT/lib/64\" 2>/dev/null && echo \" Unmounted /lib/64\"\n" + + "umount \"$CHROOT/lib\" 2>/dev/null && echo \" Unmounted /lib\"\n" + + "umount \"$CHROOT/usr/lib\" 2>/dev/null && echo \" Unmounted /usr/lib\"\n" + + "umount \"$CHROOT/usr/bin\" 2>/dev/null && echo \" Unmounted /usr/bin\"\n" + + "umount \"$CHROOT/sys\" 2>/dev/null && echo \" Unmounted /sys\"\n" + + "umount \"$CHROOT/proc\" 2>/dev/null && echo \" Unmounted /proc\"\n" + + "umount \"$CHROOT/dev\" 2>/dev/null && echo \" Unmounted /dev\"\n" + + "\n" + + "echo \"\"\n" + + "echo \"Chroot cleanup complete.\"\n" + + if not file.writeFile(script_path, script_content) + return false + end if + + // Make executable + let chmod_cmd = "chmod +x \"" + script_path + "\"" + let pid = process.process_spawn_shell(chmod_cmd) + if pid > 0 + process.process_wait(pid) + end if + + println(" Created " + script_path) + return true +end create_teardown_script + +fn create_filesystem(base: string): bool + println("Creating base filesystem structure...") + + // Create all directories using a single mkdir -p command + let cmd = "mkdir -p " + + "\"" + base + "/bin\" " + + "\"" + base + "/sbin\" " + + "\"" + base + "/lib\" " + + "\"" + base + "/lib/64\" " + + "\"" + base + "/tmp\" " + + "\"" + base + "/dev\" " + + "\"" + base + "/proc\" " + + "\"" + base + "/usr\" " + + "\"" + base + "/usr/bin\" " + + "\"" + base + "/usr/sbin\" " + + "\"" + base + "/usr/lib\" " + + "\"" + base + "/usr/lib/64\" " + + "\"" + base + "/usr/include\" " + + "\"" + base + "/usr/share\" " + + "\"" + base + "/usr/share/man\" " + + "\"" + base + "/usr/share/man/man1\" " + + "\"" + base + "/usr/share/man/man3\" " + + "\"" + base + "/usr/share/man/man5\" " + + "\"" + base + "/usr/share/man/man8\" " + + "\"" + base + "/usr/share/doc\" " + + "\"" + base + "/usr/local\" " + + "\"" + base + "/usr/local/bin\" " + + "\"" + base + "/usr/local/lib\" " + + "\"" + base + "/etc\" " + + "\"" + base + "/etc/default\" " + + "\"" + base + "/etc/init.d\" " + + "\"" + base + "/var\" " + + "\"" + base + "/var/lib\" " + + "\"" + base + "/var/log\" " + + "\"" + base + "/var/run\" " + + "\"" + base + "/var/tmp\" " + + "\"" + base + "/var/cache\" " + + "\"" + base + "/opt\" " + + "\"" + base + "/home\" " + + "\"" + base + "/root\"" + + let pid = process.process_spawn_shell(cmd) + if pid > 0 + process.process_wait(pid) + end if + + println(" Created 35 system directories") + return true +end create_filesystem + +fn create_coral_dirs(base: string): bool + println("Creating Coral directory structure...") + + let cmd = "mkdir -p " + + "\"" + base + "/etc/coral\" " + + "\"" + base + "/etc/coral/keys\" " + + "\"" + base + "/etc/coral/repos.d\" " + + "\"" + base + "/var/lib/coral\" " + + "\"" + base + "/var/lib/coral/installed\" " + + "\"" + base + "/var/lib/coral/db\" " + + "\"" + base + "/var/lib/coral/repos\" " + + "\"" + base + "/var/lib/coral/trusted-keys\" " + + "\"" + base + "/usr/zports\" " + + "\"" + base + "/usr/zports/groups\" " + + "\"" + base + "/usr/zports/pkgs\" " + + "\"" + base + "/usr/zports/pkgs/sources\" " + + "\"" + base + "/usr/zports/pkgs/packages\" " + + "\"" + base + "/usr/zports/pkgs/build\"" + + let pid = process.process_spawn_shell(cmd) + if pid > 0 + process.process_wait(pid) + end if + + println(" Created 14 Coral directories") + return true +end create_coral_dirs + +fn create_config(base: string): bool + println("Creating default configuration...") + + // Create coral.conf + let conf_path = base + "/etc/coral/coral.conf" + let conf_content = "# Coral Package Manager Configuration\n" + + "# Generated by mkchroot for testing\n" + + "\n" + + "[paths]\n" + + "# Ports tree location\n" + + "ports_dir = /usr/zports\n" + + "\n" + + "# Build directories\n" + + "sources_dir = /usr/zports/pkgs/sources\n" + + "packages_dir = /usr/zports/pkgs/packages\n" + + "build_dir = /usr/zports/pkgs/build\n" + + "\n" + + "# Package database\n" + + "db_dir = /var/lib/coral/installed\n" + + "index_dir = /var/lib/coral/db\n" + + "\n" + + "[build]\n" + + "# Number of parallel jobs (0 = auto)\n" + + "jobs = 0\n" + + "\n" + + "# Keep build directory after successful build\n" + + "keep_build = false\n" + + "\n" + + "[network]\n" + + "# Download timeout in seconds\n" + + "timeout = 30\n" + + "\n" + + "# Retry count for failed downloads\n" + + "retries = 3\n" + + if not file.writeFile(conf_path, conf_content) + println(" Failed to write coral.conf") + return false + end if + println(" Created coral.conf") + + // Create empty repos.d placeholder + let repos_path = base + "/etc/coral/repos.d/default.repo" + let repos_content = "# Default Repository Configuration\n" + + "# Add repository URLs here\n" + + "\n" + + "[zygaena-main]\n" + + "name = Zygaena Main Repository\n" + + "url = https://pkg.zygaena.org/main\n" + + "enabled = true\n" + + "priority = 10\n" + + if not file.writeFile(repos_path, repos_content) + println(" Failed to write default.repo") + return false + end if + println(" Created default.repo") + + return true +end create_config + +fn copy_ports(base: string): bool + println("Copying ports tree from " + PORTS_SOURCE + "...") + + // Check if source exists + if not dir.dir_exists(PORTS_SOURCE) + println(" WARNING: Ports source not found at " + PORTS_SOURCE) + println(" Creating empty ports tree structure...") + return create_empty_ports(base) + end if + + // Copy core category + let core_src = PORTS_SOURCE + "/core" + let core_dst = base + "/usr/zports/groups/core" + if dir.dir_exists(core_src) + println(" Copying core category...") + let cmd1 = "cp -r \"" + core_src + "\" \"" + core_dst + "\"" + let pid1 = process.process_spawn_shell(cmd1) + if pid1 > 0 + process.process_wait(pid1) + end if + end if + + // Copy base category + let base_src = PORTS_SOURCE + "/base" + let base_dst = base + "/usr/zports/groups/base" + if dir.dir_exists(base_src) + println(" Copying base category...") + let cmd2 = "cp -r \"" + base_src + "\" \"" + base_dst + "\"" + let pid2 = process.process_spawn_shell(cmd2) + if pid2 > 0 + process.process_wait(pid2) + end if + end if + + // Count copied ports + let core_count = count_ports(core_dst) + let base_count = count_ports(base_dst) + println(" Copied " + int_to_str(core_count) + " core ports") + println(" Copied " + int_to_str(base_count) + " base ports") + + return true +end copy_ports + +fn create_empty_ports(base: string): bool + // Create sample port directory + let sample_port = base + "/usr/zports/groups/core/hello" + let cmd = "mkdir -p \"" + sample_port + "\"" + let pid = process.process_spawn_shell(cmd) + if pid > 0 + process.process_wait(pid) + end if + + // Create a simple Portfile + let portfile = sample_port + "/Portfile" + let content = "# Sample port for testing\n" + + "name = hello\n" + + "version = 1.0.0\n" + + "revision = 0\n" + + "description = Hello World test package\n" + + "homepage = https://example.com\n" + + "license = MIT\n" + + "\n" + + "# No sources - just creates a hello script\n" + + "sources = []\n" + + "\n" + + "[build]\n" + + "type = script\n" + + "\n" + + "[scripts]\n" + + "build = \"\"\"\n" + + "echo 'Building hello...'\n" + + "\"\"\"\n" + + "\n" + + "install = \"\"\"\n" + + "mkdir -p $DESTDIR/usr/bin\n" + + "echo '#!/bin/sh' > $DESTDIR/usr/bin/hello\n" + + "echo 'echo Hello from Coral!' >> $DESTDIR/usr/bin/hello\n" + + "chmod +x $DESTDIR/usr/bin/hello\n" + + "\"\"\"\n" + + if not file.writeFile(portfile, content) + return false + end if + + println(" Created sample port: core/hello") + return true +end create_empty_ports + +fn count_ports(dir_path: string): int + if not dir.dir_exists(dir_path) + return 0 + end if + + mut entries: [string] = new [string](256) + let count = dir.list_dir(dir_path, entries, 256) + + // Count only directories (each is a port) + mut ports = 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) + ports = ports + 1 + end if + end if + i = i + 1 + end while + + return ports +end count_ports + +// 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 print_usage() + println("mkchroot - Create chrooted test environment for Coral") + println("") + println("Usage: mkchroot [options] [path]") + println("") + println("Options:") + println(" path Chroot base path (default: /tmp/coral-chroot)") + println(" --coral-bin <path> Copy coral binary into chroot at /usr/local/bin/coral") + println(" --help, -h Show this help") + println("") + println("Description:") + println(" Creates a minimal filesystem structure suitable for testing") + println(" Coral package manager operations. Includes:") + println("") + println(" - Base system directories (/bin, /usr, /etc, /var, etc.)") + println(" - Coral configuration in /etc/coral/") + println(" - Package database directories in /var/lib/coral/") + println(" - Ports tree in /usr/zports/") + println(" - Sample ports copied from zygaena-ports repository") + println(" - Setup/teardown scripts for chroot mount management") + println("") + println("Generated Scripts (created alongside chroot):") + println(" <path>-setup.sh Mount /dev, /proc, /sys, and host binaries") + println(" <path>-teardown.sh Unmount all chroot mounts") + println("") + println("Examples:") + println(" # Create basic chroot") + println(" mkchroot") + println(" mkchroot /tmp/test-env") + println("") + println(" # Create chroot with coral binary") + println(" mkchroot --coral-bin ./coral /tmp/test-env") + println("") + println(" # Use --root flag (recommended, no mounts needed):") + println(" coral --root /tmp/coral-chroot db init") + println(" coral --root /tmp/coral-chroot install zlib") + println(" coral --root /tmp/coral-chroot list") + println("") + println(" # Or enter chroot directly:") + println(" sudo /tmp/coral-chroot-setup.sh /tmp/coral-chroot") + println(" sudo chroot /tmp/coral-chroot /bin/bash") + println(" coral db init") + println(" coral build core/zlib") + println(" exit") + println(" sudo /tmp/coral-chroot-teardown.sh /tmp/coral-chroot") +end print_usage