GitHub

Recipe Format

Recipes are Rhai scripts that define how to acquire, build, and install packages. Each recipe uses a context map (ctx) to pass state between lifecycle phases and implements check/action function pairs.

Basic Structure

A recipe is a .rhai file with a ctx map for metadata and paired lifecycle functions: each action has an is_* check function that returns the context or throws.

let ctx = #{
    name: "mypackage",
    version: "1.0.0",
    description: "Short description",
    // Custom fields for tracking paths between phases
    source_dir: "",
    build_dir: "",
};

fn is_acquired(ctx) {
    // Check if source exists, return ctx with paths filled in
    let src = join_path(BUILD_DIR, "mypackage-" + ctx.version);
    if is_dir(src) {
        ctx.source_dir = src;
        return ctx;
    }
    throw "source not acquired";
}

fn acquire(ctx) {
    let url = "https://example.com/" + ctx.name + "-" + ctx.version + ".tar.gz";
    let archive = download(url, join_path(BUILD_DIR, ctx.name + ".tar.gz"));
    extract(archive, BUILD_DIR);
    ctx.source_dir = join_path(BUILD_DIR, ctx.name + "-" + ctx.version);
    ctx
}

fn is_built(ctx) {
    let binary = join_path(ctx.source_dir, "mypackage");
    if is_file(binary) {
        return ctx;
    }
    throw "not built";
}

fn build(ctx) {
    shell_in(ctx.source_dir, "./configure --prefix=" + PREFIX);
    shell_in(ctx.source_dir, "make -j" + NPROC);
    ctx
}

fn is_installed(ctx) {
    if is_file(join_path(PREFIX, "bin/mypackage")) {
        return ctx;
    }
    throw "not installed";
}

fn install(ctx) {
    shell_in(ctx.source_dir, "make install DESTDIR=" + PREFIX);
    ctx
}

Context Map (ctx)

The ctx map holds package metadata and state that flows through lifecycle phases:

Field Type Description
name String Package name (alphanumeric, hyphens, underscores)
version String Package version
description String Human-readable description
(custom) Any Add any fields needed to track paths, state, etc.

Functions receive ctx as a parameter and return it (possibly modified) or throw on error. This enables caching - if is_acquired(ctx) succeeds, acquire(ctx) is skipped.

Living ctx Persistence

The ctx map is a "living" object that persists across invocations. After each lifecycle phase completes, the Rust engine writes the modified ctx back to the .rhai file on disk.

This persistence model provides three key benefits:

  • Caching - If is_acquired(ctx) succeeds, acquire(ctx) is skipped, but ctx still has paths filled in from the saved state
  • Resumability - If a build crashes halfway, re-running picks up from the saved ctx state
  • Introspection - You can see the current state by reading the recipe file directly

Example: after acquire completes, the recipe file on disk is updated:

// Before acquire runs:
let ctx = #{
    name: "ripgrep",
    version: "14.1.1",
    extract_dir: "",  // Empty - not yet known
};

// After acquire completes, the file on disk becomes:
let ctx = #{
    name: "ripgrep",
    version: "14.1.1",
    extract_dir: "/tmp/recipe-abc123/ripgrep-14.1.1-x86_64-unknown-linux-musl",
};

The next time you run recipe install ripgrep, the engine loads this saved ctx and passes it to is_acquired(ctx). Since extract_dir is already populated, the check succeeds and acquire(ctx) is skipped entirely.

Built-in Constants

These read-only values are available in all recipe scripts:

Constant Example Value Description
PREFIX /usr/local Installation prefix (or $OUT if set)
BUILD_DIR /tmp/recipe-xxxx Temporary build directory
ARCH x86_64 Target architecture
NPROC 8 Number of CPU cores
RPM_PATH /var/cache/rpms RPM repository path (from env)

Lifecycle

When you run recipe install , the CLI executes these phases in order. Each phase has a check function that's called first - if it succeeds, the action is skipped:

  1. Acquire - is_acquired(ctx)acquire(ctx) - Get source materials
  2. Build - is_built(ctx)build(ctx) - Compile or transform (optional)
  3. Install - is_installed(ctx)install(ctx) - Copy files to PREFIX

This check-then-act pattern enables efficient caching. If sources are already downloaded, acquire is skipped. If already built, build is skipped.

Required Functions

Function Purpose
is_acquired(ctx) Check if source materials exist, return ctx with paths or throw
acquire(ctx) Download/copy source materials, return ctx with source_path
is_installed(ctx) Check if package is installed, return ctx or throw
install(ctx) Install files to PREFIX, return ctx

Optional Functions

Function Purpose
is_built(ctx) Check if already built, return ctx or throw
build(ctx) Compile or transform sources, return ctx
cleanup(ctx) Remove build artifacts, preserve installed files
check_update() Check for updates (return version string or ())

Examples

GitHub Release Binary

Download a pre-built release using github_download_release():

ripgrep.rhai
let ctx = #{
    name: "ripgrep",
    version: "14.1.1",
    repo: "BurntSushi/ripgrep",
    description: "Fast line-oriented search tool",
    extract_dir: "",
};

