Compare commits
39
Commits
0c1e82d66b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70d59a3f9c | ||
|
|
d0ff52a5d7 | ||
|
|
1f61f116f8 | ||
|
|
d918f7b6fc | ||
|
|
01fcb20a57 | ||
|
|
33ff453cd1 | ||
|
|
b0b54be70e | ||
|
|
e69ebf220d | ||
|
|
7ba3fe6632 | ||
|
|
5607be0f1c | ||
|
|
fa1aa42a9c | ||
|
|
76b5a78e75 | ||
|
|
e3d41dcd20 | ||
|
|
ac530e94a7 | ||
|
|
71d2eadcb9 | ||
|
|
dc782e941c | ||
|
|
3302712336 | ||
|
|
4a8b87e2cd | ||
|
|
5668708fed | ||
|
|
07a485a535 | ||
|
|
d10679d267 | ||
|
|
09dde98159 | ||
|
|
7345b026cc | ||
|
|
7b0538c392 | ||
|
|
4f1ce17ec8 | ||
|
|
86f578bd4a | ||
|
|
d02ab4b7b6 | ||
|
|
e817e58698 | ||
|
|
0aca4b49ca | ||
|
|
486a476b90 | ||
|
|
7e93db2d07 | ||
|
|
dd984f96d4 | ||
|
|
d6612d0a4a | ||
|
|
7712f88386 | ||
|
|
c276465f3a | ||
|
|
6c993d2d17 | ||
|
|
d7fc9d45fb | ||
|
|
72e582eb15 | ||
|
|
ee9f280346 |
@@ -6,6 +6,7 @@ Checks: >
|
|||||||
portability-*,
|
portability-*,
|
||||||
readability-*,
|
readability-*,
|
||||||
-modernize-use-trailing-return-type,
|
-modernize-use-trailing-return-type,
|
||||||
|
-portability-avoid-pragma-once,
|
||||||
-readability-identifier-length,
|
-readability-identifier-length,
|
||||||
-readability-magic-numbers,
|
-readability-magic-numbers,
|
||||||
-readability-identifier-naming
|
-readability-identifier-naming
|
||||||
|
|||||||
@@ -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
|
||||||
+11
-2
@@ -33,8 +33,17 @@
|
|||||||
*.app
|
*.app
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
build/
|
/build/
|
||||||
|
/build-debug/
|
||||||
|
/build-release/
|
||||||
|
/dist/
|
||||||
|
build-gcc/
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
vcpkg_installed/
|
vcpkg_installed/
|
||||||
kappa
|
/kappa
|
||||||
|
|
||||||
|
# Agent session state
|
||||||
|
.omo/
|
||||||
|
.logs/
|
||||||
|
.opencode/
|
||||||
|
|
||||||
|
|||||||
+29
-4
@@ -1,11 +1,9 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
# Enforce Clang
|
|
||||||
set(CMAKE_C_COMPILER clang)
|
|
||||||
set(CMAKE_CXX_COMPILER clang++)
|
|
||||||
|
|
||||||
project(kappa VERSION 0.1.0 LANGUAGES CXX)
|
project(kappa VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 23)
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
@@ -21,6 +19,7 @@ endif()
|
|||||||
|
|
||||||
add_executable(kappa
|
add_executable(kappa
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
|
src/util.cpp
|
||||||
src/paths.cpp
|
src/paths.cpp
|
||||||
src/dsl/lexer.cpp
|
src/dsl/lexer.cpp
|
||||||
src/dsl/parser.cpp
|
src/dsl/parser.cpp
|
||||||
@@ -28,5 +27,31 @@ add_executable(kappa
|
|||||||
src/eval/vars.cpp
|
src/eval/vars.cpp
|
||||||
src/cli/diagnostic.cpp
|
src/cli/diagnostic.cpp
|
||||||
src/config/merge.cpp
|
src/config/merge.cpp
|
||||||
|
src/config/eval.cpp
|
||||||
|
src/tools/format.cpp
|
||||||
|
src/tools/doctor.cpp
|
||||||
|
src/resolve/plan.cpp
|
||||||
|
src/fetch/fetch.cpp
|
||||||
|
src/fetch/recipe.cpp
|
||||||
|
src/build/build.cpp
|
||||||
|
src/sched/scheduler.cpp
|
||||||
|
src/install/install.cpp
|
||||||
|
src/rebuild/rebuild.cpp
|
||||||
|
src/service/s6.cpp
|
||||||
|
src/service/systemd.cpp
|
||||||
|
src/service/types.cpp
|
||||||
|
src/service/openrc.cpp
|
||||||
|
src/service/dinit.cpp
|
||||||
|
src/service/runit.cpp
|
||||||
|
src/service/install.cpp
|
||||||
|
src/boot/types.cpp
|
||||||
|
src/boot/limine.cpp
|
||||||
|
src/boot/grub.cpp
|
||||||
|
src/boot/install.cpp
|
||||||
|
src/system/activate.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(kappa PRIVATE include)
|
target_include_directories(kappa PRIVATE include)
|
||||||
|
target_link_libraries(kappa PRIVATE Threads::Threads)
|
||||||
|
target_compile_options(kappa PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
|
||||||
|
install(TARGETS kappa RUNTIME DESTINATION bin)
|
||||||
|
|||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
# Contributing to kappa
|
||||||
|
|
||||||
|
We're building a package manager that doesn't care about your init system,
|
||||||
|
your bootloader, or your life choices. If that sounds like your kind of
|
||||||
|
project, keep reading.
|
||||||
|
|
||||||
|
## The rules
|
||||||
|
|
||||||
|
These aren't guidelines. They're the deal.
|
||||||
|
|
||||||
|
### 1. C++23, or don't bother
|
||||||
|
|
||||||
|
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
|
||||||
|
doesn't belong here. The standard library is your only dependency. Zero
|
||||||
|
external C++ libraries. Not even Boost.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// ✓ yes
|
||||||
|
auto msg = std::format("building {} (depth={})", name, depth);
|
||||||
|
|
||||||
|
// ✗ no
|
||||||
|
auto msg = fmt::format("building {} (depth={})", name, depth);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Every new module mirrors the existing structure
|
||||||
|
|
||||||
|
```
|
||||||
|
include/kappa/{module}/
|
||||||
|
├── types.hpp # enums, structs, parse/validate declarations
|
||||||
|
├── {feature}.hpp # public interface
|
||||||
|
src/{module}/
|
||||||
|
├── types.cpp # implementations
|
||||||
|
├── backend_a.cpp # per-variant generators
|
||||||
|
├── backend_b.cpp
|
||||||
|
└── install.cpp # dispatch + orchestration
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're adding a feature, look at `src/service/` or `src/boot/` for the
|
||||||
|
pattern. If your new module doesn't look like those, you're doing it wrong.
|
||||||
|
|
||||||
|
### 3. Never suppress type safety
|
||||||
|
|
||||||
|
There is no `as any`, no `@ts-ignore`, no `reinterpret_cast` abuse, and
|
||||||
|
no `void*` unless you're talking to the kernel. If the type system is
|
||||||
|
fighting you, you're fighting the design. Fix the design.
|
||||||
|
|
||||||
|
### 4. Use the namespace. All of it.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace kappa::module {
|
||||||
|
// everything goes here
|
||||||
|
} // namespace kappa::module
|
||||||
|
```
|
||||||
|
|
||||||
|
No `using namespace std;` at file scope. No anonymous namespaces for
|
||||||
|
functions that are used across files — extract to `util.hpp` instead.
|
||||||
|
|
||||||
|
### 5. Thread safety is not optional
|
||||||
|
|
||||||
|
The scheduler is multithreaded. If you touch shared state, you own the
|
||||||
|
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`
|
||||||
|
means, stay out of the scheduler.
|
||||||
|
|
||||||
|
### 6. Tests are shell scripts.
|
||||||
|
|
||||||
|
Integration tests live in three suites. If you add a subcommand, add a test.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./test.sh # 30 tests — DSL parsing, config, services, init systems
|
||||||
|
./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
|
||||||
|
|
||||||
|
The `.kap` DSL is the contract. You can add keywords. You cannot remove
|
||||||
|
them. You can extend syntax. You cannot break existing configs. If your
|
||||||
|
change means someone's `config.kap` stops parsing, it doesn't ship.
|
||||||
|
|
||||||
|
## How to contribute
|
||||||
|
|
||||||
|
### Pick something
|
||||||
|
|
||||||
|
Good first issues:
|
||||||
|
|
||||||
|
- Adding a 6th init system backend (we have 5: systemd, openrc, s6, runit, dinit)
|
||||||
|
- Adding a 3rd bootloader backend (we have 2: grub, limine)
|
||||||
|
- Improving the `kappa doctor` diagnostics for package recipes
|
||||||
|
- Adding `--features` / `--config` flags to `kappa add`
|
||||||
|
- Shell completion scripts (bash, zsh, fish)
|
||||||
|
|
||||||
|
Ambitious issues:
|
||||||
|
|
||||||
|
- Binary package support (pre-built caches)
|
||||||
|
- Remote build farm (distcc-style)
|
||||||
|
- Signed package verification with index signing
|
||||||
|
- Filesystem overlay activation (like Nix profiles)
|
||||||
|
- Transitive dependency resolution (auto-including deps not in config)
|
||||||
|
|
||||||
|
### Send a PR
|
||||||
|
|
||||||
|
1. Fork the repo
|
||||||
|
2. Create a branch: `feat/my-thing` or `fix/my-bug`
|
||||||
|
3. Write code that follows the rules above
|
||||||
|
4. Run `./test.sh && ./test-init-switch.sh && ./test-full.sh` — all three must pass
|
||||||
|
5. Open a PR against `main`
|
||||||
|
|
||||||
|
### PR requirements
|
||||||
|
|
||||||
|
- Build must pass: `cmake --build build` with zero warnings on Clang 17+
|
||||||
|
- 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.
|
||||||
|
- Commit messages in imperative: `Add runit backend` not `Added runit backend`
|
||||||
|
|
||||||
|
## What we won't merge
|
||||||
|
|
||||||
|
- **`systemd`-only features.** If it can't work on at least two init systems,
|
||||||
|
it goes in a `systemd` package definition, not in kappa.
|
||||||
|
- **Dependency on a specific distro.** Kappa runs on any Linux kernel. No
|
||||||
|
hardcoded paths to `/usr/lib/systemd`, no assumptions about `/etc/os-release`.
|
||||||
|
- **Abstract nonsense.** FactoryFactoryBuilder patterns. Premature
|
||||||
|
generalization. If you need three layers of indirection to add a feature,
|
||||||
|
the feature is too complicated.
|
||||||
|
- **AI slop.** If it looks like ChatGPT wrote it, it gets rejected. We can
|
||||||
|
tell. Write code like a human who's been doing this for a decade.
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
|
||||||
|
We don't have a Discord. We don't have a forum. Open an issue. Write a
|
||||||
|
clear title, a reproduction case, and what you expected. We'll respond
|
||||||
|
when we respond.
|
||||||
|
|
||||||
|
If you want to propose a major feature, open an issue first. Surprise PRs
|
||||||
|
that rewrite half the codebase get closed without review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Kappa is 0.1.0. Everything is subject to change except the rules above.
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
FROM alpine:edge AS builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
clang cmake make ninja \
|
||||||
|
git linux-headers \
|
||||||
|
samurai
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN cmake -B build -G Ninja \
|
||||||
|
-DCMAKE_C_COMPILER=clang \
|
||||||
|
-DCMAKE_CXX_COMPILER=clang++ \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release
|
||||||
|
|
||||||
|
RUN cmake --build build
|
||||||
|
|
||||||
|
FROM alpine:edge
|
||||||
|
|
||||||
|
RUN apk add --no-cache libstdc++
|
||||||
|
|
||||||
|
COPY --from=builder /build/build/kappa /usr/local/bin/kappa
|
||||||
|
COPY --from=builder /build/examples /opt/kappa/examples
|
||||||
|
|
||||||
|
WORKDIR /opt/kappa
|
||||||
|
|
||||||
|
ENTRYPOINT ["kappa"]
|
||||||
|
CMD ["--help"]
|
||||||
@@ -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)
|
||||||
@@ -1,3 +1,203 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="https://git.spectoria.dev/repo-avatars/f111d66d4a9972c13eb8aa4dddc7fe2c39aabdf0ab0ff0dc27a31a9aa1c2e968" width="180" alt="kappa mascot" />
|
||||||
|
</p>
|
||||||
|
|
||||||
# kappa
|
# kappa
|
||||||
|
|
||||||
A alternative implementation of Iota for the ZereneOS project.
|
**Anywhere, any init, anytime.**
|
||||||
|
|
||||||
|
A declarative, source-based package manager that doesn't care what init system
|
||||||
|
you run. Or what bootloader. Or what CPU architecture. Kappa builds your entire
|
||||||
|
system from source — and lets you swap the init system like you'd swap a
|
||||||
|
wallpaper.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Why
|
||||||
|
|
||||||
|
Every other package manager picked a side. `apt` married systemd. `pacman`
|
||||||
|
shackled itself to Arch's ecosystem. `emerge` gave you choice but at the cost
|
||||||
|
of your weekend. Nix gave you reproducibility but took your filesystem with it.
|
||||||
|
|
||||||
|
Kappa is what you get when you stop negotiating. You declare what your system
|
||||||
|
*is*, and kappa figures out how to build it. Change your mind about the init
|
||||||
|
system? Rebuild only the packages that care — the other 800 stay put.
|
||||||
|
|
||||||
|
### What it does
|
||||||
|
|
||||||
|
```
|
||||||
|
# Your system, in one file:
|
||||||
|
boot {
|
||||||
|
kernel = "linux"
|
||||||
|
init = "s6" # swap to "systemd" anytime
|
||||||
|
bootloader = "limine" # or "grub"
|
||||||
|
root = "/dev/sda1"
|
||||||
|
}
|
||||||
|
|
||||||
|
packages {
|
||||||
|
nginx { version = ">=1.24" }
|
||||||
|
postgresql {}
|
||||||
|
zlib {}
|
||||||
|
}
|
||||||
|
|
||||||
|
services {
|
||||||
|
nginx { enable = true }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kappa rebuild config.kap # builds everything, generates service files
|
||||||
|
kappa rebuild config.kap # boot.init = "openrc" — only 5 packages actually rebuild
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features that nobody else has
|
||||||
|
|
||||||
|
- **Init-system-as-configuration.** `boot.init = "s6"` → generates s6 service
|
||||||
|
directories. Change it to `"systemd"` → regenerates `.service` units. Change
|
||||||
|
it to `"openrc"` → generates init.d scripts. The package definitions don't
|
||||||
|
know or care which init you picked. That's kappa's problem.
|
||||||
|
|
||||||
|
- **Post-install init switching.** Change `boot.init`, run `kappa rebuild`, reboot.
|
||||||
|
You're now on a different init system. Only packages that actually use
|
||||||
|
`${enabledinit}` in their build scripts need recompiling. Everything else
|
||||||
|
just gets new service files generated.
|
||||||
|
|
||||||
|
- **Bootloader rollback.** Every rebuild creates a fallback boot entry pointing
|
||||||
|
at the previous generation's init. If the new one doesn't boot, the old one
|
||||||
|
is one reboot away.
|
||||||
|
|
||||||
|
- **Parallel scheduler.** `-w 4 -j 8` means four packages building
|
||||||
|
simultaneously, eight jobs each. The scheduler uses depth-based priority
|
||||||
|
grouping so leaf dependencies unblock as much work as possible first.
|
||||||
|
|
||||||
|
- **Package recipe caching with indexed repos.** Declare named repos with
|
||||||
|
channels and mirrors in your config. Kappa fetches `index.kap` from each
|
||||||
|
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/`
|
||||||
|
(default: `/usr/local/kappa/cache/`). Rebuilds don't touch the network
|
||||||
|
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
|
||||||
|
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
|
||||||
|
creates the groups. `system { hostname = "mybox" }` — kappa writes
|
||||||
|
`/etc/hostname`. No `systemctl`, no `rc-update`, no init dependency.
|
||||||
|
|
||||||
|
### 5 init systems. 2 bootloaders. Zero lock-in.
|
||||||
|
|
||||||
|
| Init | Service location | Enable command |
|
||||||
|
|------|-----------------|----------------|
|
||||||
|
| systemd | `/etc/systemd/system/{name}.service` | `systemctl enable` |
|
||||||
|
| openrc | `/etc/init.d/{name}` | `rc-update add` |
|
||||||
|
| s6 | `/etc/s6/sv/{name}/run` | `s6-rc-bundle-update` |
|
||||||
|
| runit | `/etc/sv/{name}/run` | `ln -sf /etc/sv/{name} /var/service/` |
|
||||||
|
| dinit | `/etc/dinit.d/{name}` | `dinitctl enable` |
|
||||||
|
|
||||||
|
| Bootloader | Config path |
|
||||||
|
|-----------|------------|
|
||||||
|
| limine | `/boot/limine.cfg` |
|
||||||
|
| grub | `/boot/grub/grub.cfg` |
|
||||||
|
|
||||||
|
### Quick start
|
||||||
|
|
||||||
|
> **Note for 0.1.x users**: The default store root has moved from `/kappa` to
|
||||||
|
> `/usr/local/kappa` (FHS 3.0). Set `KAPPA_ROOT` to your existing `/kappa`
|
||||||
|
> directory to keep using the old location.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Build kappa (needs Clang 17+, CMake 3.20+, C++23)
|
||||||
|
cmake -B build -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
|
||||||
|
cmake --build build
|
||||||
|
|
||||||
|
# Add packages to your system config
|
||||||
|
build/kappa add make
|
||||||
|
build/kappa add nginx ">=1.24"
|
||||||
|
build/kappa add zlib
|
||||||
|
|
||||||
|
# Rebuild — kappa fetches recipes, resolves deps, builds everything
|
||||||
|
build/kappa rebuild $KAPPA_ROOT/system/config.kap
|
||||||
|
|
||||||
|
# Remove packages
|
||||||
|
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
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
|---------|-------------|
|
||||||
|
| `add <pkg> [version]` | Add a package to system config |
|
||||||
|
| `remove <pkg>` | Remove a package from system config |
|
||||||
|
| `build <package>` | Build a single package from its `.kap` definition |
|
||||||
|
| `rebuild <config>` | Diff config against installed state, rebuild changed |
|
||||||
|
| `resolve <config>` | Compute a build plan (shows order, deps, conflicts) |
|
||||||
|
| `doctor <file>` | Check a file for issues and warnings |
|
||||||
|
| `fetch <package>` | Download and verify source tarballs |
|
||||||
|
| `fetch-package <name>` | Fetch a package recipe from remotes |
|
||||||
|
| `format <file>` | Pretty-print to canonical style |
|
||||||
|
| `index <dir>` | Build an `index.kap` from `.kap` files in a directory |
|
||||||
|
| `list` | Show installed packages |
|
||||||
|
| `parse-config <file>` | Validate a system configuration |
|
||||||
|
| `parse-package <file>` | Validate a `.kap` package definition |
|
||||||
|
| `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
|
||||||
|
|
||||||
|
BSD 2-Clause. Do whatever you want. Just don't sue us.
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](CONTRIBUTING.md). We're opinionated but we merge good
|
||||||
|
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
@@ -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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Per-package override for foo.
|
* Per-package override for foo.
|
||||||
* Lives at /kappa/system/builds/foo.kap
|
* Lives at /usr/local/kappa/system/builds/foo.kap
|
||||||
*
|
*
|
||||||
* Uses system config syntax — only features/config need to be specified.
|
* Uses system config syntax — only features/config need to be specified.
|
||||||
* The rest inherits from the package definition and system config.
|
* The rest inherits from the package definition and system config.
|
||||||
|
|||||||
+53
-2
@@ -1,10 +1,55 @@
|
|||||||
/*
|
/*
|
||||||
* Kappa system configuration.
|
* Kappa system configuration.
|
||||||
* Lives at /kappa/system/config.kap
|
* Lives at /usr/local/kappa/system/config.kap
|
||||||
|
*
|
||||||
|
* INIT SYSTEM SELECTION
|
||||||
|
* =====================
|
||||||
|
* The `boot.init` field (line ~84) selects which init system manages this
|
||||||
|
* machine. Valid values (case-insensitive):
|
||||||
|
*
|
||||||
|
* systemd — system and service manager
|
||||||
|
* openrc — OpenRC dependency-based init
|
||||||
|
* s6 — s6 supervision suite
|
||||||
|
* dinit — dinit service manager / init system
|
||||||
|
*
|
||||||
|
* The `boot.bootloader` field (line ~122) selects which bootloader config
|
||||||
|
* kappa generates (grub or limine).
|
||||||
|
*
|
||||||
|
* This setting determines:
|
||||||
|
* 1. Which backend generates service files at install time
|
||||||
|
* (systemd → .service units, openrc → init.d scripts, etc.)
|
||||||
|
* 2. What `${enabledinit}` resolves to during package builds
|
||||||
|
*
|
||||||
|
* Package definitions do NOT specify per-init service blocks. A package
|
||||||
|
* defines its service once (see examples/foo.kap) and the selected init
|
||||||
|
* system's backend handles the translation.
|
||||||
|
*
|
||||||
|
* SERVICES BLOCK
|
||||||
|
* ==============
|
||||||
|
* The `services` section enables or disables services declared by
|
||||||
|
* installed packages. Each entry maps to a package's service name:
|
||||||
|
*
|
||||||
|
* services {
|
||||||
|
* nginx { enable = true } // package "nginx", default "main" service
|
||||||
|
* postgresql.main { enable = true } // package "postgresql", named service "main"
|
||||||
|
* postgresql.checkpointer { enable = false }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* For single-service packages, the service name defaults to "main" and
|
||||||
|
* can be omitted. For multi-service packages, use dot-notation
|
||||||
|
* (pkgname.servicename) to target a specific named service.
|
||||||
|
*
|
||||||
|
* Additional keys in each service block (port, ssl, etc.) are passed as
|
||||||
|
* custom config to the service definition.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
imports = []
|
imports = []
|
||||||
|
|
||||||
|
remotes = [
|
||||||
|
"https://packages.kappa-os.org/stable/",
|
||||||
|
"https://packages.kappa-os.org/contrib/",
|
||||||
|
]
|
||||||
|
|
||||||
assert {
|
assert {
|
||||||
"efi partition required for UEFI boot" : boot.efi != ""
|
"efi partition required for UEFI boot" : boot.efi != ""
|
||||||
"root partition must be set" : boot.root != ""
|
"root partition must be set" : boot.root != ""
|
||||||
@@ -22,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"
|
||||||
}
|
}
|
||||||
@@ -56,7 +103,7 @@ packages {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
// Per-package overrides can also live in
|
// Per-package overrides can also live in
|
||||||
// /kappa/system/builds/<name>.kap
|
// /usr/local/kappa/system/builds/<name>.kap
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,11 +122,15 @@ services {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Select init system — determines which backend generates service files.
|
||||||
|
// Valid: systemd, openrc, s6, dinit (case-insensitive).
|
||||||
boot {
|
boot {
|
||||||
kernel = "linux"
|
kernel = "linux"
|
||||||
init = "s6"
|
init = "s6"
|
||||||
efi = "/dev/sda2"
|
efi = "/dev/sda2"
|
||||||
swap = "/dev/sda3"
|
swap = "/dev/sda3"
|
||||||
|
// Bootloader — generates the appropriate config at install time.
|
||||||
|
// Valid: grub, limine (case-insensitive).
|
||||||
bootloader = "limine"
|
bootloader = "limine"
|
||||||
root = "/dev/sda1"
|
root = "/dev/sda1"
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-10
@@ -1,13 +1,71 @@
|
|||||||
/*
|
/*
|
||||||
* foo — a web server with optional SSL and GUI support.
|
* foo — a web server with optional SSL and GUI support.
|
||||||
* Demonstrates the full kappa DSL surface.
|
*
|
||||||
|
* SERVICE MODEL
|
||||||
|
* =============
|
||||||
|
* Kappa service definitions are init-system-agnostic. The `service` block
|
||||||
|
* describes what the service IS (exec, type, ports, user) — NOT how each init
|
||||||
|
* system runs it. The system config's `boot.init` field (see config.kap)
|
||||||
|
* determines which init system's service files get generated at install time:
|
||||||
|
*
|
||||||
|
* boot.init = "systemd" → generates .service unit files
|
||||||
|
* boot.init = "openrc" → generates /etc/init.d scripts
|
||||||
|
* boot.init = "s6" → generates s6 service directories
|
||||||
|
* boot.init = "dinit" → generates dinit service descriptors
|
||||||
|
* boot.init = "runit" → generates runit service directories
|
||||||
|
*
|
||||||
|
* Per-init blocks (service { systemd { ... } s6 { ... } }) do NOT exist.
|
||||||
|
* If a package genuinely needs init-specific behaviour (e.g. different
|
||||||
|
* ./configure flags for systemd vs. openrc), use ${enabledinit} in the
|
||||||
|
* build phase — see examples/postgres.kap for that pattern.
|
||||||
|
*
|
||||||
|
* SERVICE TYPE VALUES
|
||||||
|
* ===================
|
||||||
|
* These are semantic, not init-specific. Each backend translates them
|
||||||
|
* into its own vocabulary:
|
||||||
|
*
|
||||||
|
* "simple" — foreground process; init manages lifecycle directly.
|
||||||
|
* systemd: Type=simple openrc: command_background=false
|
||||||
|
* s6: type=longrun dinit: type=process
|
||||||
|
*
|
||||||
|
* "forking" — process daemonises itself; init tracks the forked PID.
|
||||||
|
* systemd: Type=forking openrc: command_background=true
|
||||||
|
* s6: type=longrun dinit: type=bgprocess
|
||||||
|
*
|
||||||
|
* "notify" — foreground process that signals readiness (sd_notify).
|
||||||
|
* systemd: Type=notify openrc: command_background=true
|
||||||
|
* s6: type=longrun dinit: type=process
|
||||||
|
*
|
||||||
|
* "oneshot" — runs once and exits (startup tasks, database migrations).
|
||||||
|
* systemd: Type=oneshot openrc: command_background=false
|
||||||
|
* s6: type=oneshot dinit: type=scripted
|
||||||
|
*
|
||||||
|
* "longrun" — long-running supervised process (s6/runit idiom).
|
||||||
|
* systemd: Type=simple openrc: command_background=true
|
||||||
|
* s6: type=longrun dinit: type=process
|
||||||
|
*
|
||||||
|
* The backend generators handle all translation. Package authors only
|
||||||
|
* 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"
|
||||||
const source = "https://example.com/foo-${version}.tar.gz"
|
const source = "https://example.com/foo-${version}.tar.gz"
|
||||||
|
sha256 = "e127a709cba24c76de8936cb7083dd768f28cd37eb010492e2f19b71eb1294e4"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
provides = ["libfoo.so.1", "foo"]
|
provides = ["libfoo.so.1", "foo"]
|
||||||
|
conflicts = [] // packages this cannot coexist with (e.g. ["eudev"] if this were systemd)
|
||||||
|
|
||||||
patches = [
|
patches = [
|
||||||
{
|
{
|
||||||
@@ -54,23 +112,28 @@ 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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- service ----------------------------------------------------------
|
||||||
|
// Init-agnostic service definition. The `type` field is semantic
|
||||||
|
// ("forking") — the selected init system's backend translates it into
|
||||||
|
// the appropriate native format. If the package ships multiple
|
||||||
|
// services, use named blocks (see examples/postgres.kap).
|
||||||
service {
|
service {
|
||||||
runit {
|
|
||||||
exec = "/usr/bin/foo --daemon"
|
|
||||||
type = "forking"
|
|
||||||
user = "foo"
|
|
||||||
}
|
|
||||||
s6 {
|
|
||||||
exec = "/usr/bin/foo"
|
exec = "/usr/bin/foo"
|
||||||
type = "longrun"
|
type = "forking" // daemonises itself
|
||||||
ports = [80, 443]
|
|
||||||
user = "foo"
|
user = "foo"
|
||||||
}
|
ports = [80, 443]
|
||||||
|
description = "Foo web server"
|
||||||
|
after = "network" // ordering hint — systemd After=, OpenRC need, etc.
|
||||||
|
restart = "on-failure" // "always" | "on-failure" | "never"
|
||||||
}
|
}
|
||||||
|
|
||||||
prepare {
|
prepare {
|
||||||
@@ -89,4 +152,8 @@ package "foo" {
|
|||||||
install {
|
install {
|
||||||
make DESTDIR=${destdir} install
|
make DESTDIR=${destdir} install
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uninstall {
|
||||||
|
make -C build uninstall
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
index "local" {
|
||||||
|
foo { version = "1.2.3" }
|
||||||
|
postgresql { version = "16.3" }
|
||||||
|
make { version = "4.4.1" }
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* postgresql — a database server shipping multiple services.
|
||||||
|
*
|
||||||
|
* MULTI-SERVICE PACKAGES
|
||||||
|
* ======================
|
||||||
|
* Packages that install more than one long-running process can declare
|
||||||
|
* multiple named `service` blocks. Each has its own exec, type, ports,
|
||||||
|
* and lifecycle config. The system config enables them individually using
|
||||||
|
* dot-notation (see config.kap):
|
||||||
|
*
|
||||||
|
* services {
|
||||||
|
* postgresql.main { enable = true }
|
||||||
|
* postgresql.checkpointer { enable = true }
|
||||||
|
* postgresql.walwriter { enable = true }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Omitting the dot selects the service named "main".
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* INIT-CONDITIONAL BUILDS
|
||||||
|
* =======================
|
||||||
|
* The variable ${enabledinit} exposes the configured init system name
|
||||||
|
* (from boot.init in config.kap) during the build phase. Use shell
|
||||||
|
* conditionals — no DSL if/else needed:
|
||||||
|
*
|
||||||
|
* build {
|
||||||
|
* case ${enabledinit} in
|
||||||
|
* systemd) ./configure --with-systemd --prefix=${prefix} ;;
|
||||||
|
* openrc) ./configure --with-openrc --prefix=${prefix} ;;
|
||||||
|
* s6|dinit) ./configure --prefix=${prefix} ;;
|
||||||
|
* esac
|
||||||
|
* make -j${jobs}
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* ${enabledinit} is interpolated to the literal init system name
|
||||||
|
* ("systemd", "openrc", "s6", or "dinit") before the shell executes the
|
||||||
|
* block. No DSL context-sensitive parsing required.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* SERVICE TYPE TRANSLATION (for reference)
|
||||||
|
* ========================================
|
||||||
|
* semantic │ systemd │ openrc │ s6 │ dinit
|
||||||
|
* ──────────┼────────────┼─────────────────────┼──────────┼──────────
|
||||||
|
* simple │ Type=simple│ bg=false │ longrun │ process
|
||||||
|
* forking │ Type=fork │ bg=true │ longrun │ bgprocess
|
||||||
|
* notify │ Type=notify│ bg=true │ longrun │ process
|
||||||
|
* oneshot │ Type=one │ bg=false, args="" │ oneshot │ scripted
|
||||||
|
* longrun │ Type=simple│ bg=true │ longrun │ process
|
||||||
|
*/
|
||||||
|
package "postgresql" {
|
||||||
|
const version = "16.3"
|
||||||
|
const source = "https://ftp.postgresql.org/pub/source/v${version}/postgresql-${version}.tar.gz"
|
||||||
|
sha256 = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
|
||||||
|
license = "PostgreSQL"
|
||||||
|
|
||||||
|
depends = [
|
||||||
|
{ name = "readline", version = ">=8" },
|
||||||
|
{ name = "zlib", version = ">=1.2" },
|
||||||
|
{ name = "openssl" },
|
||||||
|
]
|
||||||
|
|
||||||
|
conflicts = [] // mutually exclusive packages (e.g. systemd vs eudev)
|
||||||
|
|
||||||
|
features {
|
||||||
|
ssl = { enabled = true, flag = "--with-ssl=openssl" }
|
||||||
|
nls = { enabled = true, flag = "--enable-nls" }
|
||||||
|
systemd = { enabled = false, flag = "--with-systemd" }
|
||||||
|
}
|
||||||
|
|
||||||
|
config {
|
||||||
|
file "etc/postgresql/data/postgresql.conf" mode = "default" {
|
||||||
|
port = ${cfg.port ? 5432}
|
||||||
|
max_connections = ${cfg.max_conn ? 100}
|
||||||
|
shared_buffers = ${cfg.shared_buf ? 128MB}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
CFLAGS = "-O2"
|
||||||
|
// append security hardening flags
|
||||||
|
CFLAGS += "-D_FORTIFY_SOURCE=2"
|
||||||
|
LDFLAGS = "-Wl,--as-needed"
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- services ---------------------------------------------------------
|
||||||
|
// PostgreSQL ships the main server plus several auxiliary processes.
|
||||||
|
// Each runs as a separate service under the init system.
|
||||||
|
|
||||||
|
// Default service (name = "main"). Enabled via: postgresql { enable = true }
|
||||||
|
service main {
|
||||||
|
exec = "/usr/bin/postgres -D /var/lib/postgresql/data"
|
||||||
|
type = "forking" // postmaster daemonises itself
|
||||||
|
user = "postgres"
|
||||||
|
ports = [5432]
|
||||||
|
description = "PostgreSQL database server"
|
||||||
|
after = "network"
|
||||||
|
restart = "always"
|
||||||
|
working_dir = "/var/lib/postgresql"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background writer — handles checkpoint I/O.
|
||||||
|
service checkpointer {
|
||||||
|
exec = "/usr/bin/postgres-checkpointer"
|
||||||
|
type = "longrun"
|
||||||
|
user = "postgres"
|
||||||
|
description = "PostgreSQL checkpointer process"
|
||||||
|
restart = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
// WAL writer — flushes write-ahead log to disk.
|
||||||
|
service walwriter {
|
||||||
|
exec = "/usr/bin/postgres-walwriter"
|
||||||
|
type = "longrun"
|
||||||
|
user = "postgres"
|
||||||
|
restart = "always"
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- build phases ------------------------------------------------------
|
||||||
|
|
||||||
|
prepare {
|
||||||
|
tar xf postgresql-${version}.tar.gz
|
||||||
|
}
|
||||||
|
|
||||||
|
// init-conditional build: PostgreSQL optionally links against systemd
|
||||||
|
// for socket activation and service notification. Use ${enabledinit}
|
||||||
|
// to decide configure flags without per-init service blocks.
|
||||||
|
build {
|
||||||
|
case ${enabledinit} in
|
||||||
|
systemd) ./configure --with-systemd --with-ssl=openssl --prefix=${prefix} ;;
|
||||||
|
*) ./configure --with-ssl=openssl --prefix=${prefix} ;;
|
||||||
|
esac
|
||||||
|
make -j${jobs} world
|
||||||
|
}
|
||||||
|
|
||||||
|
check {
|
||||||
|
make check
|
||||||
|
}
|
||||||
|
|
||||||
|
install {
|
||||||
|
make DESTDIR=${destdir} install-world
|
||||||
|
}
|
||||||
|
|
||||||
|
uninstall {
|
||||||
|
make DESTDIR=${destdir} uninstall-world
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
"data directory must exist" : system.config.data_dir != ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "kappa/boot/types.hpp"
|
||||||
|
|
||||||
|
namespace kappa::boot {
|
||||||
|
|
||||||
|
struct BootSpec {
|
||||||
|
std::string kernel_path; // e.g. "/kappa/boot/kernel"
|
||||||
|
std::string init_path; // e.g. "/kappa/boot/init" (the symlink, stable across init swaps)
|
||||||
|
std::string root; // e.g. "/dev/sda1" or "PARTUUID=xxx"
|
||||||
|
std::string kernel_params; // extra kernel cmdline params
|
||||||
|
std::string init_prev; // previous init symlink for fallback entry; empty = no fallback
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string generate_bootloader_config(Bootloader bl, const BootSpec& spec);
|
||||||
|
std::string generate_limine_config(const BootSpec& spec);
|
||||||
|
std::string generate_grub_config(const BootSpec& spec);
|
||||||
|
|
||||||
|
struct BootloaderInstallResult {
|
||||||
|
bool ok;
|
||||||
|
std::string path;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
BootloaderInstallResult install_bootloader_config(Bootloader bl,
|
||||||
|
const BootSpec& spec,
|
||||||
|
std::string_view prefix = "/");
|
||||||
|
|
||||||
|
} // namespace kappa::boot
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::boot {
|
||||||
|
|
||||||
|
enum class Bootloader : std::uint8_t {
|
||||||
|
Grub,
|
||||||
|
Limine,
|
||||||
|
Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
Bootloader parse_bootloader(std::string_view name);
|
||||||
|
|
||||||
|
std::string_view to_string(Bootloader bl);
|
||||||
|
|
||||||
|
bool is_supported(std::string_view name);
|
||||||
|
|
||||||
|
std::vector<Bootloader> all_bootloaders();
|
||||||
|
|
||||||
|
std::string_view bootloader_description(Bootloader bl);
|
||||||
|
|
||||||
|
struct BootloaderPaths {
|
||||||
|
std::string config_path;
|
||||||
|
std::string install_cmd;
|
||||||
|
};
|
||||||
|
|
||||||
|
BootloaderPaths bootloader_paths(Bootloader bl, std::string_view prefix = "/");
|
||||||
|
|
||||||
|
} // namespace kappa::boot
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/dsl/ast.hpp"
|
||||||
|
#include "kappa/dsl/system.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::config {
|
||||||
|
|
||||||
|
struct AssertFailure {
|
||||||
|
std::string message;
|
||||||
|
std::string field;
|
||||||
|
std::string expected;
|
||||||
|
std::string actual;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string resolve_field(const dsl::SystemConfig& cfg, std::string_view path);
|
||||||
|
|
||||||
|
std::vector<AssertFailure> evaluate_assertions(const dsl::SystemConfig& cfg);
|
||||||
|
|
||||||
|
std::unordered_map<std::string, dsl::NamedService> resolve_services(
|
||||||
|
const dsl::SystemConfig& cfg,
|
||||||
|
const std::unordered_map<std::string, dsl::PackageDef>& packages);
|
||||||
|
|
||||||
|
} // namespace kappa::config
|
||||||
@@ -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,18 +39,25 @@ 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 ServiceInit {
|
struct NamedService {
|
||||||
|
std::string name = "main";
|
||||||
std::string exec;
|
std::string exec;
|
||||||
std::string type;
|
std::string type;
|
||||||
std::string user;
|
std::string user;
|
||||||
std::vector<int> ports;
|
std::vector<int> ports;
|
||||||
std::unordered_map<std::string, std::string> env;
|
std::unordered_map<std::string, std::string> env;
|
||||||
|
std::string description;
|
||||||
|
std::string after;
|
||||||
|
std::string restart;
|
||||||
|
std::string working_dir;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Assertion {
|
struct Assertion {
|
||||||
@@ -63,21 +71,36 @@ struct PackageDef {
|
|||||||
std::string name;
|
std::string name;
|
||||||
std::string version;
|
std::string version;
|
||||||
std::string source;
|
std::string source;
|
||||||
|
std::string sha256;
|
||||||
|
std::string sha512;
|
||||||
|
std::string md5;
|
||||||
std::string license;
|
std::string license;
|
||||||
std::vector<Dependency> depends;
|
std::vector<Dependency> depends;
|
||||||
std::vector<std::string> provides;
|
std::vector<std::string> provides;
|
||||||
std::vector<std::string> outputs;
|
std::vector<std::string> outputs;
|
||||||
|
std::vector<std::string> conflicts;
|
||||||
std::unordered_map<std::string, FeatureDef> features;
|
std::unordered_map<std::string, FeatureDef> features;
|
||||||
std::vector<ConfigFile> config_files;
|
std::vector<ConfigFile> config_files;
|
||||||
std::vector<Patch> patches;
|
std::vector<Patch> patches;
|
||||||
std::vector<EnvEntry> env_entries;
|
std::vector<EnvEntry> env_entries;
|
||||||
std::unordered_map<std::string, ServiceInit> service;
|
std::vector<NamedService> services;
|
||||||
std::vector<Assertion> assertions;
|
std::vector<Assertion> assertions;
|
||||||
std::unordered_set<std::string> const_keys;
|
std::unordered_set<std::string> const_keys;
|
||||||
Phase prepare;
|
Phase prepare;
|
||||||
Phase build;
|
Phase build;
|
||||||
Phase check;
|
Phase check;
|
||||||
Phase install;
|
Phase install;
|
||||||
|
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
|
||||||
|
|||||||
+17
-22
@@ -1,33 +1,28 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/dsl/parse_util.hpp"
|
||||||
|
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <stdexcept>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
namespace kappa::dsl {
|
namespace kappa::dsl {
|
||||||
|
|
||||||
class ParseError : public std::runtime_error {
|
inline std::string msg_expected(std::string_view expected, std::string_view got) {
|
||||||
public:
|
return std::format("expected {}, got '{}'", expected, got);
|
||||||
ParseError(int line, int col, const std::string& msg)
|
}
|
||||||
: std::runtime_error(std::format("{}:{}: {}", line, col, msg)) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
inline int parse_int(int line, int col, const std::string& lexeme) {
|
inline std::string msg_unclosed_block(std::string_view block) {
|
||||||
try {
|
return std::format("unclosed {} — missing '}}' before end of file", block);
|
||||||
std::size_t pos = 0;
|
}
|
||||||
int val = std::stoi(lexeme, &pos);
|
|
||||||
if (pos != lexeme.size()) {
|
inline std::string msg_unknown_decl(std::string_view token) {
|
||||||
throw ParseError(line, col,
|
return std::format("unknown declaration '{}'", token);
|
||||||
std::format("expected integer, got '{}'", lexeme));
|
}
|
||||||
}
|
|
||||||
return val;
|
inline std::string msg_not_a_string() {
|
||||||
} catch (const std::invalid_argument&) {
|
return "expected a quoted string value, got bare word — wrap it in \"quotes\"";
|
||||||
throw ParseError(line, col,
|
|
||||||
std::format("expected integer, got '{}'", lexeme));
|
|
||||||
} catch (const std::out_of_range&) {
|
|
||||||
throw ParseError(line, col,
|
|
||||||
std::format("integer out of range: '{}'", lexeme));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace kappa::dsl
|
} // namespace kappa::dsl
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace kappa::dsl {
|
||||||
|
|
||||||
|
class ParseError : public std::runtime_error {
|
||||||
|
public:
|
||||||
|
ParseError(int line, int col, const std::string& msg)
|
||||||
|
: std::runtime_error(std::format("{}:{}: {}", line, col, msg)) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parse an integer from a DSL token lexeme.
|
||||||
|
/// Throws ParseError on invalid input or overflow.
|
||||||
|
inline int parse_int(int line, int col, const std::string& lexeme) {
|
||||||
|
try {
|
||||||
|
std::size_t pos = 0;
|
||||||
|
int val = std::stoi(lexeme, &pos);
|
||||||
|
if (pos != lexeme.size()) {
|
||||||
|
throw ParseError(line, col,
|
||||||
|
std::format("expected an integer, got '{}'", lexeme));
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
} catch (const std::invalid_argument&) {
|
||||||
|
throw ParseError(line, col,
|
||||||
|
std::format("expected an integer, got '{}'", lexeme));
|
||||||
|
} catch (const std::out_of_range&) {
|
||||||
|
throw ParseError(line, col,
|
||||||
|
std::format("integer out of range: '{}'", lexeme));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::dsl
|
||||||
@@ -39,6 +39,11 @@ struct BootBlock {
|
|||||||
std::unordered_map<std::string, std::string> params;
|
std::unordered_map<std::string, std::string> params;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct GroupDef {
|
||||||
|
std::string name;
|
||||||
|
int gid = -1; // -1 = auto-assign
|
||||||
|
};
|
||||||
|
|
||||||
struct UserRef {
|
struct UserRef {
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string shell;
|
std::string shell;
|
||||||
@@ -52,17 +57,31 @@ 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; // 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;
|
||||||
BootBlock boot;
|
BootBlock boot;
|
||||||
std::vector<UserRef> users;
|
std::vector<UserRef> users;
|
||||||
|
std::vector<GroupDef> groups;
|
||||||
std::vector<Assertion> assertions;
|
std::vector<Assertion> assertions;
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ enum class TokenType {
|
|||||||
Lbrace, // {
|
Lbrace, // {
|
||||||
Rbrace, // }
|
Rbrace, // }
|
||||||
Equals, // =
|
Equals, // =
|
||||||
|
Plus, // +
|
||||||
Lbracket, // [
|
Lbracket, // [
|
||||||
Rbracket, // ]
|
Rbracket, // ]
|
||||||
Comma, // ,
|
Comma, // ,
|
||||||
@@ -28,6 +29,7 @@ enum class TokenType {
|
|||||||
KwDepends,
|
KwDepends,
|
||||||
KwProvides,
|
KwProvides,
|
||||||
KwOutputs,
|
KwOutputs,
|
||||||
|
KwConflicts,
|
||||||
KwFeatures,
|
KwFeatures,
|
||||||
KwConfig,
|
KwConfig,
|
||||||
KwConst,
|
KwConst,
|
||||||
@@ -37,12 +39,20 @@ enum class TokenType {
|
|||||||
KwService,
|
KwService,
|
||||||
KwAssert,
|
KwAssert,
|
||||||
KwImport,
|
KwImport,
|
||||||
|
KwSha256,
|
||||||
|
KwSha512,
|
||||||
|
KwMd5,
|
||||||
KwPrepare,
|
KwPrepare,
|
||||||
KwBuild,
|
KwBuild,
|
||||||
KwCheck,
|
KwCheck,
|
||||||
KwInstall,
|
KwInstall,
|
||||||
|
KwUninstall,
|
||||||
KwTrue,
|
KwTrue,
|
||||||
KwFalse,
|
KwFalse,
|
||||||
|
|
||||||
|
// Repo / index
|
||||||
|
KwIndex,
|
||||||
|
KwRepos,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Token {
|
struct Token {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/dsl/ast.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace kappa::fetch {
|
||||||
|
|
||||||
|
struct FetchResult {
|
||||||
|
std::filesystem::path work_dir;
|
||||||
|
std::string error;
|
||||||
|
bool ok() const { return error.empty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
FetchResult fetch(const dsl::PackageDef& pkg);
|
||||||
|
|
||||||
|
} // namespace kappa::fetch
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/dsl/system.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::fetch {
|
||||||
|
|
||||||
|
struct RecipeResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string path; // path to the cached .kap file
|
||||||
|
std::string version; // version of the fetched package
|
||||||
|
bool updated = false; // true if the cache was updated (remote was newer)
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Legacy: flat URL list (backward compat with remotes = [...])
|
||||||
|
RecipeResult fetch_recipe(const std::string& name,
|
||||||
|
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
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/resolve/plan.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::install {
|
||||||
|
|
||||||
|
struct InstallResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string hash;
|
||||||
|
std::filesystem::path store_path;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
InstallResult install(const resolve::BuildStep& step,
|
||||||
|
const std::filesystem::path& work_dir);
|
||||||
|
|
||||||
|
struct DbEntry {
|
||||||
|
std::string name;
|
||||||
|
std::string version;
|
||||||
|
std::string hash;
|
||||||
|
std::string config_hash; // FNV-1a hash of features+config, empty if unknown
|
||||||
|
std::vector<std::string> provides;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<DbEntry> read_installed();
|
||||||
|
bool write_installed(const std::vector<DbEntry>& entries);
|
||||||
|
bool record_generation(const std::vector<std::string>& hashes, int keep);
|
||||||
|
|
||||||
|
std::string compute_config_hash(
|
||||||
|
const std::unordered_map<std::string, dsl::FeatureDef>& features,
|
||||||
|
const std::unordered_map<std::string, std::string>& config);
|
||||||
|
|
||||||
|
} // namespace kappa::install
|
||||||
+14
-7
@@ -1,16 +1,23 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
namespace kappa::paths {
|
namespace kappa::paths {
|
||||||
|
|
||||||
inline const std::filesystem::path root{"/kappa"};
|
void set_root(std::string_view path);
|
||||||
inline const auto bin_dir = root / "bin";
|
|
||||||
inline const auto temp_dir = root / "temp";
|
|
||||||
inline const auto db_dir = root / "db";
|
|
||||||
inline const auto system_dir = root / "system";
|
|
||||||
inline const auto builds_dir = system_dir / "builds";
|
|
||||||
|
|
||||||
void ensure_directories();
|
std::filesystem::path root_path();
|
||||||
|
std::filesystem::path bin_dir();
|
||||||
|
std::filesystem::path temp_dir();
|
||||||
|
std::filesystem::path db_dir();
|
||||||
|
std::filesystem::path system_dir();
|
||||||
|
std::filesystem::path builds_dir();
|
||||||
|
std::filesystem::path cache_dir();
|
||||||
|
std::filesystem::path packages_dir();
|
||||||
|
|
||||||
|
bool ensure_directories();
|
||||||
|
bool directories_exist();
|
||||||
|
|
||||||
} // namespace kappa::paths
|
} // namespace kappa::paths
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/dsl/system.hpp"
|
||||||
|
#include "kappa/resolve/plan.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::rebuild {
|
||||||
|
|
||||||
|
struct ChangeSet {
|
||||||
|
std::vector<std::string> added;
|
||||||
|
std::vector<std::string> changed;
|
||||||
|
std::vector<std::string> removed;
|
||||||
|
bool kernel_changed = false;
|
||||||
|
bool init_changed = false;
|
||||||
|
bool services_changed = false;
|
||||||
|
bool bootloader_changed = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InitImpact {
|
||||||
|
std::vector<std::string> service_rebuild; // packages needing full rebuild (use ${enabledinit})
|
||||||
|
std::vector<std::string> service_only; // packages needing only service file regeneration
|
||||||
|
std::vector<std::string> skipped; // packages with no services — no action needed
|
||||||
|
};
|
||||||
|
|
||||||
|
ChangeSet compute_changes(const dsl::SystemConfig& cfg);
|
||||||
|
|
||||||
|
InitImpact compute_init_impact(const dsl::SystemConfig& cfg,
|
||||||
|
const resolve::Registry& registry);
|
||||||
|
|
||||||
|
} // namespace kappa::rebuild
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/config/merge.hpp"
|
||||||
|
#include "kappa/dsl/ast.hpp"
|
||||||
|
#include "kappa/dsl/system.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::resolve {
|
||||||
|
|
||||||
|
struct ResolvedDep {
|
||||||
|
std::string name;
|
||||||
|
std::string version_constraint;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BuildStep {
|
||||||
|
std::string name;
|
||||||
|
const dsl::PackageDef* package = nullptr;
|
||||||
|
config::ResolvedPackage resolved;
|
||||||
|
std::vector<ResolvedDep> dependencies;
|
||||||
|
std::string enabled_init; // boot.init value for ${enabledinit}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BuildPlan {
|
||||||
|
std::vector<BuildStep> steps;
|
||||||
|
std::vector<std::string> cycles;
|
||||||
|
std::vector<std::string> conflicts;
|
||||||
|
std::vector<std::string> missing;
|
||||||
|
};
|
||||||
|
|
||||||
|
using Registry = std::unordered_map<std::string, dsl::PackageDef>;
|
||||||
|
|
||||||
|
BuildPlan resolve(const dsl::SystemConfig& cfg, const Registry& registry);
|
||||||
|
|
||||||
|
} // namespace kappa::resolve
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/resolve/plan.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::sched {
|
||||||
|
|
||||||
|
struct SchedResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::vector<std::string> built;
|
||||||
|
std::vector<std::string> failed;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the build plan with worker-level (-w) and job-level (-j) parallelism.
|
||||||
|
// Workers atomically claim ready packages, build them, and notify dependents.
|
||||||
|
// The same package is never built twice — deduplicated in the dependency graph.
|
||||||
|
//
|
||||||
|
// -w N package-level parallelism (concurrent package builds)
|
||||||
|
// -j N job-level parallelism (make -jN per package)
|
||||||
|
//
|
||||||
|
// The scheduler uses depth-based priority groups (Beta → Alpha → Zeta)
|
||||||
|
// so that deep dependencies unblock as many packages as possible first.
|
||||||
|
SchedResult run(const resolve::BuildPlan& plan,
|
||||||
|
const std::string& work_root,
|
||||||
|
int workers = 1,
|
||||||
|
int jobs = 1);
|
||||||
|
|
||||||
|
} // namespace kappa::sched
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/dsl/ast.hpp"
|
||||||
|
#include "kappa/service/types.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::service {
|
||||||
|
|
||||||
|
struct ServiceSpec {
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
std::string exec;
|
||||||
|
std::string type;
|
||||||
|
std::string user;
|
||||||
|
std::vector<int> ports;
|
||||||
|
std::unordered_map<std::string, std::string> env;
|
||||||
|
std::string after;
|
||||||
|
std::string restart_policy;
|
||||||
|
std::string working_dir;
|
||||||
|
|
||||||
|
static ServiceSpec from_service_init(const dsl::NamedService& ns);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string generate_systemd_service(const ServiceSpec& spec);
|
||||||
|
std::string generate_s6_service(const ServiceSpec& spec);
|
||||||
|
std::string generate_openrc_service(const ServiceSpec& spec);
|
||||||
|
std::string generate_dinit_service(const ServiceSpec& spec);
|
||||||
|
std::string generate_runit_service(const ServiceSpec& spec);
|
||||||
|
std::string generate_service_file(InitSystem is, const ServiceSpec& spec);
|
||||||
|
|
||||||
|
struct ServiceInstallResult {
|
||||||
|
bool ok;
|
||||||
|
std::string path;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
ServiceInstallResult install_service(
|
||||||
|
InitSystem is,
|
||||||
|
const ServiceSpec& spec,
|
||||||
|
std::string_view prefix = "/");
|
||||||
|
|
||||||
|
} // namespace kappa::service
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::service {
|
||||||
|
|
||||||
|
enum class InitSystem : std::uint8_t {
|
||||||
|
Systemd,
|
||||||
|
OpenRC,
|
||||||
|
S6,
|
||||||
|
Runit,
|
||||||
|
Dinit,
|
||||||
|
Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
InitSystem parse_init_system(std::string_view name);
|
||||||
|
|
||||||
|
std::string_view to_string(InitSystem is);
|
||||||
|
|
||||||
|
bool is_supported(std::string_view name);
|
||||||
|
|
||||||
|
std::vector<InitSystem> all_systems();
|
||||||
|
|
||||||
|
std::string_view init_description(InitSystem is);
|
||||||
|
|
||||||
|
struct InitPaths {
|
||||||
|
std::string service_dir;
|
||||||
|
std::string enable_cmd;
|
||||||
|
std::string disable_cmd;
|
||||||
|
};
|
||||||
|
|
||||||
|
InitPaths init_paths(InitSystem is, std::string_view prefix = "/");
|
||||||
|
|
||||||
|
// Shebangs for s6 execline-based run scripts.
|
||||||
|
// Extracted here so backends share a single definition.
|
||||||
|
inline constexpr std::string_view s6_execline_shebang = "#!/bin/execlineb -P\n";
|
||||||
|
|
||||||
|
} // namespace kappa::service
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/dsl/system.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace kappa::system {
|
||||||
|
|
||||||
|
struct ActivateResult {
|
||||||
|
bool ok;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
ActivateResult write_hostname(const std::string& hostname,
|
||||||
|
std::string_view prefix = "/");
|
||||||
|
|
||||||
|
ActivateResult write_timezone(const std::string& timezone,
|
||||||
|
std::string_view prefix = "/");
|
||||||
|
|
||||||
|
} // namespace kappa::system
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/dsl/ast.hpp"
|
||||||
|
#include "kappa/dsl/system.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::tools {
|
||||||
|
|
||||||
|
enum class DiagSeverity { Warning, Error };
|
||||||
|
|
||||||
|
struct Diagnostic {
|
||||||
|
DiagSeverity severity;
|
||||||
|
std::string message;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Diagnostic> check_package(const dsl::PackageDef& pkg);
|
||||||
|
std::vector<Diagnostic> check_config(const dsl::SystemConfig& cfg);
|
||||||
|
|
||||||
|
} // namespace kappa::tools
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/dsl/ast.hpp"
|
||||||
|
#include "kappa/dsl/system.hpp"
|
||||||
|
|
||||||
|
#include <ostream>
|
||||||
|
|
||||||
|
namespace kappa::tools {
|
||||||
|
|
||||||
|
void format_package(std::ostream& os, const dsl::PackageDef& pkg);
|
||||||
|
void format_config(std::ostream& os, const dsl::SystemConfig& cfg);
|
||||||
|
void format_index(std::ostream& os, const dsl::IndexDef& idx);
|
||||||
|
|
||||||
|
} // namespace kappa::tools
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace kappa::util {
|
||||||
|
std::string to_lower(std::string_view sv);
|
||||||
|
std::string shell_escape(std::string_view s);
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "binutils"
|
|
||||||
version = "2.46.1"
|
|
||||||
description = "GNU Binary Utilities (ld, as, objdump, readelf)"
|
|
||||||
|
|
||||||
[source]
|
|
||||||
url = "https://ftp.gnu.org/gnu/${name}/${name}-${version}.tar.xz" # ${} is string substitution
|
|
||||||
sha256 = "e127a709cba24c76de8936cb7083dd768f28cd37eb010492e2f19b71eb1294e4" # or sha512, or md5
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
deps = ["zlib", "gettext"]
|
|
||||||
|
|
||||||
[build]
|
|
||||||
system = "autotools" # or cargo, make, cmake, or meson
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#include "kappa/boot/bootloader.hpp"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace kappa::boot {
|
||||||
|
|
||||||
|
std::string generate_grub_config(const BootSpec& spec) {
|
||||||
|
std::ostringstream oss;
|
||||||
|
|
||||||
|
oss << "# Generated by kappa — do not edit manually\n";
|
||||||
|
oss << "# GRUB boot entry\n";
|
||||||
|
oss << "\n";
|
||||||
|
oss << "set timeout=5\n";
|
||||||
|
oss << "set default=0\n";
|
||||||
|
oss << "\n";
|
||||||
|
oss << "menuentry \"Kappa\" {\n";
|
||||||
|
|
||||||
|
oss << std::format(" linux {} init={} root={}",
|
||||||
|
spec.kernel_path,
|
||||||
|
spec.init_path,
|
||||||
|
spec.root);
|
||||||
|
|
||||||
|
if (!spec.kernel_params.empty()) {
|
||||||
|
oss << " " << spec.kernel_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
oss << "\n";
|
||||||
|
oss << "}\n";
|
||||||
|
|
||||||
|
if (!spec.init_prev.empty()) {
|
||||||
|
oss << "\n";
|
||||||
|
oss << "menuentry \"Kappa (fallback)\" {\n";
|
||||||
|
oss << std::format(" linux {} init={} root={}",
|
||||||
|
spec.kernel_path,
|
||||||
|
spec.init_prev,
|
||||||
|
spec.root);
|
||||||
|
if (!spec.kernel_params.empty()) {
|
||||||
|
oss << " " << spec.kernel_params;
|
||||||
|
}
|
||||||
|
oss << "\n";
|
||||||
|
oss << "}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::boot
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#include "kappa/boot/bootloader.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace kappa::boot {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Backend config-file generators (defined in separate .cpp files)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// generate_bootloader_config — dispatch to the correct backend
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
std::string generate_bootloader_config(Bootloader bl, const BootSpec& spec) {
|
||||||
|
switch (bl) {
|
||||||
|
case Bootloader::Grub:
|
||||||
|
return generate_grub_config(spec);
|
||||||
|
case Bootloader::Limine:
|
||||||
|
return generate_limine_config(spec);
|
||||||
|
case Bootloader::Unknown:
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::string read_current_init(std::string_view prefix) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
fs::path init_link = fs::path(prefix) / "boot/init";
|
||||||
|
std::error_code ec;
|
||||||
|
if (!fs::is_symlink(init_link, ec)) return {};
|
||||||
|
auto target = fs::read_symlink(init_link, ec);
|
||||||
|
if (ec) return {};
|
||||||
|
return target.string();
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// install_bootloader_config — write the generated config file to disk
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
BootloaderInstallResult install_bootloader_config(Bootloader bl,
|
||||||
|
const BootSpec& spec,
|
||||||
|
std::string_view prefix) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
auto paths = bootloader_paths(bl, prefix);
|
||||||
|
if (paths.config_path.empty()) {
|
||||||
|
return {false, {}, "Unknown bootloader"};
|
||||||
|
}
|
||||||
|
|
||||||
|
BootSpec spec_copy = spec;
|
||||||
|
if (spec_copy.init_prev.empty()) {
|
||||||
|
spec_copy.init_prev = read_current_init(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content = generate_bootloader_config(bl, spec_copy);
|
||||||
|
if (content.empty()) {
|
||||||
|
return {false, {}, "Failed to generate bootloader config"};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
|
||||||
|
fs::path file_path = paths.config_path;
|
||||||
|
fs::create_directories(file_path.parent_path(), ec);
|
||||||
|
if (ec) {
|
||||||
|
return {false, {}, ec.message()};
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::ofstream out(file_path);
|
||||||
|
if (!out) {
|
||||||
|
return {false, {},
|
||||||
|
std::format("Failed to write {}", file_path.string())};
|
||||||
|
}
|
||||||
|
out << content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {true, file_path.string(), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::boot
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#include "kappa/boot/bootloader.hpp"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace kappa::boot {
|
||||||
|
|
||||||
|
std::string generate_limine_config(const BootSpec& spec) {
|
||||||
|
std::ostringstream out;
|
||||||
|
|
||||||
|
out << "# Generated by kappa — do not edit manually\n"
|
||||||
|
<< "# Limine boot entry\n"
|
||||||
|
<< "\n"
|
||||||
|
<< ":Kappa\n"
|
||||||
|
<< " protocol: linux\n"
|
||||||
|
<< std::format(" kernel_path: boot():{}\n", spec.kernel_path);
|
||||||
|
|
||||||
|
out << std::format(" kernel_cmdline: init={} root={}",
|
||||||
|
spec.init_path, spec.root);
|
||||||
|
if (!spec.kernel_params.empty()) {
|
||||||
|
out << " " << spec.kernel_params;
|
||||||
|
}
|
||||||
|
out << "\n";
|
||||||
|
|
||||||
|
if (!spec.init_prev.empty()) {
|
||||||
|
out << "\n"
|
||||||
|
<< ":Kappa (fallback)\n"
|
||||||
|
<< " protocol: linux\n"
|
||||||
|
<< std::format(" kernel_path: boot():{}\n", spec.kernel_path);
|
||||||
|
out << std::format(" kernel_cmdline: init={} root={}",
|
||||||
|
spec.init_prev, spec.root);
|
||||||
|
if (!spec.kernel_params.empty()) {
|
||||||
|
out << " " << spec.kernel_params;
|
||||||
|
}
|
||||||
|
out << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::boot
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#include "kappa/boot/types.hpp"
|
||||||
|
#include "kappa/util.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
namespace kappa::boot {
|
||||||
|
|
||||||
|
Bootloader parse_bootloader(std::string_view name) {
|
||||||
|
auto lower = util::to_lower(name);
|
||||||
|
if (lower == "grub") return Bootloader::Grub;
|
||||||
|
if (lower == "limine") return Bootloader::Limine;
|
||||||
|
return Bootloader::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view to_string(Bootloader bl) {
|
||||||
|
switch (bl) {
|
||||||
|
case Bootloader::Grub: return "grub";
|
||||||
|
case Bootloader::Limine: return "limine";
|
||||||
|
case Bootloader::Unknown: return "unknown";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_supported(std::string_view name) {
|
||||||
|
return parse_bootloader(name) != Bootloader::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Bootloader> all_bootloaders() {
|
||||||
|
return {Bootloader::Grub, Bootloader::Limine};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view bootloader_description(Bootloader bl) {
|
||||||
|
switch (bl) {
|
||||||
|
case Bootloader::Grub:
|
||||||
|
return "GRUB — GRand Unified Bootloader";
|
||||||
|
case Bootloader::Limine:
|
||||||
|
return "Limine — modern multiprotocol bootloader";
|
||||||
|
case Bootloader::Unknown:
|
||||||
|
return "unknown bootloader";
|
||||||
|
}
|
||||||
|
return "unknown bootloader";
|
||||||
|
}
|
||||||
|
|
||||||
|
BootloaderPaths bootloader_paths(Bootloader bl, std::string_view prefix) {
|
||||||
|
switch (bl) {
|
||||||
|
case Bootloader::Grub:
|
||||||
|
return {
|
||||||
|
.config_path = (std::filesystem::path(prefix) / "boot/grub/grub.cfg").string(),
|
||||||
|
.install_cmd = "grub-install",
|
||||||
|
};
|
||||||
|
case Bootloader::Limine:
|
||||||
|
return {
|
||||||
|
.config_path = (std::filesystem::path(prefix) / "boot/limine/limine.cfg").string(),
|
||||||
|
.install_cmd = "limine",
|
||||||
|
};
|
||||||
|
case Bootloader::Unknown:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::boot
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#include "kappa/config/eval.hpp"
|
||||||
|
#include "kappa/service/types.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace kappa::config {
|
||||||
|
|
||||||
|
std::string resolve_field(const dsl::SystemConfig& cfg, std::string_view path) {
|
||||||
|
auto dot = path.find('.');
|
||||||
|
auto ns = path.substr(0, dot);
|
||||||
|
|
||||||
|
if (ns == "boot") {
|
||||||
|
auto key = path.substr(dot + 1);
|
||||||
|
if (key == "kernel") { return cfg.boot.kernel; }
|
||||||
|
if (key == "init") { return cfg.boot.init; }
|
||||||
|
if (key == "efi") { return cfg.boot.efi; }
|
||||||
|
if (key == "swap") { return cfg.boot.swap; }
|
||||||
|
if (key == "root") { return cfg.boot.root; }
|
||||||
|
if (key == "bootloader") { return cfg.boot.bootloader; }
|
||||||
|
auto it = cfg.boot.params.find(std::string(key));
|
||||||
|
if (it != cfg.boot.params.end()) { return it->second; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ns == "system") {
|
||||||
|
auto key = path.substr(dot + 1);
|
||||||
|
if (key == "hostname") { return cfg.system.hostname; }
|
||||||
|
if (key == "timezone") { return cfg.system.timezone; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ns == "features") {
|
||||||
|
auto key = path.substr(dot + 1);
|
||||||
|
auto it = cfg.system.features.find(std::string(key));
|
||||||
|
if (it != cfg.system.features.end()) {
|
||||||
|
return it->second.enabled ? "true" : "false";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<AssertFailure> evaluate_assertions(const dsl::SystemConfig& cfg) {
|
||||||
|
std::vector<AssertFailure> failures;
|
||||||
|
|
||||||
|
for (auto& a : cfg.assertions) {
|
||||||
|
auto actual = resolve_field(cfg, a.field);
|
||||||
|
bool pass = false;
|
||||||
|
|
||||||
|
if (a.op == "==") {
|
||||||
|
pass = (actual == a.value);
|
||||||
|
} else if (a.op == "!=") {
|
||||||
|
pass = (actual != a.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pass) {
|
||||||
|
failures.push_back({a.message, a.field, a.value, actual});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
std::unordered_map<std::string, dsl::NamedService> resolve_services(
|
||||||
|
const dsl::SystemConfig& cfg,
|
||||||
|
const std::unordered_map<std::string, dsl::PackageDef>& packages)
|
||||||
|
{
|
||||||
|
std::unordered_map<std::string, dsl::NamedService> resolved;
|
||||||
|
auto init_system = cfg.boot.init;
|
||||||
|
|
||||||
|
auto is = kappa::service::parse_init_system(init_system);
|
||||||
|
if (is == kappa::service::InitSystem::Unknown) {
|
||||||
|
std::cerr << "warning: unknown init system '" << init_system
|
||||||
|
<< "' — no services will be configured\n";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& svc : cfg.services) {
|
||||||
|
if (!svc.enable) { continue; }
|
||||||
|
|
||||||
|
// svc.name can be "postgresql" or "postgresql.checkpointer"
|
||||||
|
auto dot = svc.name.find('.');
|
||||||
|
auto pkg_name = (dot != std::string::npos)
|
||||||
|
? svc.name.substr(0, dot)
|
||||||
|
: svc.name;
|
||||||
|
auto svc_name = (dot != std::string::npos)
|
||||||
|
? svc.name.substr(dot + 1)
|
||||||
|
: std::string("main");
|
||||||
|
|
||||||
|
auto pit = packages.find(std::string(pkg_name));
|
||||||
|
if (pit == packages.end()) { continue; }
|
||||||
|
|
||||||
|
auto& pkg = pit->second;
|
||||||
|
for (auto& ns : pkg.services) {
|
||||||
|
if (ns.name == svc_name) {
|
||||||
|
resolved[svc.name] = ns;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::config
|
||||||
+18
-2
@@ -13,6 +13,7 @@ static const std::unordered_map<std::string_view, TokenType> keywords = {
|
|||||||
{"depends", TokenType::KwDepends},
|
{"depends", TokenType::KwDepends},
|
||||||
{"provides", TokenType::KwProvides},
|
{"provides", TokenType::KwProvides},
|
||||||
{"outputs", TokenType::KwOutputs},
|
{"outputs", TokenType::KwOutputs},
|
||||||
|
{"conflicts", TokenType::KwConflicts},
|
||||||
{"features", TokenType::KwFeatures},
|
{"features", TokenType::KwFeatures},
|
||||||
{"config", TokenType::KwConfig},
|
{"config", TokenType::KwConfig},
|
||||||
{"const", TokenType::KwConst},
|
{"const", TokenType::KwConst},
|
||||||
@@ -20,14 +21,20 @@ static const std::unordered_map<std::string_view, TokenType> keywords = {
|
|||||||
{"patches", TokenType::KwPatches},
|
{"patches", TokenType::KwPatches},
|
||||||
{"env", TokenType::KwEnv},
|
{"env", TokenType::KwEnv},
|
||||||
{"service", TokenType::KwService},
|
{"service", TokenType::KwService},
|
||||||
|
{"sha256", TokenType::KwSha256},
|
||||||
|
{"sha512", TokenType::KwSha512},
|
||||||
|
{"md5", TokenType::KwMd5},
|
||||||
{"assert", TokenType::KwAssert},
|
{"assert", TokenType::KwAssert},
|
||||||
{"import", TokenType::KwImport},
|
{"import", TokenType::KwImport},
|
||||||
{"prepare", TokenType::KwPrepare},
|
{"prepare", TokenType::KwPrepare},
|
||||||
{"build", TokenType::KwBuild},
|
{"build", TokenType::KwBuild},
|
||||||
{"check", TokenType::KwCheck},
|
{"check", TokenType::KwCheck},
|
||||||
{"install", TokenType::KwInstall},
|
{"install", TokenType::KwInstall},
|
||||||
|
{"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) {
|
||||||
@@ -37,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 ",";
|
||||||
@@ -48,6 +56,7 @@ std::string_view token_name(TokenType type) {
|
|||||||
case TokenType::KwDepends: return "depends";
|
case TokenType::KwDepends: return "depends";
|
||||||
case TokenType::KwProvides: return "provides";
|
case TokenType::KwProvides: return "provides";
|
||||||
case TokenType::KwOutputs: return "outputs";
|
case TokenType::KwOutputs: return "outputs";
|
||||||
|
case TokenType::KwConflicts: return "conflicts";
|
||||||
case TokenType::KwFeatures: return "features";
|
case TokenType::KwFeatures: return "features";
|
||||||
case TokenType::KwConfig: return "config";
|
case TokenType::KwConfig: return "config";
|
||||||
case TokenType::KwConst: return "const";
|
case TokenType::KwConst: return "const";
|
||||||
@@ -55,14 +64,20 @@ std::string_view token_name(TokenType type) {
|
|||||||
case TokenType::KwPatches: return "patches";
|
case TokenType::KwPatches: return "patches";
|
||||||
case TokenType::KwEnv: return "env";
|
case TokenType::KwEnv: return "env";
|
||||||
case TokenType::KwService: return "service";
|
case TokenType::KwService: return "service";
|
||||||
|
case TokenType::KwSha256: return "sha256";
|
||||||
|
case TokenType::KwSha512: return "sha512";
|
||||||
|
case TokenType::KwMd5: return "md5";
|
||||||
case TokenType::KwAssert: return "assert";
|
case TokenType::KwAssert: return "assert";
|
||||||
case TokenType::KwImport: return "import";
|
case TokenType::KwImport: return "import";
|
||||||
case TokenType::KwPrepare: return "prepare";
|
case TokenType::KwPrepare: return "prepare";
|
||||||
case TokenType::KwBuild: return "build";
|
case TokenType::KwBuild: return "build";
|
||||||
case TokenType::KwCheck: return "check";
|
case TokenType::KwCheck: return "check";
|
||||||
case TokenType::KwInstall: return "install";
|
case TokenType::KwInstall: return "install";
|
||||||
|
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 "?";
|
||||||
}
|
}
|
||||||
@@ -133,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();
|
||||||
@@ -145,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 == ',') { break; }
|
if (c == '"' || c == '=' || c == '+' || c == '[' || c == ']' || c == ',') { break; }
|
||||||
lexeme += advance();
|
lexeme += advance();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,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_};
|
||||||
|
|||||||
+79
-25
@@ -45,9 +45,15 @@ void Parser::skip_newlines() {
|
|||||||
|
|
||||||
Token Parser::consume(TokenType type) {
|
Token Parser::consume(TokenType type) {
|
||||||
if (!at(type)) {
|
if (!at(type)) {
|
||||||
|
if (type == TokenType::Rbrace && at(TokenType::Eof)) {
|
||||||
throw ParseError(current_.line, current_.col,
|
throw ParseError(current_.line, current_.col,
|
||||||
std::format("expected '{}', got '{}'",
|
msg_unclosed_block("block"));
|
||||||
token_name(type), token_name(current_.type)));
|
}
|
||||||
|
if (type == TokenType::String && at(TokenType::Ident)) {
|
||||||
|
throw ParseError(current_.line, current_.col, msg_not_a_string());
|
||||||
|
}
|
||||||
|
throw ParseError(current_.line, current_.col,
|
||||||
|
msg_expected(token_name(type), token_name(current_.type)));
|
||||||
}
|
}
|
||||||
Token t = std::move(current_);
|
Token t = std::move(current_);
|
||||||
advance();
|
advance();
|
||||||
@@ -117,6 +123,28 @@ void Parser::parse_body(PackageDef& pkg) {
|
|||||||
pkg.license = consume(TokenType::String).lexeme;
|
pkg.license = consume(TokenType::String).lexeme;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TokenType::KwSha256:
|
||||||
|
consume(TokenType::KwSha256);
|
||||||
|
consume(TokenType::Equals);
|
||||||
|
pkg.sha256 = consume(TokenType::String).lexeme;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TokenType::KwSha512:
|
||||||
|
consume(TokenType::KwSha512);
|
||||||
|
consume(TokenType::Equals);
|
||||||
|
pkg.sha512 = consume(TokenType::String).lexeme;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TokenType::KwMd5:
|
||||||
|
consume(TokenType::KwMd5);
|
||||||
|
consume(TokenType::Equals);
|
||||||
|
pkg.md5 = consume(TokenType::String).lexeme;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TokenType::Ident:
|
||||||
|
throw ParseError(current_.line, current_.col,
|
||||||
|
msg_unknown_decl(current_.lexeme));
|
||||||
|
|
||||||
case TokenType::KwProvides:
|
case TokenType::KwProvides:
|
||||||
consume(TokenType::KwProvides);
|
consume(TokenType::KwProvides);
|
||||||
consume(TokenType::Equals);
|
consume(TokenType::Equals);
|
||||||
@@ -129,6 +157,12 @@ void Parser::parse_body(PackageDef& pkg) {
|
|||||||
pkg.outputs = parse_string_list();
|
pkg.outputs = parse_string_list();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TokenType::KwConflicts:
|
||||||
|
consume(TokenType::KwConflicts);
|
||||||
|
consume(TokenType::Equals);
|
||||||
|
pkg.conflicts = parse_string_list();
|
||||||
|
break;
|
||||||
|
|
||||||
case TokenType::KwDepends:
|
case TokenType::KwDepends:
|
||||||
consume(TokenType::KwDepends);
|
consume(TokenType::KwDepends);
|
||||||
consume(TokenType::Equals);
|
consume(TokenType::Equals);
|
||||||
@@ -190,47 +224,50 @@ 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)) {
|
||||||
soft = true;
|
|
||||||
advance();
|
advance();
|
||||||
|
consume(TokenType::Equals);
|
||||||
|
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);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TokenType::KwService:
|
case TokenType::KwService: {
|
||||||
consume(TokenType::KwService);
|
consume(TokenType::KwService);
|
||||||
consume(TokenType::Lbrace);
|
NamedService ns;
|
||||||
skip_newlines();
|
if (!at(TokenType::Lbrace)) {
|
||||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
ns.name = current_.lexeme;
|
||||||
if (at(TokenType::Newline)) { advance(); continue; }
|
|
||||||
auto init_name = current_.lexeme;
|
|
||||||
advance();
|
advance();
|
||||||
|
}
|
||||||
consume(TokenType::Lbrace);
|
consume(TokenType::Lbrace);
|
||||||
skip_newlines();
|
skip_newlines();
|
||||||
ServiceInit si;
|
|
||||||
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 = current_.lexeme;
|
auto key = current_.lexeme;
|
||||||
advance();
|
advance();
|
||||||
consume(TokenType::Equals);
|
consume(TokenType::Equals);
|
||||||
if (key == "exec") {
|
if (key == "exec") {
|
||||||
si.exec = consume(TokenType::String).lexeme;
|
ns.exec = consume(TokenType::String).lexeme;
|
||||||
} else if (key == "type") {
|
} else if (key == "type") {
|
||||||
si.type = consume(TokenType::String).lexeme;
|
ns.type = consume(TokenType::String).lexeme;
|
||||||
} else if (key == "user") {
|
} else if (key == "user") {
|
||||||
si.user = consume(TokenType::String).lexeme;
|
ns.user = consume(TokenType::String).lexeme;
|
||||||
} else if (key == "ports") {
|
} else if (key == "ports") {
|
||||||
consume(TokenType::Lbracket);
|
consume(TokenType::Lbracket);
|
||||||
skip_newlines();
|
skip_newlines();
|
||||||
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||||
si.ports.push_back(
|
ns.ports.push_back(
|
||||||
parse_int(current_.line, current_.col,
|
parse_int(current_.line, current_.col,
|
||||||
current_.lexeme));
|
current_.lexeme));
|
||||||
advance();
|
advance();
|
||||||
@@ -239,17 +276,23 @@ void Parser::parse_body(PackageDef& pkg) {
|
|||||||
skip_newlines();
|
skip_newlines();
|
||||||
}
|
}
|
||||||
consume(TokenType::Rbracket);
|
consume(TokenType::Rbracket);
|
||||||
|
} else if (key == "description") {
|
||||||
|
ns.description = consume(TokenType::String).lexeme;
|
||||||
|
} else if (key == "after") {
|
||||||
|
ns.after = consume(TokenType::String).lexeme;
|
||||||
|
} else if (key == "restart") {
|
||||||
|
ns.restart = consume(TokenType::String).lexeme;
|
||||||
|
} else if (key == "working_dir") {
|
||||||
|
ns.working_dir = consume(TokenType::String).lexeme;
|
||||||
} else {
|
} else {
|
||||||
si.env[key] = consume(TokenType::String).lexeme;
|
ns.env[key] = consume(TokenType::String).lexeme;
|
||||||
}
|
}
|
||||||
skip_newlines();
|
skip_newlines();
|
||||||
}
|
}
|
||||||
consume(TokenType::Rbrace);
|
consume(TokenType::Rbrace);
|
||||||
pkg.service[std::string(init_name)] = std::move(si);
|
pkg.services.push_back(std::move(ns));
|
||||||
skip_newlines();
|
|
||||||
}
|
|
||||||
consume(TokenType::Rbrace);
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case TokenType::KwAssert:
|
case TokenType::KwAssert:
|
||||||
consume(TokenType::KwAssert);
|
consume(TokenType::KwAssert);
|
||||||
@@ -269,6 +312,10 @@ void Parser::parse_body(PackageDef& pkg) {
|
|||||||
} else {
|
} else {
|
||||||
a.op = "=";
|
a.op = "=";
|
||||||
}
|
}
|
||||||
|
} else if (at(TokenType::Ident) && current_.lexeme == "!") {
|
||||||
|
advance();
|
||||||
|
consume(TokenType::Equals);
|
||||||
|
a.op = "!=";
|
||||||
} else {
|
} else {
|
||||||
a.op = current_.lexeme; advance();
|
a.op = current_.lexeme; advance();
|
||||||
}
|
}
|
||||||
@@ -307,6 +354,11 @@ void Parser::parse_body(PackageDef& pkg) {
|
|||||||
pkg.install = parse_phase();
|
pkg.install = parse_phase();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TokenType::KwUninstall:
|
||||||
|
consume(TokenType::KwUninstall);
|
||||||
|
pkg.uninstall = parse_phase();
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw ParseError(current_.line, current_.col,
|
throw ParseError(current_.line, current_.col,
|
||||||
std::format("unexpected token '{}' in package body",
|
std::format("unexpected token '{}' in package body",
|
||||||
@@ -334,9 +386,9 @@ Dependency Parser::parse_dependency_item() {
|
|||||||
auto raw = consume(TokenType::String).lexeme;
|
auto raw = consume(TokenType::String).lexeme;
|
||||||
auto colon = raw.find(':');
|
auto colon = raw.find(':');
|
||||||
if (colon != std::string::npos) {
|
if (colon != std::string::npos) {
|
||||||
return {raw.substr(0, colon), "", raw.substr(colon + 1)};
|
return {raw.substr(0, colon), "", raw.substr(colon + 1), ""};
|
||||||
}
|
}
|
||||||
return {std::move(raw), "", ""};
|
return {std::move(raw), "", "", ""};
|
||||||
}
|
}
|
||||||
|
|
||||||
consume(TokenType::Lbrace);
|
consume(TokenType::Lbrace);
|
||||||
@@ -480,7 +532,9 @@ Phase Parser::parse_phase() {
|
|||||||
Command Parser::parse_command_line() {
|
Command Parser::parse_command_line() {
|
||||||
std::string cmd;
|
std::string cmd;
|
||||||
while (!at(TokenType::Newline) && !at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
while (!at(TokenType::Newline) && !at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||||
if (!cmd.empty() && !at(TokenType::Comma)) { cmd += ' '; }
|
if (!cmd.empty() && !at(TokenType::Comma)
|
||||||
|
&& current_.type != TokenType::Equals
|
||||||
|
&& !cmd.ends_with('=')) { cmd += ' '; }
|
||||||
cmd += current_.lexeme;
|
cmd += current_.lexeme;
|
||||||
advance();
|
advance();
|
||||||
}
|
}
|
||||||
|
|||||||
+226
-9
@@ -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>
|
||||||
@@ -28,6 +29,8 @@ private:
|
|||||||
void parse_services_block(SystemConfig& cfg);
|
void parse_services_block(SystemConfig& cfg);
|
||||||
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_repos_block(SystemConfig& cfg);
|
||||||
|
|
||||||
std::string consume_ident();
|
std::string consume_ident();
|
||||||
std::string consume_string();
|
std::string consume_string();
|
||||||
@@ -44,9 +47,15 @@ void SysParser::skip_newlines() {
|
|||||||
|
|
||||||
Token SysParser::consume(TokenType type) {
|
Token SysParser::consume(TokenType type) {
|
||||||
if (!at(type)) {
|
if (!at(type)) {
|
||||||
|
if (type == TokenType::Rbrace && at(TokenType::Eof)) {
|
||||||
throw ParseError(current_.line, current_.col,
|
throw ParseError(current_.line, current_.col,
|
||||||
std::format("expected '{}', got '{}'",
|
msg_unclosed_block("block"));
|
||||||
token_name(type), token_name(current_.type)));
|
}
|
||||||
|
if (type == TokenType::String && at(TokenType::Ident)) {
|
||||||
|
throw ParseError(current_.line, current_.col, msg_not_a_string());
|
||||||
|
}
|
||||||
|
throw ParseError(current_.line, current_.col,
|
||||||
|
msg_expected(token_name(type), token_name(current_.type)));
|
||||||
}
|
}
|
||||||
Token t = std::move(current_);
|
Token t = std::move(current_);
|
||||||
advance();
|
advance();
|
||||||
@@ -59,6 +68,9 @@ std::string SysParser::consume_ident() {
|
|||||||
at(TokenType::KwService) ||
|
at(TokenType::KwService) ||
|
||||||
at(TokenType::KwAssert) ||
|
at(TokenType::KwAssert) ||
|
||||||
at(TokenType::KwImport) ||
|
at(TokenType::KwImport) ||
|
||||||
|
at(TokenType::KwSha256) ||
|
||||||
|
at(TokenType::KwSha512) ||
|
||||||
|
at(TokenType::KwMd5) ||
|
||||||
at(TokenType::KwConfig) ||
|
at(TokenType::KwConfig) ||
|
||||||
at(TokenType::KwFeatures) ||
|
at(TokenType::KwFeatures) ||
|
||||||
at(TokenType::KwVersion) ||
|
at(TokenType::KwVersion) ||
|
||||||
@@ -75,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;
|
||||||
@@ -121,6 +135,17 @@ SystemConfig SysParser::parse() {
|
|||||||
skip_newlines();
|
skip_newlines();
|
||||||
}
|
}
|
||||||
consume(TokenType::Rbracket);
|
consume(TokenType::Rbracket);
|
||||||
|
} else if (kw == "remotes") {
|
||||||
|
consume(TokenType::Equals);
|
||||||
|
consume(TokenType::Lbracket);
|
||||||
|
skip_newlines();
|
||||||
|
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||||
|
cfg.remotes.push_back(consume(TokenType::String).lexeme);
|
||||||
|
skip_newlines();
|
||||||
|
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||||
|
skip_newlines();
|
||||||
|
}
|
||||||
|
consume(TokenType::Rbracket);
|
||||||
} else if (kw == "assert") {
|
} else if (kw == "assert") {
|
||||||
consume(TokenType::Lbrace);
|
consume(TokenType::Lbrace);
|
||||||
skip_newlines();
|
skip_newlines();
|
||||||
@@ -131,12 +156,16 @@ SystemConfig SysParser::parse() {
|
|||||||
consume(TokenType::Ident); // ":"
|
consume(TokenType::Ident); // ":"
|
||||||
a.field = current_.lexeme; advance();
|
a.field = current_.lexeme; advance();
|
||||||
if (at(TokenType::Equals)) {
|
if (at(TokenType::Equals)) {
|
||||||
advance(); // first =
|
advance();
|
||||||
if (at(TokenType::Equals)) {
|
if (at(TokenType::Equals)) {
|
||||||
a.op = "=="; advance(); // second =
|
a.op = "=="; advance();
|
||||||
} else {
|
} else {
|
||||||
a.op = "=";
|
a.op = "=";
|
||||||
}
|
}
|
||||||
|
} else if (at(TokenType::Ident) && current_.lexeme == "!") {
|
||||||
|
advance();
|
||||||
|
consume(TokenType::Equals);
|
||||||
|
a.op = "!=";
|
||||||
} else {
|
} else {
|
||||||
a.op = current_.lexeme; advance();
|
a.op = current_.lexeme; advance();
|
||||||
}
|
}
|
||||||
@@ -158,6 +187,8 @@ SystemConfig SysParser::parse() {
|
|||||||
else if (kw == "services") { parse_services_block(cfg); }
|
else if (kw == "services") { parse_services_block(cfg); }
|
||||||
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 == "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));
|
||||||
@@ -188,14 +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)) {
|
||||||
soft = true;
|
|
||||||
advance();
|
advance();
|
||||||
|
consume(TokenType::Equals);
|
||||||
|
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);
|
||||||
@@ -397,6 +433,91 @@ void SysParser::parse_users_block(SystemConfig& cfg) {
|
|||||||
consume(TokenType::Rbrace);
|
consume(TokenType::Rbrace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SysParser::parse_groups_block(SystemConfig& cfg) {
|
||||||
|
consume(TokenType::Lbrace);
|
||||||
|
skip_newlines();
|
||||||
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||||
|
skip_newlines();
|
||||||
|
if (at(TokenType::Rbrace)) break;
|
||||||
|
GroupDef g;
|
||||||
|
g.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();
|
||||||
|
consume(TokenType::Equals);
|
||||||
|
if (key == "gid") {
|
||||||
|
g.gid = parse_int(current_.line, current_.col, current_.lexeme);
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
skip_newlines();
|
||||||
|
}
|
||||||
|
consume(TokenType::Rbrace);
|
||||||
|
}
|
||||||
|
cfg.groups.push_back(std::move(g));
|
||||||
|
}
|
||||||
|
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();
|
||||||
@@ -406,12 +527,15 @@ static void merge_config(SystemConfig& base, SystemConfig&& imported) {
|
|||||||
if (!imported.system.hostname.empty()) { base.system.hostname = std::move(imported.system.hostname); }
|
if (!imported.system.hostname.empty()) { base.system.hostname = std::move(imported.system.hostname); }
|
||||||
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.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; }
|
||||||
for (auto& p : imported.packages) { base.packages.push_back(std::move(p)); }
|
for (auto& p : imported.packages) { base.packages.push_back(std::move(p)); }
|
||||||
for (auto& s : imported.services) { base.services.push_back(std::move(s)); }
|
for (auto& s : imported.services) { base.services.push_back(std::move(s)); }
|
||||||
for (auto& u : imported.users) { base.users.push_back(std::move(u)); }
|
for (auto& u : imported.users) { base.users.push_back(std::move(u)); }
|
||||||
|
for (auto& g : imported.groups) { base.groups.push_back(std::move(g)); }
|
||||||
if (!imported.boot.kernel.empty()) { base.boot.kernel = std::move(imported.boot.kernel); }
|
if (!imported.boot.kernel.empty()) { base.boot.kernel = std::move(imported.boot.kernel); }
|
||||||
if (!imported.boot.init.empty()) { base.boot.init = std::move(imported.boot.init); }
|
if (!imported.boot.init.empty()) { base.boot.init = std::move(imported.boot.init); }
|
||||||
if (!imported.boot.efi.empty()) { base.boot.efi = std::move(imported.boot.efi); }
|
if (!imported.boot.efi.empty()) { base.boot.efi = std::move(imported.boot.efi); }
|
||||||
@@ -436,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
|
||||||
|
|||||||
+3
-1
@@ -1,4 +1,5 @@
|
|||||||
#include "kappa/eval/vars.hpp"
|
#include "kappa/eval/vars.hpp"
|
||||||
|
#include "kappa/paths.hpp"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -9,8 +10,9 @@ Scope make_default_scope() {
|
|||||||
s.builtins["prefix"] = "/usr";
|
s.builtins["prefix"] = "/usr";
|
||||||
s.builtins["jobs"] = "1";
|
s.builtins["jobs"] = "1";
|
||||||
s.builtins["jobopts"] = "-j1";
|
s.builtins["jobopts"] = "-j1";
|
||||||
s.builtins["destdir"] = "/kappa/temp/destdir";
|
s.builtins["destdir"] = (paths::temp_dir() / "destdir").string();
|
||||||
s.builtins["userargs"] = "";
|
s.builtins["userargs"] = "";
|
||||||
|
s.builtins["enabledinit"] = "";
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
#include "kappa/fetch/fetch.hpp"
|
||||||
|
#include "kappa/paths.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <string>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::fetch {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using namespace std::string_view_literals;
|
||||||
|
|
||||||
|
static int exec_cmd(const std::vector<std::string>& argv) {
|
||||||
|
if (argv.empty()) { return -1; }
|
||||||
|
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;
|
||||||
|
pid_t w;
|
||||||
|
do { w = waitpid(pid, &status, 0); } while (w == -1 && errno == EINTR);
|
||||||
|
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string exec_capture(const std::vector<std::string>& argv) {
|
||||||
|
int pipefd[2];
|
||||||
|
if (pipe(pipefd) != 0) { return ""; }
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
close(pipefd[0]);
|
||||||
|
dup2(pipefd[1], STDOUT_FILENO);
|
||||||
|
close(pipefd[1]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
execvp(cargs[0], cargs.data());
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid < 0) { close(pipefd[0]); close(pipefd[1]); return ""; }
|
||||||
|
|
||||||
|
close(pipefd[1]);
|
||||||
|
std::array<char, 256> buf;
|
||||||
|
std::string result;
|
||||||
|
ssize_t n;
|
||||||
|
while ((n = read(pipefd[0], buf.data(), buf.size() - 1)) > 0) {
|
||||||
|
buf[static_cast<std::size_t>(n)] = '\0';
|
||||||
|
result += buf.data();
|
||||||
|
}
|
||||||
|
close(pipefd[0]);
|
||||||
|
pid_t w;
|
||||||
|
do { w = waitpid(pid, nullptr, 0); } while (w == -1 && errno == EINTR);
|
||||||
|
|
||||||
|
if (!result.empty() && result.back() == '\n') { result.pop_back(); }
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string interpret_url(const dsl::PackageDef& pkg) {
|
||||||
|
auto url = pkg.source;
|
||||||
|
for (auto& [from, to] : {
|
||||||
|
std::pair{"${name}"sv, std::string_view(pkg.name)},
|
||||||
|
std::pair{"${version}"sv, std::string_view(pkg.version)}}) {
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while ((pos = url.find(from, pos)) != std::string::npos) {
|
||||||
|
url.replace(pos, from.size(), to);
|
||||||
|
pos += to.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool verify_hash(const fs::path& file, std::string_view algo,
|
||||||
|
std::string_view expected) {
|
||||||
|
if (expected.empty()) { return true; }
|
||||||
|
std::string tool;
|
||||||
|
if (algo == "sha256") { tool = "sha256sum"; }
|
||||||
|
else if (algo == "sha512") { tool = "sha512sum"; }
|
||||||
|
else if (algo == "md5") { tool = "md5sum"; }
|
||||||
|
else { return false; }
|
||||||
|
|
||||||
|
auto output = exec_capture({tool, file.string()});
|
||||||
|
if (output.empty()) { return false; }
|
||||||
|
|
||||||
|
auto space = output.find(' ');
|
||||||
|
auto computed = (space != std::string::npos)
|
||||||
|
? output.substr(0, space)
|
||||||
|
: output;
|
||||||
|
return computed == expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
FetchResult fetch(const dsl::PackageDef& pkg) {
|
||||||
|
FetchResult result;
|
||||||
|
|
||||||
|
auto url = interpret_url(pkg);
|
||||||
|
if (url.empty()) {
|
||||||
|
result.error = "empty source URL";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ext_pos = url.rfind('.');
|
||||||
|
auto ext = (ext_pos != std::string::npos)
|
||||||
|
? url.substr(ext_pos + 1)
|
||||||
|
: std::string{};
|
||||||
|
if (ext == "gz" || ext == "xz" || ext == "zst") {
|
||||||
|
auto prev = url.rfind('.', ext_pos - 1);
|
||||||
|
if (prev != std::string::npos) {
|
||||||
|
std::string compound{url.substr(prev + 1,
|
||||||
|
ext_pos - prev - 1)};
|
||||||
|
ext = compound + "." + ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto dest_name = pkg.name + "-" + pkg.version;
|
||||||
|
// Sanitize: replace path separators to prevent traversal
|
||||||
|
for (auto& c : dest_name) {
|
||||||
|
if (c == '/' || c == '\\') c = '_';
|
||||||
|
}
|
||||||
|
auto cache_path = paths::cache_dir() / (dest_name + "." + ext);
|
||||||
|
bool from_cache = false;
|
||||||
|
fs::path dest_file;
|
||||||
|
if (fs::exists(cache_path)) {
|
||||||
|
dest_file = cache_path;
|
||||||
|
from_cache = true;
|
||||||
|
} else {
|
||||||
|
dest_file = fs::path(paths::temp_dir()) / (dest_name + "." + ext);
|
||||||
|
}
|
||||||
|
result.work_dir = fs::path(paths::temp_dir()) / dest_name;
|
||||||
|
|
||||||
|
if (ext == "git") {
|
||||||
|
int rc = exec_cmd({"git", "clone", url, result.work_dir.string()});
|
||||||
|
if (rc != 0) { result.error = "git clone failed"; return result; }
|
||||||
|
} else {
|
||||||
|
if (!from_cache) {
|
||||||
|
fs::create_directories(paths::temp_dir());
|
||||||
|
int rc = exec_cmd({"curl", "-L", "-o", dest_file.string(), url});
|
||||||
|
if (rc != 0) { result.error = "download failed"; return result; }
|
||||||
|
std::error_code ec;
|
||||||
|
// Atomic cache write: write to .tmp then rename
|
||||||
|
auto cache_tmp = fs::path(cache_path.string() + ".tmp");
|
||||||
|
fs::copy(dest_file, cache_tmp, ec);
|
||||||
|
if (!ec) {
|
||||||
|
fs::rename(cache_tmp, cache_path, ec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool verified = false;
|
||||||
|
for (auto algo : {"sha512", "sha256", "md5"}) {
|
||||||
|
std::string_view expected;
|
||||||
|
if (std::string_view(algo) == "sha512") { expected = pkg.sha512; }
|
||||||
|
else if (std::string_view(algo) == "sha256") { expected = pkg.sha256; }
|
||||||
|
else { expected = pkg.md5; }
|
||||||
|
|
||||||
|
if (!expected.empty()) {
|
||||||
|
if (verify_hash(dest_file, algo, expected)) {
|
||||||
|
verified = true;
|
||||||
|
} else {
|
||||||
|
result.error = std::format("{} mismatch", algo);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!verified && (!pkg.sha256.empty() || !pkg.sha512.empty() || !pkg.md5.empty())) {
|
||||||
|
result.error = "hash verification failed";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto extract_cmd = std::string{"tar"};
|
||||||
|
if (ext == "zip") {
|
||||||
|
int rc2 = exec_cmd({"unzip", "-o", dest_file.string(),
|
||||||
|
"-d", result.work_dir.string()});
|
||||||
|
if (rc2 != 0) { result.error = "extraction failed"; return result; }
|
||||||
|
} else {
|
||||||
|
int rc2 = exec_cmd({"tar", "xf", dest_file.string(),
|
||||||
|
"-C", paths::temp_dir().string()});
|
||||||
|
if (rc2 != 0) { result.error = "extraction failed"; return result; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!from_cache) {
|
||||||
|
fs::remove(dest_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& patch : pkg.patches) {
|
||||||
|
auto patch_path = patch.url;
|
||||||
|
auto patch_file = patch_path;
|
||||||
|
if (patch_path.starts_with("http")) {
|
||||||
|
auto local = fs::path(paths::temp_dir())
|
||||||
|
/ fs::path(patch_path).filename();
|
||||||
|
int rc = exec_cmd({"curl", "-L", "-o", local.string(),
|
||||||
|
patch_path});
|
||||||
|
if (rc != 0) { continue; }
|
||||||
|
patch_file = local.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!patch.sha256.empty()) {
|
||||||
|
if (!verify_hash(patch_file, "sha256", patch.sha256)) {
|
||||||
|
result.error = "patch hash mismatch: " + patch.url;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int prc = exec_cmd({"patch", "-p" + std::to_string(patch.level),
|
||||||
|
"-d", result.work_dir.string(), "-i", patch_file});
|
||||||
|
if (prc != 0) {
|
||||||
|
result.error = "patch failed: " + patch.url;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::fetch
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
#include "kappa/fetch/recipe.hpp"
|
||||||
|
|
||||||
|
#include "kappa/dsl/parser.hpp"
|
||||||
|
#include "kappa/dsl/system.hpp"
|
||||||
|
#include "kappa/paths.hpp"
|
||||||
|
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::fetch {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
int exec_curl(const std::string& output_path, const std::string& url) {
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
execlp("curl", "curl", "-Lsf", "-o", output_path.c_str(), url.c_str(), nullptr);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
if (pid < 0) return -1;
|
||||||
|
int status = 0;
|
||||||
|
while (waitpid(pid, &status, 0) == -1 && errno == EINTR) {}
|
||||||
|
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
RecipeResult fetch_recipe(const std::string& name,
|
||||||
|
const std::vector<std::string>& remotes) {
|
||||||
|
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 (...) {
|
||||||
|
// Corrupt cache — will re-download
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try each remote
|
||||||
|
std::string best_content;
|
||||||
|
std::string best_version;
|
||||||
|
std::string best_url;
|
||||||
|
|
||||||
|
for (auto& remote : remotes) {
|
||||||
|
auto url = remote;
|
||||||
|
if (!url.empty() && url.back() != '/') url += '/';
|
||||||
|
url += name + ".kap";
|
||||||
|
|
||||||
|
// Download to temp
|
||||||
|
auto temp_path = paths::temp_dir() / (name + ".kap.tmp");
|
||||||
|
int rc = exec_curl(temp_path.string(), url);
|
||||||
|
if (rc != 0) continue;
|
||||||
|
|
||||||
|
// Parse downloaded file
|
||||||
|
std::ifstream in(temp_path);
|
||||||
|
if (!in) { std::filesystem::remove(temp_path); continue; }
|
||||||
|
std::ostringstream buf;
|
||||||
|
buf << in.rdbuf();
|
||||||
|
in.close();
|
||||||
|
|
||||||
|
std::string remote_version;
|
||||||
|
try {
|
||||||
|
auto pkg = dsl::parse(buf.str());
|
||||||
|
remote_version = pkg.version;
|
||||||
|
} catch (...) {
|
||||||
|
std::filesystem::remove(temp_path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare versions — keep the best (highest)
|
||||||
|
// Simple string comparison for now; semver later
|
||||||
|
if (remote_version > best_version) {
|
||||||
|
best_version = remote_version;
|
||||||
|
best_content = buf.str();
|
||||||
|
best_url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::remove(temp_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best_content.empty()) {
|
||||||
|
if (!cached_version.empty()) {
|
||||||
|
// No remote available but have cache
|
||||||
|
result.ok = true;
|
||||||
|
result.path = cache_path.string();
|
||||||
|
result.version = cached_version;
|
||||||
|
result.updated = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.error = "package '" + name + "' not found in any remote";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cache if remote is newer
|
||||||
|
if (best_version > cached_version || cached_version.empty()) {
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(paths::packages_dir(), ec);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
#include "kappa/install/install.hpp"
|
||||||
|
#include "kappa/paths.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace kappa::install {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
std::string compute_config_hash(
|
||||||
|
const std::unordered_map<std::string, dsl::FeatureDef>& features,
|
||||||
|
const std::unordered_map<std::string, std::string>& config) {
|
||||||
|
// Serialize features (sorted by key): "key1=enabled/flag|key2=..."
|
||||||
|
std::vector<std::pair<std::string_view, const dsl::FeatureDef*>> feat_sorted;
|
||||||
|
for (auto& [k, v] : features) feat_sorted.emplace_back(k, &v);
|
||||||
|
std::sort(feat_sorted.begin(), feat_sorted.end());
|
||||||
|
std::string serialized;
|
||||||
|
for (auto& [k, f] : feat_sorted) {
|
||||||
|
if (!serialized.empty()) serialized += '|';
|
||||||
|
serialized += k;
|
||||||
|
serialized += '=';
|
||||||
|
if (f->force) serialized += "force:";
|
||||||
|
serialized += f->enabled ? "1:" : "0:";
|
||||||
|
serialized += f->flag;
|
||||||
|
}
|
||||||
|
serialized += '\n';
|
||||||
|
// Serialize config (sorted by key): "key1=val1|key2=val2"
|
||||||
|
std::vector<std::pair<std::string_view, std::string_view>> cfg_sorted;
|
||||||
|
for (auto& [k, v] : config) cfg_sorted.emplace_back(k, v);
|
||||||
|
std::sort(cfg_sorted.begin(), cfg_sorted.end());
|
||||||
|
for (auto& [k, v] : cfg_sorted) {
|
||||||
|
serialized += k;
|
||||||
|
serialized += '=';
|
||||||
|
serialized += v;
|
||||||
|
serialized += '|';
|
||||||
|
}
|
||||||
|
// FNV-1a 64-bit hash
|
||||||
|
std::uint64_t h = 14695981039346656037ULL;
|
||||||
|
for (char c : serialized) {
|
||||||
|
h ^= static_cast<std::uint64_t>(static_cast<unsigned char>(c));
|
||||||
|
h *= 1099511628211ULL;
|
||||||
|
}
|
||||||
|
return std::format("{:016x}", h);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string db_file() {
|
||||||
|
return (fs::path(paths::db_dir()) / "installed").string();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::uint64_t fnv1a(std::string_view s) {
|
||||||
|
std::uint64_t h = 14695981039346656037ULL;
|
||||||
|
for (char c : s) { h ^= static_cast<std::uint64_t>(static_cast<unsigned char>(c)); h *= 1099511628211ULL; }
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallResult install(const resolve::BuildStep& step,
|
||||||
|
const fs::path& work_dir) {
|
||||||
|
InstallResult result;
|
||||||
|
|
||||||
|
auto hash = std::format("{:016x}",
|
||||||
|
fnv1a(step.name + "\0" + step.resolved.original.version));
|
||||||
|
result.hash = hash;
|
||||||
|
|
||||||
|
auto dest = fs::path(paths::bin_dir()) / hash;
|
||||||
|
result.store_path = dest;
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
fs::create_directories(dest, ec);
|
||||||
|
if (ec) {
|
||||||
|
result.error = "cannot create store directory";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto src = work_dir / "destdir";
|
||||||
|
if (fs::exists(src)) {
|
||||||
|
for (auto& entry : fs::recursive_directory_iterator(src, ec)) {
|
||||||
|
if (ec) { break; }
|
||||||
|
auto rel = fs::relative(entry.path(), src);
|
||||||
|
auto target = dest / rel;
|
||||||
|
if (entry.is_directory()) {
|
||||||
|
fs::create_directories(target, ec);
|
||||||
|
} else {
|
||||||
|
fs::create_directories(target.parent_path(), ec);
|
||||||
|
if (!ec) { fs::rename(entry.path(), target, ec); }
|
||||||
|
}
|
||||||
|
if (ec) { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto entries = read_installed();
|
||||||
|
std::string ch = "0000000000000000";
|
||||||
|
if (!step.resolved.features.empty() || !step.resolved.config.empty()) {
|
||||||
|
ch = compute_config_hash(step.resolved.features, step.resolved.config);
|
||||||
|
}
|
||||||
|
entries.push_back({step.name, step.resolved.original.version,
|
||||||
|
hash, ch, step.resolved.original.provides});
|
||||||
|
if (!write_installed(entries)) {
|
||||||
|
result.error = "failed to write installed DB";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> hashes;
|
||||||
|
for (auto& e : entries) { hashes.push_back(e.hash); }
|
||||||
|
record_generation(hashes, 5);
|
||||||
|
|
||||||
|
result.ok = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<DbEntry> read_installed() {
|
||||||
|
std::vector<DbEntry> entries;
|
||||||
|
std::ifstream in(db_file());
|
||||||
|
if (!in) { return entries; }
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(in, line)) {
|
||||||
|
if (line.empty()) { continue; }
|
||||||
|
std::istringstream iss(line);
|
||||||
|
DbEntry e;
|
||||||
|
iss >> e.name >> e.version >> e.hash;
|
||||||
|
std::string token;
|
||||||
|
auto pos = iss.tellg();
|
||||||
|
if (iss >> token && token.size() == 16
|
||||||
|
&& std::all_of(token.begin(), token.end(), [](char c) {
|
||||||
|
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
|
||||||
|
})) {
|
||||||
|
e.config_hash = token;
|
||||||
|
} else {
|
||||||
|
if (pos != std::streampos(-1)) {
|
||||||
|
iss.clear();
|
||||||
|
iss.seekg(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string prov;
|
||||||
|
while (iss >> prov) { e.provides.push_back(prov); }
|
||||||
|
entries.push_back(std::move(e));
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool write_installed(const std::vector<DbEntry>& entries) {
|
||||||
|
std::error_code ec;
|
||||||
|
fs::create_directories(paths::db_dir(), ec);
|
||||||
|
auto tmp = db_file() + ".tmp";
|
||||||
|
{
|
||||||
|
std::ofstream out(tmp);
|
||||||
|
if (!out) return false;
|
||||||
|
for (auto& e : entries) {
|
||||||
|
out << e.name << ' ' << e.version << ' ' << e.hash;
|
||||||
|
if (!e.config_hash.empty() && e.config_hash != "0000000000000000") {
|
||||||
|
out << ' ' << e.config_hash;
|
||||||
|
}
|
||||||
|
for (auto& p : e.provides) { out << ' ' << p; }
|
||||||
|
out << '\n';
|
||||||
|
}
|
||||||
|
out.close();
|
||||||
|
if (out.fail()) {
|
||||||
|
std::filesystem::remove(tmp, ec);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::filesystem::rename(tmp, db_file(), ec);
|
||||||
|
return !ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool record_generation(const std::vector<std::string>& hashes, int keep) {
|
||||||
|
auto gen_dir = fs::path(paths::db_dir()) / "generations";
|
||||||
|
std::error_code ec;
|
||||||
|
fs::create_directories(gen_dir, ec);
|
||||||
|
|
||||||
|
int gen = 1;
|
||||||
|
for (auto& entry : fs::directory_iterator(gen_dir, ec)) {
|
||||||
|
auto name = entry.path().filename().string();
|
||||||
|
if (name.starts_with("gen-")) { ++gen; }
|
||||||
|
}
|
||||||
|
|
||||||
|
auto gen_file = gen_dir / std::format("gen-{:04d}", gen);
|
||||||
|
std::ofstream out(gen_file);
|
||||||
|
if (!out) { return false; }
|
||||||
|
for (auto& h : hashes) { out << h << '\n'; }
|
||||||
|
|
||||||
|
std::vector<fs::path> gens;
|
||||||
|
for (auto& entry : fs::directory_iterator(gen_dir, ec)) {
|
||||||
|
gens.push_back(entry.path());
|
||||||
|
}
|
||||||
|
std::sort(gens.begin(), gens.end());
|
||||||
|
|
||||||
|
while (static_cast<int>(gens.size()) > keep && keep > 0) {
|
||||||
|
fs::remove_all(gens.front(), ec);
|
||||||
|
gens.erase(gens.begin());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::install
|
||||||
+1194
-9
File diff suppressed because it is too large
Load Diff
+46
-6
@@ -1,16 +1,56 @@
|
|||||||
#include "kappa/paths.hpp"
|
#include "kappa/paths.hpp"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
|
|
||||||
namespace kappa::paths {
|
namespace kappa::paths {
|
||||||
|
|
||||||
void ensure_directories() {
|
static std::filesystem::path g_root = [] {
|
||||||
std::error_code ec;
|
if (auto* env = std::getenv("KAPPA_ROOT"); env != nullptr) {
|
||||||
|
return std::filesystem::path{env};
|
||||||
|
}
|
||||||
|
return std::filesystem::path{"/usr/local/kappa"};
|
||||||
|
}();
|
||||||
|
|
||||||
std::filesystem::create_directories(bin_dir, ec);
|
void set_root(std::string_view path) { g_root = path; }
|
||||||
std::filesystem::create_directories(temp_dir, ec);
|
|
||||||
std::filesystem::create_directories(db_dir, ec);
|
std::filesystem::path root_path() { return g_root; }
|
||||||
std::filesystem::create_directories(builds_dir, ec);
|
std::filesystem::path bin_dir() { return g_root / "bin"; }
|
||||||
|
std::filesystem::path temp_dir() { return g_root / "temp"; }
|
||||||
|
std::filesystem::path db_dir() { return g_root / "db"; }
|
||||||
|
std::filesystem::path system_dir() { return g_root / "system"; }
|
||||||
|
std::filesystem::path builds_dir() { return g_root / "system" / "builds"; }
|
||||||
|
std::filesystem::path cache_dir() { return g_root / "cache"; }
|
||||||
|
std::filesystem::path packages_dir() { return cache_dir() / "packages"; }
|
||||||
|
|
||||||
|
bool ensure_directories() {
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(bin_dir(), ec);
|
||||||
|
if (ec) return false;
|
||||||
|
std::filesystem::create_directories(temp_dir(), ec);
|
||||||
|
if (ec) return false;
|
||||||
|
std::filesystem::create_directories(db_dir(), ec);
|
||||||
|
if (ec) return false;
|
||||||
|
std::filesystem::create_directories(system_dir(), ec);
|
||||||
|
if (ec) return false;
|
||||||
|
std::filesystem::create_directories(builds_dir(), ec);
|
||||||
|
if (ec) return false;
|
||||||
|
std::filesystem::create_directories(cache_dir(), ec);
|
||||||
|
if (ec) return false;
|
||||||
|
std::filesystem::create_directories(packages_dir(), ec);
|
||||||
|
if (ec) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool directories_exist() {
|
||||||
|
return std::filesystem::exists(g_root)
|
||||||
|
&& std::filesystem::exists(bin_dir())
|
||||||
|
&& std::filesystem::exists(temp_dir())
|
||||||
|
&& std::filesystem::exists(db_dir())
|
||||||
|
&& std::filesystem::exists(system_dir())
|
||||||
|
&& std::filesystem::exists(builds_dir())
|
||||||
|
&& std::filesystem::exists(cache_dir())
|
||||||
|
&& std::filesystem::exists(packages_dir());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace kappa::paths
|
} // namespace kappa::paths
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#include "kappa/rebuild/rebuild.hpp"
|
||||||
|
#include "kappa/install/install.hpp"
|
||||||
|
#include "kappa/service/types.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
namespace kappa::rebuild {
|
||||||
|
|
||||||
|
ChangeSet compute_changes(const dsl::SystemConfig& cfg) {
|
||||||
|
ChangeSet cs;
|
||||||
|
auto installed = install::read_installed();
|
||||||
|
|
||||||
|
std::set<std::string> installed_names;
|
||||||
|
for (auto& e : installed) { installed_names.insert(e.name); }
|
||||||
|
|
||||||
|
std::set<std::string> config_names;
|
||||||
|
for (auto& p : cfg.packages) { config_names.insert(p.name); }
|
||||||
|
|
||||||
|
for (auto& p : cfg.packages) {
|
||||||
|
if (!installed_names.contains(p.name)) {
|
||||||
|
cs.added.push_back(p.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
for (auto& e : installed) {
|
||||||
|
if (e.name != p.name) { continue; }
|
||||||
|
found = true;
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
if (!p.version.empty() && p.version != e.version) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!changed && (!p.features.empty() || !p.config.empty())) {
|
||||||
|
if (e.config_hash.empty()) {
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
auto ch = install::compute_config_hash(p.features, p.config);
|
||||||
|
changed = (ch != e.config_hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) { cs.changed.push_back(p.name); }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!found) { cs.added.push_back(p.name); }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& e : installed) {
|
||||||
|
if (!config_names.contains(e.name)) {
|
||||||
|
cs.removed.push_back(e.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& e : installed) {
|
||||||
|
// Compare the installed version (which stores the identifier string
|
||||||
|
// for virtual packages like init/kernel/bootloader) against the
|
||||||
|
// config value. These virtual entries have their identity in the
|
||||||
|
// version field, not the hash field.
|
||||||
|
if (e.name == "kernel" && e.version != cfg.boot.kernel) {
|
||||||
|
cs.kernel_changed = true;
|
||||||
|
}
|
||||||
|
if (e.name == "init" && e.version != cfg.boot.init) {
|
||||||
|
cs.init_changed = true;
|
||||||
|
}
|
||||||
|
if (e.name == "bootloader" && e.version != cfg.boot.bootloader) {
|
||||||
|
cs.bootloader_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& svc : cfg.services) {
|
||||||
|
if (svc.enable) { cs.services_changed = true; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
return cs;
|
||||||
|
}
|
||||||
|
|
||||||
|
InitImpact compute_init_impact(const dsl::SystemConfig& cfg,
|
||||||
|
const resolve::Registry& registry) {
|
||||||
|
InitImpact impact;
|
||||||
|
auto init_system = cfg.boot.init;
|
||||||
|
|
||||||
|
// Only compute impact if an init system is actually configured
|
||||||
|
if (init_system.empty()) { return impact; }
|
||||||
|
|
||||||
|
auto is = kappa::service::parse_init_system(init_system);
|
||||||
|
if (is == kappa::service::InitSystem::Unknown) {
|
||||||
|
std::cerr << "warning: unknown init system '" << init_system
|
||||||
|
<< "' — cannot compute init impact\n";
|
||||||
|
return impact;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each package in the config that has a matching registry entry...
|
||||||
|
for (auto& pref : cfg.packages) {
|
||||||
|
auto rit = registry.find(pref.name);
|
||||||
|
if (rit == registry.end()) { continue; }
|
||||||
|
|
||||||
|
auto& pkg = rit->second;
|
||||||
|
|
||||||
|
// No services — nothing to do
|
||||||
|
if (pkg.services.empty()) {
|
||||||
|
impact.skipped.push_back(pkg.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if build scripts reference ${enabledinit}
|
||||||
|
bool uses_enabledinit = false;
|
||||||
|
auto check_phase = [&](const dsl::Phase& phase) {
|
||||||
|
for (auto& cmd : phase.commands) {
|
||||||
|
if (cmd.find("${enabledinit}") != std::string::npos) {
|
||||||
|
uses_enabledinit = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check_phase(pkg.prepare);
|
||||||
|
if (!uses_enabledinit) check_phase(pkg.build);
|
||||||
|
if (!uses_enabledinit) check_phase(pkg.check);
|
||||||
|
if (!uses_enabledinit) check_phase(pkg.install);
|
||||||
|
|
||||||
|
if (uses_enabledinit) {
|
||||||
|
impact.service_rebuild.push_back(pkg.name);
|
||||||
|
} else {
|
||||||
|
impact.service_only.push_back(pkg.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return impact;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::rebuild
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
#include "kappa/resolve/plan.hpp"
|
||||||
|
#include "kappa/config/merge.hpp"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <queue>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace kappa::resolve {
|
||||||
|
|
||||||
|
BuildPlan resolve(const dsl::SystemConfig& cfg, const Registry& registry) {
|
||||||
|
BuildPlan plan;
|
||||||
|
std::unordered_map<std::string, std::size_t> name_to_idx;
|
||||||
|
std::vector<BuildStep> nodes;
|
||||||
|
|
||||||
|
for (auto& pref : cfg.packages) {
|
||||||
|
auto rit = registry.find(pref.name);
|
||||||
|
if (rit == registry.end()) {
|
||||||
|
plan.missing.push_back(pref.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& pkg = rit->second;
|
||||||
|
auto resolved = config::resolve_package(pkg, cfg.system, pref);
|
||||||
|
|
||||||
|
BuildStep step;
|
||||||
|
step.name = pkg.name;
|
||||||
|
step.package = &pkg;
|
||||||
|
step.resolved = std::move(resolved);
|
||||||
|
step.enabled_init = cfg.boot.init;
|
||||||
|
|
||||||
|
for (auto& dep : pkg.depends) {
|
||||||
|
if (!dep.feature.empty()) {
|
||||||
|
auto fit = step.resolved.features.find(dep.feature);
|
||||||
|
if (fit == step.resolved.features.end() || !fit->second.enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step.dependencies.push_back({dep.name, dep.version});
|
||||||
|
}
|
||||||
|
|
||||||
|
name_to_idx[step.name] = nodes.size();
|
||||||
|
nodes.push_back(std::move(step));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
for (auto& node : nodes) { selected.insert(node.name); }
|
||||||
|
|
||||||
|
for (auto& node : nodes) {
|
||||||
|
if (!node.package) continue;
|
||||||
|
for (auto& conflict_name : node.package->conflicts) {
|
||||||
|
if (selected.contains(conflict_name)) {
|
||||||
|
plan.conflicts.push_back(
|
||||||
|
std::format("{} conflicts with {}", node.name, conflict_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> in_degree(nodes.size(), 0);
|
||||||
|
std::vector<std::vector<std::size_t>> adj(nodes.size());
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < nodes.size(); ++i) {
|
||||||
|
for (auto& dep : nodes[i].dependencies) {
|
||||||
|
auto it = name_to_idx.find(dep.name);
|
||||||
|
if (it != name_to_idx.end()) {
|
||||||
|
adj[it->second].push_back(i);
|
||||||
|
in_degree[i]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::queue<std::size_t> q;
|
||||||
|
for (std::size_t i = 0; i < nodes.size(); ++i) {
|
||||||
|
if (in_degree[i] == 0) { q.push(i); }
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<bool> visited(nodes.size(), false);
|
||||||
|
while (!q.empty()) {
|
||||||
|
auto u = q.front(); q.pop();
|
||||||
|
visited[u] = true;
|
||||||
|
plan.steps.push_back(nodes[u]);
|
||||||
|
for (auto v : adj[u]) {
|
||||||
|
if (--in_degree[v] == 0) { q.push(v); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < nodes.size(); ++i) {
|
||||||
|
if (!visited[i]) {
|
||||||
|
plan.cycles.push_back(nodes[i].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::resolve
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
#include "kappa/sched/scheduler.hpp"
|
||||||
|
#include "kappa/build/build.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <format>
|
||||||
|
#include <iostream>
|
||||||
|
#include <mutex>
|
||||||
|
#include <queue>
|
||||||
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kappa::sched {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal node tracked per package during scheduling
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct Node {
|
||||||
|
const resolve::BuildStep* step = nullptr;
|
||||||
|
int pending_deps = 0; // dependencies not yet built
|
||||||
|
std::vector<std::size_t> dependents; // packages waiting on this one
|
||||||
|
int depth = 0; // distance from deepest leaf
|
||||||
|
bool claimed = false;
|
||||||
|
build::BuildResult result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Priority queue keyed by depth group (Beta/Alpha/Zeta cycle)
|
||||||
|
// Deeper packages (higher depth) get priority so they unblock more work.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct ReadyOrder {
|
||||||
|
bool operator()(const Node* a, const Node* b) const {
|
||||||
|
return a->depth < b->depth; // max-heap by depth
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Three-level depth grouping: Beta → Alpha → Zeta → Beta → ...
|
||||||
|
// Returns a priority value: Beta = 2, Alpha = 1, Zeta = 0.
|
||||||
|
// Higher value = build sooner.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static int level_priority(int depth) {
|
||||||
|
int level = depth % 3;
|
||||||
|
// Beta=0, Alpha=1, Zeta=2
|
||||||
|
// Beta should go first (priority 2), Alpha second (1), Zeta last (0)
|
||||||
|
return (3 - level) % 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Scheduler state shared between workers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct Scheduler {
|
||||||
|
std::vector<Node> nodes;
|
||||||
|
std::unordered_map<std::string, std::size_t> name_to_idx;
|
||||||
|
|
||||||
|
// Ready packages grouped by priority → max-heap
|
||||||
|
std::priority_queue<Node*, std::vector<Node*>, ReadyOrder> ready[3];
|
||||||
|
|
||||||
|
// Packages waiting (pending_deps > 0 but not yet ready)
|
||||||
|
std::unordered_set<std::size_t> waiting;
|
||||||
|
|
||||||
|
std::mutex mtx;
|
||||||
|
std::condition_variable cv;
|
||||||
|
|
||||||
|
std::atomic<int> active_workers{0};
|
||||||
|
std::atomic<int> completed{0};
|
||||||
|
std::atomic<bool> stop{false};
|
||||||
|
int total_packages = 0;
|
||||||
|
|
||||||
|
std::string work_root;
|
||||||
|
int jobs_per_worker = 1;
|
||||||
|
|
||||||
|
SchedResult result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Enqueue a node into the ready queue at the correct priority level
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static void enqueue_ready(Scheduler& s, std::size_t idx) {
|
||||||
|
Node& node = s.nodes[idx];
|
||||||
|
int prio = level_priority(node.depth);
|
||||||
|
s.ready[prio].push(&node);
|
||||||
|
s.waiting.erase(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Try to claim the next ready package from any priority level.
|
||||||
|
// Returns nullptr if nothing is ready.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static Node* claim_next(Scheduler& s) {
|
||||||
|
// Check Beta (0), then Alpha (1), then Zeta (2)
|
||||||
|
for (int p = 2; p >= 0; --p) {
|
||||||
|
auto& q = s.ready[p];
|
||||||
|
if (q.empty()) continue;
|
||||||
|
Node* node = q.top();
|
||||||
|
q.pop();
|
||||||
|
node->claimed = true;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Worker loop
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static void worker_loop(Scheduler& s) {
|
||||||
|
s.active_workers.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
Node* node = nullptr;
|
||||||
|
{
|
||||||
|
std::unique_lock lock(s.mtx);
|
||||||
|
s.cv.wait(lock, [&] {
|
||||||
|
return s.stop.load(std::memory_order_acquire)
|
||||||
|
|| !s.ready[0].empty()
|
||||||
|
|| !s.ready[1].empty()
|
||||||
|
|| !s.ready[2].empty()
|
||||||
|
|| s.completed.load(std::memory_order_acquire) >= s.total_packages;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (s.stop.load(std::memory_order_acquire)) break;
|
||||||
|
if (s.completed.load(std::memory_order_acquire) >= s.total_packages) break;
|
||||||
|
node = claim_next(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node == nullptr) continue;
|
||||||
|
|
||||||
|
// Build the package
|
||||||
|
std::cout << std::format(" building {} (depth={})\n",
|
||||||
|
node->step->name, node->depth);
|
||||||
|
auto r = build::build(*node->step,
|
||||||
|
s.work_root + "/" + node->step->name,
|
||||||
|
s.jobs_per_worker);
|
||||||
|
node->result = r;
|
||||||
|
|
||||||
|
// Mark complete and notify dependents
|
||||||
|
{
|
||||||
|
std::lock_guard lock(s.mtx);
|
||||||
|
s.completed.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if (r.ok) {
|
||||||
|
s.result.built.push_back(node->step->name);
|
||||||
|
} else {
|
||||||
|
s.result.failed.push_back(node->step->name);
|
||||||
|
s.result.ok = false;
|
||||||
|
s.stop.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wake up dependents
|
||||||
|
for (auto dep_idx : node->dependents) {
|
||||||
|
Node& dep = s.nodes[dep_idx];
|
||||||
|
dep.pending_deps--;
|
||||||
|
if (dep.pending_deps == 0) {
|
||||||
|
enqueue_ready(s, dep_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal completion or failure
|
||||||
|
{
|
||||||
|
std::lock_guard lock(s.mtx);
|
||||||
|
if (s.completed.load(std::memory_order_acquire) >= s.total_packages) {
|
||||||
|
s.stop.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.cv.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
s.active_workers.fetch_sub(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Compute depth for each node (distance from deepest leaf)
|
||||||
|
// Uses post-order traversal: depth = 1 + max(dep depths), leaf = 1
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static void compute_depths(Scheduler& s) {
|
||||||
|
// Start from leaves (pending_deps == 0)
|
||||||
|
std::queue<std::size_t> leaf_queue;
|
||||||
|
for (std::size_t i = 0; i < s.nodes.size(); ++i) {
|
||||||
|
if (s.nodes[i].pending_deps == 0) {
|
||||||
|
s.nodes[i].depth = 1;
|
||||||
|
leaf_queue.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate upward: when a dependent is processed, its depth
|
||||||
|
// is 1 + max of its dependency depths.
|
||||||
|
// For simplicity, we approximate: depth = level from leaves.
|
||||||
|
// This is fine for prioritization — deeper = unblocks more.
|
||||||
|
std::vector<int> rem_deps(s.nodes.size());
|
||||||
|
for (std::size_t i = 0; i < s.nodes.size(); ++i) {
|
||||||
|
rem_deps[i] = static_cast<int>(s.nodes[i].dependents.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!leaf_queue.empty()) {
|
||||||
|
auto u = leaf_queue.front();
|
||||||
|
leaf_queue.pop();
|
||||||
|
Node& node = s.nodes[u];
|
||||||
|
|
||||||
|
if (node.step == nullptr) continue;
|
||||||
|
for (auto dep_idx : node.dependents) {
|
||||||
|
Node& dep_node = s.nodes[dep_idx];
|
||||||
|
if (node.depth + 1 > dep_node.depth) {
|
||||||
|
dep_node.depth = node.depth + 1;
|
||||||
|
}
|
||||||
|
if (--rem_deps[dep_idx] == 0) {
|
||||||
|
leaf_queue.push(dep_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public entry point
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
SchedResult run(const resolve::BuildPlan& plan,
|
||||||
|
const std::string& work_root,
|
||||||
|
int workers,
|
||||||
|
int jobs) {
|
||||||
|
if (plan.steps.empty()) {
|
||||||
|
return {true, {}, {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
Scheduler s;
|
||||||
|
s.work_root = work_root;
|
||||||
|
s.jobs_per_worker = std::max(1, jobs);
|
||||||
|
s.total_packages = static_cast<int>(plan.steps.size());
|
||||||
|
s.nodes.resize(plan.steps.size());
|
||||||
|
|
||||||
|
// Build name → index map
|
||||||
|
for (std::size_t i = 0; i < plan.steps.size(); ++i) {
|
||||||
|
s.name_to_idx[plan.steps[i].name] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire up nodes: dependencies, dependents, pending_deps
|
||||||
|
for (std::size_t i = 0; i < plan.steps.size(); ++i) {
|
||||||
|
Node& node = s.nodes[i];
|
||||||
|
node.step = &plan.steps[i];
|
||||||
|
node.depth = 0;
|
||||||
|
|
||||||
|
for (auto& dep : plan.steps[i].dependencies) {
|
||||||
|
auto it = s.name_to_idx.find(dep.name);
|
||||||
|
if (it == s.name_to_idx.end()) {
|
||||||
|
std::cerr << std::format("warning: dependency '{}' of '{}' not in build plan\n",
|
||||||
|
dep.name, plan.steps[i].name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dep → i (dependency is upstream)
|
||||||
|
Node& dep_node = s.nodes[it->second];
|
||||||
|
dep_node.dependents.push_back(i);
|
||||||
|
node.pending_deps++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute depths for priority
|
||||||
|
compute_depths(s);
|
||||||
|
|
||||||
|
// Enqueue root nodes (pending_deps == 0)
|
||||||
|
for (std::size_t i = 0; i < s.nodes.size(); ++i) {
|
||||||
|
if (s.nodes[i].pending_deps == 0) {
|
||||||
|
enqueue_ready(s, i);
|
||||||
|
} else {
|
||||||
|
s.waiting.insert(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int num_workers = std::max(1, std::min(workers, s.total_packages));
|
||||||
|
std::cout << std::format("scheduler: {} packages, {} workers, {} jobs/worker\n",
|
||||||
|
s.total_packages, num_workers, s.jobs_per_worker);
|
||||||
|
|
||||||
|
// Spawn workers
|
||||||
|
std::vector<std::thread> threads;
|
||||||
|
for (int w = 0; w < num_workers; ++w) {
|
||||||
|
threads.emplace_back(worker_loop, std::ref(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for workers to finish
|
||||||
|
for (auto& t : threads) {
|
||||||
|
if (t.joinable()) t.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
s.result.ok = s.result.failed.empty();
|
||||||
|
return s.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::sched
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#include "kappa/service/service.hpp"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace kappa::service {
|
||||||
|
|
||||||
|
std::string generate_dinit_service(const ServiceSpec& spec) {
|
||||||
|
std::ostringstream out;
|
||||||
|
|
||||||
|
// Header
|
||||||
|
out << std::format("# Generated by kappa — do not edit manually\n");
|
||||||
|
out << std::format("# dinit service: {}\n", spec.name);
|
||||||
|
out << "\n";
|
||||||
|
|
||||||
|
// Type mapping
|
||||||
|
std::string dinit_type;
|
||||||
|
if (spec.type == "simple" || spec.type == "notify" ||
|
||||||
|
spec.type == "longrun") {
|
||||||
|
dinit_type = "process";
|
||||||
|
} else if (spec.type == "forking") {
|
||||||
|
dinit_type = "bgprocess";
|
||||||
|
} else if (spec.type == "oneshot") {
|
||||||
|
dinit_type = "scripted";
|
||||||
|
} else {
|
||||||
|
dinit_type = "process";
|
||||||
|
}
|
||||||
|
|
||||||
|
out << std::format("type = {}\n", dinit_type);
|
||||||
|
out << std::format("command = {}\n", spec.exec);
|
||||||
|
|
||||||
|
// Restart policy
|
||||||
|
if (spec.restart_policy == "always") {
|
||||||
|
out << "restart = true\n";
|
||||||
|
} else if (spec.restart_policy == "on-failure") {
|
||||||
|
out << "restart = true\n";
|
||||||
|
} else if (spec.restart_policy == "never") {
|
||||||
|
out << "restart = false\n";
|
||||||
|
} else if (!spec.restart_policy.empty()) {
|
||||||
|
out << "restart = true\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// depends-on
|
||||||
|
if (!spec.after.empty()) {
|
||||||
|
out << std::format("depends-on = {}\n", spec.after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// working-dir
|
||||||
|
if (!spec.working_dir.empty()) {
|
||||||
|
out << std::format("working-dir = {}\n", spec.working_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// run-as
|
||||||
|
if (!spec.user.empty()) {
|
||||||
|
out << std::format("run-as = {}\n", spec.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variables — dinit uses env-file directive
|
||||||
|
if (!spec.env.empty()) {
|
||||||
|
out << std::format("env-file = {}.env\n", spec.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// description
|
||||||
|
if (!spec.description.empty()) {
|
||||||
|
out << std::format("description = {}\n", spec.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::service
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
#include "kappa/service/service.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
namespace kappa::service {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Backend service-file generators (defined in separate .cpp files)
|
||||||
|
// systemd_service and s6_service are declared in service.hpp
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ServiceSpec::from_service_init
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ServiceSpec ServiceSpec::from_service_init(const dsl::NamedService& ns) {
|
||||||
|
ServiceSpec spec;
|
||||||
|
spec.name = ns.name;
|
||||||
|
spec.description = ns.description;
|
||||||
|
spec.exec = ns.exec;
|
||||||
|
spec.user = ns.user;
|
||||||
|
spec.ports = ns.ports;
|
||||||
|
spec.env = ns.env;
|
||||||
|
spec.after = ns.after;
|
||||||
|
spec.working_dir = ns.working_dir;
|
||||||
|
|
||||||
|
// Normalize type
|
||||||
|
if (ns.type == "forking" || ns.type == "longrun" ||
|
||||||
|
ns.type == "notify" || ns.type == "oneshot") {
|
||||||
|
spec.type = ns.type;
|
||||||
|
} else {
|
||||||
|
spec.type = "simple";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart policy
|
||||||
|
spec.restart_policy = ns.restart;
|
||||||
|
if (spec.restart_policy.empty() && ns.type == "longrun") {
|
||||||
|
spec.restart_policy = "always";
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// generate_service_file — dispatch to the correct backend
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
std::string generate_service_file(InitSystem is, const ServiceSpec& spec) {
|
||||||
|
switch (is) {
|
||||||
|
case InitSystem::Systemd:
|
||||||
|
return generate_systemd_service(spec);
|
||||||
|
case InitSystem::OpenRC:
|
||||||
|
return generate_openrc_service(spec);
|
||||||
|
case InitSystem::S6:
|
||||||
|
return generate_s6_service(spec);
|
||||||
|
case InitSystem::Dinit:
|
||||||
|
return generate_dinit_service(spec);
|
||||||
|
case InitSystem::Runit:
|
||||||
|
return generate_runit_service(spec);
|
||||||
|
case InitSystem::Unknown:
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// install_service — write the generated file(s) to disk
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ServiceInstallResult install_service(InitSystem is,
|
||||||
|
const ServiceSpec& spec,
|
||||||
|
std::string_view prefix) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
auto paths = init_paths(is, prefix);
|
||||||
|
if (paths.service_dir.empty()) {
|
||||||
|
return {false, {}, "Unknown init system"};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
|
||||||
|
// ---- S6: directory-based layout (type + run) ----
|
||||||
|
// S6: directory-based layout. Content generated inline rather than
|
||||||
|
// calling generate_s6_service() to avoid parsing the combined output.
|
||||||
|
if (is == InitSystem::S6) {
|
||||||
|
fs::path svc_dir = fs::path(paths.service_dir) / spec.name;
|
||||||
|
fs::create_directories(svc_dir, ec);
|
||||||
|
if (ec) {
|
||||||
|
return {false, {}, ec.message()};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- type file ---
|
||||||
|
{
|
||||||
|
fs::path type_path = svc_dir / "type";
|
||||||
|
std::ofstream out(type_path);
|
||||||
|
if (!out) {
|
||||||
|
return {false, {},
|
||||||
|
std::format("Failed to write {}", type_path.string())};
|
||||||
|
}
|
||||||
|
out << ((spec.type == "oneshot") ? "oneshot" : "longrun");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- run file ---
|
||||||
|
fs::path run_path = svc_dir / "run";
|
||||||
|
{
|
||||||
|
std::ofstream out(run_path);
|
||||||
|
if (!out) {
|
||||||
|
return {false, {},
|
||||||
|
std::format("Failed to write {}", run_path.string())};
|
||||||
|
}
|
||||||
|
out << s6_execline_shebang;
|
||||||
|
out << "# Generated by kappa — do not edit manually\n";
|
||||||
|
out << std::format("# s6 service: {}\n", spec.name);
|
||||||
|
if (!spec.working_dir.empty()) {
|
||||||
|
out << std::format("cd {}\n", spec.working_dir);
|
||||||
|
}
|
||||||
|
for (const auto& [key, value] : spec.env) {
|
||||||
|
out << std::format("export {} \"{}\"\n", key, value);
|
||||||
|
}
|
||||||
|
if (!spec.user.empty()) {
|
||||||
|
out << std::format("s6-setuidgid {}\n", spec.user);
|
||||||
|
}
|
||||||
|
out << spec.exec << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make run file executable
|
||||||
|
fs::permissions(run_path,
|
||||||
|
fs::perms::owner_exec | fs::perms::group_exec |
|
||||||
|
fs::perms::others_exec,
|
||||||
|
fs::perm_options::add, ec);
|
||||||
|
|
||||||
|
return {true, svc_dir.string(), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Runit: directory-based layout (run) ----
|
||||||
|
if (is == InitSystem::Runit) {
|
||||||
|
fs::path svc_dir = fs::path(paths.service_dir) / spec.name;
|
||||||
|
fs::create_directories(svc_dir, ec);
|
||||||
|
if (ec) {
|
||||||
|
return {false, {}, ec.message()};
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path run_path = svc_dir / "run";
|
||||||
|
{
|
||||||
|
std::ofstream out(run_path);
|
||||||
|
if (!out) {
|
||||||
|
return {false, {},
|
||||||
|
std::format("Failed to write {}", run_path.string())};
|
||||||
|
}
|
||||||
|
out << generate_runit_service(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make run executable
|
||||||
|
fs::permissions(run_path,
|
||||||
|
fs::perms::owner_exec | fs::perms::group_exec |
|
||||||
|
fs::perms::others_exec,
|
||||||
|
fs::perm_options::add, ec);
|
||||||
|
|
||||||
|
return {true, svc_dir.string(), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Systemd / OpenRC / Dinit: single service file ----
|
||||||
|
std::string content = generate_service_file(is, spec);
|
||||||
|
if (content.empty()) {
|
||||||
|
return {false, {}, "Failed to generate service file for init system"};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine filename
|
||||||
|
std::string filename;
|
||||||
|
switch (is) {
|
||||||
|
case InitSystem::Systemd:
|
||||||
|
filename = std::format("{}.service", spec.name);
|
||||||
|
break;
|
||||||
|
case InitSystem::OpenRC:
|
||||||
|
case InitSystem::Dinit:
|
||||||
|
filename = spec.name;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return {false, {}, "Unknown init system"};
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path file_path = fs::path(paths.service_dir) / filename;
|
||||||
|
fs::create_directories(file_path.parent_path(), ec);
|
||||||
|
if (ec) {
|
||||||
|
return {false, {}, ec.message()};
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::ofstream out(file_path);
|
||||||
|
if (!out) {
|
||||||
|
return {false, {},
|
||||||
|
std::format("Failed to write {}", file_path.string())};
|
||||||
|
}
|
||||||
|
out << content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dinit: also write companion .env file
|
||||||
|
if (is == InitSystem::Dinit && !spec.env.empty()) {
|
||||||
|
fs::path env_path = fs::path(paths.service_dir) / std::format("{}.env", spec.name);
|
||||||
|
std::ofstream env_out(env_path);
|
||||||
|
if (env_out) {
|
||||||
|
for (const auto& [key, value] : spec.env) {
|
||||||
|
env_out << key << "=" << value << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {true, file_path.string(), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::service
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
#include "kappa/service/service.hpp"
|
||||||
|
#include "kappa/util.hpp"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace kappa::service {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
static constexpr std::string_view openrc_run_shebang = "#!/sbin/openrc-run\n";
|
||||||
|
|
||||||
|
bool is_background_type(std::string_view type) {
|
||||||
|
return type == "longrun" || type == "notify" || type == "forking";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string generate_openrc_service(const ServiceSpec& spec) {
|
||||||
|
std::ostringstream os;
|
||||||
|
|
||||||
|
// Shebang and header
|
||||||
|
os << openrc_run_shebang;
|
||||||
|
os << "# Generated by kappa — do not edit manually\n";
|
||||||
|
|
||||||
|
// Description
|
||||||
|
auto desc = spec.description.empty()
|
||||||
|
? spec.name + " service"
|
||||||
|
: spec.description;
|
||||||
|
os << std::format("description=\"{}\"\n", util::shell_escape(desc));
|
||||||
|
|
||||||
|
// Command
|
||||||
|
os << std::format("\ncommand=\"{}\"\n", util::shell_escape(spec.exec));
|
||||||
|
|
||||||
|
// User
|
||||||
|
if (!spec.user.empty()) {
|
||||||
|
os << std::format("command_user=\"{}\"\n", util::shell_escape(spec.user));
|
||||||
|
}
|
||||||
|
|
||||||
|
// command_background and command_args (type-specific)
|
||||||
|
if (spec.type == "oneshot") {
|
||||||
|
os << "command_background=false\n";
|
||||||
|
os << "command_args=\"\"\n";
|
||||||
|
} else if (is_background_type(spec.type)) {
|
||||||
|
os << "command_background=true\n";
|
||||||
|
} else if (!spec.working_dir.empty()) {
|
||||||
|
os << "command_background=true\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Working directory
|
||||||
|
if (!spec.working_dir.empty()) {
|
||||||
|
os << std::format("directory=\"{}\"\n",
|
||||||
|
util::shell_escape(spec.working_dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depend block (if after or ports)
|
||||||
|
bool has_depend = !spec.after.empty() || !spec.ports.empty();
|
||||||
|
if (has_depend) {
|
||||||
|
os << "\ndepend() {\n";
|
||||||
|
if (!spec.after.empty()) {
|
||||||
|
os << std::format(" need {}\n", spec.after);
|
||||||
|
}
|
||||||
|
if (!spec.ports.empty()) {
|
||||||
|
os << " use net\n";
|
||||||
|
}
|
||||||
|
os << "}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart policy comment
|
||||||
|
if (!spec.restart_policy.empty()) {
|
||||||
|
os << std::format("# restart policy: {}\n", spec.restart_policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment exports
|
||||||
|
for (auto& [key, value] : spec.env) {
|
||||||
|
os << std::format("export {}=\"{}\"\n",
|
||||||
|
key, util::shell_escape(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::service
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#include "kappa/service/service.hpp"
|
||||||
|
#include "kappa/util.hpp"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace kappa::service {
|
||||||
|
|
||||||
|
std::string generate_runit_service(const ServiceSpec& spec) {
|
||||||
|
std::ostringstream out;
|
||||||
|
|
||||||
|
// Shebang
|
||||||
|
out << "#!/bin/sh\n";
|
||||||
|
|
||||||
|
// Header
|
||||||
|
out << "# Generated by kappa — do not edit manually\n";
|
||||||
|
out << std::format("# runit service: {}\n", spec.name);
|
||||||
|
out << std::format("# Type: {}\n", spec.type);
|
||||||
|
|
||||||
|
// Forking note
|
||||||
|
if (spec.type == "forking") {
|
||||||
|
out << "# NOTE: runit requires foreground execution.\n";
|
||||||
|
out << "# If the daemon forks, pass --foreground or equivalent"
|
||||||
|
" flag.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Oneshot note
|
||||||
|
if (spec.type == "oneshot") {
|
||||||
|
out << "# NOTE: runit has no native oneshot support. This service"
|
||||||
|
" will restart on exit.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect stderr to stdout for logging
|
||||||
|
out << "exec 2>&1\n";
|
||||||
|
|
||||||
|
// Working directory
|
||||||
|
if (!spec.working_dir.empty()) {
|
||||||
|
out << std::format("cd \"{}\"\n", util::shell_escape(spec.working_dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
for (const auto& [key, value] : spec.env) {
|
||||||
|
out << std::format("export {}=\"{}\"\n", key, util::shell_escape(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final exec — replace the shell with the daemon
|
||||||
|
if (!spec.user.empty()) {
|
||||||
|
out << std::format("exec chpst -u {} {}\n",
|
||||||
|
util::shell_escape(spec.user), spec.exec);
|
||||||
|
} else {
|
||||||
|
out << "exec " << spec.exec << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::service
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#include "kappa/service/service.hpp"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace kappa::service {
|
||||||
|
|
||||||
|
std::string generate_s6_service(const ServiceSpec& spec) {
|
||||||
|
std::ostringstream out;
|
||||||
|
|
||||||
|
// --- type file content ---
|
||||||
|
std::string type_content;
|
||||||
|
if (spec.type == "oneshot") {
|
||||||
|
type_content = "oneshot";
|
||||||
|
} else {
|
||||||
|
// "longrun", "notify", and anything else map to longrun in s6
|
||||||
|
type_content = "longrun";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- run file content ---
|
||||||
|
std::ostringstream run;
|
||||||
|
run << s6_execline_shebang;
|
||||||
|
run << "# Generated by kappa — do not edit manually\n";
|
||||||
|
run << std::format("# s6 service: {}\n", spec.name);
|
||||||
|
|
||||||
|
if (!spec.working_dir.empty()) {
|
||||||
|
run << std::format("cd {}\n", spec.working_dir);
|
||||||
|
}
|
||||||
|
for (const auto& [key, value] : spec.env) {
|
||||||
|
run << std::format("export {} \"{}\"\n", key, value);
|
||||||
|
}
|
||||||
|
if (!spec.user.empty()) {
|
||||||
|
run << std::format("s6-setuidgid {}\n", spec.user);
|
||||||
|
}
|
||||||
|
run << spec.exec << "\n";
|
||||||
|
|
||||||
|
// --- combined output ---
|
||||||
|
out << std::format("# --- s6 service directory: {} ---\n", spec.name);
|
||||||
|
out << "# file: type\n";
|
||||||
|
out << type_content << "\n";
|
||||||
|
out << "# file: run\n";
|
||||||
|
out << run.str();
|
||||||
|
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::service
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#include "kappa/service/service.hpp"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace kappa::service {
|
||||||
|
|
||||||
|
std::string generate_systemd_service(const ServiceSpec& spec) {
|
||||||
|
std::ostringstream out;
|
||||||
|
|
||||||
|
// --- [Unit] ---
|
||||||
|
out << "[Unit]\n";
|
||||||
|
out << std::format("Description={}\n",
|
||||||
|
spec.description.empty() ? spec.name : spec.description);
|
||||||
|
if (!spec.after.empty()) {
|
||||||
|
out << std::format("After={}\n", spec.after);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- [Service] ---
|
||||||
|
out << "\n[Service]\n";
|
||||||
|
out << std::format("ExecStart={}\n", spec.exec);
|
||||||
|
|
||||||
|
if (spec.type == "simple") {
|
||||||
|
out << "Type=simple\n";
|
||||||
|
} else if (spec.type == "forking") {
|
||||||
|
out << "Type=forking\n";
|
||||||
|
} else if (spec.type == "oneshot") {
|
||||||
|
out << "Type=oneshot\n";
|
||||||
|
} else if (spec.type == "notify") {
|
||||||
|
out << "Type=notify\n";
|
||||||
|
} else {
|
||||||
|
out << "Type=simple\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!spec.user.empty()) {
|
||||||
|
out << std::format("User={}\n", spec.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spec.restart_policy == "always") {
|
||||||
|
out << "Restart=always\n";
|
||||||
|
} else if (spec.restart_policy == "on-failure") {
|
||||||
|
out << "Restart=on-failure\n";
|
||||||
|
} else if (spec.restart_policy == "never") {
|
||||||
|
out << "Restart=no\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!spec.working_dir.empty()) {
|
||||||
|
out << std::format("WorkingDirectory={}\n", spec.working_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [key, value] : spec.env) {
|
||||||
|
out << std::format("Environment=\"{0}={1}\"\n", key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- [Install] ---
|
||||||
|
out << "\n[Install]\n";
|
||||||
|
out << "WantedBy=multi-user.target\n";
|
||||||
|
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::service
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#include "kappa/service/types.hpp"
|
||||||
|
#include "kappa/util.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
namespace kappa::service {
|
||||||
|
|
||||||
|
InitSystem parse_init_system(std::string_view name) {
|
||||||
|
auto lower = util::to_lower(name);
|
||||||
|
if (lower == "systemd") return InitSystem::Systemd;
|
||||||
|
if (lower == "openrc") return InitSystem::OpenRC;
|
||||||
|
if (lower == "s6") return InitSystem::S6;
|
||||||
|
if (lower == "runit") return InitSystem::Runit;
|
||||||
|
if (lower == "dinit") return InitSystem::Dinit;
|
||||||
|
return InitSystem::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view to_string(InitSystem is) {
|
||||||
|
switch (is) {
|
||||||
|
case InitSystem::Systemd: return "systemd";
|
||||||
|
case InitSystem::OpenRC: return "openrc";
|
||||||
|
case InitSystem::S6: return "s6";
|
||||||
|
case InitSystem::Runit: return "runit";
|
||||||
|
case InitSystem::Dinit: return "dinit";
|
||||||
|
case InitSystem::Unknown: return "unknown";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_supported(std::string_view name) {
|
||||||
|
return parse_init_system(name) != InitSystem::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<InitSystem> all_systems() {
|
||||||
|
return {InitSystem::Systemd, InitSystem::OpenRC,
|
||||||
|
InitSystem::S6, InitSystem::Runit, InitSystem::Dinit};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view init_description(InitSystem is) {
|
||||||
|
switch (is) {
|
||||||
|
case InitSystem::Systemd:
|
||||||
|
return "systemd — system and service manager";
|
||||||
|
case InitSystem::OpenRC:
|
||||||
|
return "OpenRC — dependency-based init system";
|
||||||
|
case InitSystem::S6:
|
||||||
|
return "s6 — s6 supervision suite";
|
||||||
|
case InitSystem::Runit:
|
||||||
|
return "runit — supervision suite";
|
||||||
|
case InitSystem::Dinit:
|
||||||
|
return "dinit — service manager / init system";
|
||||||
|
case InitSystem::Unknown:
|
||||||
|
return "unknown init system";
|
||||||
|
}
|
||||||
|
return "unknown init system";
|
||||||
|
}
|
||||||
|
|
||||||
|
InitPaths init_paths(InitSystem is, std::string_view prefix) {
|
||||||
|
switch (is) {
|
||||||
|
case InitSystem::Systemd:
|
||||||
|
return {
|
||||||
|
.service_dir = (std::filesystem::path(prefix) / "etc/systemd/system").string(),
|
||||||
|
.enable_cmd = "systemctl enable",
|
||||||
|
.disable_cmd = "systemctl disable",
|
||||||
|
};
|
||||||
|
case InitSystem::OpenRC:
|
||||||
|
return {
|
||||||
|
.service_dir = (std::filesystem::path(prefix) / "etc/init.d").string(),
|
||||||
|
.enable_cmd = "rc-update add",
|
||||||
|
.disable_cmd = "rc-update del",
|
||||||
|
};
|
||||||
|
case InitSystem::S6:
|
||||||
|
return {
|
||||||
|
.service_dir = (std::filesystem::path(prefix) / "etc/s6/sv").string(),
|
||||||
|
.enable_cmd = "s6-rc-bundle-update",
|
||||||
|
.disable_cmd = "s6-rc-bundle-update delete",
|
||||||
|
};
|
||||||
|
case InitSystem::Runit:
|
||||||
|
return {
|
||||||
|
.service_dir = (std::filesystem::path(prefix) / "etc/sv").string(),
|
||||||
|
.enable_cmd = std::format("ln -sf {}/etc/sv/{{name}} {}/var/service/", prefix, prefix),
|
||||||
|
.disable_cmd = std::format("rm -f {}/var/service/{{name}}", prefix),
|
||||||
|
};
|
||||||
|
case InitSystem::Dinit:
|
||||||
|
return {
|
||||||
|
.service_dir = (std::filesystem::path(prefix) / "etc/dinit.d").string(),
|
||||||
|
.enable_cmd = "dinitctl enable",
|
||||||
|
.disable_cmd = "dinitctl disable",
|
||||||
|
};
|
||||||
|
case InitSystem::Unknown:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::service
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#include "kappa/system/activate.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace kappa::system {
|
||||||
|
|
||||||
|
ActivateResult write_hostname(const std::string& hostname,
|
||||||
|
std::string_view prefix) {
|
||||||
|
if (hostname.empty()) {
|
||||||
|
return {false, "hostname is empty"};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path path = std::filesystem::path(prefix) / "etc/hostname";
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(path.parent_path(), ec);
|
||||||
|
if (ec) {
|
||||||
|
return {false, std::format("cannot create {}: {}", path.parent_path().string(), ec.message())};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream out(path);
|
||||||
|
if (!out) {
|
||||||
|
return {false, std::format("cannot write {}", path.string())};
|
||||||
|
}
|
||||||
|
out << hostname << "\n";
|
||||||
|
return {true, {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
ActivateResult write_timezone(const std::string& timezone,
|
||||||
|
std::string_view prefix) {
|
||||||
|
if (timezone.empty()) {
|
||||||
|
return {false, "timezone is empty"};
|
||||||
|
}
|
||||||
|
|
||||||
|
// /etc/localtime is a symlink to zoneinfo data
|
||||||
|
std::filesystem::path localtime = std::filesystem::path(prefix) / "etc/localtime";
|
||||||
|
|
||||||
|
static constexpr std::array<std::string_view, 4> zoneinfo_dirs = {
|
||||||
|
"usr/share/zoneinfo", // glibc/FHS standard
|
||||||
|
"etc/zoneinfo", // NixOS
|
||||||
|
"share/zoneinfo", // Guix, some prefix installs
|
||||||
|
"usr/lib/zoneinfo", // alternative
|
||||||
|
};
|
||||||
|
|
||||||
|
std::filesystem::path zoneinfo;
|
||||||
|
for (auto dir : zoneinfo_dirs) {
|
||||||
|
auto candidate = std::filesystem::path(prefix) / dir / timezone;
|
||||||
|
if (std::filesystem::exists(candidate)) {
|
||||||
|
zoneinfo = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoneinfo.empty()) {
|
||||||
|
std::string tried;
|
||||||
|
for (size_t i = 0; i < zoneinfo_dirs.size(); ++i) {
|
||||||
|
if (i > 0) tried += ", ";
|
||||||
|
tried += (std::filesystem::path(prefix) / zoneinfo_dirs[i] / timezone).string();
|
||||||
|
}
|
||||||
|
return {false,
|
||||||
|
std::format("timezone data '{}' not found in any known location: tried {}",
|
||||||
|
timezone, tried)};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(localtime.parent_path(), ec);
|
||||||
|
// Remove existing symlink/file if present
|
||||||
|
std::filesystem::remove(localtime, ec);
|
||||||
|
std::filesystem::create_symlink(zoneinfo, localtime, ec);
|
||||||
|
if (ec) {
|
||||||
|
return {false, std::format("cannot create symlink {}: {}", localtime.string(), ec.message())};
|
||||||
|
}
|
||||||
|
return {true, {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::system
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#include "kappa/tools/doctor.hpp"
|
||||||
|
#include "kappa/boot/types.hpp"
|
||||||
|
#include "kappa/service/types.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
namespace kappa::tools {
|
||||||
|
|
||||||
|
std::vector<Diagnostic> check_package(const dsl::PackageDef& pkg) {
|
||||||
|
std::vector<Diagnostic> diags;
|
||||||
|
|
||||||
|
if (pkg.name.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Error, "package name is empty"});
|
||||||
|
}
|
||||||
|
if (pkg.version.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Error, "version is not set"});
|
||||||
|
}
|
||||||
|
if (pkg.source.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Error, "source URL is not set"});
|
||||||
|
}
|
||||||
|
if (pkg.license.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning, "license is not specified"});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& [key, feat] : pkg.features) {
|
||||||
|
if (feat.force && !feat.enabled) {
|
||||||
|
diags.push_back({DiagSeverity::Warning,
|
||||||
|
"feature '" + key + "' is forced but disabled"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& dep : pkg.depends) {
|
||||||
|
if (dep.name.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning,
|
||||||
|
"dependency has an empty name"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& c : pkg.conflicts) {
|
||||||
|
if (c.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning,
|
||||||
|
"conflict entry has an empty name"});
|
||||||
|
}
|
||||||
|
if (c == pkg.name) {
|
||||||
|
diags.push_back({DiagSeverity::Error,
|
||||||
|
"package conflicts with itself: '" + c + "'"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pkg.config_files.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning,
|
||||||
|
"no config files defined — package has no runtime configuration"});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& cf : pkg.config_files) {
|
||||||
|
if (cf.entries.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning,
|
||||||
|
"config file '" + cf.path + "' has no entries"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pkg.assertions.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning,
|
||||||
|
std::to_string(pkg.assertions.size())
|
||||||
|
+ " assertions defined — run 'kappa validate' to check them"});
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Diagnostic> check_config(const dsl::SystemConfig& cfg) {
|
||||||
|
std::vector<Diagnostic> diags;
|
||||||
|
|
||||||
|
if (cfg.system.hostname.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning, "system hostname is not set"});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.boot.kernel.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning, "boot kernel is not specified"});
|
||||||
|
}
|
||||||
|
if (cfg.boot.init.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning, "boot init system is not specified"});
|
||||||
|
}
|
||||||
|
if (!cfg.boot.init.empty()) {
|
||||||
|
auto is = kappa::service::parse_init_system(cfg.boot.init);
|
||||||
|
if (is == kappa::service::InitSystem::Unknown) {
|
||||||
|
diags.push_back({DiagSeverity::Warning,
|
||||||
|
"boot.init '" + cfg.boot.init + "' is not a recognized init system — supported: systemd, openrc, s6, runit, dinit"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cfg.boot.init.empty() && !cfg.services.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning,
|
||||||
|
std::to_string(cfg.services.size()) + " service(s) defined but no init system configured — set boot.init"});
|
||||||
|
}
|
||||||
|
if (cfg.boot.bootloader.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning, "boot bootloader is not specified"});
|
||||||
|
} else {
|
||||||
|
auto bl = kappa::boot::parse_bootloader(cfg.boot.bootloader);
|
||||||
|
if (bl == kappa::boot::Bootloader::Unknown) {
|
||||||
|
diags.push_back({DiagSeverity::Warning,
|
||||||
|
"boot.bootloader '" + cfg.boot.bootloader + "' is not a recognized bootloader — supported: grub, limine"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Validate boot partitions
|
||||||
|
if (cfg.boot.efi.empty() && cfg.boot.root.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Error, "no boot partitions defined (efi or root)"});
|
||||||
|
} else {
|
||||||
|
// Check that each specified partition exists
|
||||||
|
auto check_partition = [&](const std::string& path, const char* label) {
|
||||||
|
if (path.empty()) return;
|
||||||
|
std::error_code ec;
|
||||||
|
if (!std::filesystem::exists(path, ec)) {
|
||||||
|
diags.push_back({DiagSeverity::Error,
|
||||||
|
std::string("boot.") + label + " '" + path + "' does not exist"});
|
||||||
|
} else {
|
||||||
|
struct stat st;
|
||||||
|
if (stat(path.c_str(), &st) == 0 && !S_ISBLK(st.st_mode)) {
|
||||||
|
diags.push_back({DiagSeverity::Warning,
|
||||||
|
std::string("boot.") + label + " '" + path + "' is not a block device"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
check_partition(cfg.boot.efi, "efi");
|
||||||
|
check_partition(cfg.boot.root, "root");
|
||||||
|
check_partition(cfg.boot.swap, "swap");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.packages.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning, "no packages declared — nothing will be installed"});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& svc : cfg.services) {
|
||||||
|
if (svc.enable && svc.name.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning, "enabled service has no name"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.users.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning, "no users defined — root account has no shell"});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& u : cfg.users) {
|
||||||
|
if (u.name == "root" && u.shell.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Error, "root user has no shell set"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.system.rollback.keep < 0) {
|
||||||
|
diags.push_back({DiagSeverity::Error, "rollback keep is negative"});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cfg.assertions.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning,
|
||||||
|
std::to_string(cfg.assertions.size())
|
||||||
|
+ " assertions defined — run 'kappa validate' to check them"});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& g : cfg.groups) {
|
||||||
|
if (g.name.empty()) {
|
||||||
|
diags.push_back({DiagSeverity::Warning, "group has no name"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::tools
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
#include "kappa/tools/format.hpp"
|
||||||
|
|
||||||
|
namespace kappa::tools {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Indent {
|
||||||
|
int n = 0;
|
||||||
|
Indent(int i) : n(i) {}
|
||||||
|
friend std::ostream& operator<<(std::ostream& os, const Indent& in) {
|
||||||
|
for (int i = 0; i < in.n; ++i) { os << " "; }
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static void write_env(std::ostream& os, int d,
|
||||||
|
const std::vector<dsl::EnvEntry>& entries) {
|
||||||
|
if (entries.empty()) { return; }
|
||||||
|
os << Indent(d) << "env {\n";
|
||||||
|
for (auto& e : entries) {
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
os << Indent(d) << "}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_features(std::ostream& os, int d,
|
||||||
|
const std::unordered_map<std::string, dsl::FeatureDef>& feats) {
|
||||||
|
if (feats.empty()) { return; }
|
||||||
|
std::vector<std::string> keys;
|
||||||
|
for (auto& [k, _] : feats) keys.push_back(k);
|
||||||
|
std::sort(keys.begin(), keys.end());
|
||||||
|
os << Indent(d) << "features {\n";
|
||||||
|
for (auto& k : keys) {
|
||||||
|
auto& f = feats.at(k);
|
||||||
|
if (f.flag.empty() && !f.force) {
|
||||||
|
os << Indent(d + 1) << k << " = " << (f.enabled ? "true" : "false") << "\n";
|
||||||
|
} else {
|
||||||
|
os << Indent(d + 1) << k << " = { enabled = "
|
||||||
|
<< (f.enabled ? "true" : "false");
|
||||||
|
if (f.force) { os << ", force = true"; }
|
||||||
|
if (!f.flag.empty()) { os << ", flag = \"" << f.flag << "\""; }
|
||||||
|
os << " }\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os << Indent(d) << "}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_phase(std::ostream& os, int d, const char* name,
|
||||||
|
const dsl::Phase& phase) {
|
||||||
|
if (phase.commands.empty()) { return; }
|
||||||
|
os << Indent(d) << name << " {\n";
|
||||||
|
for (auto& cmd : phase.commands) {
|
||||||
|
os << Indent(d + 1) << cmd << "\n";
|
||||||
|
}
|
||||||
|
os << Indent(d) << "}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_boot_block(std::ostream& os, int d, const dsl::BootBlock& boot) {
|
||||||
|
os << Indent(d) << "boot {\n";
|
||||||
|
auto w = [&](const char* k, const std::string& v) {
|
||||||
|
if (!v.empty()) { os << Indent(d + 1) << k << " = \"" << v << "\"\n"; }
|
||||||
|
};
|
||||||
|
w("kernel", boot.kernel);
|
||||||
|
w("init", boot.init);
|
||||||
|
w("efi", boot.efi);
|
||||||
|
w("swap", boot.swap);
|
||||||
|
w("root", boot.root);
|
||||||
|
w("bootloader", boot.bootloader);
|
||||||
|
for (auto& [k, v] : boot.params) {
|
||||||
|
os << Indent(d + 1) << k << " = \"" << v << "\"\n";
|
||||||
|
}
|
||||||
|
os << Indent(d) << "}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void format_package(std::ostream& os, const dsl::PackageDef& pkg) {
|
||||||
|
os << "package \"" << pkg.name << "\" {\n";
|
||||||
|
int d = 1;
|
||||||
|
|
||||||
|
if (!pkg.version.empty()) {
|
||||||
|
auto c = pkg.const_keys.contains("version") ? "const " : "";
|
||||||
|
os << Indent(d) << c << "version = \"" << pkg.version << "\"\n";
|
||||||
|
}
|
||||||
|
if (!pkg.source.empty()) {
|
||||||
|
auto c = pkg.const_keys.contains("source") ? "const " : "";
|
||||||
|
os << Indent(d) << c << "source = \"" << pkg.source << "\"\n";
|
||||||
|
}
|
||||||
|
if (!pkg.license.empty()) {
|
||||||
|
os << Indent(d) << "license = \"" << pkg.license << "\"\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pkg.provides.empty()) {
|
||||||
|
os << Indent(d) << "provides = [";
|
||||||
|
for (std::size_t i = 0; i < pkg.provides.size(); ++i) {
|
||||||
|
if (i > 0) { os << ", "; }
|
||||||
|
os << '"' << pkg.provides[i] << '"';
|
||||||
|
}
|
||||||
|
os << "]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pkg.outputs.empty()) {
|
||||||
|
os << Indent(d) << "outputs = [";
|
||||||
|
for (std::size_t i = 0; i < pkg.outputs.size(); ++i) {
|
||||||
|
if (i > 0) { os << ", "; }
|
||||||
|
os << '"' << pkg.outputs[i] << '"';
|
||||||
|
}
|
||||||
|
os << "]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pkg.conflicts.empty()) {
|
||||||
|
os << Indent(d) << "conflicts = [";
|
||||||
|
for (std::size_t i = 0; i < pkg.conflicts.size(); ++i) {
|
||||||
|
if (i > 0) { os << ", "; }
|
||||||
|
os << '"' << pkg.conflicts[i] << '"';
|
||||||
|
}
|
||||||
|
os << "]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pkg.patches.empty()) {
|
||||||
|
os << Indent(d) << "patches = [\n";
|
||||||
|
for (auto& p : pkg.patches) {
|
||||||
|
if (p.sha256.empty()) {
|
||||||
|
os << Indent(d + 1) << '"' << p.url << "\",\n";
|
||||||
|
} else {
|
||||||
|
os << Indent(d + 1) << "{ url = \"" << p.url
|
||||||
|
<< "\", sha256 = \"" << p.sha256
|
||||||
|
<< "\", level = " << p.level << " },\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os << Indent(d) << "]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pkg.depends.empty()) {
|
||||||
|
os << Indent(d) << "depends = [\n";
|
||||||
|
for (auto& dep : pkg.depends) {
|
||||||
|
os << Indent(d + 1);
|
||||||
|
if (dep.version.empty() && dep.output.empty() && dep.feature.empty()) {
|
||||||
|
os << '"' << dep.name << '"';
|
||||||
|
} else {
|
||||||
|
os << "{ name = \"" << dep.name << '"';
|
||||||
|
if (!dep.version.empty()) { os << ", version = \"" << dep.version << '"'; }
|
||||||
|
if (!dep.output.empty()) { os << ", output = \"" << dep.output << '"'; }
|
||||||
|
if (!dep.feature.empty()) { os << ", feature = \"" << dep.feature << '"'; }
|
||||||
|
os << " }";
|
||||||
|
}
|
||||||
|
os << ",\n";
|
||||||
|
}
|
||||||
|
os << Indent(d) << "]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
write_features(os, d, pkg.features);
|
||||||
|
write_env(os, d, pkg.env_entries);
|
||||||
|
|
||||||
|
for (auto& cf : pkg.config_files) {
|
||||||
|
os << Indent(d) << "config {\n";
|
||||||
|
os << Indent(d + 1) << "file \"" << cf.path
|
||||||
|
<< "\" mode = \"" << cf.mode << "\" {\n";
|
||||||
|
for (auto& [k, v] : cf.entries) {
|
||||||
|
os << Indent(d + 2) << k << " = " << v << "\n";
|
||||||
|
}
|
||||||
|
os << Indent(d + 1) << "}\n";
|
||||||
|
os << Indent(d) << "}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pkg.services.empty()) {
|
||||||
|
for (auto& ns : pkg.services) {
|
||||||
|
if (ns.name != "main") {
|
||||||
|
os << Indent(d) << "service " << ns.name << " {\n";
|
||||||
|
} else {
|
||||||
|
os << Indent(d) << "service {\n";
|
||||||
|
}
|
||||||
|
if (!ns.exec.empty()) { os << Indent(d + 1) << "exec = \"" << ns.exec << "\"\n"; }
|
||||||
|
if (!ns.type.empty()) { os << Indent(d + 1) << "type = \"" << ns.type << "\"\n"; }
|
||||||
|
if (!ns.user.empty()) { os << Indent(d + 1) << "user = \"" << ns.user << "\"\n"; }
|
||||||
|
if (!ns.ports.empty()) {
|
||||||
|
os << Indent(d + 1) << "ports = [";
|
||||||
|
for (std::size_t i = 0; i < ns.ports.size(); ++i) {
|
||||||
|
if (i > 0) { os << ", "; }
|
||||||
|
os << ns.ports[i];
|
||||||
|
}
|
||||||
|
os << "]\n";
|
||||||
|
}
|
||||||
|
if (!ns.description.empty()) { os << Indent(d + 1) << "description = \"" << ns.description << "\"\n"; }
|
||||||
|
if (!ns.after.empty()) { os << Indent(d + 1) << "after = \"" << ns.after << "\"\n"; }
|
||||||
|
if (!ns.restart.empty()) { os << Indent(d + 1) << "restart = \"" << ns.restart << "\"\n"; }
|
||||||
|
if (!ns.working_dir.empty()) { os << Indent(d + 1) << "working_dir = \"" << ns.working_dir << "\"\n"; }
|
||||||
|
for (auto& [k, v] : ns.env) {
|
||||||
|
os << Indent(d + 1) << k << " = \"" << v << "\"\n";
|
||||||
|
}
|
||||||
|
os << Indent(d) << "}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pkg.assertions.empty()) {
|
||||||
|
os << Indent(d) << "assert {\n";
|
||||||
|
for (auto& a : pkg.assertions) {
|
||||||
|
os << Indent(d + 1) << '"' << a.message << "\" : "
|
||||||
|
<< a.field << " " << a.op;
|
||||||
|
if (!a.value.empty()) { os << " \"" << a.value << '"'; }
|
||||||
|
os << "\n";
|
||||||
|
}
|
||||||
|
os << Indent(d) << "}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
write_phase(os, d, "prepare", pkg.prepare);
|
||||||
|
write_phase(os, d, "build", pkg.build);
|
||||||
|
write_phase(os, d, "check", pkg.check);
|
||||||
|
write_phase(os, d, "install", pkg.install);
|
||||||
|
write_phase(os, d, "uninstall", pkg.uninstall);
|
||||||
|
|
||||||
|
os << "}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void format_config(std::ostream& os, const dsl::SystemConfig& cfg) {
|
||||||
|
if (!cfg.imports.empty()) {
|
||||||
|
os << "imports = [";
|
||||||
|
for (std::size_t i = 0; i < cfg.imports.size(); ++i) {
|
||||||
|
if (i > 0) { os << ", "; }
|
||||||
|
os << '"' << cfg.imports[i] << '"';
|
||||||
|
}
|
||||||
|
os << "]\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cfg.remotes.empty()) {
|
||||||
|
os << "remotes = [\n";
|
||||||
|
for (auto& r : cfg.remotes) {
|
||||||
|
os << " \"" << r << "\",\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()) {
|
||||||
|
os << "assert {\n";
|
||||||
|
for (auto& a : cfg.assertions) {
|
||||||
|
os << " \"" << a.message << "\" : "
|
||||||
|
<< a.field << " " << a.op
|
||||||
|
<< " \"" << a.value << '"' << "\n";
|
||||||
|
}
|
||||||
|
os << "}\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& s = cfg.system;
|
||||||
|
os << "system {\n";
|
||||||
|
if (!s.hostname.empty()) { os << " hostname = \"" << s.hostname << "\"\n"; }
|
||||||
|
if (!s.timezone.empty()) { os << " timezone = \"" << s.timezone << "\"\n"; }
|
||||||
|
write_features(os, 1, s.features);
|
||||||
|
write_env(os, 1, s.env);
|
||||||
|
if (!s.config.empty()) {
|
||||||
|
os << " config {\n";
|
||||||
|
std::vector<std::string> cfg_keys;
|
||||||
|
for (auto& [k, _] : s.config) cfg_keys.push_back(k);
|
||||||
|
std::sort(cfg_keys.begin(), cfg_keys.end());
|
||||||
|
for (auto& k : cfg_keys) {
|
||||||
|
os << " " << k << " = \"" << s.config.at(k) << "\"\n";
|
||||||
|
}
|
||||||
|
os << " }\n";
|
||||||
|
}
|
||||||
|
if (s.rollback.keep > 0) {
|
||||||
|
os << " rollback {\n keep = " << s.rollback.keep << "\n }\n";
|
||||||
|
}
|
||||||
|
os << "}\n\n";
|
||||||
|
|
||||||
|
if (!cfg.packages.empty()) {
|
||||||
|
os << "packages {\n";
|
||||||
|
for (auto& p : cfg.packages) {
|
||||||
|
os << " " << p.name;
|
||||||
|
if (p.version.empty() && p.features.empty() && p.config.empty()) {
|
||||||
|
os << " {}\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
os << " {\n";
|
||||||
|
if (!p.version.empty()) { os << " version = \"" << p.version << "\"\n"; }
|
||||||
|
write_features(os, 2, p.features);
|
||||||
|
if (!p.config.empty()) {
|
||||||
|
os << " config {\n";
|
||||||
|
std::vector<std::string> pcfg_keys;
|
||||||
|
for (auto& [k, _] : p.config) pcfg_keys.push_back(k);
|
||||||
|
std::sort(pcfg_keys.begin(), pcfg_keys.end());
|
||||||
|
for (auto& k : pcfg_keys) {
|
||||||
|
os << " " << k << " = " << p.config.at(k) << "\n";
|
||||||
|
}
|
||||||
|
os << " }\n";
|
||||||
|
}
|
||||||
|
os << " }\n";
|
||||||
|
}
|
||||||
|
os << "}\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cfg.services.empty()) {
|
||||||
|
os << "services {\n";
|
||||||
|
for (auto& svc : cfg.services) {
|
||||||
|
os << " " << svc.name << " {\n";
|
||||||
|
os << " enable = " << (svc.enable ? "true" : "false") << "\n";
|
||||||
|
for (auto& [k, v] : svc.config) {
|
||||||
|
os << " " << k << " = " << v << "\n";
|
||||||
|
}
|
||||||
|
os << " }\n";
|
||||||
|
}
|
||||||
|
os << "}\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cfg.groups.empty()) {
|
||||||
|
os << "groups {\n";
|
||||||
|
for (auto& g : cfg.groups) {
|
||||||
|
os << " " << g.name;
|
||||||
|
if (g.gid < 0) {
|
||||||
|
os << " {}\n";
|
||||||
|
} else {
|
||||||
|
os << " {\n gid = " << g.gid << "\n }\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os << "}\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
write_boot_block(os, 0, cfg.boot);
|
||||||
|
os << "\n";
|
||||||
|
|
||||||
|
if (!cfg.users.empty()) {
|
||||||
|
os << "users {\n";
|
||||||
|
for (auto& u : cfg.users) {
|
||||||
|
os << " " << u.name << " {\n";
|
||||||
|
if (!u.shell.empty()) { os << " shell = \"" << u.shell << "\"\n"; }
|
||||||
|
if (!u.groups.empty()) {
|
||||||
|
os << " groups = [";
|
||||||
|
for (std::size_t i = 0; i < u.groups.size(); ++i) {
|
||||||
|
if (i > 0) { os << ", "; }
|
||||||
|
os << '"' << u.groups[i] << '"';
|
||||||
|
}
|
||||||
|
os << "]\n";
|
||||||
|
}
|
||||||
|
for (auto& [k, v] : u.extra) {
|
||||||
|
os << " " << k << " = \"" << v << "\"\n";
|
||||||
|
}
|
||||||
|
os << " }\n";
|
||||||
|
}
|
||||||
|
os << "}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#include "kappa/util.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
namespace kappa::util {
|
||||||
|
|
||||||
|
std::string to_lower(std::string_view sv) {
|
||||||
|
std::string s(sv);
|
||||||
|
std::ranges::transform(s, s.begin(),
|
||||||
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string shell_escape(std::string_view s) {
|
||||||
|
std::string result;
|
||||||
|
result.reserve(s.size());
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '"' || c == '\\' || c == '$' || c == '`') {
|
||||||
|
result += '\\';
|
||||||
|
}
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+1201
File diff suppressed because it is too large
Load Diff
Executable
+350
@@ -0,0 +1,350 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# kappa init-switching integration test via systemd-nspawn
|
||||||
|
# Requires: systemd-nspawn, debootstrap (or pacstrap), kappa binary
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
KAPPA_BIN="${KAPPA_BIN:-./build/kappa}"
|
||||||
|
ROOTFS="./test-rootfs"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo ""
|
||||||
|
echo "Cleaning up..."
|
||||||
|
rm -rf "$ROOTFS" /tmp/kappa-test-*.kap 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local desc="$1" cmd="$2" expect="$3"
|
||||||
|
echo -n " $desc ... "
|
||||||
|
local out
|
||||||
|
out=$(eval "$cmd" 2>&1) || true
|
||||||
|
if echo "$out" | grep -q "$expect"; then
|
||||||
|
echo -e "${GREEN}OK${NC}"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo -e "${RED}FAIL${NC}"
|
||||||
|
echo " expected: $expect"
|
||||||
|
echo " got: $(echo "$out" | head -3 | tr '\n' ' ')"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo " kappa init-switching integration tests"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 1. Create a minimal rootfs directory structure
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
echo "--- Setting up test rootfs ---"
|
||||||
|
|
||||||
|
mkdir -p "$ROOTFS"/{etc,usr/local/bin,kappa/{boot,db,store,temp},var/service}
|
||||||
|
cp "$KAPPA_BIN" "$ROOTFS/usr/local/bin/kappa"
|
||||||
|
chmod +x "$ROOTFS/usr/local/bin/kappa"
|
||||||
|
|
||||||
|
export KAPPA_ROOT="$ROOTFS/kappa"
|
||||||
|
|
||||||
|
# Create a minimal package registry (simulating installed packages)
|
||||||
|
mkdir -p "$KAPPA_ROOT/db"
|
||||||
|
|
||||||
|
# Simulate an installed system with nginx (has services), zlib (no services),
|
||||||
|
# and postgresql (multi-service, uses ${enabledinit})
|
||||||
|
cat > /tmp/kappa-test-nginx.kap << 'EOF'
|
||||||
|
package "nginx" {
|
||||||
|
version = "1.24"
|
||||||
|
source = "https://nginx.org/nginx-1.24.tar.gz"
|
||||||
|
service {
|
||||||
|
exec = "/usr/bin/nginx"
|
||||||
|
type = "forking"
|
||||||
|
ports = [80, 443]
|
||||||
|
user = "www-data"
|
||||||
|
description = "Nginx web server"
|
||||||
|
}
|
||||||
|
build { ./configure --prefix=${prefix}; make -j${jobs} }
|
||||||
|
install { make DESTDIR=${destdir} install }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > /tmp/kappa-test-zlib.kap << 'EOF'
|
||||||
|
package "zlib" {
|
||||||
|
version = "1.3"
|
||||||
|
source = "https://zlib.net/zlib-1.3.tar.gz"
|
||||||
|
build { ./configure --prefix=${prefix}; make -j${jobs} }
|
||||||
|
install { make DESTDIR=${destdir} install }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > /tmp/kappa-test-postgres.kap << 'EOF'
|
||||||
|
package "postgresql" {
|
||||||
|
version = "16.3"
|
||||||
|
source = "https://ftp.postgresql.org/source/postgresql-16.3.tar.gz"
|
||||||
|
service main {
|
||||||
|
exec = "/usr/bin/postgres"
|
||||||
|
type = "forking"
|
||||||
|
ports = [5432]
|
||||||
|
user = "postgres"
|
||||||
|
description = "PostgreSQL database server"
|
||||||
|
}
|
||||||
|
service checkpointer {
|
||||||
|
exec = "/usr/bin/postgres-checkpointer"
|
||||||
|
type = "longrun"
|
||||||
|
user = "postgres"
|
||||||
|
}
|
||||||
|
build {
|
||||||
|
case ${enabledinit} in
|
||||||
|
systemd) ./configure --with-systemd --prefix=${prefix} ;;
|
||||||
|
*) ./configure --prefix=${prefix} ;;
|
||||||
|
esac
|
||||||
|
make -j${jobs}
|
||||||
|
}
|
||||||
|
install { make DESTDIR=${destdir} install }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Write installed DB (simulate nginx, zlib, postgresql already installed)
|
||||||
|
# Format: name version hash [provides...]
|
||||||
|
mkdir -p "$(dirname "$KAPPA_ROOT/db/installed")"
|
||||||
|
cat > "$KAPPA_ROOT/db/installed" << 'EOF'
|
||||||
|
nginx 1.24 a1b2c3d4e5f6a7b8
|
||||||
|
zlib 1.3 b2c3d4e5f6a7b8c9
|
||||||
|
postgresql 16.3 c3d4e5f6a7b8c9d0
|
||||||
|
init a1b2c3d4e5f6a7b8
|
||||||
|
kernel d4e5f6a7b8c9d0e1
|
||||||
|
bootloader e5f6a7b8c9d0e1f2
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 2. Init system switching tests
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
echo "--- Init system switching ---"
|
||||||
|
|
||||||
|
# Base config template
|
||||||
|
config_template() {
|
||||||
|
local init="$1"
|
||||||
|
cat << EOF
|
||||||
|
system { hostname = "kappa-test" }
|
||||||
|
packages {
|
||||||
|
nginx {}
|
||||||
|
zlib {}
|
||||||
|
postgresql {}
|
||||||
|
$init {}
|
||||||
|
}
|
||||||
|
services {
|
||||||
|
nginx { enable = true }
|
||||||
|
postgresql { enable = true }
|
||||||
|
postgresql.checkpointer { enable = true }
|
||||||
|
}
|
||||||
|
boot {
|
||||||
|
kernel = "linux"
|
||||||
|
init = "$init"
|
||||||
|
root = "/dev/sda1"
|
||||||
|
bootloader = "limine"
|
||||||
|
}
|
||||||
|
groups {
|
||||||
|
wheel { gid = 998 }
|
||||||
|
}
|
||||||
|
users {
|
||||||
|
root { shell = "/bin/zsh" }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test systemd config
|
||||||
|
config_template "systemd" > /tmp/kappa-test-systemd.kap
|
||||||
|
check "parse systemd config" \
|
||||||
|
"$KAPPA_BIN parse-config /tmp/kappa-test-systemd.kap" \
|
||||||
|
'systemd — system and service manager'
|
||||||
|
|
||||||
|
check "systemd bootloader in config" \
|
||||||
|
"$KAPPA_BIN parse-config /tmp/kappa-test-systemd.kap" \
|
||||||
|
'Limine'
|
||||||
|
|
||||||
|
# Test openrc config
|
||||||
|
config_template "openrc" > /tmp/kappa-test-openrc.kap
|
||||||
|
check "parse openrc config" \
|
||||||
|
"$KAPPA_BIN parse-config /tmp/kappa-test-openrc.kap" \
|
||||||
|
'OpenRC — dependency-based init'
|
||||||
|
|
||||||
|
# Test s6 config
|
||||||
|
config_template "s6" > /tmp/kappa-test-s6.kap
|
||||||
|
check "parse s6 config" \
|
||||||
|
"$KAPPA_BIN parse-config /tmp/kappa-test-s6.kap" \
|
||||||
|
's6 — s6 supervision suite'
|
||||||
|
|
||||||
|
# Test runit config
|
||||||
|
config_template "runit" > /tmp/kappa-test-runit.kap
|
||||||
|
check "parse runit config" \
|
||||||
|
"$KAPPA_BIN parse-config /tmp/kappa-test-runit.kap" \
|
||||||
|
'runit — supervision suite'
|
||||||
|
|
||||||
|
# Test dinit config
|
||||||
|
config_template "dinit" > /tmp/kappa-test-dinit.kap
|
||||||
|
check "parse dinit config" \
|
||||||
|
"$KAPPA_BIN parse-config /tmp/kappa-test-dinit.kap" \
|
||||||
|
'dinit — service manager'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 3. Service generation verification
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
echo "--- Service file generation ---"
|
||||||
|
|
||||||
|
# Verify service specs are parsed correctly per init
|
||||||
|
for init in systemd openrc s6 runit dinit; do
|
||||||
|
check "service block present for $init config" \
|
||||||
|
"$KAPPA_BIN format /tmp/kappa-test-$init.kap" \
|
||||||
|
'enable = true'
|
||||||
|
done
|
||||||
|
|
||||||
|
# Verify the format output contains correct service fields
|
||||||
|
check "nginx service has type forking" \
|
||||||
|
"$KAPPA_BIN format /tmp/kappa-test-nginx.kap" \
|
||||||
|
'type = "forking"'
|
||||||
|
|
||||||
|
check "postgresql has named service checkpointer" \
|
||||||
|
"$KAPPA_BIN format /tmp/kappa-test-postgres.kap" \
|
||||||
|
'service checkpointer'
|
||||||
|
|
||||||
|
check "postgresql build uses enabledinit" \
|
||||||
|
"$KAPPA_BIN format /tmp/kappa-test-postgres.kap" \
|
||||||
|
'enabledinit'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 4. Rebuild impact analysis
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
echo "--- Rebuild impact analysis ---"
|
||||||
|
|
||||||
|
# Write an initial installed state matching s6 config
|
||||||
|
# (reuse the same installed DB from above)
|
||||||
|
|
||||||
|
config_template "s6" > /tmp/kappa-test-rebuild-s6.kap
|
||||||
|
# The packages list includes "s6" which isn't in the installed DB,
|
||||||
|
# so rebuild correctly shows it as a new package to build.
|
||||||
|
check "rebuild detects new init package" \
|
||||||
|
"$KAPPA_BIN rebuild --dry-run /tmp/kappa-test-rebuild-s6.kap 2>&1" \
|
||||||
|
'packages to rebuild'
|
||||||
|
|
||||||
|
# Now test switching FROM s6 TO systemd
|
||||||
|
config_template "systemd" > /tmp/kappa-test-rebuild-systemd.kap
|
||||||
|
|
||||||
|
# The rebuild should detect init_changed
|
||||||
|
# Note: this requires the installed DB to have the old init hash
|
||||||
|
check "rebuild detects init change" \
|
||||||
|
"$KAPPA_BIN rebuild --dry-run /tmp/kappa-test-rebuild-systemd.kap 2>&1" \
|
||||||
|
'packages to rebuild'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 5. Bootloader switching tests
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
echo "--- Bootloader switching ---"
|
||||||
|
|
||||||
|
for bl in grub limine; do
|
||||||
|
cat > "/tmp/kappa-test-bl-$bl.kap" << KAPEOF
|
||||||
|
system { hostname = "test" }
|
||||||
|
packages { nginx {} }
|
||||||
|
services {}
|
||||||
|
boot {
|
||||||
|
kernel = "linux"
|
||||||
|
init = "s6"
|
||||||
|
root = "/dev/sda1"
|
||||||
|
bootloader = "$bl"
|
||||||
|
}
|
||||||
|
users { root { shell = "/bin/sh" } }
|
||||||
|
KAPEOF
|
||||||
|
check "bootloader $bl recognized in config" \
|
||||||
|
"$KAPPA_BIN parse-config /tmp/kappa-test-bl-$bl.kap" \
|
||||||
|
"$bl"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test unknown bootloader
|
||||||
|
cat > /tmp/kappa-test-badbl.kap << 'KAPEOF'
|
||||||
|
system { hostname = "test" }
|
||||||
|
packages {}
|
||||||
|
services {}
|
||||||
|
boot {
|
||||||
|
kernel = "linux"
|
||||||
|
init = "s6"
|
||||||
|
root = "/dev/sda1"
|
||||||
|
bootloader = "lilo"
|
||||||
|
}
|
||||||
|
users { root { shell = "/bin/sh" } }
|
||||||
|
KAPEOF
|
||||||
|
check "doctor warns on unknown bootloader" \
|
||||||
|
"$KAPPA_BIN doctor /tmp/kappa-test-badbl.kap 2>&1" \
|
||||||
|
'not a recognized bootloader'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 6. Groups and conflicts
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
echo "--- Groups and conflicts ---"
|
||||||
|
|
||||||
|
cat > /tmp/kappa-test-groups2.kap << 'KAPEOF'
|
||||||
|
system { hostname = "test" }
|
||||||
|
packages {}
|
||||||
|
services {}
|
||||||
|
boot {
|
||||||
|
kernel = "linux"
|
||||||
|
init = "s6"
|
||||||
|
root = "/dev/sda1"
|
||||||
|
bootloader = "limine"
|
||||||
|
}
|
||||||
|
groups {
|
||||||
|
wheel { gid = 998 }
|
||||||
|
audio {}
|
||||||
|
docker { gid = 995 }
|
||||||
|
}
|
||||||
|
users { root { shell = "/bin/sh" } }
|
||||||
|
KAPEOF
|
||||||
|
|
||||||
|
check "groups with gid format correctly" \
|
||||||
|
"$KAPPA_BIN format /tmp/kappa-test-groups2.kap" \
|
||||||
|
'gid = 998'
|
||||||
|
|
||||||
|
check "groups without gid format as empty" \
|
||||||
|
"$KAPPA_BIN format /tmp/kappa-test-groups2.kap" \
|
||||||
|
'audio {}'
|
||||||
|
|
||||||
|
# Conflicts test
|
||||||
|
cat > /tmp/kappa-test-conflict-pkg.kap << 'KAPEOF'
|
||||||
|
package "systemd" {
|
||||||
|
version = "255"
|
||||||
|
source = "https://example.com/systemd.tar.gz"
|
||||||
|
provides = ["udev", "logind"]
|
||||||
|
conflicts = ["eudev", "elogind"]
|
||||||
|
build { make }
|
||||||
|
install { make install }
|
||||||
|
}
|
||||||
|
KAPEOF
|
||||||
|
|
||||||
|
check "conflicts parse correctly" \
|
||||||
|
"$KAPPA_BIN parse-package /tmp/kappa-test-conflict-pkg.kap" \
|
||||||
|
'valid'
|
||||||
|
|
||||||
|
check "conflicts format round-trips" \
|
||||||
|
"$KAPPA_BIN format /tmp/kappa-test-conflict-pkg.kap" \
|
||||||
|
'conflicts = \['
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo -e " Results: ${GREEN}$PASS passed${NC}, ${RED}$FAIL failed${NC}"
|
||||||
|
echo "============================================"
|
||||||
|
|
||||||
|
[ "$FAIL" -eq 0 ] || exit 1
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# $KAPPA integration test suite
|
||||||
|
# Run: docker run --rm -v $(pwd):/opt/$KAPPA kappa-test ./test.sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
KAPPA="${KAPPA_BIN:-./build/kappa}"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local desc="$1" cmd="$2" expect="$3"
|
||||||
|
local out
|
||||||
|
out=$(eval "$cmd" 2>&1) || true
|
||||||
|
if echo "$out" | grep -q "$expect"; then
|
||||||
|
echo " ✓ $desc"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo " ✗ $desc"
|
||||||
|
echo " expected: $expect"
|
||||||
|
echo " got: $(echo "$out" | head -3)"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Package DSL tests ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check "parse foo.kap" \
|
||||||
|
"$KAPPA parse-package examples/foo.kap" \
|
||||||
|
'package "foo" 1.2.3 — valid'
|
||||||
|
|
||||||
|
check "parse postgres.kap (multi-service)" \
|
||||||
|
"$KAPPA parse-package examples/postgres.kap" \
|
||||||
|
'package "postgresql" 16.3 — valid'
|
||||||
|
|
||||||
|
check "format foo.kap round-trips service block" \
|
||||||
|
"$KAPPA format examples/foo.kap" \
|
||||||
|
'service {'
|
||||||
|
|
||||||
|
check "format postgres.kap shows named services" \
|
||||||
|
"$KAPPA format examples/postgres.kap" \
|
||||||
|
'service checkpointer {'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Config DSL tests ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check "parse config.kap" \
|
||||||
|
"$KAPPA parse-config examples/config.kap" \
|
||||||
|
'system config — valid'
|
||||||
|
|
||||||
|
check "config.kap shows init description" \
|
||||||
|
"$KAPPA parse-config examples/config.kap" \
|
||||||
|
's6 — s6 supervision suite'
|
||||||
|
|
||||||
|
check "config.kap shows bootloader description" \
|
||||||
|
"$KAPPA parse-config examples/config.kap" \
|
||||||
|
'Limine — modern multiprotocol bootloader'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Doctor tests ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check "doctor foo.kap" \
|
||||||
|
"$KAPPA doctor examples/foo.kap" \
|
||||||
|
'no issues found'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Validate tests ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check "validate foo.kap" \
|
||||||
|
"$KAPPA validate examples/foo.kap" \
|
||||||
|
'valid package'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Rebuild tests ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check "rebuild detects new packages" \
|
||||||
|
"$KAPPA rebuild examples/config.kap" \
|
||||||
|
'packages to rebuild'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Init system recognition tests ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create temp config with each init system
|
||||||
|
for init in systemd openrc s6 runit dinit; do
|
||||||
|
cat > /tmp/test_init.kap << KAPEOF
|
||||||
|
system { hostname = "test" }
|
||||||
|
packages {}
|
||||||
|
services {}
|
||||||
|
boot {
|
||||||
|
kernel = "linux"
|
||||||
|
init = "$init"
|
||||||
|
root = "/dev/sda1"
|
||||||
|
bootloader = "limine"
|
||||||
|
}
|
||||||
|
users { root { shell = "/bin/sh" } }
|
||||||
|
KAPEOF
|
||||||
|
check "recognizes init=$init" \
|
||||||
|
"$KAPPA parse-config /tmp/test_init.kap" \
|
||||||
|
"$init"
|
||||||
|
|
||||||
|
check "doctor accepts init=$init" \
|
||||||
|
"$KAPPA doctor /tmp/test_init.kap 2>&1" \
|
||||||
|
""
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test unknown init
|
||||||
|
cat > /tmp/test_badinit.kap << 'KAPEOF'
|
||||||
|
system { hostname = "test" }
|
||||||
|
packages {}
|
||||||
|
services {}
|
||||||
|
boot {
|
||||||
|
kernel = "linux"
|
||||||
|
init = "fakething"
|
||||||
|
root = "/dev/sda1"
|
||||||
|
bootloader = "limine"
|
||||||
|
}
|
||||||
|
users { root { shell = "/bin/sh" } }
|
||||||
|
KAPEOF
|
||||||
|
check "warns on unknown init" \
|
||||||
|
"$KAPPA doctor /tmp/test_badinit.kap 2>&1" \
|
||||||
|
'not a recognized init system'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Bootloader recognition tests ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
for bl in grub limine; do
|
||||||
|
cat > /tmp/test_bl.kap << KAPEOF
|
||||||
|
system { hostname = "test" }
|
||||||
|
packages {}
|
||||||
|
services {}
|
||||||
|
boot {
|
||||||
|
kernel = "linux"
|
||||||
|
init = "s6"
|
||||||
|
root = "/dev/sda1"
|
||||||
|
bootloader = "$bl"
|
||||||
|
}
|
||||||
|
users { root { shell = "/bin/sh" } }
|
||||||
|
KAPEOF
|
||||||
|
check "recognizes bootloader=$bl" \
|
||||||
|
"$KAPPA parse-config /tmp/test_bl.kap" \
|
||||||
|
"$bl"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Service generation tests ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test that format output contains service fields for each backend type hint
|
||||||
|
check "service format includes type field" \
|
||||||
|
"$KAPPA format examples/foo.kap" \
|
||||||
|
'type = "forking"'
|
||||||
|
|
||||||
|
check "service format includes ports" \
|
||||||
|
"$KAPPA format examples/foo.kap" \
|
||||||
|
'ports = \['
|
||||||
|
|
||||||
|
check "service format includes description" \
|
||||||
|
"$KAPPA format examples/foo.kap" \
|
||||||
|
'description = "Foo web server"'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Groups DSL tests ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cat > /tmp/test_groups.kap << 'KAPEOF'
|
||||||
|
system { hostname = "test" }
|
||||||
|
packages {}
|
||||||
|
services {}
|
||||||
|
boot {
|
||||||
|
kernel = "linux"
|
||||||
|
init = "s6"
|
||||||
|
root = "/dev/sda1"
|
||||||
|
bootloader = "limine"
|
||||||
|
}
|
||||||
|
groups {
|
||||||
|
wheel { gid = 998 }
|
||||||
|
audio {}
|
||||||
|
docker { gid = 995 }
|
||||||
|
}
|
||||||
|
users { root { shell = "/bin/sh" } }
|
||||||
|
KAPEOF
|
||||||
|
|
||||||
|
check "parse groups block" \
|
||||||
|
"$KAPPA parse-config /tmp/test_groups.kap" \
|
||||||
|
'valid'
|
||||||
|
|
||||||
|
check "format groups round-trips" \
|
||||||
|
"$KAPPA format /tmp/test_groups.kap" \
|
||||||
|
'wheel {'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Conflicts tests ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cat > /tmp/test_conflict.kap << 'KAPEOF'
|
||||||
|
package "systemd" {
|
||||||
|
version = "255"
|
||||||
|
source = "https://example.com/systemd-255.tar.gz"
|
||||||
|
provides = ["udev", "logind"]
|
||||||
|
conflicts = ["eudev", "elogind"]
|
||||||
|
build { make }
|
||||||
|
install { make install }
|
||||||
|
}
|
||||||
|
KAPEOF
|
||||||
|
|
||||||
|
check "parse conflicts in package def" \
|
||||||
|
"$KAPPA parse-package /tmp/test_conflict.kap" \
|
||||||
|
'valid'
|
||||||
|
|
||||||
|
check "format shows conflicts" \
|
||||||
|
"$KAPPA format /tmp/test_conflict.kap" \
|
||||||
|
'conflicts ='
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo " Results: $PASS passed, $FAIL failed"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
[ "$FAIL" -eq 0 ] || exit 1
|
||||||
Reference in New Issue
Block a user