Skip to content

llar Build Matrix

Rick Guo edited this page Jul 29, 2026 · 7 revisions

One version of a library can be built in several ways. For example, the same source may produce these variants:

linux + amd64 + shared=OFF
linux + arm64 + shared=OFF
linux + amd64 + shared=ON

LLAR uses a build matrix to describe these differences. The selected matrix values tell a formula which dependencies, flags, commands, and output it should use. A different accepted selection may produce a different reusable artifact.

LLAR's build matrix is inspired by Go build constraints. For example, when building for Linux, Go includes a file marked with //go:build linux and leaves out files for other targets. The constraint selects source files for one build.

LLAR instead builds reusable artifacts across a dependency graph. A build such as:

libpng: os=linux, arch=arm64 | shared=ON
  zlib: os=linux, arch=arm64

selects a libpng artifact and the matching zlib dependency artifact. Some values must reach dependencies, while others belong only to the current package. This leads to LLAR's matrix model.

Matrix Values

LLAR separates matrix values into two groups:

Group Meaning Examples
Require Build environment that dependencies may also need to know. os, arch, libc, ABI
Options Build choices for the current package. shared, debug, zlib, tests

The available os and arch values are not defined separately by each formula. They are expected to come from LLAR's platform and toolchain support as described by the Cross Compile design, which is still WIP.

For example, a future LLAR integration may use esp32-clang internally to make arch=xtensa-esp32 available as a build target. The request selects the target architecture; LLAR chooses and configures the compiler behind it. A formula only decides whether its library supports that target.

The Go representation is:

type Matrix struct {
    Require        map[string][]string
    Options        map[string][]string
    DefaultOptions map[string][]string
}

A build request is flat. The caller does not need to decide whether a value is Require or Options. A formula makes that decision by reading the value through target.require or target.options.

Using Matrix Values

Formula callbacks read the selected values through target:

import "slices"

onRequire (proj, deps) => {
    if slices.contains(target.options["zlib"], "ON") {
        deps.require "madler/zlib", "v1.3.1"
    }
}

onBuild ctx => {
    if slices.contains(target.require["os"], "linux") {
        ...
    }

    if slices.contains(target.options["shared"], "ON") {
        ...
    }
}

In this example:

  • os is required build environment and is passed through dependency resolution.
  • zlib and shared are choices used only by formulas that support them.

Only values read by a formula affect that module's build variant. An unrelated request value does not create another artifact for the module.

Defaults

defaults supplies option values when the request does not provide them:

defaults {
    "debug": "OFF",
    "shared": "OFF",
}

An explicitly requested value replaces the default for the same key. Defaults apply only to Options; they do not set os, arch, or other Require values.

Dependencies

Suppose the request is:

os=linux, arch=arm64, zlib=ON

pnggroup/libpng reads the zlib option and depends on madler/zlib. Their effective matrices are:

pnggroup/libpng: os=linux, arch=arm64 | zlib=ON
madler/zlib:     os=linux, arch=arm64

The environment values reach the dependency. The zlib option stays with libpng because zlib's own formula does not use that option.

Possible Combinations

When a matrix has several values, each choice can produce another build variant:

os:     [linux, darwin]
arch:   [amd64, arm64]
shared: [ON, OFF]

This gives up to eight combinations:

2 operating systems x 2 architectures x 2 shared modes = 8

The matrix describes what can be selected. LLAR may prebuild some combinations and build others only when requested.

Filter

Not every combination is supported by every project. A formula can reject an unsupported selection with filter:

filter => {
    if slices.contains(target.require["arch"], "xtensa-esp32") {
        return false
    }
    return true
}

LLAR sets target before running the filter. When the filter returns false, LLAR stops before dependency discovery and building for that selection.

In this future example, LLAR may know how to build for xtensa-esp32, while the current library does not support that architecture. Filter rejects the library-specific limitation without removing the target from LLAR's platform support.

Best Practice: Keep Options Independent

Matrix keys should be understandable on their own. Choosing one key should not make another key appear, disappear, or change its meaning.

Dependent keys quickly create confusing combinations. For example:

tls:             [native, openssl]
openssl-linkage: [static, shared]

What does this mean?

tls=native, openssl-linkage=static

The linkage value has no effect because OpenSSL is not selected. It still makes the matrix look larger and may create different cache names for builds that are actually the same.

Represent the real choices as one option instead:

tls: [native, openssl-static, openssl-shared]

Independent options make formulas easier to write because:

  • every combination has a clear meaning;
  • defaults cannot produce half-valid selections;
  • cache and artifact names do not contain irrelevant values;
  • dependencies can receive Require values without learning option rules from another package;
  • LLAR can discover and test each matrix key without first satisfying another key.

Use Filter for genuine platform limitations. When two options describe one combined choice, merge them into one option instead of connecting them with Filter rules.

Clone this wiki locally