fn is_acquired(ctx) {
    let dir = join_path(BUILD_DIR, "ripgrep-" + ctx.version + "-" + ARCH + "-unknown-linux-musl");
    if is_file(join_path(dir, "rg")) {
        ctx.extract_dir = dir;
        return ctx;
    }
    throw "not acquired";
}

fn acquire(ctx) {
    mkdir(BUILD_DIR);
    let pattern = "ripgrep-*-" + ARCH + "-unknown-linux-musl.tar.gz";
    let archive = github_download_release(ctx.repo, ctx.version, pattern);
    extract(archive, BUILD_DIR);
    ctx.extract_dir = join_path(BUILD_DIR, "ripgrep-" + ctx.version + "-" + ARCH + "-unknown-linux-musl");
    ctx
}

fn is_installed(ctx) {
    if is_file(join_path(PREFIX, "bin/rg")) {
        return ctx;
    }
    throw "not installed";
}

fn install(ctx) {
    mkdir(join_path(PREFIX, "bin"));
    shell("install -Dm755 " + join_path(ctx.extract_dir, "rg") + " " + join_path(PREFIX, "bin/rg"));
    shell("install -Dm644 " + join_path(ctx.extract_dir, "doc/rg.1") + " " + join_path(PREFIX, "share/man/man1/rg.1"));
    ctx
}

See Helper Functions for github_download_release(), extract(), etc.

Source Build

Build from source using configure/make:

bash.rhai
let ctx = #{
    name: "bash",
    version: "5.2.37",
    description: "GNU Bourne-Again Shell",
    source_dir: "",
};

fn is_acquired(ctx) {
    let src = join_path(BUILD_DIR, ctx.name + "-" + ctx.version);
    if is_file(join_path(src, "configure")) {
        ctx.source_dir = src;
        return ctx;
    }
    throw "source not acquired";
}

fn acquire(ctx) {
    mkdir(BUILD_DIR);
    let url = "https://ftp.gnu.org/gnu/bash/bash-" + ctx.version + ".tar.gz";
    let archive = download(url, join_path(BUILD_DIR, "bash.tar.gz"));
    extract(archive, BUILD_DIR);
    ctx.source_dir = join_path(BUILD_DIR, ctx.name + "-" + ctx.version);
    ctx
}

fn is_built(ctx) {
    if is_file(join_path(ctx.source_dir, "bash")) {
        return ctx;
    }
    throw "not built";
}

fn build(ctx) {
    shell_in(ctx.source_dir, "./configure --prefix=" + PREFIX);
    shell_in(ctx.source_dir, "make -j" + NPROC);
    ctx
}

fn is_installed(ctx) {
    if is_file(join_path(PREFIX, "bin/bash")) {
        return ctx;
    }
    throw "not installed";
}

fn install(ctx) {
    shell_in(ctx.source_dir, "make install DESTDIR=" + PREFIX);
    ctx
}

Git Clone Source

Clone source from git using git_clone_depth():

neovim.rhai
let ctx = #{
    name: "neovim",
    version: "0.10.0",
    repo: "https://github.com/neovim/neovim.git",
    description: "Vim-fork focused on extensibility",
    source_dir: "",
};

fn is_acquired(ctx) {
    let src = join_path(BUILD_DIR, "neovim");
    if is_file(join_path(src, "CMakeLists.txt")) {
        ctx.source_dir = src;
        return ctx;
    }
    throw "source not acquired";
}

fn acquire(ctx) {
    mkdir(BUILD_DIR);
    let src = git_clone_depth(ctx.repo, join_path(BUILD_DIR, "neovim"), 1);
    shell_in(src, "git checkout v" + ctx.version);
    ctx.source_dir = src;
    ctx
}

fn is_built(ctx) {
    if is_file(join_path(ctx.source_dir, "build/bin/nvim")) {
        return ctx;
    }
    throw "not built";
}

fn build(ctx) {
    shell_in(ctx.source_dir, "make CMAKE_BUILD_TYPE=Release");
    ctx
}

fn is_installed(ctx) {
    if is_file(join_path(PREFIX, "bin/nvim")) {
        return ctx;
    }
    throw "not installed";
}

fn install(ctx) {
    shell_in(ctx.source_dir, "make install CMAKE_INSTALL_PREFIX=" + PREFIX);
    ctx
}

With Update Checking

Implement check_update() to enable recipe update:

fn check_update() {
    let latest = github_latest_release("BurntSushi/ripgrep");
    if latest != ctx.version {
        latest  // Return new version
    } else {
        ()      // No update available
    }
}

When an update is found, recipe upgrade will remove the old version and install the new one.

Package Resolution

When you run recipe install , the CLI looks for recipes in this order:

  1. If contains / or ends with .rhai: treat as explicit path
  2. /.rhai
  3. //.rhai (subdirectory style)

The default recipes directory is ~/.local/share/recipe/recipes/. Override with --recipes-path or RECIPE_PATH environment variable.

See Also