Skip to content

llar Formula

Rick Guo edited this page Jul 30, 2026 · 8 revisions

LLAR Formula

An LLAR Formula describes how a versioned upstream project becomes an installable LLAR module. It connects four pieces of information:

  • which upstream repository and versions the Formula covers;
  • which direct dependencies each requested version uses;
  • how the source is built and installed;
  • how another project can consume and verify the installed result.

A Formula is stored in a _llar.gox file. The file uses XGo syntax, but most of its content is ordinary Go plus a small LLAR-specific surface such as id, fromVer, onRequire, onBuild, and onTest.

Here is the overall shape:

id "example/libalpha"

fromVer "v1.2.0"

onRequire (proj, deps) => {
    // Read dependency information from the requested upstream version.
}

onBuild ctx => {
    // Build the source and install it into ctx.outputDir.
}

onTest ctx => {
    // Build and run a consumer test against ctx.outputDir.
}

onBuild is the only required hook. Dependency discovery, matrix selection, and installed-output tests are added when the project needs them.

Reading Formula Source

XGo is a superset of Go, so normal Go declarations, expressions, control flow, imports, and calls remain valid. Formulae commonly use a few shorter XGo forms:

data := os.readFile(path)!       // os.ReadFile(path), panic on error
installDir := ctx.outputDir      // zero-argument OutputDir call
ctx.setMetadata flags           // command-style method call
onBuild ctx => { ... }          // lambda passed to onBuild

The lowercase spelling changes only the first letter of an exported Go name, so os.readFile resolves to os.ReadFile and ctx.setMetadata resolves to ctx.SetMetadata. The original Go spelling is also valid.

The Formula file itself is a classfile. LLAR supplies the generated Go type, entrypoint, and base class, so the file has no package main, main function, or explicit Formula struct. Imports, types, constants, fields, and helper functions appear before the first Formula statement, normally before id.

The rest of this page follows the order in which a Formula usually takes shape.

1. Source And Module Identity

An LLAR module id is the upstream GitHub repository path without the host name. The fictional repository github.com/example/libalpha therefore has the module id example/libalpha:

id "example/libalpha"

Formula work begins from an exact upstream tag. Its build files show which build system, dependencies, install rules, package metadata, and consumer tests actually exist for that version. Those source files are the basis for the Formula; settings from another project are not assumed to apply.

A module directory contains versions.json and one or more Formula versions:

example/libalpha/
  versions.json
  v1.2.0/
    Libalpha_llar.gox
  v2.0.0/
    Libalpha_llar.gox

The filename before the first underscore becomes the generated class name. It must be a valid Go identifier, which is why the example uses Libalpha_llar.gox.

fromVer marks the first upstream version handled by that Formula:

fromVer "v1.2.0"

If the next Formula starts at v2.0.0, the first Formula covers every selected version from v1.2.0 up to, but not including, v2.0.0. fromVer is therefore a range boundary rather than a declaration for one exact version. Its value is a string literal because LLAR reads it while selecting the Formula.

This range is important when dependencies are described: a build recipe can remain unchanged across several releases even though upstream changes a dependency version.

2. Dependencies Across The Version Range

onRequire provides the direct dependencies for the exact upstream version being requested. The proj value reads files from that version, while deps collects the dependencies found there:

import "strings"

onRequire (proj, deps) => {
    data, err := proj.readFile("dependency-version.txt")
    if err != nil {
        return
    }

    version := strings.trimSpace(string(data))
    if version != "" {
        deps.require "example/libbeta", version
    }
}

Suppose v1.2.0 records v3.1.0 in that file and v1.8.0 records v3.4.0. Both upstream versions can use the same Formula, while onRequire still returns the dependency chosen by each upstream release. A new Formula is only needed when the build recipe or another Formula boundary actually changes.

Only direct dependencies belong here. LLAR resolves their transitive dependencies separately. When upstream uses a build-system name instead of an LLAR module id, the Formula contains the explicit mapping between those names; LLAR does not infer it.

The versions.json Fallback

Dynamic discovery is not always possible. A dependency file may be absent from an older source release, or it may name a dependency without giving a version. versions.json provides a conservative fallback for those cases:

{
  "path": "example/libalpha",
  "deps": {
    "v1.2.0": [
      {"path": "example/libbeta", "version": "v3.0.0"}
    ],
    "v1.8.0": [
      {"path": "example/libbeta", "version": "v3.0.0"}
    ]
  }
}

The fallback does not need to follow the newest upstream dependency. In this example, v3.0.0 is useful because it is known to work throughout the Formula range. onRequire still returns a newer version whenever the requested source records one.

Fallback entries are keyed by exact upstream version:

  • when onRequire returns usable versioned dependencies, that result is used;
  • when it returns a dependency with an empty version, the matching entry for that source version can fill the version;
  • when it returns no usable dependencies, the complete entry for that source version becomes the fallback.

