Skip to content

DSL Patterns

Leonard Ramminger edited this page Aug 9, 2026 · 1 revision

DSL Patterns

Reusable patterns for common build.lua designs.

Env helper at the top

beez.config(require("config"))

local function env_or(key, default)
    local value = beez.env(key)
    if value == nil then
        return default
    end
    return value
end

local BUILD_TYPE = env_or("BUILD_TYPE", "Release")
local BUILD_DIR = "build/" .. BUILD_TYPE

Use locals when many steps share the same paths or flags.

Incremental per-file check (success cache)

Pattern for lint, format-check, or analyze tools that run per source file:

configure_step("lint", {
    patterns = { "src/**/*.cpp" },
    lint_rev = "1",
})

step({
    name = "lint",
    phase = "qa",
    scope = "default",
    mutate = { "src/**/*.cpp" },
    run = function(ctx)
        local config = ctx.get_config()
        local files = ctx.glob(config.patterns)
        local failed = 0

        for _, path in ipairs(files) do
            if ctx.file_success_cached(path) then
                goto continue
            end

            local job = ctx:spawn({
                name = "lint-" .. path,
                cmd = "clang-tidy " .. path,
            })
            local code = ctx:wait(job)
            if code ~= 0 then
                ctx.record_file_cache_miss(path)
                failed = failed + 1
            else
                ctx.cache_file_success(path)
            end

            ::continue::
        end

        return failed > 0 and 1 or 0
    end,
})

Key points:

  • lint_rev in config: bump when tool config changes
  • mutate on step: marks files the step touches (ordering + semantics)
  • record_file_cache_miss / cache_file_success: track per-file state
  • Workers: parallel lint per file

Shell-only pipeline

Minimal project without Lua callbacks:

step({
    name = "configure",
    phase = "build",
    scope = "default",
    run = "cmake -B build",
})
step({
    name = "compile",
    phase = "build",
    scope = "default",
    run = "cmake --build build",
})

order("configure", "compile")

workflow("build", {
    { phase = "build", scope = "default" },
})

task("clean", "rm -rf build")

Add input/output globs when you want step caching.

Task as workflow shortcut

When you do not need phase grouping:

task("ci", {
    { name = "configure" },
    { name = "compile" },
    { name = "test" },
})

Sequential only. For parallel phase+scope blocks, use a workflow.

Separate config module

Keep build.lua readable:

config.lua

return {
    cache = { path = ".cache" },
    env = { vars = { BUILD_TYPE = "Release" } },
}

build.lua

beez.config(require("config"))
-- steps, workflows, tasks ...

Revision bumps

When a QA tool changes but sources do not, bump a revision in step config:

configure_step("format", { format_rev = "2" })
configure_step("lint",    { lint_rev = "3" })

This invalidates success cache entries that depend on config fingerprint.

Shared glob lists

local CXX_PATTERNS = {
    "src/**/*.cpp",
    "include/**/*.hpp",
}

configure_step("lint", { patterns = CXX_PATTERNS })
configure_step("format", { patterns = CXX_PATTERNS })

When to use what

Need Use
One shell command task("x", "cmd")
Fixed step chain task("x", { ... })
Phase groups + parallel scopes workflow()
Per-file incremental tool Lua callback + success cache
Parallel shell in one step ctx:spawn / ctx:wait_all
Skip unchanged build steps input / output on shell steps

Next steps

Clone this wiki locally