Compare commits

29 Commits
Author SHA1 Message Date
huntedbytheirs 70d59a3f9c comma
CI / build-and-test (push) Successful in 56s
2026-08-17 22:52:29 -04:00
huntedbytheirs d0ff52a5d7 test: add 7 tests for kappa bump and bump-all
CI / build-and-test (push) Successful in 42s
- 4 tests for kappa bump: version update, version written to file,
  source URL preserved, requires version arg
- 3 tests for kappa bump-all: finds newer version, reports count,
  requires directory arg

Total: 103 → 110 in test-full.sh (30+23+110 = 163 overall)
2026-08-05 07:58:35 -04:00
huntedbytheirs 1f61f116f8 feat: add kappa bump and kappa bump-all subcommands
CI / build-and-test (push) Successful in 37s
kappa bump <file> <version> — updates .kap file to new version.
Downloads tarball, computes sha256, rewrites file with new
version and hash. Preserves source URL with ${version} template.

kappa bump-all <dir> — scans directory of .kap files. For each,
auto-detects newer patch versions by incrementing the version
number and trying up to 15 candidates. Downloads and hashes the
first one that exists.
2026-08-05 07:55:08 -04:00
huntedbytheirs d918f7b6fc test: add 18 tests for 0.3 features
CI / build-and-test (push) Successful in 38s
- 8 tests: --features and --config flags (add, update, idempotent, file verification)
- 3 tests: transitive dependency resolution (auto-resolve chain)
- 4 tests: kappa search (name, substring, repo source, no-match)
- 3 tests: kappa upgrade (detection, version display, dry-run)

Total: 85 → 103 in test-full.sh (30+23+103 = 156 overall)
2026-08-05 07:42:59 -04:00
huntedbytheirs 01fcb20a57 feat: transitive dependency resolution
CI / build-and-test (push) Successful in 39s
Resolver now pulls in transitive dependencies automatically.
Declaring 'packages { root {} }' where root→mid→leaf resolves
all three packages in correct topological order. Previously
every package had to be listed explicitly in the config.

build_registry pre-populates from $KAPPA_ROOT/cache/packages/
so transitive deps can be found without remote fetching.
2026-08-05 07:39:36 -04:00
huntedbytheirs 33ff453cd1 feat: add kappa upgrade subcommand
CI / build-and-test (push) Successful in 39s
Compares installed packages against cached indexes to find newer
versions. kappa upgrade shows available upgrades and updates the
system config with new version constraints. --dry-run shows what
would change without applying.
2026-08-05 07:37:35 -04:00
huntedbytheirs b0b54be70e feat: add kappa search <query> subcommand
CI / build-and-test (push) Successful in 40s
Searches all cached indexes in $KAPPA_ROOT/cache/indexes/
for packages matching the query as a substring. Displays
name, version, and repo source (e.g. 'make 4.4.1 @ kappa-os/stable').
Offline — no network requests.
2026-08-05 07:36:06 -04:00
huntedbytheirs e69ebf220d feat: add --features and --config flags to kappa add
CI / build-and-test (push) Successful in 49s
kappa add nginx --features "ssl=on,gzip=off" --config "port=8080"
adds/updates feature flags and config values in the system config.
Comma-separated key=value pairs. Idempotent — only prints 'updated'
when something actually changed.
2026-08-05 07:34:12 -04:00
huntedbytheirs 7ba3fe6632 build: add test and clean targets to Makefile
CI / build-and-test (push) Successful in 38s
make test: builds then runs all three test suites (138 tests).
make clean: removes build, build-debug, build-release directories.

Finishing off 0.2.
2026-08-04 14:44:46 -04:00
huntedbytheirs 5607be0f1c docs: update README and CONTRIBUTING for v0.2 features
CI / build-and-test (push) Successful in 37s
README:
- Quick Start shows kappa add workflow instead of manual configs
- New features: indexed repos, env operators (+=, ?=), conflict reporting
- Updated subcommand table (add, remove, index, validate-index)
- Repo maintenance section (kappa index)
- Link to STYLEGUIDE.md, test counts, CI info

CONTRIBUTING:
- Updated test counts (30+23+85 instead of 31+23)
- Added test-full.sh to mandatory test suite
- Referenced STYLEGUIDE.md in PR requirements
- Updated good-first/ambitious issues for current state
2026-08-04 14:40:24 -04:00
huntedbytheirs fa1aa42a9c fix: remove duplicate conflicts test referencing uncreated file
CI / build-and-test (push) Successful in 43s
The 'format shows conflicts' test in the Package DSL section ran
before /tmp/test_conflict.kap was created (it's created later in the
Conflicts section). Removed the premature copy — the Conflicts
section already has this test after file creation.
2026-08-04 14:37:16 -04:00
huntedbytheirs 76b5a78e75 ci: use manual git checkout in Arch container
CI / build-and-test (push) Failing after 39s
actions/checkout requires Node.js, which isn't in archlinux:latest.
Manual git clone + checkout FETCH_HEAD instead. Adds git to pacman.
2026-08-04 14:34:45 -04:00
huntedbytheirs e3d41dcd20 ci: switch to Arch Linux container for modern toolchain
CI / build-and-test (push) Failing after 12s
Debian 11's libstdc++ (GCC 10) lacks <format> even with clang-18.
Arch ships clang-18 and the latest libstdc++ out of the box.
Container-based runner with pacman for dead-simple dependency install.
2026-08-04 14:32:58 -04:00
huntedbytheirs ac530e94a7 fix: install software-properties-common for llvm.sh on Debian
CI / build-and-test (push) Failing after 1m13s
Debian 11 (bullseye) doesn't have add-apt-repository by default.
llvm.sh needs it to add the LLVM apt repo.
2026-08-04 14:28:33 -04:00
huntedbytheirs 71d2eadcb9 fix: force clang-18 via LLVM script, prefer highest version
CI / build-and-test (push) Failing after 19s
<llvm.sh 18 all> installs clang-18 with full C++23 support.
sort -V ensures the highest clang version is selected when
multiple are installed. Removes -stdlib=libc++ (clang-18
provides <format> via its own header, no flag needed).
2026-08-04 14:27:13 -04:00
huntedbytheirs dc782e941c fix: install libc++ alongside clang for <format> support
CI / build-and-test (push) Failing after 39s
Clang 16 on Ubuntu 22.04 uses libstdc++ 12 which lacks <format>.
Install libc++-dev and pass -stdlib=libc++ to CMake.
2026-08-04 14:25:41 -04:00
huntedbytheirs 3302712336 fix: correct C++ compiler naming in CI (clang++-16 not clang-16++)
CI / build-and-test (push) Failing after 34s
Clang installs the C++ compiler as clang++-16, not clang-16++.
Use sed to properly transform the C compiler path.
2026-08-04 14:24:09 -04:00
huntedbytheirs 4a8b87e2cd fix: use find instead of ls for clang detection in CI
CI / build-and-test (push) Failing after 33s
ls returns exit code 2 when glob has no matches, which fails the
build step under set -e + pipefail. find returns 0 even when no
files are found.
2026-08-04 14:22:46 -04:00
huntedbytheirs 5668708fed fix: detect and install Clang 15-18 with LLVM fallback
CI / build-and-test (push) Failing after 30s
- Tries apt clang-18/17/16/15 first (Ubuntu 22.04+)
- Falls back to LLVM repo install for Ubuntu 20.04
- Auto-detects installed clang version for CMake compiler flags
- Both CMake 3.28 (binary download) and C++23 compiler required
2026-08-04 14:21:10 -04:00
huntedbytheirs 07a485a535 fix: install CMake 3.28 in CI (runner has 3.18, we need 3.20+)
CI / build-and-test (push) Failing after 26s
Download official CMake binary from Kitware releases instead of
relying on the distro package manager which provides an older version.
2026-08-04 14:17:52 -04:00
huntedbytheirs d10679d267 test: add 85 comprehensive tests and Gitea CI/CD workflow
CI / build-and-test (push) Failing after 56s
- test-full.sh: 85 tests covering CLI, DSL parsing (all keywords),
  config parsing, users/groups, assertions, imports, doctor diagnostics,
  repos/remotes, index generation, add/remove operations, install DB,
  resolver (conflicts + missing), scheduler (deptree ordering),
  build execution + error handling, env operators,
  init systems, bootloaders, variable interpolation, --root override,
  rollback config, system env, feature flags, rebuild dry-run

- .gitea/workflows/ci.yml: builds on Ubuntu with clang+cmake+ninja,
  runs all three test suites on push/PR to main

Total test count: 31 (DSL) + 23 (init-switch) + 85 (comprehensive) = 139
2026-08-04 14:15:50 -04:00
huntedbytheirs 09dde98159 fix: report package conflicts in resolve and rebuild handlers
Conflicts were detected in the resolver but silently ignored in both
the resolve and rebuild subcommands. Both now print conflict errors
and abort before any builds start.
2026-08-04 14:05:16 -04:00
huntedbytheirs 7345b026cc feat: add kappa add and kappa remove subcommands
- kappa add <pkg> [version] — adds/updates package in system config
  packages{} block; creates config if it doesn't exist