versions.json is present even when all current versions can be discovered dynamically. Its deps object can be empty when no fallback is needed.

3. Build Variants When They Matter

Many projects build only one way and need no matrix code. When a build changes with the environment or a package choice, the selected values are available through target.

target.require contains environment requirements that participate in dependency resolution, such as os, arch, ABI, libc, or toolchain. target.options contains choices owned by this package, such as static versus shared output or an optional feature.

For example, a package with a shared option can provide a default and read the selected value during the build:

import "slices"

defaults {
    "shared": "OFF",
}

onBuild ctx => {
    shared := slices.contains(target.options["shared"], "ON")
    // The selected value is passed to the verified upstream build option.
}

defaults initializes package options; it does not define their legal values and does not set environment requirements. If only ON and OFF are valid, filter can reject other selections before dependency discovery or building:

filter => {
    for _, value := range target.options["shared"] {
        if value != "ON" && value != "OFF" {
            return false
        }
    }
    return true
}

The same selected target is visible to filter, onRequire, and onBuild. That allows an option to affect both the dependency set and the build command without maintaining two separate configurations.

4. Build And Install

onBuild turns the selected upstream source into an installed result. Its context provides the two directories that define this boundary:

  • ctx.SourceDir is the temporary checkout of the requested upstream tag;
  • ctx.outputDir is the install directory assigned to the current module.

Build files can live under ctx.SourceDir, but the headers, libraries, tools, and package metadata that make up the final result are installed under ctx.outputDir.

CMake

The CMake helper models configure, build, and install as one workflow:

onBuild ctx => {
    installDir := ctx.outputDir
    buildDir := ctx.SourceDir + "/_build"
    c := cmake.new(ctx.SourceDir, buildDir, installDir)

    for _, dep := range ctx.Proj.Deps {
        c.use ctx.outputDir(dep)
    }

    c.configure
    c.build
    c.install
}

c.use adds an installed dependency to the build search paths. The dependency directory always comes from ctx.outputDir(dep), so the Formula is independent of LLAR's cache layout.

The current CMake methods return no value. They panic when configuration, building, or installation fails, and LLAR turns that panic into a Formula error. Build-system settings such as generators, policy versions, build types, toolchains, tests, or shared-library switches appear only when the selected upstream source requires them.

Common CMake calls are:

Call Meaning
cmake.new(source, build, install) create a CMake workflow
c.use(root) add an installed dependency to search paths
c.buildType(name) select a CMake build type
c.toolchain(path) set CMAKE_TOOLCHAIN_FILE
c.define(key, value) add a string cache definition
c.defineBool(key, value) add a boolean cache definition
c.configure(args...) configure with optional extra arguments
c.build(args...) build with optional extra arguments
c.install(args...) install with optional extra arguments

Autotools

The equivalent Autotools flow uses an out-of-source build directory and the same assigned install directory:

onBuild ctx => {
    installDir := ctx.outputDir
    buildDir := ctx.SourceDir + "/_build"
    a := autotools.new(ctx.SourceDir, buildDir, installDir)

    for _, dep := range ctx.Proj.Deps {
        a.use ctx.outputDir(dep)
    }

    a.configure
    a.build
    a.install
}

a.configure supplies --prefix=<installDir>, followed by any extra arguments. Like the CMake helper, configure, build, and install panic on failure. Steps such as autoreconf are separate commands when the selected source actually requires them.

Other Commands

Formulae inherit gsh command execution. An unresolved command-style name is treated as an executable:

codegen! "--output", generatedDir

This is equivalent in shape to exec.Command("codegen", "--output", generatedDir). Arguments remain separate strings; there is no shell to expand pipes, redirects, wildcards, command substitutions, or &&.

exec is useful when the executable name is stored in a value, contains punctuation, or collides with an XGo name:

exec "build-tool", "--generate", generatedDir
lastErr!

Every command updates lastErr. The ! form turns a non-nil error into a panic, which reaches the Formula boundary. When a command is checked manually, lastErr is read immediately because the next command replaces it.

5. Consumer Metadata

After installation, LLAR also needs to know how another project consumes the result. ctx.setMetadata stores that information. For a C or C++ library this usually means verified compiler and linker flags, preferably obtained from metadata installed by the project itself.

The following fragment asks the installed package-config file for its public flags:

import "strings"

c.use installDir
capout => {
    exec "pkg-config", "--cflags", "--libs", "libalpha"
}
lastErr!
ctx.setMetadata strings.trimSpace(output)

This code belongs inside onBuild, after c.install. capout captures stdout as output; command failure is still reported through lastErr.

Metadata describes the current package's public consumer interface. It is not a dependency list or a build log. Dependency metadata is added only when the current package's public interface actually exposes that dependency. In that case, ctx.buildResult(dep) returns the dependency's build result and result.metadata() reads its consumer metadata.

6. Verify The Installed Result

onTest answers a different question from onBuild: can a consumer use what was installed? A useful test compiles or links a small consumer against ctx.outputDir and then runs or loads the result.

onTest ctx => {
    installDir := ctx.outputDir
    testSource := ctx.SourceDir + "/consumer"
    testBuild := ctx.SourceDir + "/_consumer_build"

    tc := cmake.new(testSource, testBuild, "")
    tc.use installDir
    for _, dep := range ctx.Proj.Deps {
        tc.use ctx.outputDir(dep)
    }

    tc.configure
    tc.build

    exec testBuild + "/alpha-check"
    lastErr!
}

The verification build has its own directory. This matters because LLAR can run onTest against either a fresh build or an existing cached installation; the scratch directory previously used by onBuild may not exist. onTest does not install the package again or replace its metadata.

Most tests fail at the first unsuccessful operation. When several independent checks are worth reporting together, their non-nil errors can instead be added to ctx.Errs before the hook returns.

7. Run The Formula

llar test exercises dependency discovery, matrix selection, installation, metadata, and onTest together:

llar test -v ./example/libalpha@v1.2.0

The fromVer boundary is the first useful test point. Other representative versions in the same range show whether onRequire follows upstream dependency changes without selecting a different Formula. Running the same version again also checks that onTest works with a cached installation.

When explicit matrix values are involved, the test names every required environment dimension and package option:

llar test -v ./example/libalpha@v1.2.0 \
  --os "$(go env GOOS)" --arch "$(go env GOARCH)" \
  --option shared=ON

Putting The Pieces Together

The following fictional Formula combines the stages above. The filenames, dependency file, CMake option, package-config name, and consumer target are all placeholders for facts that would come from the selected upstream source.

import (
    "slices"
    "strings"
)

id "example/libalpha"

fromVer "v1.2.0"

defaults {
    "shared": "OFF",
}

filter => {
    for _, value := range target.options["shared"] {
        if value != "ON" && value != "OFF" {
            return false
        }
    }
    return true
}

onRequire (proj, deps) => {
    data, err := proj.readFile("dependency-version.txt")
    if err != nil {
        return
    }

    version := strings.trimSpace(string(data))
    if version != "" {
        deps.require "example/libbeta", version
    }
}

onBuild ctx => {
    installDir := ctx.outputDir
    buildDir := ctx.SourceDir + "/_build"
    c := cmake.new(ctx.SourceDir, buildDir, installDir)

    for _, dep := range ctx.Proj.Deps {
        c.use ctx.outputDir(dep)
    }

    shared := slices.contains(target.options["shared"], "ON")
    c.defineBool "LIBALPHA_SHARED", shared
    c.configure
    c.build
    c.install

    c.use installDir
    capout => {
        exec "pkg-config", "--cflags", "--libs", "libalpha"
    }
    lastErr!
    ctx.setMetadata strings.trimSpace(output)
}

onTest ctx => {
    installDir := ctx.outputDir
    testSource := ctx.SourceDir + "/consumer"
    testBuild := ctx.SourceDir + "/_consumer_build"

    tc := cmake.new(testSource, testBuild, "")
    tc.use installDir
    for _, dep := range ctx.Proj.Deps {
        tc.use ctx.outputDir(dep)
    }
    tc.configure
    tc.build

    exec testBuild + "/alpha-check"
    lastErr!
}

Formula Surface At A Glance

Surface Role
id "owner/repo" module id served by the Formula
fromVer "version" first upstream version in the Formula's range
defaults {...} default values for package options
filter => { ... } acceptance check for the selected matrix
onRequire (proj, deps) => { ... } direct dependencies for the requested upstream version
onBuild ctx => { ... } build and install into the assigned output directory
onTest ctx => { ... } build and run installed-output verification

The hook values provide these commonly used members:

Name Meaning
proj.readFile(path) read a file from the requested upstream version during onRequire
deps.require(path, version) add one direct dependency
target.require selected environment requirements as map[string][]string
target.options selected package options as map[string][]string
ctx.SourceDir temporary checkout of the requested upstream source
ctx.Proj.Deps resolved dependencies available to the build or test
ctx.outputDir current module's assigned install directory
ctx.outputDir(dep) installed output directory of a dependency
ctx.buildResult(dep) build result and availability of a dependency
ctx.setMetadata(value) set the current module's consumer metadata
ctx.Errs.add(err) accumulate a non-nil hook error

Custom Version Ordering

Most modules need no comparator; LLAR's default comparison handles numeric segments in ordinary version strings. A module whose real tags require another ordering can add one _cmp.gox file beside versions.json:

compareVer (a, b) => {
    return semver.Compare(a.Version, b.Version)
}

Semantic-version comparison applies only when every relevant upstream tag and Formula boundary is valid semantic-version syntax.

Clone this wiki locally