Local-first package recipe executor for LevitateOS.
- Recipes are Rhai scripts.
- State lives in the recipe file itself as a
ctxmap (let ctx = #{ ... };) and is persisted after each phase. - The CLI is designed to keep stdout machine-readable (final
ctxJSON) and send logs/tool output to stderr.
If you are looking for deeper docs:
- Recipe authoring guide (current implementation):
WRITING_RECIPES.md - Spec and design requirements:
REQUIREMENTS.md - Current helper API surface (authoritative):
HELPERS_AUDIT.md - Installed man pages:
recipe(1),recipe-recipe(5),recipe-helpers(7) - Lifecycle notes:
PHASES.md
Alpha. The implementation is intentionally explicit and conservative.
Implemented:
- Phase executor with
is_*checks,acquire/build/install, ctx persistence //! extends: <base.rhai>(AST merge: base runs first, child overrides)- Per-recipe execution lock (
.rhai.lock) - Build dependency resolver (
depsandbuild_deps) that installs tool recipes intoBUILD_DIR/.tools - LLM helpers (
llm_extract, etc) and an opt-in LLM-based repair loop (--autofix)
Not implemented yet (still in the spec):
- Sysroot/prefix confinement for safe A/B composition
- Atomic staging/commit and
installed_filestracking - Higher-level install helpers (
install_bin,install_to_dir, etc.) - Update/upgrade lifecycle commands
REQUIREMENTS.md is the target specification. The current binary is narrower.
- There are no
--sysrootor--prefixCLI flags yet. - The current CLI supports
install,remove,cleanup,isinstalled,isbuilt,isacquired,list,info, andhash. - Recipes currently get
RECIPE_DIR,BUILD_DIR,ARCH,NPROC, andRPM_PATH. - Base/dependency execution may also provide
BASE_RECIPE_DIRandTOOLS_PREFIX. - Filesystem helpers operate on explicit paths. Higher-level helpers such as
install_binandinstall_to_dirare not implemented yet. cleanup(ctx, reason)with two arguments is required by this repository's install flow.
If you are debugging behavior, trust the current source and this README before you trust the broader spec.
To build and run the current implementation, assume at least:
- Rust toolchain:
cargo,rustc git- POSIX
sh - A working C toolchain for Rust crates with native code
On fresh Fedora minimal, the minimum known bootstrap is:
sudo dnf install -y rust cargo gcc git pkgconf-pkg-configrecipe itself is mostly Rust, but the current dependency set includes native archive/compression crates (bzip2, xz2, zstd), so a compiler toolchain is part of the practical bootstrap.
| Command | When it is needed |
|---|---|
git |
git_clone* helpers, autofix patch application, git repo root detection |
sh |
shell* helpers |
tar |
extract_from_tarball() helper only |
df |
check_disk_space() helper only |
codex / claude |
LLM helpers and recipe install --autofix only |
| arbitrary host commands | when a recipe uses exec* or shell* to call them |
Important: the core extract() helper is native Rust and does not rely on host tar/unzip.
If you cloned LevitateOS and only need this submodule:
git submodule update --init -- tools/recipeBuild from the submodule itself:
cd tools/recipe
cargo build
./target/debug/recipe --helpBuild from the LevitateOS repo root:
cargo build --manifest-path tools/recipe/Cargo.tomlOptional local install:
cd tools/recipe
cargo install --path .Tests are not standalone today. Cargo.toml has a path dev-dependency on ../../testing/cheat-test, so cargo test expects a normal LevitateOS checkout around this submodule.
For reproducible distro-native container runs, use the checked-in Podman runner:
tools/recipe/scripts/podman-test.sh alpine
tools/recipe/scripts/podman-test.sh fedora
tools/recipe/scripts/podman-test.sh rocky
tools/recipe/scripts/podman-test.sh allThis script mounts tools/recipe and testing/cheat-test, bootstraps upstream rustup inside the container, and runs the full suite with an isolated CARGO_TARGET_DIR=/tmp/recipe-target so container results are not polluted by host build artifacts.
- If
--recipes-pathis not set, Recipe uses$RECIPE_PATH, otherwise~/.local/share/recipe/recipes. - The recipes directory is created automatically if it does not exist.
- If
--build-diris not set, Recipe creates a temporary build directory and keeps it instead of auto-deleting it. --json-output <file>is the safest way to consume result JSON in scripts, because recipe logs and helper output are sent to stderr.
This crate is configured for cargo-generate-rpm.
cargo install cargo-generate-rpm
cargo build --release
cargo generate-rpmBy default, the RPM is written under target/generate-rpm/.
Build a release .apk in an Alpine 3.23 container:
tools/recipe/scripts/build-apk.shBy default, the APK is written under target/packages/apk/.
The recipe CLI prints the final ctx JSON to stdout (or writes it to --json-output <file>).
- All recipe logs, phase banners, helper traces, shell output, and LLM provider output are written to stderr.
- If you want a clean JSON pipeline, prefer
--json-outputfor long-running installs.
Recipes use a ctx map for state.
- Check functions (
is_*) shouldthrowwhen the phase needs to run. - Phase functions (
acquire/build/install/remove/cleanup) takectxand return an updatedctx. cleanup(ctx, reason)is required for normal installs in this repo. If you do not need cleanup, make it a no-op that returnsctx.
Minimal example:
let ctx = #{
name: "ripgrep",
version: "14.1.0",
url: "https://example.com/ripgrep.tar.gz",
sha256: "abc123...",
archive: "",
installed: false,
};
fn is_acquired(ctx) {
if ctx.archive == "" || !is_file(ctx.archive) { throw "not acquired"; }
ctx
}
fn acquire(ctx) {
mkdir(BUILD_DIR);
let archive = download(ctx.url, join_path(BUILD_DIR, "src.tar.gz"));
verify_sha256(archive, ctx.sha256);
ctx.archive = archive;
ctx
}
fn is_installed(ctx) {
if !ctx.installed { throw "not installed"; }
ctx
}
fn install(ctx) {
// Place files using helpers or shell/exec.
ctx.installed = true;
ctx
}
fn cleanup(ctx, reason) {
// Called automatically on success/failure paths, and via `recipe cleanup`.
ctx
}
cleanup(ctx, reason) is invoked with:
manualforrecipe cleanupauto.acquire.success,auto.acquire.failureauto.build.success,auto.build.failureauto.install.success,auto.install.failure
At the top of a recipe, you can declare a base recipe:
//! extends: linux-base.rhai
Behavior:
- Base recipe is compiled first, then merged with the child AST.
- Child functions with the same name and arity override base functions.
- Top-level statements run base-first, then child.
- Recursive
extendsis rejected.
Recipes may declare:
let deps = ["foo", "bar"];for dependencies needed across phases.let build_deps = ["linux-deps"];for tool dependencies needed only when building.
The resolver:
- Executes dependency recipes from
--recipes-path. - Installs tools into
BUILD_DIR/.tools. - Prepends
.tools/{usr/bin,usr/sbin,bin,sbin}toPATHfor the duration of the phase. - Exposes
TOOLS_PREFIXto dependency recipes.
recipe install <name-or-path>
recipe remove <name-or-path>
recipe cleanup <name-or-path> [--reason <reason>]
recipe isinstalled <name-or-path>
recipe isbuilt <name-or-path>
recipe isacquired <name-or-path>
recipe list
recipe info <name-or-path>
recipe hash <file>Resolution for <name-or-path>:
- Absolute paths are used as-is.
- Relative paths are tried as-is, then under
--recipes-path, then with.rhaiappended.
-r, --recipes-path <dir>: where to search for<name>.rhaiand resolve//! extends:-b, --build-dir <dir>: where downloads/build artifacts go (otherwise a kept temp dir)--define KEY=VALUE: inject constants into the Rhai scope before execution (repeatable; available to install/remove/cleanup/is*)--json-output <file>: write the final ctx JSON to a file (stdout stays quiet)--llm-profile <name>: select a profile from XDGrecipe/llm.toml(see below)
recipe cleanupdefaults toreason = "manual".- Use
recipe cleanup <name-or-path> --reason <reason>to pass a custom manual lifecycle reason.
recipe install also supports:
--autofix: on selected failures, ask the configured provider to return a unified diff, apply it, and retry--autofix-attempts <n>: maximum patch attempts (default: 2)--autofix-cwd <dir>: working directory used for LLM invocation andgit apply(defaults to detected git repo root)--autofix-prompt-file <file>: append extra instructions to the autofix prompt--autofix-allow-path <path>: constrain patched files to allowed roots (repeatable)
Recipe integrates with external CLIs for two things:
- Rhai helpers:
llm_extract,llm_find_latest_version,llm_find_download_url - The opt-in repair loop:
recipe install --autofix
Design constraints:
- Codex and Claude have equal footing. There is no implicit fallback.
- Recipe does not interpret model output. It returns raw text, or in autofix mode it applies a unified diff.
Config is loaded from:
$XDG_CONFIG_DIRS/recipe/llm.toml(default:/etc/xdg/recipe/llm.toml)$XDG_CONFIG_HOME/recipe/llm.toml(default:~/.config/recipe/llm.toml)
Multiple files are merged (user config overrides system config).
Example:
version = 1
default_provider = "codex" # or "claude" (required)
default_profile = "kernels_nightly" # optional
timeout_secs = 300
max_output_bytes = 10485760
max_input_bytes = 10485760
[providers.codex]
bin = "codex"
args = ["--sandbox", "read-only", "--skip-git-repo-check"]
[providers.claude]
bin = "claude"
args = ["-p", "--output-format", "text", "--no-chrome"]
[profiles.kernels_nightly]
default_provider = "codex"
[profiles.kernels_nightly.providers.codex]
model = "gpt-5.3-codex"
effort = "xhigh" # mapped to Codex config `model_reasoning_effort`Provider keys you can set (globally under [providers.*] or per profile under [profiles.<name>.providers.*]):
bin: executable name or pathargs: extra CLI argsmodel: provider-specific model ideffort: provider-specific effort controlconfig: (Codex only) repeated--config key=valueoverridesenv: environment variables to add to the provider process
Notes:
- Codex is invoked via
codex execand Recipe disables the Codexshell_toolfor these calls. - MCP servers are hard-disabled for Codex runs by overriding
mcp_servers.<name>.enabled=falseat invocation time.
Autofix prompt sources (in order):
- A built-in base prompt (unified diff only, no prose)
- Optional recipe-supplied block starting with
// AUTOFIX_PROMPT:in the recipe (or its base recipe) - Optional
--autofix-prompt-filecontents
Recipe-supplied prompt block format:
// AUTOFIX_PROMPT: High-level instruction line.
// More instructions...
// Blank line ends the block.
Autofix guardrails:
- Patch output must be a unified diff suitable for
git apply. - Patch paths must stay within
--autofix-allow-pathroots. - Patches that modify
is_installed/is_built/is_acquiredor introduce|| trueare rejected.
If --recipes-path is not provided, the CLI uses:
$RECIPE_PATHif set- otherwise
$XDG_DATA_HOME/recipe/recipes(default:~/.local/share/recipe/recipes)
cd tools/recipe
cargo build
# or from the LevitateOS repo root
cargo build --manifest-path tools/recipe/Cargo.tomlInstall the binary into Cargo's bin directory:
cd tools/recipe
cargo install --path .MIT OR Apache-2.0