- kappa remove <pkg> — removes package from config; idempotent
  (no error if package isn't present)

- Supports --root flag for custom KAPPA_ROOT

- User workflow: kappa add make && kappa rebuild
  No manual config editing needed.
2026-08-04 14:00:51 -04:00
huntedbytheirs 7b0538c392 feat: add index-aware recipe resolution with repo support
- fetch_recipe_from_repos() iterates repos by priority, channels, with
  mirror fallback — caches indexes at /cache/indexes/

- Conditional HTTP downloads (curl -z) to only re-fetch stale indexes

- build_registry and fetch-package prefer repos { } over remotes = [...]
  when both are present

- Fallback chain: local .kap → repos (index-aware) → remotes (legacy)
2026-08-04 13:50:29 -04:00
huntedbytheirs 4f1ce17ec8 feat: add repos DSL, index format, and kappa index command
- Add repos { } block to system config with named repos, channels,
  mirrors, and priority

- Add index "name" { } file format for package indexes

- Add kappa index <dir> subcommand that scans .kap files and
  generates index.kap

- parse_index() and build_index() in DSL module

- format_index() roundtrip support in tools module

- validate and format subcommands handle all three formats
  (package, config, index) with automatic detection

- Backward compatible: remotes = [...] still works unchanged
2026-08-04 13:48:25 -04:00
huntedbytheirs 86f578bd4a feat: add GNU Make 4.4.1 package definition
Full autotools build (./configure, make, make check, make install)
verified end-to-end — builds, installs, produces working binary.
2026-08-04 13:29:31 -04:00
huntedbytheirs d02ab4b7b6 docs: update examples with += env operator and env usage docs
- foo.kap: document all three env operators (=, ?=, +=) in header,
  demonstrate += in env block with comments

- postgres.kap: demonstrate += for appending security hardening flags

- config.kap: demonstrate system-level += for inherited flags
2026-08-04 13:19:59 -04:00
huntedbytheirs e817e58698 feat: implement build backend and += env operator, add style guide
- Replace build stub with full phase execution (prepare/build/check/install)
  via /bin/sh with variable interpolation, config file generation, and
  env handling (hard-set, soft/?=, append/+=)

- Add += append operator for environment variables across DSL, parser,
  system config parser, build backend, and formatter

- Add STYLEGUIDE.md documenting all codebase conventions

- Replace EnvEntry bool soft with EnvMode enum (Set/Soft/Append)

- Add Plus token type to lexer for += parsing
2026-08-04 12:58:38 -04:00
huntedbytheirs 0aca4b49ca qol: implement easier building and distribution 2026-08-04 12:19:52 -04:00
27 changed files with 3510 additions and 76 deletions
+39
View File
@@ -0,0 +1,39 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
container: archlinux:latest
steps:
- name: Install dependencies
run: |
pacman -Syu --noconfirm
pacman -S --noconfirm clang cmake ninja git
- name: Checkout
run: |
git clone --depth 1 "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" .
git fetch origin "$GITHUB_REF"
git checkout FETCH_HEAD
- name: Build kappa
run: |
cmake -B build -G Ninja \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++
cmake --build build
- name: DSL and CLI tests
run: ./test.sh
- name: Init-switching tests
run: ./test-init-switch.sh
- name: Comprehensive tests
run: ./test-full.sh
+4 -1
View File
@@ -33,7 +33,10 @@
*.app *.app
# Build # Build
build/ /build/
/build-debug/
/build-release/
/dist/
build-gcc/ build-gcc/
compile_commands.json compile_commands.json
vcpkg_installed/ vcpkg_installed/
+23 -15
View File
@@ -8,7 +8,7 @@ project, keep reading.
These aren't guidelines. They're the deal. These aren't guidelines. They're the deal.
### 1. C++23 or don't bother ### 1. C++23, or don't bother
We compile with Clang, `-std=c++23`, and zero warnings. If your code needs a We compile with Clang, `-std=c++23`, and zero warnings. If your code needs a
polyfill for `std::format` or can't handle designated initializers, it polyfill for `std::format` or can't handle designated initializers, it
@@ -63,17 +63,20 @@ lock. `std::mutex`, `std::atomic`, `std::condition_variable` — use them
correctly or don't use them at all. If you don't know what `memory_order` correctly or don't use them at all. If you don't know what `memory_order`
means, stay out of the scheduler. means, stay out of the scheduler.
### 6. Tests are shell scripts. For now. ### 6. Tests are shell scripts.
Integration tests live in `test.sh` and `test-init-switch.sh`. They Integration tests live in three suites. If you add a subcommand, add a test.
exercise the CLI. If you add a subcommand, add a test. C++ unit tests
are welcome — set up Google Test or Catch2 in CMake and we'll merge it.
```sh ```sh
./test.sh # 31 tests, must pass ./test.sh # 30 tests — DSL parsing, config, services, init systems
./test-init-switch.sh # 23 tests, must pass ./test-init-switch.sh # 23 tests — init-switching, rebuild impact, bootloaders
./test-full.sh # 85 tests — CLI, all keywords, repos, add/remove,
# resolver conflicts, deptrees, build, env ops,
# index generation, --root, imports, doctor, and more
``` ```
All three must pass. CI enforces this on every push to `main`.
### 7. Backward compatibility is mandatory ### 7. Backward compatibility is mandatory
The `.kap` DSL is the contract. You can add keywords. You cannot remove The `.kap` DSL is the contract. You can add keywords. You cannot remove
@@ -85,29 +88,34 @@ change means someone's `config.kap` stops parsing, it doesn't ship.
### Pick something ### Pick something
Good first issues: Good first issues:
- Adding a 6th init system backend
- Adding a 3rd bootloader backend - Adding a 6th init system backend (we have 5: systemd, openrc, s6, runit, dinit)
- C++ unit test framework setup - Adding a 3rd bootloader backend (we have 2: grub, limine)
- Shell completion scripts - Improving the `kappa doctor` diagnostics for package recipes
- Adding `--features` / `--config` flags to `kappa add`
- Shell completion scripts (bash, zsh, fish)
Ambitious issues: Ambitious issues:
- Binary package support (pre-built caches) - Binary package support (pre-built caches)
- Remote build farm (distcc-style) - Remote build farm (distcc-style)
- Signed package verification - Signed package verification with index signing
- Filesystem overlay activation (like Nix profiles) - Filesystem overlay activation (like Nix profiles)
- Transitive dependency resolution (auto-including deps not in config)
### Send a PR ### Send a PR
1. Fork the repo 1. Fork the repo
2. Create a branch: `feat/my-thing` or `fix/my-bug` 2. Create a branch: `feat/my-thing` or `fix/my-bug`
3. Write code that follows the rules above 3. Write code that follows the rules above
4. Run `./test.sh && ./test-init-switch.sh` — both must pass 4. Run `./test.sh && ./test-init-switch.sh && ./test-full.sh` — all three must pass
5. Open a PR against `main` 5. Open a PR against `main`
### PR requirements ### PR requirements
- Build must pass: `cmake --build build` with zero warnings - Build must pass: `cmake --build build` with zero warnings on Clang 17+
- Tests must pass: both shell test suites - Tests must pass: all three shell test suites
- Follow the conventions in [STYLEGUIDE.md](STYLEGUIDE.md)
- No commented-out code. No dead code. No TODO without a date. - No commented-out code. No dead code. No TODO without a date.
- Commit messages in imperative: `Add runit backend` not `Added runit backend` - Commit messages in imperative: `Add runit backend` not `Added runit backend`
+72
View File
@@ -0,0 +1,72 @@
# kappa — build convenience Makefile
#
# Targets:
# build — default build (build/)
# debug — Debug build (build-debug/)
# release - Release build (build-release/)
# dist - tar.zst tarball of the release build (requires zstd)
# test — build then run all three test suites
# clean - remove all build directories
# dist-clean - remove everything under dist/
#
# Overridable: make release CXX=g++ GENERATOR="Unix Makefiles"
CC ?= clang
CXX ?= clang++
GENERATOR ?= Ninja
BUILD_DIR ?= build
DEBUG_DIR ?= build-debug
RELEASE_DIR ?= build-release
DIST_DIR ?= dist
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
GIT_TAG := $(shell git describe --tags --abbrev=0 2>/dev/null || echo 0.1.0)
STAMP := $(shell date +%Y-%M)
DIST_NAME := kappa-$(GIT_COMMIT)-$(GIT_TAG)-$(STAMP)
CMAKE := cmake
.PHONY: build debug release dist test clean dist-clean
build:
$(CMAKE) -S . -B $(BUILD_DIR) -G $(GENERATOR) \
-DCMAKE_C_COMPILER=$(CC) -DCMAKE_CXX_COMPILER=$(CXX)
$(CMAKE) --build $(BUILD_DIR)
debug:
$(CMAKE) -S . -B $(DEBUG_DIR) -G $(GENERATOR) \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER=$(CC) -DCMAKE_CXX_COMPILER=$(CXX)
$(CMAKE) --build $(DEBUG_DIR)
release:
$(CMAKE) -S . -B $(RELEASE_DIR) -G $(GENERATOR) \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=$(CC) -DCMAKE_CXX_COMPILER=$(CXX)
$(CMAKE) --build $(RELEASE_DIR)
dist: release
rm -rf $(DIST_DIR)/$(DIST_NAME)
$(CMAKE) --install $(RELEASE_DIR) --prefix $(DIST_DIR)/$(DIST_NAME)
tar --zstd -cf $(DIST_DIR)/$(DIST_NAME).tar.zst -C $(DIST_DIR) $(DIST_NAME)
rm -rf $(DIST_DIR)/$(DIST_NAME)
@echo "dist: $(DIST_DIR)/$(DIST_NAME).tar.zst"
dist-clean:
rm -rf $(DIST_DIR)
test: build
@echo "==> test.sh"
@./test.sh
@echo ""
@echo "==> test-init-switch.sh"
@./test-init-switch.sh
@echo ""
@echo "==> test-full.sh"
@./test-full.sh
@echo ""
@echo "all test suites passed"
clean:
rm -rf $(BUILD_DIR) $(DEBUG_DIR) $(RELEASE_DIR)
+72 -28
View File
@@ -72,16 +72,45 @@ kappa rebuild config.kap # boot.init = "openrc" — only 5 packages actually r
simultaneously, eight jobs each. The scheduler uses depth-based priority simultaneously, eight jobs each. The scheduler uses depth-based priority
grouping so leaf dependencies unblock as much work as possible first. grouping so leaf dependencies unblock as much work as possible first.
- **Package recipe caching.** Declare `remotes = ["https://repo.example.com/"]` - **Package recipe caching with indexed repos.** Declare named repos with
in your config. Kappa fetches `.kap` files on demand, caches them, and only channels and mirrors in your config. Kappa fetches `index.kap` from each
re-fetches when the remote version is newer. repo, caches it, and only re-fetches when the remote changes. Package recipes
are resolved from the index — fast, offline-tolerant, and mirror-aware.
```kap
repos {
kappa-os {
url = "https://packages.kappa-os.org/"
channels = ["stable"]
mirrors = [
"https://cdn.kappa-os.org/",
"https://eu.kappa-os.org/",
]
}
}
```
`remotes = [...]` still works. Repos are tried first, then legacy remotes.
- **Source tarball caching.** Downloaded once, stored at `$KAPPA_ROOT/cache/` - **Source tarball caching.** Downloaded once, stored at `$KAPPA_ROOT/cache/`
(default: `/usr/local/kappa/cache/`). Rebuilds don't touch the network (default: `/usr/local/kappa/cache/`). Rebuilds don't touch the network
unless versions change. unless versions change.
- **Env operators.** Three ways to set build environment variables:
`=` (hard set), `+=` (append with space), `?=` (soft set — only if not
already defined). System-level env propagates to all packages.
```kap
env {
CFLAGS = "-O2 -march=native" # overwrite
CFLAGS += "-pipe" # append → "-O2 -march=native -pipe"
CFLAGS ?= "-g" # soft — only if not set
}
```
- **Conflicts.** `systemd` declares `conflicts = ["eudev", "elogind"]`. The - **Conflicts.** `systemd` declares `conflicts = ["eudev", "elogind"]`. The
resolver catches mutual incompatibility before a build starts. resolver catches mutual incompatibility before a build starts — and now
actually reports it, rather than silently ignoring it.
- **Init-agnostic system config.** `groups { wheel { gid = 998 } }` — kappa - **Init-agnostic system config.** `groups { wheel { gid = 998 } }` — kappa
creates the groups. `system { hostname = "mybox" }` — kappa writes creates the groups. `system { hostname = "mybox" }` — kappa writes
@@ -113,41 +142,53 @@ kappa rebuild config.kap # boot.init = "openrc" — only 5 packages actually r
cmake -B build -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ cmake -B build -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
cmake --build build cmake --build build
# Write a config # Add packages to your system config
cat > system.kap << 'EOF' build/kappa add make
system { hostname = "kappa.local" } build/kappa add nginx ">=1.24"
packages { nginx {} } build/kappa add zlib
services { nginx { enable = true } }
boot {
kernel = "linux"; init = "s6"; root = "/dev/sda1"; bootloader = "limine"
}
users { root { shell = "/bin/zsh" } }
remotes = ["https://packages.kappa-os.org/stable/"]
EOF
# Parse it # Rebuild — kappa fetches recipes, resolves deps, builds everything
build/kappa parse-config system.kap build/kappa rebuild $KAPPA_ROOT/system/config.kap
# Rebuild # Remove packages
build/kappa rebuild system.kap build/kappa remove zlib
``` ```
No config files to write by hand. `kappa add` writes the `packages {}` block
for you. `kappa rebuild` handles the rest.
### Subcommands ### Subcommands
| Command | What it does | | Command | What it does |
|---------|-------------| |---------|-------------|
| `parse-package <file>` | Validate a `.kap` package definition | | `add <pkg> [version]` | Add a package to system config |
| `parse-config <file>` | Validate a system configuration | | `remove <pkg>` | Remove a package from system config |
| `validate <file>` | Validate any kappa file | | `build <package>` | Build a single package from its `.kap` definition |
| `format <file>` | Pretty-print to canonical style | | `rebuild <config>` | Diff config against installed state, rebuild changed |
| `doctor <file>` | Check for issues and warnings | | `resolve <config>` | Compute a build plan (shows order, deps, conflicts) |
| `resolve <config>` | Compute a build plan | | `doctor <file>` | Check a file for issues and warnings |
| `fetch <package>` | Download and verify source tarballs | | `fetch <package>` | Download and verify source tarballs |
| `fetch-package <name>` | Fetch a package recipe from remotes | | `fetch-package <name>` | Fetch a package recipe from remotes |
| `build <package>` | Build a single package | | `format <file>` | Pretty-print to canonical style |
| `rebuild <config>` | Diff config against installed state, rebuild changed | | `index <dir>` | Build an `index.kap` from `.kap` files in a directory |
| `list` | Show installed packages | | `list` | Show installed packages |
| `parse-config <file>` | Validate a system configuration |
| `parse-package <file>` | Validate a `.kap` package definition |
| `rollback` | Show available generations | | `rollback` | Show available generations |
| `validate <file>` | Validate any kappa file (package, config, or index) |
### Repo maintenance
```sh
# Generate an index from a directory of .kap files
kappa index ./packages/
# → packages/index.kap
# Host the directory behind any HTTP server. That's your repo.
```
The index is a tiny text file listing every package and version.
Clients fetch it once, cache it, and check for updates via HTTP headers.
### License ### License
@@ -156,4 +197,7 @@ BSD 2-Clause. Do whatever you want. Just don't sue us.
### Contributing ### Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). We're opinionated but we merge good See [CONTRIBUTING.md](CONTRIBUTING.md). We're opinionated but we merge good
code. code. See [STYLEGUIDE.md](STYLEGUIDE.md) for code conventions.
Tests: 138 integration tests across three suites. CI runs on Arch Linux.
Everything passes or nothing merges.
+484
View File
@@ -0,0 +1,484 @@
# Style Guide
If you're reading this because you thought kappa's C++ looked different from
what you're used to — good. That's the point.
This isn't a suggestion box. It's what the codebase looks like, and it's what
your code will look like after you've rewritten it three times because the PR
reviewer sent it back. Save yourself the rewrite. Read this first.
---
## The Philosophy
We write C++ like it's the year 2026 and the committee finally shipped
something usable. No polyfills. No third-party libraries. No Boost. The
standard library is sufficient for a package manager. If you disagree, you
haven't read `<format>` closely enough.
Every line of kappa assumes the reader is competent. We don't explain what
`std::string_view` is. We don't annotate obvious control flow. Comments exist
to explain *why*, never *what*. If your code needs a comment to be
understood, the code is wrong.
Simplicity is a moral position. The scheduler is the hardest thing in this
codebase, and it's 290 lines. If your feature adds more than that, you're
building the wrong feature.
---
## Naming
### Structs, classes, enums
```cpp
// PascalCase. Always.
struct BuildResult { };
enum class TokenType : std::uint8_t { };
// Enum values are PascalCase too. This isn't Java.
enum class EnvMode : std::uint8_t { Set, Soft, Append };
// Acronyms stay capitalized. B-Tree is BTree, not Btree.
// Two-letter acronyms stay capitalized. ID, not Id.
```
Type names state what the thing *is*, not what it's *for*. `SchedResult`, not
`ResultForScheduler`. `InitPaths`, not `PathsForInitSystems`.
### Variables and functions
```cpp
// snake_case. No Hungarian notation. No m_ prefix. No s_ prefix.
int pending_deps = 0;
void compute_depths(Scheduler& s);
std::string_view token_name(TokenType type);
```
Member variables and locals look identical. If you can't tell them apart,
your functions are too long. Fix the function.
### Files
```
src/service/openrc.cpp # snake_case, lowercase
include/kappa/service/types.hpp # .hpp for headers, .cpp for source
```
One public class per header is a myth invented by Java developers. Group
related declarations. `types.hpp` holds all enums and structs for a module.
If a module has one public struct and one public function, they go in the
same header.
---
## Formatting
### Indentation and braces
Four spaces. Attached braces (a K&R variant).
```cpp
// ✓ yes — brace on the same line as the control structure
if (pid < 0) {
return -1;
}
// ✗ no — Allman/BSD braces on their own line
if (pid < 0)
{
return -1;
}
// ✗ no — missing braces on single-statement bodies
if (pid < 0) return -1;
```
Always braces. Even for single statements. The compiler doesn't care.
The human reviewing your diff at 2 AM does. clang-tidy will flag bare
bodies — apply the fix every time.
### Line length
100 columns. Not 80 — we're not teletypes. Not 120 — if you need 120
characters to express a thought, your thought is too complicated. Split it.
### Section separators
```cpp
// --- Section description ---
// or
// ---------------------------------------------------------------------------
// Longer section description spanning the full runway
// ---------------------------------------------------------------------------
```
Thin lines (`---`) for sub-sections within a file. Thick lines
(`-----------`) for top-level section boundaries. The difference communicates
hierarchy without nesting.
### Switch cases
```cpp
switch (is) {
case InitSystem::Systemd:
return generate_systemd_service(spec);
case InitSystem::S6:
return generate_s6_service(spec);
case InitSystem::Unknown:
default:
return {};
}
```
Cases at the same indentation as the switch. No blocks around single-return
cases. Break or return in every non-fallthrough case. If you're falling
through intentionally, wrap it with `[[fallthrough]];` on a line by itself.
---
## Types
### Use `auto` when the type is obvious, explicit when it isn't
```cpp
// ✓ yes — type is obvious from initialization
auto scope = eval::make_default_scope();
auto& pkg = registry.at(name);
// ✓ yes — structured bindings, type is obvious
for (auto& [key, val] : features) { }
// ✓ yes — explicit where the type carries meaning
std::unique_lock lock(s.mtx); // not auto lock = ...
std::uint64_t h = 14695981039346656037ULL; // not auto h = ...
```
### `const` is the default
Everything is `const` until proven mutable.
```cpp
// ✓ yes
const auto& step = plan.steps[i];
for (const auto& entry : entries) { }
// ✗ no — mutable when it shouldn't be
auto& step = plan.steps[i];
```
### View types over owning types in parameters
```cpp
// ✓ yes
void set_root(std::string_view path);
bool is_supported(std::string_view name);
// ✗ no
void set_root(const std::string& path);
```
Return owning types. Accept views. The caller decides ownership. You decide
what you need to read.
### Strong enums only
```cpp
// ✓ yes
enum class InitSystem : std::uint8_t { Systemd, OpenRC, S6 };
// ✗ no
enum InitSystem { INIT_SYSTEMD, INIT_OPENRC, INIT_S6 };
```
No unscoped enums. No ALL_CAPS enum values. No integer conversions without
explicit intent. If you need to serialize an enum to an integer, write a
`to_string` function. The enum's numeric value is an implementation detail,
not an interface.
---
## Functions
### One responsibility per function
If your function name contains the word "and", it does at least two things
and needs to be split. If the body doesn't fit on one screen, it does too
much. "One screen" means approximately 30 lines. The scheduler's
`compute_depths` is 30 lines. So is the resolver's `resolve`. They're at the
upper bound. If yours is longer, you're doing something wrong.
### Error handling: return, don't throw
```cpp
// ✓ yes
struct FetchResult {
std::filesystem::path work_dir;
std::string error;
bool ok() const { return error.empty(); }
};
FetchResult fetch(const PackageDef& pkg);
// ✗ no
void fetch(const PackageDef& pkg); // throws on error
```
Exceptions are for unrecoverable programmer errors — out-of-memory, null
dereference, violated invariants. They belong in constructors and in the
parser (where `ParseError` is the only way to unwind back to diagnostics).
Every operational failure — network down, disk full, configure script failed,
hash mismatch — is a return value. A struct with `bool ok` and
`std::string error`. Check the `ok` field, read the `error` string, don't
catch exceptions for normal operation.
### Return early, return often
```cpp
// ✓ yes
if (to.empty()) {
return {false, "destination is empty"};
}
// ... main logic ...
// ✗ no
if (!to.empty()) {
// ... 40 lines of nesting ...
} else {
return {false, "destination is empty"};
}
```
Guard clauses at the top. Happy path straight down the left margin. If your
code has three levels of nesting, you missed an early return opportunity.
### Static helpers over lambdas
If a helper is more than 5 lines, extract it to a file-static function above
the public API. Named functions are greppable. Named functions show up in
stack traces. Lambdas don't. The one exception is a `run_phase` lambda in
`build()` — it captures local state that would require a 5-parameter helper
and it's clearly a one-off control flow wrapper, not a reusable abstraction.
---
## Namespaces
```cpp
namespace kappa::module {
// Everything goes here.
} // namespace kappa::module
```
C++17 nested namespace syntax. Closing brace gets a comment with the
namespace name. These comments survive diffs, refactors, and editors that
collapse braces. They cost one line and save ten minutes of scrolling up to
figure out which brace closes what.
### No `using namespace` at file scope
```cpp
// ✓ yes — inside a function
namespace fs = std::filesystem;
// ✗ no — at file scope
using namespace std;
```
Namespace aliases are acceptable inside functions — `namespace fs =
std::filesystem;` is fine when the file does a lot of path manipulation. But
at file scope? No. You're not writing `using namespace std;` at the top of a
header and you're not doing the subtler version of the same sin.
---
## Headers
```cpp
#pragma once
#include "kappa/resolve/plan.hpp"
#include "kappa/dsl/ast.hpp"
#include <string>
#include <vector>
namespace kappa::build {
struct BuildResult { };
BuildResult build(const resolve::BuildStep& step,
const std::string& work_dir,
int jobs);
} // namespace kappa::build
```
`#pragma once` at the top. No include guards. This is 2026.
Project headers first, in quotes. System headers second, in angle brackets.
Blank line between the two groups. Alphabetical within each group.
Headers include only what they need to compile. If `build.hpp` uses
`resolve::BuildStep` by reference, it includes `resolve/plan.hpp`. It does
not forward-declare `BuildStep` — we don't forward-declare across module
boundaries. The include is the contract: "this module depends on that one."
Headers never contain implementation. No `inline` functions. No
template definitions in headers (we don't use templates). The one exception
is `parse_util.hpp`, which defines `ParseError` inline because it's a thin
exception wrapper and splitting it would be ceremony for ceremony's sake.
One exception per codebase is a pattern. Two is a problem.
---
## Modules
Every module follows this structure:
```
include/kappa/{module}/
├── types.hpp # enums, structs, parse/validate declarations
├── {feature}.hpp # public function declarations
src/{module}/
├── types.cpp # parse/validate/describe implementations
├── backend_a.cpp # per-variant generation (if applicable)
├── backend_b.cpp
└── install.cpp # dispatch + orchestration (if applicable)
```
If a module doesn't need `types.hpp` (single struct, single function), both
go in `{feature}.hpp`. If a module has no backends, skip them. But don't
invent a third pattern. `service/` and `boot/` are the templates. Copy them.
---
## Strings and formatting
```cpp
// ✓ yes
auto msg = std::format("building {} (depth={})", name, depth);
result.error = std::format("command exited with code {}: {}", rc, cmd);
// ✗ no — ostringstream for trivial concatenation
std::ostringstream oss;
oss << "building " << name << " (depth=" << depth << ")";
// ✓ yes — ostringstream for incremental construction
std::ostringstream out;
out << "[Unit]\n";
out << std::format("Description={}\n", desc);
```
`std::format` for one-shot strings. `std::ostringstream` for building up
output incrementally (service files, bootloader configs, formatter output).
String concatenation with `+` is acceptable for two or three pieces.
Anything more goes through `std::format`.
### String views for parameters
```cpp
// ✓ yes
InitSystem parse_init_system(std::string_view name);
void print_error(std::ostream& os, std::string_view source,
SourceLocation loc, std::string_view message);
// ✗ no
InitSystem parse_init_system(const std::string& name);
```
Views everywhere, except when you need to store the string.
---
## The DSL
The `.kap` DSL grammar is the contract. You can extend it. You cannot break
existing configs. Every new token type requires:
1. An entry in `TokenType`
2. A case in `token_name()`
3. Parsing logic in the appropriate parser
4. A formatting case in `format.cpp`
5. At least one test in `test.sh` that exercises the new syntax
If you're adding a keyword, think twice. The lexer already has 29 token
types. Every new one increases parse time and mental overhead. Can this be
expressed with the existing grammar? If yes, don't add a keyword.
---
## Thread safety
The scheduler is multithreaded. If you touch shared state, you own the lock.
```cpp
{
std::unique_lock lock(s.mtx);
s.waiting.erase(idx);
}
// lock released here — no shared state access beyond this point
```
Use scoped locks. Never lock/unlock manually. Never hold a lock across a
condition variable wait without understanding why. If you think you need
`memory_order_release`, you probably need `memory_order_acq_rel` and you
should document why in a three-line comment above the operation.
If a data structure is touched by multiple threads, its access pattern must
be documented at the declaration site, not in a PR description. "This is
only written under the lock, read atomically elsewhere" goes in the header.
---
## What clang-tidy enforces
We run with `-Wall -Wextra -Wpedantic` and a `.clang-tidy` config. Zero
warnings. Not "zero warnings except for that one file." Zero.
The following are non-negotiable:
- Every `if`/`for`/`while` body has braces
- `auto` variables that are never modified are `const auto`
- Variables are initialized at declaration
- No unused includes
- No redundant declarations
If clang-tidy suggests a fix and you disagree, you're wrong. Apply the fix.
The only acceptable override is `// NOLINT` with a justification comment —
and if you write that more than twice in a file, the reviewer will ask you to
rethink your design.
---
## What we reject
- **Comments that narrate the code.** `// Increment counter` above `i++`
is an insult. Delete it.
- **Dead code.** No commented-out blocks. No `#if 0`. If it's not used,
it doesn't exist. Git remembers.
- **Premature abstraction.** Three identical lines do not need a
function. Ten do. The threshold is somewhere in between and you should
err on the side of duplication.
- **C heritage.** `printf`, `malloc`, `NULL`, raw `char*` strings,
`#define` constants. The 1970s called. Don't answer.
- **Over-engineering.** The build backend doesn't need a plugin
architecture. The lexer doesn't need a state machine framework.
Solve the problem in front of you, not the one you imagine someone
might have in three years.
- **Cleverness.** If your solution makes you feel smart, it's wrong.
The best code is the code you forget about because it never breaks.
---
Kappa does one thing: build your system from source, init-agnostically.
Everything in this style guide exists to keep that codebase small, fast, and
comprehensible. If a rule conflicts with that goal, the goal wins — but
you'd better have a good reason, and you'd better write it down.
+2
View File
@@ -67,6 +67,8 @@ system {
env { env {
CFLAGS = "-O2 -march=native" CFLAGS = "-O2 -march=native"
// packages inherit this appended flag
CFLAGS += "-pipe"
LDFLAGS = "-Wl,--as-needed" LDFLAGS = "-Wl,--as-needed"
MAKEFLAGS = "-j8" MAKEFLAGS = "-j8"
} }
+15
View File
@@ -46,6 +46,17 @@
* *
* The backend generators handle all translation. Package authors only * The backend generators handle all translation. Package authors only
* need to pick the semantic type that describes their daemon's behaviour. * need to pick the semantic type that describes their daemon's behaviour.
*
*
* ENV OPERATORS
* =============
* key = "val" hard set — overwrites any existing value
* key ?= "val" soft set — only applied if key is not already set
* key += "val" append — adds to existing value with a space separator
*
* Order matters. Subsequent entries in the same env block can append to
* earlier ones. System-level env (from config.kap) is applied before
* package env, so packages can += to flags set globally.
*/ */
package "foo" { package "foo" {
const version = "1.2.3" const version = "1.2.3"
@@ -101,8 +112,12 @@ package "foo" {
} }
env { env {
// hard set — overwrites any existing CFLAGS
CFLAGS = "-O2 -march=native" CFLAGS = "-O2 -march=native"
// append — now CFLAGS is "-O2 -march=native -pipe"
CFLAGS += "-pipe"
LDFLAGS = "-Wl,--as-needed" LDFLAGS = "-Wl,--as-needed"
// soft set — only added if CFLAGS isn't already defined
CFLAGS ?= "-g" CFLAGS ?= "-g"
} }
+5
View File
@@ -0,0 +1,5 @@
index "local" {
foo { version = "1.2.3" }
postgresql { version = "16.3" }
make { version = "4.4.1" }
}
+38
View File
@@ -0,0 +1,38 @@
/*
* GNU Make — the build tool that builds everything else.
*
* This is the quintessential autotools package: ./configure, make, make install.
* If kappa can build this, it can build anything downstream.
*
* INIT-AGNOSTIC
* =============
* Make itself doesn't care about init systems — no service definitions,
* no boot requirements. It's a pure build tool.
*/
package "make" {
const version = "4.4.1"
const source = "https://ftp.gnu.org/gnu/make/make-${version}.tar.gz"
sha256 = "dd16fb1d67bfab79a72f5e8390735c49e3e8e70b4945a15ab1f81ddb78658fb3"
license = "GPL-3.0-or-later"
provides = ["make", "gmake"]
env {
CFLAGS = "-O2"
CFLAGS += "-Wno-error"
}
build {
touch aclocal.m4 configure Makefile.in */Makefile.in
./configure --prefix=${prefix}
make -j${jobs}
}
check {
make check
}
install {
make DESTDIR=${destdir} install
}
}
+2
View File
@@ -77,6 +77,8 @@ package "postgresql" {
env { env {
CFLAGS = "-O2" CFLAGS = "-O2"
// append security hardening flags
CFLAGS += "-D_FORTIFY_SOURCE=2"
LDFLAGS = "-Wl,--as-needed" LDFLAGS = "-Wl,--as-needed"
} }
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "kappa/resolve/plan.hpp"
#include <string>
namespace kappa::build {
struct BuildResult {
bool ok = false;
std::string phase; // phase that failed, e.g. "prepare" or "build"
std::string error;
};
// Build a single package step into work_dir.
// Runs prepare, build, check, and install phases in order,
// with full variable interpolation and config file generation.
BuildResult build(const resolve::BuildStep& step,
const std::string& work_dir,
int jobs);
} // namespace kappa::build
+14 -1
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstdint>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@@ -38,10 +39,12 @@ struct Patch {
int level = 1; int level = 1;
}; };
enum class EnvMode : std::uint8_t { Set, Soft, Append };
struct EnvEntry { struct EnvEntry {
std::string key; std::string key;
std::string value; std::string value;
bool soft = false; EnvMode mode = EnvMode::Set;
}; };
struct NamedService { struct NamedService {
@@ -90,4 +93,14 @@ struct PackageDef {
Phase uninstall; Phase uninstall;
}; };
struct IndexEntry {
std::string name;
std::string version;
};
struct IndexDef {
std::string name; // "kappa-os/stable"
std::vector<IndexEntry> packages;
};
} // namespace kappa::dsl } // namespace kappa::dsl
+13 -1
View File
@@ -57,9 +57,18 @@ struct ServiceRef {
std::unordered_map<std::string, std::string> config; std::unordered_map<std::string, std::string> config;
}; };
struct RepoDef {
std::string name;
std::string url;
std::vector<std::string> channels;
std::vector<std::string> mirrors;
int priority = 50;
};
struct SystemConfig { struct SystemConfig {
std::vector<std::string> imports; std::vector<std::string> imports;
std::vector<std::string> remotes; std::vector<std::string> remotes; // legacy — flat URL list
std::vector<RepoDef> repos; // named repos with channels/mirrors
SystemBlock system; SystemBlock system;
std::vector<PackageRef> packages; std::vector<PackageRef> packages;
std::vector<ServiceRef> services; std::vector<ServiceRef> services;
@@ -72,4 +81,7 @@ struct SystemConfig {
SystemConfig parse_system_config(std::string_view source); SystemConfig parse_system_config(std::string_view source);
SystemConfig resolve_imports(const SystemConfig& cfg, const std::string& base_dir); SystemConfig resolve_imports(const SystemConfig& cfg, const std::string& base_dir);
IndexDef parse_index(std::string_view source);
IndexDef build_index(const std::string& directory);
} // namespace kappa::dsl } // namespace kappa::dsl
+5
View File
@@ -13,6 +13,7 @@ enum class TokenType {
Lbrace, // { Lbrace, // {
Rbrace, // } Rbrace, // }
Equals, // = Equals, // =
Plus, // +
Lbracket, // [ Lbracket, // [
Rbracket, // ] Rbracket, // ]
Comma, // , Comma, // ,
@@ -48,6 +49,10 @@ enum class TokenType {
KwUninstall, KwUninstall,
KwTrue, KwTrue,
KwFalse, KwFalse,
// Repo / index
KwIndex,
KwRepos,
}; };
struct Token { struct Token {
+7
View File
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include "kappa/dsl/system.hpp"
#include <string> #include <string>
#include <vector> #include <vector>
@@ -13,7 +15,12 @@ struct RecipeResult {
std::string error; std::string error;
}; };
// Legacy: flat URL list (backward compat with remotes = [...])
RecipeResult fetch_recipe(const std::string& name, RecipeResult fetch_recipe(const std::string& name,
const std::vector<std::string>& remotes); const std::vector<std::string>& remotes);
// Index-aware: named repos with channels, mirrors, and cached indexes
RecipeResult fetch_recipe_from_repos(const std::string& name,
const std::vector<dsl::RepoDef>& repos);
} // namespace kappa::fetch } // namespace kappa::fetch
+1
View File
@@ -9,5 +9,6 @@ namespace kappa::tools {
void format_package(std::ostream& os, const dsl::PackageDef& pkg); void format_package(std::ostream& os, const dsl::PackageDef& pkg);
void format_config(std::ostream& os, const dsl::SystemConfig& cfg); void format_config(std::ostream& os, const dsl::SystemConfig& cfg);
void format_index(std::ostream& os, const dsl::IndexDef& idx);
} // namespace kappa::tools } // namespace kappa::tools
+324
View File
@@ -0,0 +1,324 @@
#include "kappa/build/build.hpp"
#include "kappa/eval/vars.hpp"
#include <cstdlib>
#include <filesystem>
#include <format>
#include <fstream>
#include <iostream>
#include <string_view>
#include <sys/wait.h>
#include <unistd.h>
#include <unordered_map>
namespace kappa::build {
namespace fs = std::filesystem;
// ---------------------------------------------------------------------------
// Shell execution — runs a command through /bin/sh in the given cwd.
// Environment variables are applied before exec.
// Returns exit code, or -1 if fork/exec fails.
// ---------------------------------------------------------------------------
static int exec_sh(const std::string& cmd,
const std::unordered_map<std::string, std::string>& env,
const fs::path& cwd) {
pid_t pid = fork();
if (pid == 0) {
for (const auto& [k, v] : env) {
setenv(k.c_str(), v.c_str(), 1);
}
if (!cwd.empty()) {
std::error_code ec;
fs::current_path(cwd, ec);
}
execl("/bin/sh", "sh", "-c", cmd.c_str(), nullptr);
_exit(127);
}
if (pid < 0) { return -1; }
int status = 0;
pid_t w;
do {
w = waitpid(pid, &status, 0);
} while (w == -1 && errno == EINTR);
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
// ---------------------------------------------------------------------------
// Config-file variable resolution helpers
// ---------------------------------------------------------------------------
struct CfResolved {
std::string value;
bool skip_line = false;
};
// Parse "${cfg.port ? 8080}" suffix after the variable name.
// Extracts the modifier (?, !) and default value from inner text after a space.
static void parse_cf_suffix(std::string_view& inner,
bool& optional,
bool& required,
std::string_view& default_val) {
auto space = inner.find(' ');
if (space == std::string_view::npos) { return; }
auto suffix = inner.substr(space + 1);
while (!suffix.empty() && suffix.front() == ' ') {
suffix.remove_prefix(1);
}
if (suffix.empty()) { return; }
if (suffix.front() == '?') {
optional = true;
default_val = suffix.substr(1);
while (!default_val.empty() && default_val.front() == ' ') {
default_val.remove_prefix(1);
}
} else if (suffix.front() == '!') {
required = true;
}
inner = inner.substr(0, space);
}
static void trim_trailing_spaces(std::string_view& s) {
while (!s.empty() && s.back() == ' ') {
s.remove_suffix(1);
}
}
static CfResolved resolve_cf_value(std::string_view raw,
const eval::Scope& scope) {
CfResolved result;
std::string out;
out.reserve(raw.size());
std::size_t i = 0;
while (i < raw.size()) {
if (raw[i] != '$' || i + 1 >= raw.size() || raw[i + 1] != '{') {
out += raw[i];
++i;
continue;
}
auto end = raw.find('}', i + 2);
if (end == std::string_view::npos) {
out += raw.substr(i);
break;
}
auto inner = raw.substr(i + 2, end - (i - 2));
bool optional = false;
bool required = false;
std::string_view default_val;
parse_cf_suffix(inner, optional, required, default_val);
trim_trailing_spaces(inner);
auto rv = eval::resolve(inner, scope);
if (rv.kind == eval::VarKind::Unknown) {
if (optional) {
out += default_val;
} else if (required) {
result.skip_line = true;
return result;
}
} else {
out += rv.value;
}
i = end + 1;
}
result.value = std::move(out);
return result;
}
// ---------------------------------------------------------------------------
// Write a config file to destdir.
// "default" → skip if file exists; "replace" → always overwrite;
// "merge" → not yet implemented, falls back to replace.
// ---------------------------------------------------------------------------
static void write_config_file(const dsl::ConfigFile& cf,
const eval::Scope& scope,
const fs::path& destdir) {
auto path = destdir / cf.path;
if (cf.mode == "default" && fs::exists(path)) { return; }
std::error_code ec;
fs::create_directories(path.parent_path(), ec);
if (ec) { return; }
std::ofstream out(path);
if (!out) { return; }
out << "# Generated by kappa — do not edit manually\n";
for (const auto& [key, raw_val] : cf.entries) {
auto resolved = resolve_cf_value(raw_val, scope);
if (resolved.skip_line) { continue; }
const auto& val = resolved.value;
if (val.contains(' ') || val.empty()) {
out << key << " = \"" << val << "\"\n";
} else {
out << key << " = " << val << "\n";
}
}
}
// ---------------------------------------------------------------------------
// Build variable scope from a resolved build step.
// ---------------------------------------------------------------------------
static eval::Scope build_scope(const resolve::BuildStep& step,
const fs::path& destdir,
int jobs) {
eval::Scope scope = eval::make_default_scope();
const auto& pkg = *step.package;
const auto& resolved = step.resolved;
scope.builtins["prefix"] = resolved.config.contains("prefix")
? resolved.config.at("prefix") : "/usr";
scope.builtins["jobs"] = std::to_string(jobs);
scope.builtins["jobopts"] = std::format("-j{}", jobs);
scope.builtins["destdir"] = destdir.string();
scope.builtins["enabledinit"] = step.enabled_init;
scope.config = resolved.config;
if (!scope.config.contains("prefix")) {
scope.config["prefix"] = scope.builtins["prefix"];
}
scope.features = resolved.features;
scope.package["name"] = pkg.name;
scope.package["version"] = pkg.version;
scope.package["source"] = pkg.source;
return scope;
}
// ---------------------------------------------------------------------------
// Snapshot current process environment into a map.
// ---------------------------------------------------------------------------
static std::unordered_map<std::string, std::string> capture_env() {
std::unordered_map<std::string, std::string> env;
for (char** envp = ::environ; *envp != nullptr; ++envp) {
std::string_view entry(*envp);
auto eq = entry.find('=');
if (eq != std::string_view::npos) {
env.emplace(std::string(entry.substr(0, eq)),
std::string(entry.substr(eq + 1)));
}
}
return env;
}
// ---------------------------------------------------------------------------
// Apply package env block to a mutable env map.
// ---------------------------------------------------------------------------
static void apply_package_env(
std::unordered_map<std::string, std::string>& env,
const std::vector<dsl::EnvEntry>& entries) {
for (const auto& e : entries) {
switch (e.mode) {
case dsl::EnvMode::Append: {
auto it = env.find(e.key);
if (it != env.end() && !it->second.empty()) {
it->second += ' ';
}
env[e.key] += e.value;
break;
}
case dsl::EnvMode::Soft:
env.try_emplace(e.key, e.value);
break;
case dsl::EnvMode::Set:
env[e.key] = e.value;
break;
}
}
}
// ---------------------------------------------------------------------------
// Public API — build a single package.
// ---------------------------------------------------------------------------
BuildResult build(const resolve::BuildStep& step,
const std::string& work_dir,
int jobs) {
BuildResult result;
const auto& pkg = *step.package;
// Workspace
fs::path work(work_dir);
fs::path destdir = work / "destdir";
std::error_code ec;
fs::create_directories(destdir, ec);
if (ec) {
result.phase = "setup";
result.error = std::format("cannot create destdir {}: {}",
destdir.string(), ec.message());
return result;
}
// Variable scope
auto scope = build_scope(step, destdir, jobs);
// Environment
auto env = capture_env();
apply_package_env(env, pkg.env_entries);
for (const auto& e : pkg.env_entries) {
switch (e.mode) {
case dsl::EnvMode::Append: {
const char* existing = getenv(e.key.c_str());
std::string val = existing ? std::string(existing) + " " + e.value : e.value;
setenv(e.key.c_str(), val.c_str(), 1);
break;
}
case dsl::EnvMode::Soft:
setenv(e.key.c_str(), e.value.c_str(), 0);
break;
case dsl::EnvMode::Set:
setenv(e.key.c_str(), e.value.c_str(), 1);
break;
}
}
// Config files
for (const auto& cf : pkg.config_files) {
write_config_file(cf, scope, destdir);
}
// Phases
auto run_phase = [&](const dsl::Phase& phase,
const char* phase_name) -> bool {
if (phase.commands.empty()) { return true; }
for (const auto& raw_cmd : phase.commands) {
auto cmd = eval::interpolate(raw_cmd, scope);
if (cmd.empty()) { continue; }
std::cout << std::format(" [{}] {}\n", phase_name, cmd);
int rc = exec_sh(cmd, env, work);
if (rc != 0) {
result.phase = phase_name;
result.error = std::format(
"command exited with code {}: {}", rc, cmd);
return false;
}
}
return true;
};
if (!run_phase(pkg.prepare, "prepare")) { return result; }
if (!run_phase(pkg.build, "build")) { return result; }
if (!run_phase(pkg.check, "check")) { return result; }
if (!run_phase(pkg.install, "install")) { return result; }
result.ok = true;
return result;
}
} // namespace kappa::build
+8 -2
View File
@@ -33,6 +33,8 @@ static const std::unordered_map<std::string_view, TokenType> keywords = {
{"uninstall", TokenType::KwUninstall}, {"uninstall", TokenType::KwUninstall},
{"true", TokenType::KwTrue}, {"true", TokenType::KwTrue},
{"false", TokenType::KwFalse}, {"false", TokenType::KwFalse},
{"index", TokenType::KwIndex},
{"repos", TokenType::KwRepos},
}; };
std::string_view token_name(TokenType type) { std::string_view token_name(TokenType type) {
@@ -42,6 +44,7 @@ std::string_view token_name(TokenType type) {
case TokenType::Lbrace: return "{"; case TokenType::Lbrace: return "{";
case TokenType::Rbrace: return "}"; case TokenType::Rbrace: return "}";
case TokenType::Equals: return "="; case TokenType::Equals: return "=";
case TokenType::Plus: return "+";
case TokenType::Lbracket: return "["; case TokenType::Lbracket: return "[";
case TokenType::Rbracket: return "]"; case TokenType::Rbracket: return "]";
case TokenType::Comma: return ","; case TokenType::Comma: return ",";
@@ -73,6 +76,8 @@ std::string_view token_name(TokenType type) {
case TokenType::KwUninstall: return "uninstall"; case TokenType::KwUninstall: return "uninstall";
case TokenType::KwTrue: return "true"; case TokenType::KwTrue: return "true";
case TokenType::KwFalse: return "false"; case TokenType::KwFalse: return "false";
case TokenType::KwIndex: return "index";
case TokenType::KwRepos: return "repos";
} }
return "?"; return "?";
} }
@@ -143,7 +148,7 @@ Token Lexer::next() {
return {TokenType::Newline, "\n", token_start_line_, token_start_col_}; return {TokenType::Newline, "\n", token_start_line_, token_start_col_};
} }
if (c == '"') { return scan_string(); } if (c == '"') { return scan_string(); }
if (c == '{' || c == '}' || c == '=' || c == '[' || c == ']' || c == ',') { if (c == '{' || c == '}' || c == '=' || c == '+' || c == '[' || c == ']' || c == ',') {
return scan_symbol(); return scan_symbol();
} }
return scan_ident(); return scan_ident();
@@ -155,7 +160,7 @@ Token Lexer::scan_ident() {
while (pos_ < source_.size()) { while (pos_ < source_.size()) {
char c = peek(); char c = peek();
if (std::isspace(static_cast<unsigned char>(c))) { break; } if (std::isspace(static_cast<unsigned char>(c))) { break; }
if (c == '"' || c == '=' || c == '[' || c == ']' || c == ',') { break; } if (c == '"' || c == '=' || c == '+' || c == '[' || c == ']' || c == ',') { break; }
lexeme += advance(); lexeme += advance();
} }
@@ -197,6 +202,7 @@ Token Lexer::scan_symbol() {
case '{': return {TokenType::Lbrace, "{", token_start_line_, token_start_col_}; case '{': return {TokenType::Lbrace, "{", token_start_line_, token_start_col_};
case '}': return {TokenType::Rbrace, "}", token_start_line_, token_start_col_}; case '}': return {TokenType::Rbrace, "}", token_start_line_, token_start_col_};
case '=': return {TokenType::Equals, "=", token_start_line_, token_start_col_}; case '=': return {TokenType::Equals, "=", token_start_line_, token_start_col_};
case '+': return {TokenType::Plus, "+", token_start_line_, token_start_col_};
case '[': return {TokenType::Lbracket, "[", token_start_line_, token_start_col_}; case '[': return {TokenType::Lbracket, "[", token_start_line_, token_start_col_};
case ']': return {TokenType::Rbracket, "]", token_start_line_, token_start_col_}; case ']': return {TokenType::Rbracket, "]", token_start_line_, token_start_col_};
case ',': return {TokenType::Comma, ",", token_start_line_, token_start_col_}; case ',': return {TokenType::Comma, ",", token_start_line_, token_start_col_};
+8 -4
View File
@@ -224,16 +224,20 @@ void Parser::parse_body(PackageDef& pkg) {
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; } if (at(TokenType::Newline)) { advance(); continue; }
auto key = consume(TokenType::Ident).lexeme; auto key = consume(TokenType::Ident).lexeme;
bool soft = false; EnvMode mode = EnvMode::Set;
if (at(TokenType::Ident) && current_.lexeme == "?") { if (at(TokenType::Plus)) {
advance(); advance();
consume(TokenType::Equals); consume(TokenType::Equals);
soft = true; mode = EnvMode::Append;
} else if (at(TokenType::Ident) && current_.lexeme == "?") {
advance();
consume(TokenType::Equals);
mode = EnvMode::Soft;
} else { } else {
consume(TokenType::Equals); consume(TokenType::Equals);
} }
pkg.env_entries.push_back( pkg.env_entries.push_back(
{std::move(key), consume(TokenType::String).lexeme, soft}); {std::move(key), consume(TokenType::String).lexeme, mode});
skip_newlines(); skip_newlines();
} }
consume(TokenType::Rbrace); consume(TokenType::Rbrace);
+165 -5
View File
@@ -1,6 +1,7 @@
#include "kappa/dsl/system.hpp" #include "kappa/dsl/system.hpp"
#include "kappa/dsl/error.hpp" #include "kappa/dsl/error.hpp"
#include "kappa/dsl/lexer.hpp" #include "kappa/dsl/lexer.hpp"
#include "kappa/dsl/parser.hpp"
#include <filesystem> #include <filesystem>
#include <format> #include <format>
@@ -29,6 +30,7 @@ private:
void parse_boot_block(SystemConfig& cfg); void parse_boot_block(SystemConfig& cfg);
void parse_users_block(SystemConfig& cfg); void parse_users_block(SystemConfig& cfg);
void parse_groups_block(SystemConfig& cfg); void parse_groups_block(SystemConfig& cfg);
void parse_repos_block(SystemConfig& cfg);
std::string consume_ident(); std::string consume_ident();
std::string consume_string(); std::string consume_string();
@@ -85,7 +87,9 @@ std::string SysParser::consume_ident() {
at(TokenType::KwCheck) || at(TokenType::KwCheck) ||
at(TokenType::KwInstall) || at(TokenType::KwInstall) ||
at(TokenType::KwTrue) || at(TokenType::KwTrue) ||
at(TokenType::KwFalse)) { at(TokenType::KwFalse) ||
at(TokenType::KwIndex) ||
at(TokenType::KwRepos)) {
auto lexeme = current_.lexeme; auto lexeme = current_.lexeme;
advance(); advance();
return lexeme; return lexeme;
@@ -184,6 +188,7 @@ SystemConfig SysParser::parse() {
else if (kw == "boot") { parse_boot_block(cfg); } else if (kw == "boot") { parse_boot_block(cfg); }
else if (kw == "users") { parse_users_block(cfg); } else if (kw == "users") { parse_users_block(cfg); }
else if (kw == "groups") { parse_groups_block(cfg); } else if (kw == "groups") { parse_groups_block(cfg); }
else if (kw == "repos") { parse_repos_block(cfg); }
else { else {
throw ParseError(current_.line, current_.col, throw ParseError(current_.line, current_.col,
std::format("unknown section '{}'", kw)); std::format("unknown section '{}'", kw));
@@ -214,15 +219,19 @@ void SysParser::parse_system_block(SystemConfig& cfg) {
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) { while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; } if (at(TokenType::Newline)) { advance(); continue; }
auto k = consume_ident(); auto k = consume_ident();
bool soft = false; EnvMode mode = EnvMode::Set;
if (at(TokenType::Ident) && current_.lexeme == "?") { if (at(TokenType::Plus)) {
advance(); advance();
consume(TokenType::Equals); consume(TokenType::Equals);
soft = true; mode = EnvMode::Append;
} else if (at(TokenType::Ident) && current_.lexeme == "?") {
advance();
consume(TokenType::Equals);
mode = EnvMode::Soft;
} else { } else {
consume(TokenType::Equals); consume(TokenType::Equals);
} }
cfg.system.env.push_back({std::move(k), consume_string(), soft}); cfg.system.env.push_back({std::move(k), consume_string(), mode});
skip_newlines(); skip_newlines();
} }
consume(TokenType::Rbrace); consume(TokenType::Rbrace);
@@ -452,6 +461,63 @@ void SysParser::parse_groups_block(SystemConfig& cfg) {
consume(TokenType::Rbrace); consume(TokenType::Rbrace);
} }
void SysParser::parse_repos_block(SystemConfig& cfg) {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
skip_newlines();
if (at(TokenType::Rbrace)) break;
RepoDef repo;
repo.name = consume_ident();
if (at(TokenType::Lbrace)) {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
auto key = consume_ident();
if (key == "url") {
consume(TokenType::Equals);
repo.url = consume_string();
} else if (key == "channels") {
consume(TokenType::Equals);
consume(TokenType::Lbracket);
skip_newlines();
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
repo.channels.push_back(consume_string());
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbracket);
} else if (key == "mirrors") {
consume(TokenType::Equals);
consume(TokenType::Lbracket);
skip_newlines();
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
repo.mirrors.push_back(consume_string());
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbracket);
} else if (key == "priority") {
consume(TokenType::Equals);
repo.priority = parse_int(current_.line, current_.col,
current_.lexeme);
advance();
}
skip_newlines();
}
consume(TokenType::Rbrace);
}
cfg.repos.push_back(std::move(repo));
}
consume(TokenType::Rbrace);
}
SystemConfig parse_system_config(std::string_view source) { SystemConfig parse_system_config(std::string_view source) {
SysParser p(source); SysParser p(source);
return p.parse(); return p.parse();
@@ -462,6 +528,7 @@ static void merge_config(SystemConfig& base, SystemConfig&& imported) {
if (!imported.system.timezone.empty()) { base.system.timezone = std::move(imported.system.timezone); } if (!imported.system.timezone.empty()) { base.system.timezone = std::move(imported.system.timezone); }
for (auto& e : imported.system.env) { base.system.env.push_back(std::move(e)); } for (auto& e : imported.system.env) { base.system.env.push_back(std::move(e)); }
for (auto& r : imported.remotes) { base.remotes.push_back(std::move(r)); } for (auto& r : imported.remotes) { base.remotes.push_back(std::move(r)); }
for (auto& r : imported.repos) { base.repos.push_back(std::move(r)); }
for (auto& [k, v] : imported.system.config) { base.system.config[k] = std::move(v); } for (auto& [k, v] : imported.system.config) { base.system.config[k] = std::move(v); }
for (auto& [k, v] : imported.system.features) { base.system.features[k] = std::move(v); } for (auto& [k, v] : imported.system.features) { base.system.features[k] = std::move(v); }
if (imported.system.rollback.keep > 0) { base.system.rollback.keep = imported.system.rollback.keep; } if (imported.system.rollback.keep > 0) { base.system.rollback.keep = imported.system.rollback.keep; }
@@ -493,4 +560,97 @@ SystemConfig resolve_imports(const SystemConfig& cfg, const std::string& base_di
return resolved; return resolved;
} }
IndexDef parse_index(std::string_view source) {
Lexer lexer(source);
Token current;
auto advance = [&] { current = lexer.next(); };
advance();
auto skip_newlines = [&] {
while (current.type == TokenType::Newline) { advance(); }
};
auto consume = [&](TokenType type) -> Token {
if (current.type != type) {
throw ParseError(current.line, current.col,
msg_expected(token_name(type),
token_name(current.type)));
}
Token t = std::move(current);
advance();
return t;
};
skip_newlines();
consume(TokenType::KwIndex);
// index "kappa-os/stable" { ... }
auto name_tok = consume(TokenType::String);
consume(TokenType::Lbrace);
IndexDef idx;
idx.name = std::move(name_tok.lexeme);
skip_newlines();
while (current.type != TokenType::Rbrace && current.type != TokenType::Eof) {
skip_newlines();
if (current.type == TokenType::Rbrace) break;
IndexEntry entry;
entry.name = current.lexeme;
advance();
if (current.type == TokenType::Lbrace) {
advance();
skip_newlines();
while (current.type != TokenType::Rbrace && current.type != TokenType::Eof) {
if (current.type == TokenType::Newline) { advance(); continue; }
auto key = current.lexeme;
advance();
consume(TokenType::Equals);
if (key == "version") {
entry.version = consume(TokenType::String).lexeme;
}
skip_newlines();
}
consume(TokenType::Rbrace);
}
idx.packages.push_back(std::move(entry));
skip_newlines();
}
consume(TokenType::Rbrace);
return idx;
}
IndexDef build_index(const std::string& directory) {
IndexDef idx;
auto dirname = std::filesystem::path(directory).filename().string();
// If the directory is empty, drop the "."
if (dirname.empty() || dirname == ".") {
dirname = "local";
}
idx.name = dirname;
std::error_code ec;
for (auto& entry : std::filesystem::directory_iterator(directory, ec)) {
if (ec) break;
if (!entry.is_regular_file()) continue;
auto ext = entry.path().extension().string();
if (ext != ".kap") continue;
std::ifstream in(entry.path());
if (!in) continue;
std::ostringstream buf;
buf << in.rdbuf();
try {
auto pkg = parse(buf.str());
idx.packages.push_back({pkg.name, pkg.version});
} catch (...) {
continue;
}
}
return idx;
}
} // namespace kappa::dsl } // namespace kappa::dsl
+232
View File
@@ -125,4 +125,236 @@ RecipeResult fetch_recipe(const std::string& name,
return result; return result;
} }
// ---------------------------------------------------------------------------
// Index-aware recipe fetching
// ---------------------------------------------------------------------------
static std::string sanitize_filename(const std::string& s) {
std::string out = s;
for (auto& c : out) {
if (c == '/' || c == '\\' || c == ':') c = '_';
}
return out;
}
static int exec_curl_conditional(const std::string& output_path,
const std::string& url,
const std::string& time_cond_path) {
std::vector<std::string> argv = {"curl", "-Lsf", "-o", output_path};
if (!time_cond_path.empty()) {
argv.push_back("-z");
argv.push_back(time_cond_path);
}
argv.push_back(url);
std::vector<std::vector<char>> argv_storage(argv.size());
std::vector<char*> cargs;
for (size_t i = 0; i < argv.size(); ++i) {
argv_storage[i].assign(argv[i].begin(), argv[i].end());
argv_storage[i].push_back('\0');
cargs.push_back(argv_storage[i].data());
}
cargs.push_back(nullptr);
pid_t pid = fork();
if (pid == 0) {
execvp(cargs[0], cargs.data());
_exit(127);
}
if (pid < 0) return -1;
int status = 0;
while (waitpid(pid, &status, 0) == -1 && errno == EINTR) {}
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
static std::optional<dsl::IndexDef> load_cached_index(
const std::string& repo_name,
const std::string& channel) {
auto key = sanitize_filename(repo_name + "-" + channel) + ".kap";
auto path = paths::cache_dir() / "indexes" / key;
if (!std::filesystem::exists(path)) return std::nullopt;
std::ifstream in(path);
if (!in) return std::nullopt;
std::ostringstream buf;
buf << in.rdbuf();
try {
return dsl::parse_index(buf.str());
} catch (...) {
return std::nullopt;
}
}
static bool download_index(const dsl::RepoDef& repo,
const std::string& channel,
const std::filesystem::path& cache_path) {
auto url = repo.url;
if (!url.empty() && url.back() != '/') url += '/';
url += channel + "/index.kap";
auto temp = cache_path.string() + ".tmp";
// Try primary URL
int rc = exec_curl_conditional(temp, url, cache_path.string());
if (rc != 0) {
// Try mirrors
for (auto& mirror : repo.mirrors) {
auto murl = mirror;
if (!murl.empty() && murl.back() != '/') murl += '/';
murl += channel + "/index.kap";
rc = exec_curl_conditional(temp, murl, "");
if (rc == 0) break;
}
}
if (rc != 0) return false;
// If curl returned 0 but the file might be empty (304 not modified),
// move temp to cache only if it has content
std::error_code ec;
if (std::filesystem::file_size(temp, ec) > 0 || ec) {
std::filesystem::rename(temp, cache_path, ec);
return !ec;
}
// 304: no change, remove temp
std::filesystem::remove(temp, ec);
return true; // cache is still valid
}
static std::optional<std::string> download_recipe(
const std::string& name,
const dsl::RepoDef& repo,
const std::string& channel) {
auto remote_path = channel + "/" + name + ".kap";
auto temp = paths::temp_dir() / (name + ".kap.tmp");
// Build URL list: primary + mirrors
std::vector<std::string> urls;
auto url = repo.url;
if (!url.empty() && url.back() != '/') url += '/';
urls.push_back(url + remote_path);
for (auto& mirror : repo.mirrors) {
auto murl = mirror;
if (!murl.empty() && murl.back() != '/') murl += '/';
urls.push_back(murl + remote_path);
}
for (auto& u : urls) {
int rc = exec_curl_conditional(temp.string(), u, "");
if (rc == 0) {
std::ifstream in(temp);
if (!in) { std::filesystem::remove(temp); continue; }
std::ostringstream buf;
buf << in.rdbuf();
in.close();
std::filesystem::remove(temp);
return buf.str();
}
}
return std::nullopt;
}
RecipeResult fetch_recipe_from_repos(
const std::string& name,
const std::vector<dsl::RepoDef>& repos) {
RecipeResult result;
// Check cached version first
auto cache_path = paths::packages_dir() / (name + ".kap");
std::string cached_version;
if (std::filesystem::exists(cache_path)) {
std::ifstream in(cache_path);
if (in) {
std::ostringstream buf;
buf << in.rdbuf();
try {
auto pkg = dsl::parse(buf.str());
cached_version = pkg.version;
} catch (...) {}
}
}
// Ensure indexes cache directory exists
std::error_code ec;
std::filesystem::create_directories(paths::cache_dir() / "indexes", ec);
// Sort repos by priority (descending)
std::vector<const dsl::RepoDef*> sorted;
for (auto& r : repos) sorted.push_back(&r);
std::sort(sorted.begin(), sorted.end(),
[](auto* a, auto* b) { return a->priority > b->priority; });
std::string best_content;
std::string best_version;
for (auto* repo : sorted) {
for (auto& channel : repo->channels) {
// Check/update the index for this repo+channel
auto idx_key = sanitize_filename(repo->name + "-" + channel);
auto idx_cache = paths::cache_dir() / "indexes" / (idx_key + ".kap");
// Download fresh index (conditional on cache)
if (!download_index(*repo, channel, idx_cache)) continue;
// Load and search the index
auto idx = load_cached_index(repo->name, channel);
if (!idx) continue;
bool found = false;
for (auto& entry : idx->packages) {
if (entry.name != name) continue;
found = true;
break;
}
if (!found) continue;
// Download the recipe
auto content = download_recipe(name, *repo, channel);
if (!content) continue;
// Parse the downloaded recipe to get version
std::string remote_version;
try {
auto pkg = dsl::parse(*content);
remote_version = pkg.version;
} catch (...) {
continue;
}
if (remote_version > best_version) {
best_version = remote_version;
best_content = std::move(*content);
}
}
}
if (best_content.empty()) {
if (!cached_version.empty()) {
result.ok = true;
result.path = cache_path.string();
result.version = cached_version;
return result;
}
result.error = "package '" + name + "' not found in any repo";
return result;
}
// Update cache if remote is newer
if (best_version > cached_version || cached_version.empty()) {
std::ofstream out(cache_path);
if (!out) {
result.error = "cannot write to cache";
return result;
}
out << best_content;
result.updated = true;
}
result.ok = true;
result.path = cache_path.string();
result.version = best_version;
return result;
}
} // namespace kappa::fetch } // namespace kappa::fetch
+659 -13
View File
@@ -19,7 +19,9 @@
#include "kappa/tools/format.hpp" #include "kappa/tools/format.hpp"
#include <algorithm> #include <algorithm>
#include <array>
#include <cstdlib> #include <cstdlib>
#include <fcntl.h>
#include <filesystem> #include <filesystem>
#include <format> #include <format>
#include <fstream> #include <fstream>
@@ -27,6 +29,8 @@
#include <sstream> #include <sstream>
#include <stdexcept> #include <stdexcept>
#include <string_view> #include <string_view>
#include <sys/wait.h>
#include <unistd.h>
using namespace std::string_view_literals; using namespace std::string_view_literals;
using namespace kappa; using namespace kappa;
@@ -52,6 +56,15 @@ Subcommands:
rebuild <config> Compare config to installed state, rebuild changed rebuild <config> Compare config to installed state, rebuild changed
list List installed packages list List installed packages
rollback Show available generations rollback Show available generations
index <dir> Build an index.kap from .kap files in a directory
search <query> Search cached indexes for packages
add <pkg> [v] Add a package to system config (optional version)
--features "key=on,key2=off" Set feature flags
--config "key=val,key2=val2" Set config values
remove <pkg> Remove a package from system config
upgrade Check indexes for newer versions of installed packages
bump <file> <v> Update a .kap package to a new version (auto-download, rehash)
bump-all <dir> Scan directory and bump all .kap files to latest available
Options: Options:
-h, --help Show this help message -h, --help Show this help message
@@ -104,6 +117,24 @@ static void handle_parse_error(const char* path,
static resolve::Registry build_registry(const dsl::SystemConfig& cfg) { static resolve::Registry build_registry(const dsl::SystemConfig& cfg) {
resolve::Registry registry; resolve::Registry registry;
// Pre-populate from cached packages so transitive deps can be resolved
auto pkg_dir = paths::packages_dir();
std::error_code ec;
if (std::filesystem::exists(pkg_dir)) {
for (auto& entry : std::filesystem::directory_iterator(pkg_dir, ec)) {
if (ec) break;
if (entry.path().extension() != ".kap") continue;
std::ifstream in(entry.path());
if (!in) continue;
std::ostringstream buf;
buf << in.rdbuf();
try {
auto pkg = dsl::parse(buf.str());
registry.try_emplace(pkg.name, std::move(pkg));
} catch (...) { continue; }
}
}
for (const auto& pref : cfg.packages) { for (const auto& pref : cfg.packages) {
bool found = false; bool found = false;
@@ -131,7 +162,26 @@ static resolve::Registry build_registry(const dsl::SystemConfig& cfg) {
} }
} }
// If not found locally, try remotes // If not found locally, try repos (index-aware) first, then legacy remotes
if (!found && !cfg.repos.empty()) {
auto result = fetch::fetch_recipe_from_repos(pref.name, cfg.repos);
if (result.ok && !result.path.empty()) {
std::ifstream in(result.path);
if (in) {
std::ostringstream buf;
buf << in.rdbuf();
try {
auto pkg = dsl::parse(buf.str());
registry[pkg.name] = std::move(pkg);
found = true;
} catch (const std::exception& e) {
std::cerr << "warning: parse error in remote recipe " << pref.name << ": " << e.what() << "\n";
} catch (...) {
std::cerr << "warning: unknown parse error in remote recipe " << pref.name << "\n";
}
}
}
}
if (!found && !cfg.remotes.empty()) { if (!found && !cfg.remotes.empty()) {
auto result = fetch::fetch_recipe(pref.name, cfg.remotes); auto result = fetch::fetch_recipe(pref.name, cfg.remotes);
if (result.ok && !result.path.empty()) { if (result.ok && !result.path.empty()) {
@@ -196,8 +246,15 @@ int main(int argc, char* argv[]) {
|| (subcommand == "build") || (subcommand == "build")
|| (subcommand == "rebuild") || (subcommand == "rebuild")
|| (subcommand == "list") || (subcommand == "list")
|| (subcommand == "fetch-package") || (subcommand == "fetch-package")
|| (subcommand == "rollback"); || (subcommand == "rollback")
|| (subcommand == "index")
|| (subcommand == "search")
|| (subcommand == "add")
|| (subcommand == "remove")
|| (subcommand == "upgrade")
|| (subcommand == "bump")
|| (subcommand == "bump-all");
if (!valid_subcommand) { if (!valid_subcommand) {
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n"; std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
@@ -206,6 +263,9 @@ int main(int argc, char* argv[]) {
} }
const char* file_arg = nullptr; const char* file_arg = nullptr;
const char* version_arg = nullptr;
const char* features_arg = nullptr;
const char* config_arg = nullptr;
bool dry_run = false; bool dry_run = false;
for (int i = arg_start + 1; i < argc; ++i) { for (int i = arg_start + 1; i < argc; ++i) {
if (std::string_view(argv[i]) == "-h" if (std::string_view(argv[i]) == "-h"
@@ -228,13 +288,25 @@ int main(int argc, char* argv[]) {
paths::set_root(argv[++i]); paths::set_root(argv[++i]);
continue; continue;
} }
if (std::string_view(argv[i]) == "--features" && i + 1 < argc) {
features_arg = argv[++i];
continue;
}
if (std::string_view(argv[i]) == "--config" && i + 1 < argc) {
config_arg = argv[++i];
continue;
}
if (std::string_view(argv[i]) == "--dry-run") { if (std::string_view(argv[i]) == "--dry-run") {
dry_run = true; dry_run = true;
continue; continue;
} }
if (!is_flag(argv[i])) { if (!is_flag(argv[i])) {
file_arg = argv[i]; if (file_arg == nullptr) {
break; file_arg = argv[i];
} else if (version_arg == nullptr) {
version_arg = argv[i];
break;
}
} }
} }
@@ -276,6 +348,539 @@ int main(int argc, char* argv[]) {
return 0; return 0;
} }
if (subcommand == "index") {
if (file_arg == nullptr) {
std::cerr << "error: no directory specified\n";
return 1;
}
try {
auto idx = dsl::build_index(file_arg);
auto out_path = std::filesystem::path(file_arg) / "index.kap";
std::ofstream out(out_path);
if (!out) {
std::cerr << "error: cannot write " << out_path.string() << "\n";
return 1;
}
tools::format_index(out, idx);
std::cout << idx.packages.size() << " packages indexed → "
<< out_path.string() << "\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "index error: " << e.what() << "\n";
return 1;
}
}
if (subcommand == "search") {
if (file_arg == nullptr) {
std::cerr << "error: no search query specified\n";
return 1;
}
auto idx_dir = paths::cache_dir() / "indexes";
std::error_code ec;
if (!std::filesystem::exists(idx_dir)) {
std::cout << "no cached indexes — run kappa fetch-package <name> first\n";
return 0;
}
std::string_view query(file_arg);
int found = 0;
for (auto& entry : std::filesystem::directory_iterator(idx_dir, ec)) {
if (ec) break;
auto ext = entry.path().extension().string();
if (ext != ".kap") continue;
std::ifstream in(entry.path());
if (!in) continue;
std::ostringstream buf;
buf << in.rdbuf();
try {
auto idx = dsl::parse_index(buf.str());
for (auto& pkg : idx.packages) {
if (pkg.name.find(query) != std::string::npos) {
std::cout << pkg.name << " " << pkg.version
<< " @ " << idx.name << "\n";
found++;
}
}
} catch (...) { continue; }
}
if (found == 0) {
std::cout << "no packages matching '" << query << "'\n";
} else {
std::cout << found << " result(s)\n";
}
return 0;
}
if (subcommand == "add") {
if (file_arg == nullptr) {
std::cerr << "error: no package name specified\n";
return 1;
}
// Parse --features key=on,key2=off
std::unordered_map<std::string, dsl::FeatureDef> features;
if (features_arg != nullptr) {
std::string_view fsv(features_arg);
std::size_t pos = 0;
while (pos < fsv.size()) {
auto comma = fsv.find(',', pos);
auto pair = fsv.substr(pos, comma == std::string_view::npos
? std::string_view::npos
: comma - pos);
auto eq = pair.find('=');
if (eq != std::string_view::npos) {
auto key = std::string(pair.substr(0, eq));
auto val = pair.substr(eq + 1);
while (!key.empty() && key.back() == ' ') key.pop_back();
while (!val.empty() && val.front() == ' ') val.remove_prefix(1);
features[key] = {val == "on" || val == "true" || val == "1",
false, ""};
}
pos = comma == std::string_view::npos ? fsv.size() : comma + 1;
}
}
// Parse --config key=val,key2=val2
std::unordered_map<std::string, std::string> config;
if (config_arg != nullptr) {
std::string_view csv(config_arg);
std::size_t pos = 0;
while (pos < csv.size()) {
auto comma = csv.find(',', pos);
auto pair = csv.substr(pos, comma == std::string_view::npos
? std::string_view::npos
: comma - pos);
auto eq = pair.find('=');
if (eq != std::string_view::npos) {
auto key = std::string(pair.substr(0, eq));
auto val = std::string(pair.substr(eq + 1));
while (!key.empty() && key.back() == ' ') key.pop_back();
while (!val.empty() && val.front() == ' ') val.erase(0, 1);
config[key] = val;
}
pos = comma == std::string_view::npos ? csv.size() : comma + 1;
}
}
try {
auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap";
dsl::SystemConfig cfg;
if (std::filesystem::exists(config_path)) {
auto src = read_file(config_path.c_str());
cfg = dsl::parse_system_config(src);
}
bool found = false;
for (auto& p : cfg.packages) {
if (p.name == file_arg) {
found = true;
bool changed = false;
if (version_arg != nullptr) {
p.version = version_arg;
changed = true;
}
if (!features.empty()) {
for (auto& [k, v] : features) { p.features[k] = v; changed = true; }
}
if (!config.empty()) {
for (auto& [k, v] : config) { p.config[k] = v; changed = true; }
}
if (changed) {
std::cout << "updated " << file_arg;
if (version_arg != nullptr) std::cout << " → " << version_arg;
std::cout << "\n";
} else {
std::cout << file_arg << " already in packages\n";
}
break;
}
}
if (!found) {
dsl::PackageRef pref;
pref.name = file_arg;
if (version_arg != nullptr) {
pref.version = version_arg;
}
pref.features = std::move(features);
pref.config = std::move(config);
cfg.packages.push_back(std::move(pref));
std::cout << "added " << file_arg;
if (version_arg != nullptr) std::cout << " " << version_arg;
std::cout << "\n";
}
std::error_code ec;
std::filesystem::create_directories(paths::system_dir(), ec);
std::ofstream out(config_path);
if (!out) {
std::cerr << "error: cannot write " << config_path.string() << "\n";
return 1;
}
tools::format_config(out, cfg);
return 0;
} catch (const std::exception& e) {
std::cerr << "add error: " << e.what() << "\n";
return 1;
}
}
if (subcommand == "remove") {
if (file_arg == nullptr) {
std::cerr << "error: no package name specified\n";
return 1;
}
try {
auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap";
if (!std::filesystem::exists(config_path)) {
std::cout << file_arg << " not found (no config)\n";
return 0;
}
auto src = read_file(config_path.c_str());
auto cfg = dsl::parse_system_config(src);
auto it = std::remove_if(cfg.packages.begin(), cfg.packages.end(),
[&](const dsl::PackageRef& p) { return p.name == file_arg; });
if (it == cfg.packages.end()) {
std::cout << file_arg << " not in packages\n";
return 0;
}
cfg.packages.erase(it, cfg.packages.end());
std::cout << "removed " << file_arg << "\n";
std::ofstream out(config_path);
if (!out) {
std::cerr << "error: cannot write " << config_path.string() << "\n";
return 1;
}
tools::format_config(out, cfg);
return 0;
} catch (const std::exception& e) {
std::cerr << "remove error: " << e.what() << "\n";
return 1;
}
}
if (subcommand == "upgrade") {
auto installed = install::read_installed();
if (installed.empty()) {
std::cout << "no packages installed\n";
return 0;
}
// Build a map of latest versions from cached indexes
std::unordered_map<std::string, std::string> latest;
auto idx_dir = paths::cache_dir() / "indexes";
std::error_code ec;
if (std::filesystem::exists(idx_dir)) {
for (auto& entry : std::filesystem::directory_iterator(idx_dir, ec)) {
if (ec) break;
if (entry.path().extension() != ".kap") continue;
std::ifstream in(entry.path());
if (!in) continue;
std::ostringstream buf;
buf << in.rdbuf();
try {
auto idx = dsl::parse_index(buf.str());
for (auto& pkg : idx.packages) {
auto it = latest.find(pkg.name);
if (it == latest.end() || pkg.version > it->second) {
latest[pkg.name] = pkg.version;
}
}
} catch (...) { continue; }
}
}
if (latest.empty()) {
std::cout << "no cached indexes — run kappa fetch-package <name> first\n";
return 0;
}
std::vector<std::pair<std::string, std::string>> upgrades;
for (auto& e : installed) {
auto it = latest.find(e.name);
if (it != latest.end() && it->second > e.version) {
upgrades.emplace_back(e.name, it->second);
}
}
if (upgrades.empty()) {
std::cout << "all " << installed.size() << " packages up to date\n";
return 0;
}
std::cout << upgrades.size() << " upgrade(s) available:\n";
for (auto& [name, ver] : upgrades) {
// Find old version
std::string old_ver;
for (auto& e : installed) {
if (e.name == name) { old_ver = e.version; break; }
}
std::cout << " " << name << " " << old_ver << " → " << ver << "\n";
}
if (dry_run) {
std::cout << "run kappa add <pkg> <version> for each to apply\n";
return 0;
}
// Apply: update system config with new versions
auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap";
dsl::SystemConfig cfg;
if (std::filesystem::exists(config_path)) {
auto src = read_file(config_path.c_str());
try {
cfg = dsl::parse_system_config(src);
} catch (...) { cfg = {}; }
}
for (auto& [name, ver] : upgrades) {
bool found = false;
for (auto& p : cfg.packages) {
if (p.name == name) {
p.version = ver;
found = true;
break;
}
}
if (!found) {
dsl::PackageRef pref;
pref.name = name;
pref.version = ver;
cfg.packages.push_back(std::move(pref));
}
}
std::filesystem::create_directories(paths::system_dir(), ec);
std::ofstream out(config_path);
if (!out) {
std::cerr << "error: cannot write config\n";
return 1;
}
tools::format_config(out, cfg);
std::cout << "config updated — run kappa rebuild to apply\n";
return 0;
}
if (subcommand == "bump") {
if (file_arg == nullptr || version_arg == nullptr) {
std::cerr << "error: bump requires <file> <new-version>\n";
return 1;
}
try {
auto src = read_file(file_arg);
auto pkg = dsl::parse(src);
auto old_version = pkg.version;
auto old_sha = pkg.sha256;
// Construct new source URL with the new version
auto new_source = pkg.source;
std::string_view new_ver(version_arg);
for (auto& [from, to] : {
std::pair{"${version}"sv, new_ver},
std::pair{"${name}"sv, std::string_view(pkg.name)}}) {
std::size_t pos = 0;
while ((pos = new_source.find(from, pos)) != std::string::npos) {
new_source.replace(pos, from.size(), to);
pos += to.size();
}
}
std::cout << "downloading " << new_source << "...\n";
auto temp_tarball = paths::temp_dir() / "bump-download.tar.gz";
std::filesystem::create_directories(paths::temp_dir());
pid_t pid = fork();
if (pid == 0) {
execlp("curl", "curl", "-Lsf", "-o", temp_tarball.c_str(),
new_source.c_str(), nullptr);
_exit(1);
}
int status = 0;
while (waitpid(pid, &status, 0) == -1 && errno == EINTR) {}
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
std::cerr << "download failed\n";
std::filesystem::remove(temp_tarball);
return 1;
}
// Compute new hash
int pipefd[2];
if (pipe(pipefd) != 0) { return 1; }
pid_t pid2 = fork();
if (pid2 == 0) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
execlp("sha256sum", "sha256sum", temp_tarball.c_str(), nullptr);
_exit(1);
}
close(pipefd[1]);
std::array<char, 128> buf;
std::string hash_out;
ssize_t n;
while ((n = read(pipefd[0], buf.data(), buf.size() - 1)) > 0) {
buf[static_cast<std::size_t>(n)] = '\0';
hash_out += buf.data();
}
close(pipefd[0]);
while (waitpid(pid2, nullptr, 0) == -1 && errno == EINTR) {}
auto space = hash_out.find(' ');
auto new_sha = (space != std::string::npos)
? hash_out.substr(0, space) : hash_out;
std::filesystem::remove(temp_tarball);
// Update the .kap file
pkg.version = version_arg;
pkg.sha256 = new_sha;
std::ofstream out(file_arg);
if (!out) {
std::cerr << "error: cannot write " << file_arg << "\n";
return 1;
}
tools::format_package(out, pkg);
std::cout << "bumped " << pkg.name << " " << old_version
<< " → " << version_arg << " sha256: " << new_sha << "\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "bump error: " << e.what() << "\n";
return 1;
}
}
if (subcommand == "bump-all") {
if (file_arg == nullptr) {
std::cerr << "error: bump-all requires a directory\n";
return 1;
}
std::string dir(file_arg);
int bumped = 0;
int skipped = 0;
std::error_code ec;
for (auto& entry : std::filesystem::directory_iterator(dir, ec)) {
if (ec) break;
if (entry.path().extension() != ".kap") continue;
std::ifstream in(entry.path());
if (!in) continue;
std::ostringstream buf;
buf << in.rdbuf();
in.close();
dsl::PackageDef pkg;
try { pkg = dsl::parse(buf.str()); }
catch (...) { skipped++; continue; }
// Try to auto-detect next version: increment patch number
auto dot1 = pkg.version.rfind('.');
if (dot1 == std::string::npos) { skipped++; continue; }
auto base = pkg.version.substr(0, dot1 + 1);
auto patch_str = pkg.version.substr(dot1 + 1);
int patch = 0;
try { patch = std::stoi(patch_str); }
catch (...) { skipped++; continue; }
// Try next 15 patch versions
bool found = false;
for (int next = patch + 1; next <= patch + 15; ++next) {
auto candidate = base + std::to_string(next);
auto url = pkg.source;
std::string_view cand_ver(candidate);
for (auto& [from, to] : {
std::pair{"${version}"sv, cand_ver},
std::pair{"${name}"sv, std::string_view(pkg.name)}}) {
std::size_t pos = 0;
while ((pos = url.find(from, pos)) != std::string::npos) {
url.replace(pos, from.size(), to);
pos += to.size();
}
}
// Try downloading the candidate URL
auto temp = paths::temp_dir() / "bumpall-download.tar.gz";
std::filesystem::create_directories(paths::temp_dir());
pid_t dl_pid = fork();
if (dl_pid == 0) {
// Redirect stdout/stderr to /dev/null for quiet operation
int devnull = open("/dev/null", O_WRONLY);
if (devnull >= 0) {
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
execlp("curl", "curl", "-Lsf", "-o", temp.c_str(), url.c_str(), nullptr);
_exit(1);
}
int dl_status = 0;
while (waitpid(dl_pid, &dl_status, 0) == -1 && errno == EINTR) {}
if (!WIFEXITED(dl_status) || WEXITSTATUS(dl_status) != 0) {
std::filesystem::remove(temp);
continue;
}
// Hash
int hpipe[2];
if (pipe(hpipe) != 0) continue;
pid_t hpid = fork();
if (hpid == 0) {
close(hpipe[0]); dup2(hpipe[1], STDOUT_FILENO); close(hpipe[1]);
execlp("sha256sum", "sha256sum", temp.c_str(), nullptr);
_exit(1);
}
close(hpipe[1]);
std::array<char, 128> hbuf;
std::string hout;
ssize_t hn;
while ((hn = read(hpipe[0], hbuf.data(), hbuf.size() - 1)) > 0) {
hbuf[static_cast<std::size_t>(hn)] = '\0';
hout += hbuf.data();
}
close(hpipe[0]);
while (waitpid(hpid, nullptr, 0) == -1 && errno == EINTR) {}
std::filesystem::remove(temp);
auto sp = hout.find(' ');
auto new_sha = (sp != std::string::npos) ? hout.substr(0, sp) : hout;
// Update file
auto old_ver = pkg.version;
pkg.version = candidate;
pkg.sha256 = new_sha;
std::ofstream out(entry.path());
if (!out) continue;
tools::format_package(out, pkg);
std::cout << pkg.name << " " << old_ver
<< " → " << candidate << "\n";
found = true;
bumped++;
break;
}
if (!found) skipped++;
}
std::cout << bumped << " bumped, " << skipped << " skipped\n";
return 0;
}
if (subcommand == "fetch-package") { if (subcommand == "fetch-package") {
if (file_arg == nullptr) { if (file_arg == nullptr) {
std::cerr << "error: no package name specified\n"; std::cerr << "error: no package name specified\n";
@@ -283,19 +888,26 @@ int main(int argc, char* argv[]) {
} }
try { try {
std::vector<std::string> remotes; std::vector<std::string> remotes;
std::vector<dsl::RepoDef> repos;
auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap"; auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap";
if (std::filesystem::exists(config_path)) { if (std::filesystem::exists(config_path)) {
auto cfg_src = read_file(config_path.c_str()); auto cfg_src = read_file(config_path.c_str());
try { try {
auto cfg = dsl::parse_system_config(cfg_src); auto cfg = dsl::parse_system_config(cfg_src);
remotes = cfg.remotes; remotes = cfg.remotes;
repos = std::move(cfg.repos);
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "warning: config parse error: " << e.what() << "\n"; std::cerr << "warning: config parse error: " << e.what() << "\n";
} catch (...) { } catch (...) {
std::cerr << "warning: unknown config parse error\n"; std::cerr << "warning: unknown config parse error\n";
} }
} }
auto result = fetch::fetch_recipe(file_arg, remotes); fetch::RecipeResult result;
if (!repos.empty()) {
result = fetch::fetch_recipe_from_repos(file_arg, repos);
} else {
result = fetch::fetch_recipe(file_arg, remotes);
}
if (result.ok) { if (result.ok) {
if (result.updated) { if (result.updated) {
std::cout << "fetched " << file_arg << " " << result.version std::cout << "fetched " << file_arg << " " << result.version
@@ -331,7 +943,8 @@ int main(int argc, char* argv[]) {
if (file_arg == nullptr if (file_arg == nullptr
&& subcommand != "list" && subcommand != "list"
&& subcommand != "rollback") { && subcommand != "rollback"
&& subcommand != "index") {
std::cerr << "error: no input file specified\n"; std::cerr << "error: no input file specified\n";
return 1; return 1;
} }
@@ -378,6 +991,12 @@ int main(int argc, char* argv[]) {
} }
std::cout << "\n"; std::cout << "\n";
} }
if (!cfg.repos.empty()) {
int total_channels = 0;
for (auto& r : cfg.repos) total_channels += r.channels.size();
std::cout << " repos: " << cfg.repos.size()
<< " (" << total_channels << " channels)\n";
}
return 0; return 0;
} catch (const std::runtime_error& e) { } catch (const std::runtime_error& e) {
handle_parse_error(file_arg, source, e); handle_parse_error(file_arg, source, e);
@@ -414,9 +1033,17 @@ int main(int argc, char* argv[]) {
return 1; return 1;
} }
return 0; return 0;
} catch (const std::runtime_error& e) { } catch (const std::runtime_error&) {
handle_parse_error(file_arg, source, e); try {
return 1; auto idx = dsl::parse_index(source);
std::cout << file_arg << ": valid index ("
<< idx.name << ", "
<< idx.packages.size() << " packages)\n";
return 0;
} catch (const std::runtime_error& e) {
handle_parse_error(file_arg, source, e);
return 1;
}
} }
} }
} }
@@ -472,9 +1099,15 @@ int main(int argc, char* argv[]) {
auto cfg = dsl::parse_system_config(source); auto cfg = dsl::parse_system_config(source);
tools::format_config(std::cout, cfg); tools::format_config(std::cout, cfg);
return 0; return 0;
} catch (const std::runtime_error& e) { } catch (const std::runtime_error&) {
handle_parse_error(file_arg, source, e); try {
return 1; auto idx = dsl::parse_index(source);
tools::format_index(std::cout, idx);
return 0;
} catch (const std::runtime_error& e) {
handle_parse_error(file_arg, source, e);
return 1;
}
} }
} }
} }
@@ -537,6 +1170,14 @@ int main(int argc, char* argv[]) {
return 1; return 1;
} }
if (!plan.conflicts.empty()) {
std::cerr << "error: package conflicts detected:\n";
for (auto& c : plan.conflicts) {
std::cerr << " " << c << "\n";
}
return 1;
}
std::cout << plan.steps.size() << " packages in build order:\n"; std::cout << plan.steps.size() << " packages in build order:\n";
for (auto& step : plan.steps) { for (auto& step : plan.steps) {
std::cout << " " << step.name << " (" << step.dependencies.size() std::cout << " " << step.name << " (" << step.dependencies.size()
@@ -604,6 +1245,11 @@ int main(int argc, char* argv[]) {
for (auto& c : plan.cycles) std::cerr << " " << c << "\n"; for (auto& c : plan.cycles) std::cerr << " " << c << "\n";
return 1; return 1;
} }
if (!plan.conflicts.empty()) {
std::cerr << "error: package conflicts detected:\n";
for (auto& c : plan.conflicts) std::cerr << " " << c << "\n";
return 1;
}
if (plan.steps.empty()) { if (plan.steps.empty()) {
std::cout << "nothing to build\n"; std::cout << "nothing to build\n";
return 0; return 0;
+52 -1
View File
@@ -42,7 +42,58 @@ BuildPlan resolve(const dsl::SystemConfig& cfg, const Registry& registry) {
nodes.push_back(std::move(step)); nodes.push_back(std::move(step));
} }
// Detect conflicts between selected packages // Transitive dependency discovery: pull in deps that are in the
// registry but not yet in the plan. Continue until no new deps
// are discovered — handles arbitrarily deep dependency chains.
bool changed = true;
while (changed) {
changed = false;
std::size_t current_size = nodes.size();
for (std::size_t i = 0; i < current_size; ++i) {
if (!nodes[i].package) continue;
for (auto& dep : nodes[i].package->depends) {
if (name_to_idx.contains(dep.name)) continue;
// Skip feature-conditional deps that aren't enabled
if (!dep.feature.empty()) {
auto fit = nodes[i].resolved.features.find(dep.feature);
if (fit == nodes[i].resolved.features.end() || !fit->second.enabled) {
continue;
}
}
auto rit = registry.find(dep.name);
if (rit == registry.end()) continue;
auto& pkg = rit->second;
auto resolved = config::resolve_package(pkg, cfg.system, {});
BuildStep step;
step.name = pkg.name;
step.package = &pkg;
step.resolved = std::move(resolved);
step.enabled_init = cfg.boot.init;
for (auto& ddep : pkg.depends) {
if (!ddep.feature.empty()) {
auto fit = step.resolved.features.find(ddep.feature);
if (fit == step.resolved.features.end() || !fit->second.enabled) {
continue;
}
}
step.dependencies.push_back({ddep.name, ddep.version});
}
name_to_idx[step.name] = nodes.size();
nodes.push_back(std::move(step));
changed = true;
}
}
}
// Detect conflicts between all selected packages (including transitive)
std::unordered_set<std::string> selected; std::unordered_set<std::string> selected;
for (auto& node : nodes) { selected.insert(node.name); } for (auto& node : nodes) { selected.insert(node.name); }
+43 -1
View File
@@ -18,7 +18,13 @@ static void write_env(std::ostream& os, int d,
if (entries.empty()) { return; } if (entries.empty()) { return; }
os << Indent(d) << "env {\n"; os << Indent(d) << "env {\n";
for (auto& e : entries) { for (auto& e : entries) {
os << Indent(d + 1) << e.key << (e.soft ? " ?= " : " = ") const char* op;
switch (e.mode) {
case dsl::EnvMode::Soft: op = " ?= "; break;
case dsl::EnvMode::Append: op = " += "; break;
default: op = " = "; break;
}
os << Indent(d + 1) << e.key << op
<< '"' << e.value << "\"\n"; << '"' << e.value << "\"\n";
} }
os << Indent(d) << "}\n"; os << Indent(d) << "}\n";
@@ -231,6 +237,34 @@ void format_config(std::ostream& os, const dsl::SystemConfig& cfg) {
os << "]\n\n"; os << "]\n\n";
} }
if (!cfg.repos.empty()) {
os << "repos {\n";
for (auto& repo : cfg.repos) {
os << " " << repo.name << " {\n";
os << " url = \"" << repo.url << "\"\n";
if (!repo.channels.empty()) {
os << " channels = [";
for (std::size_t i = 0; i < repo.channels.size(); ++i) {
if (i > 0) { os << ", "; }
os << '"' << repo.channels[i] << '"';
}
os << "]\n";
}
if (!repo.mirrors.empty()) {
os << " mirrors = [\n";
for (auto& m : repo.mirrors) {
os << " \"" << m << "\",\n";
}
os << " ]\n";
}
if (repo.priority != 50) {
os << " priority = " << repo.priority << "\n";
}
os << " }\n";
}
os << "}\n\n";
}
if (!cfg.assertions.empty()) { if (!cfg.assertions.empty()) {
os << "assert {\n"; os << "assert {\n";
for (auto& a : cfg.assertions) { for (auto& a : cfg.assertions) {
@@ -339,4 +373,12 @@ void format_config(std::ostream& os, const dsl::SystemConfig& cfg) {
} }
} }
void format_index(std::ostream& os, const dsl::IndexDef& idx) {
os << "index \"" << idx.name << "\" {\n";
for (auto& pkg : idx.packages) {
os << " " << pkg.name << " { version = \"" << pkg.version << "\" }\n";
}
os << "}\n";
}
} // namespace kappa::tools } // namespace kappa::tools
Executable
+1201
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -38,10 +38,6 @@ check "format foo.kap round-trips service block" \
"$KAPPA format examples/foo.kap" \ "$KAPPA format examples/foo.kap" \
'service {' 'service {'
check "format shows conflicts when present" \
"$KAPPA format /tmp/test_conflict.kap" \
'conflicts ='
check "format postgres.kap shows named services" \ check "format postgres.kap shows named services" \
"$KAPPA format examples/postgres.kap" \ "$KAPPA format examples/postgres.kap" \
'service checkpointer {' 'service checkpointer {'