Skip to content

LevitateOS/recipe

Repository files navigation

recipe

Local-first package recipe executor for LevitateOS.

  • Recipes are Rhai scripts.
  • State lives in the recipe file itself as a ctx map (let ctx = #{ ... };) and is persisted after each phase.
  • The CLI is designed to keep stdout machine-readable (final ctx JSON) 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

Status

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 (deps and build_deps) that installs tool recipes into BUILD_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_files tracking
  • Higher-level install helpers (install_bin, install_to_dir, etc.)
  • Update/upgrade lifecycle commands

Current Implementation Reality

REQUIREMENTS.md is the target specification. The current binary is narrower.

  • There are no --sysroot or --prefix CLI flags yet.
  • The current CLI supports install, remove, cleanup, isinstalled, isbuilt, isacquired, list, info, and hash.
  • Recipes currently get RECIPE_DIR, BUILD_DIR, ARCH, NPROC, and RPM_PATH.
  • Base/dependency execution may also provide BASE_RECIPE_DIR and TOOLS_PREFIX.
  • Filesystem helpers operate on explicit paths. Higher-level helpers such as install_bin and install_to_dir are 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.

Host Requirements

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-config

recipe 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.

Optional External Commands by Feature

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.

Bootstrap on a Fresh Machine

If you cloned LevitateOS and only need this submodule:

git submodule update --init -- tools/recipe

Build from the submodule itself:

cd tools/recipe
cargo build
./target/debug/recipe --help

Build from the LevitateOS repo root:

cargo build --manifest-path tools/recipe/Cargo.toml

Optional 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 all

This 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.

Operational Defaults

  • If --recipes-path is not set, Recipe uses $RECIPE_PATH, otherwise ~/.local/share/recipe/recipes.
  • The recipes directory is created automatically if it does not exist.
  • If --build-dir is 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.

RPM Packaging (Rust Native)

This crate is configured for cargo-generate-rpm.

cargo install cargo-generate-rpm
cargo build --release
cargo generate-rpm

By default, the RPM is written under target/generate-rpm/.

Alpine APK Packaging

Build a release .apk in an Alpine 3.23 container:

tools/recipe/scripts/build-apk.sh

By default, the APK is written under target/packages/apk/.

Output Discipline (Important)

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-output for long-running installs.

Recipe Model

Recipes use a ctx map for state.

  • Check functions (is_*) should throw when the phase needs to run.
  • Phase functions (acquire/build/install/remove/cleanup) take ctx and return an updated ctx.
  • cleanup(ctx, reason) is required for normal installs in this repo. If you do not need cleanup, make it a no-op that returns ctx.

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 Reasons

cleanup(ctx, reason) is invoked with:

  • manual for recipe cleanup
  • auto.acquire.success, auto.acquire.failure
  • auto.build.success, auto.build.failure
  • auto.install.success, auto.install.failure

Extending Recipes

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 extends is rejected.

Dependency Recipes (deps and build_deps)

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} to PATH for the duration of the phase.
  • Exposes TOOLS_PREFIX to dependency recipes.

CLI

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 .rhai appended.

Global Flags

  • -r, --recipes-path <dir>: where to search for <name>.rhai and 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 XDG recipe/llm.toml (see below)

Cleanup Reason

  • recipe cleanup defaults to reason = "manual".
  • Use recipe cleanup <name-or-path> --reason <reason> to pass a custom manual lifecycle reason.

Install Flags (Autofix)

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 and git 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)

LLM Integration

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.

XDG Config: recipe/llm.toml

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 path
  • args: extra CLI args
  • model: provider-specific model id
  • effort: provider-specific effort control
  • config: (Codex only) repeated --config key=value overrides
  • env: environment variables to add to the provider process

Notes:

  • Codex is invoked via codex exec and Recipe disables the Codex shell_tool for these calls.
  • MCP servers are hard-disabled for Codex runs by overriding mcp_servers.<name>.enabled=false at invocation time.

Autofix Prompts

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-file contents

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-path roots.
  • Patches that modify is_installed/is_built/is_acquired or introduce || true are rejected.

Default Recipes Path

If --recipes-path is not provided, the CLI uses:

  • $RECIPE_PATH if set
  • otherwise $XDG_DATA_HOME/recipe/recipes (default: ~/.local/share/recipe/recipes)

Building

cd tools/recipe
cargo build

# or from the LevitateOS repo root
cargo build --manifest-path tools/recipe/Cargo.toml

Install the binary into Cargo's bin directory:

cd tools/recipe
cargo install --path .

License

MIT OR Apache-2.0

About

Rhai-based package manager where recipes are executable code, not config files. Installation state lives in the recipe files themselves—no database. Minimal executor provides helpers; recipes do the work.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors