Skip to content

Cross compile Design

Rick Guo edited this page Aug 10, 2026 · 15 revisions

Cross Compilation

1. Summary

LLAR provides non-invasive C-family cross compilation. Existing Formulas keep using their current CMake, configure, pkg-config, and direct compiler commands. They do not add a second cross-compilation recipe or manually map the selected OS and architecture to compiler flags.

LLAR performs the cross-compilation work around the Formula:

  1. cmd/llar reads the selected target from formula.Matrix.
  2. It selects and prepares the target sysroot as an ordinary Formula.
  3. llvm.New prepares target-specific compiler and linker commands.
  4. c.NewTarget projects those commands into the C build mechanisms used by the Formula.
  5. build.Builder applies the target while the existing Formula hooks run.

The core rule is:

Formula code describes how to build the project. LLAR supplies where the result runs and the compiler, linker, SDK, and build-system settings required to build for that target.

Formula configuration remains authoritative. LLAR fills missing target settings but does not replace an explicit CMake toolchain, configure option, configure environment value, or explicit tool path.

The generic build module remains language-neutral. It receives a build.Target, applies its command patches, and does not know that the target represents C, LLVM, a sysroot, or cross compilation.

2. Research And Validation

The design was derived from existing package and build systems, then checked with Linux ELF, Darwin Mach-O, Autoconf, and real zlib experiments.

2.1 Ecosystem Research

The investigated systems place the same concepts at different boundaries:

System Model Relevant conclusion for LLAR
Conan 2 Build and host profiles are separate. A profile or generator can provide compiler executables and a separate tools.build:sysroot. Official examples usually consume complete external SDKs or cross toolchains. Compiler and sysroot can be separate prepared inputs, but the sysroot must be a complete target environment rather than an arbitrary libc file.
Xmake A standalone toolchain is normally a complete SDK containing tools, headers, and libraries. A build must receive a coherent target environment, even if LLAR stores its components separately.
Bazel Execution and target platforms select a language-specific toolchain. A C++ toolchain owns its tools, sysroot, flags, and runtimes. Generic build orchestration should depend on a target contract; C-specific facts belong to a C implementation.
Nixpkgs Compiler, libc, and other components are separate derivations, then bound by stdenv, package sets, and wrappers. Physical separation is viable, but LLAR does not adopt Nix's wrapper and runtime-store linkage model.

References:

This comparison led to two design choices.

First, LLAR manages a complete sysroot: target headers, startup objects, link-time libraries, loader conventions, and related SDK content. It does not treat libc.so as an ordinary library that can be exchanged independently of the target ABI.

Second, LLAR uses LLVM's per-invocation target model. Clang accepts a target and sysroot as command arguments, while a GCC cross compiler normally has its target encoded in the compiler instance. LLVM can therefore prepare one explicit command for each selected target without changing Formula code.

2.2 Linux Runtime-Linking Experiment

The Linux experiment used:

build platform:  Linux amd64, Ubuntu 18.04
target platform: Linux arm64
cross packages:  glibc 2.27 development packages
runtime:         Linux arm64, Ubuntu 22.04, glibc 2.35

An AArch64 shared library using malloc and free was built under /work, moved to /artifact, linked to a consumer, and run on the target system. The consumer printed:

probe=42

The generated library and consumer recorded:

DT_NEEDED: libc.so.6
VERNEED:   GLIBC_2.17
loader:    /lib/ld-linux-aarch64.so.1

It did not contain /work, the cross sysroot path, LLAR workspace paths, RPATH, or RUNPATH. The target loader bound malloc@GLIBC_2.17 and free@GLIBC_2.17 to the target machine's glibc 2.35.

This establishes the Linux runtime rule used by the proposal:

A sysroot supplies the build-time ABI baseline. It does not replace the target operating system's runtime libc or change the standard dynamic-linking convention.

The experiment used glibc 2.27 packages. The observed symbols required only GLIBC_2.17, but that does not make the experiment equivalent to building against a complete glibc 2.17 sysroot.

2.3 Darwin Runtime-Linking Experiment

The Darwin experiment used:

build platform:  Linux arm64
compiler:        Clang 14
linker:          ld64.lld
SDK:             macOS 14.4 SDK
target:          arm64-apple-macos11.0
runtime:         macOS arm64

The generated Mach-O library and consumer were moved away from their build directory and executed on macOS. The consumer printed:

probe=42

The artifacts contained @rpath/libprobe.dylib and /usr/lib/libSystem.B.dylib, but no /work, /sdk, or LLAR workspace path. The ad-hoc signature passed validation, and dyld bound _malloc and _free to the target system's libSystem.

This establishes the corresponding Darwin rule:

The SDK supplies build-time target declarations and link stubs. The installed artifact continues to use the target system's normal dyld and libSystem conventions.

The experiment validates command and runtime behavior. It does not define how a macOS SDK is acquired or distributed.

2.4 Configure-Script Experiment

Configure scripts do not share one universal target interface.

A GNU Autoconf probe built with a cross compiler but without --host failed with status 77:

checking whether we are cross compiling...
configure: error: cannot run C compiled programs.
If you meant to cross compile, use `--host'.

The same probe succeeded after receiving the target host tuple and reported cross compilation:

./configure --host=aarch64-apple-darwin
checking whether we are cross compiling... yes

However, zlib 1.3.1 uses a custom script named configure. It contains CHOST, does not declare --host, and rejects an injected --host argument.

The resulting rule is based on the actual script rather than its filename:

  • every configure script receives missing compiler and binary-tool commands;
  • LLAR appends --host only when the script contains that literal option;
  • CHOST alone does not match;
  • a Formula-provided --host is preserved.

2.5 zlib End-To-End Validation

The complete LLAR path was exercised with an unchanged zlib 1.3.1 source tree:

Build platform Target platform Target environment Result
Linux arm64 Darwin arm64 Xcode 14.5 SDK fixture macOS consumer printed zlib=1.3.1 compressed=27
Linux amd64 Linux arm64 Bootlin AArch64 glibc 2.24 sysroot Linux arm64 consumer printed zlib=1.3.1 compressed=27

These experiments validate target injection, source build, archive creation, artifact relocation, target linking, and target execution. The Bootlin fixture was glibc 2.24 and therefore does not validate the fixed glibc 2.17 Formula.

The experiments in this section exercise C libraries. The C++ command path is part of the same target contract, but these results are not presented as C++ runtime compatibility evidence.

3. Proposed Design

3.1 Non-Invasive Rule

Cross compilation is activated by the selected matrix, not by Formula code. For example, an existing CMake Formula continues to run:

cmake configure
cmake build
cmake install

When the selected OS or architecture differs from the host, LLAR supplies the target toolchain and sysroot to those same commands. The Formula does not add a cross-specific branch.

The same rule applies to configure scripts, pkg-config, and direct C tools. LLAR observes commands at the existing execbroker boundary and adds only the missing target defaults.

This rule assumes that the upstream build system has a cross-compilation path. LLAR supplies target facts but does not invent project-specific results for a configure-time test that requires executing a target binary.

3.2 Target And Sysroot Selection

Cross compilation is active when the selected target OS or architecture differs from the host. Built-in C targets select these default sysroot Formulas:

Target OS Target architecture Default sysroot Formula
linux amd64 bminor/glibc@glibc-2.17
linux arm64 bminor/glibc@glibc-2.17
darwin amd64 joseluisq/macosx-sdks@14.5
darwin arm64 joseluisq/macosx-sdks@14.5

A sysroot is an ordinary Formula and module.Version. cmd/llar passes it as a generic modules.Options.Roots entry so it participates in the same MVS resolution as the requested Formula. The module loader does not know that the extra root is a sysroot.

Linux reserves the presence of Matrix.Require["libc"] as the convention for a Formula-owned libc selection. If the key exists, cmd/llar does not add the fixed glibc Formula. LLAR does not normalize or interpret the value.

A sysroot Formula publishes its actual directory through existing C metadata:

--sysroot=<absolute output path>

The path may be the Formula output root or a directory below it. The CLI reads the path through x/metadata/cc; it does not infer the path from the module installation directory.

3.3 Sysroot Preparation

The sysroot path exists only after the sysroot Formula has been built or restored. Preparation therefore uses the same Builder.Build entrypoint twice:

  1. Prepare an LLVM toolchain for the target OS and architecture without a sysroot.
  2. Create a bootstrap C target from that toolchain.
  3. Build or restore the selected sysroot Formula.
  4. Parse the exact sysroot path from its C metadata.
  5. Prepare the final LLVM toolchain with the parsed sysroot.
  6. Create the final C target and build the resolved Formula graph.

The second build reuses the sysroot through the normal build cache. There is no separate cross-compilation builder and no sysroot knowledge inside build.

sequenceDiagram
  participant CLI as "cmd/llar"
  participant C as "internal/build/c"
  participant Modules as "internal/modules"
  participant LLVM as "internal/build/c/llvm"
  participant Build as "internal/build"
  participant Metadata as "x/metadata/cc"

  CLI->>C: "Sysroot(target OS, target arch)"
  C-->>CLI: "module.Version"
  CLI->>Modules: "Load(root, Matrix, Roots: [sysroot])"
  Modules-->>CLI: "one MVS-resolved graph"
  CLI->>LLVM: "New(OS, Arch)"
  LLVM-->>CLI: "bootstrap c.Toolchain"
  CLI->>C: "NewTarget(bootstrap Toolchain)"
  C-->>CLI: "bootstrap build.Target"
  CLI->>Build: "Build(sysroot)"
  Build-->>CLI: "sysroot Result"
  CLI->>Metadata: "Parse(Result.Metadata)"
  Metadata-->>CLI: "sysroot path"
  CLI->>LLVM: "New(OS, Arch, Sysroot)"
  LLVM-->>CLI: "final c.Toolchain"
  CLI->>C: "NewTarget(final Toolchain, Sysroot)"
  C-->>CLI: "final build.Target"
  CLI->>Build: "Build(resolved graph)"
  Build-->>CLI: "target Results"
Loading

3.4 LLVM Command Preparation

llvm.New owns LLVM-specific target spelling, linker selection, sysroot flag syntax, and executable lookup:

OS/architecture Clang target Raw linker Sysroot argument
linux/amd64 x86_64-linux-gnu ld.lld --sysroot=<path>
linux/arm64 aarch64-linux-gnu ld.lld --sysroot=<path>
darwin/amd64 x86_64-apple-macos10.13 ld64.lld -isysroot<path>
darwin/arm64 arm64-apple-macos11.0 ld64.lld -isysroot<path>

Compiler and linker commands are slices because mandatory arguments are part of the prepared command:

CC = ["/usr/bin/clang", "--target=aarch64-linux-gnu", "-fuse-ld=lld", "--sysroot=/sdk"]

The C module consumes these commands without deriving LLVM triples, choosing LLD, or translating sysroot syntax.

3.5 Command Injection

build.Builder converts each brokered command into build.Command, calls Target.Use, and applies the returned build.Patch within the build's existing execbroker.Scope.

This reaches commands launched through CMake helpers, Autotools helpers, gsh, and direct brokered execution. Child build tools receive target compiler commands through the CMake or configure projection rather than through a process-global environment.

CMake

On the first CMake configure command, c.Target lazily creates a toolchain file and appends it unless the Formula already supplied CMAKE_TOOLCHAIN_FILE or --toolchain. CMake build and install commands do not create or receive the file.

The toolchain file sets missing values for:

CMAKE_SYSTEM_NAME
CMAKE_SYSTEM_PROCESSOR
CMAKE_C_COMPILER
CMAKE_CXX_COMPILER
CMAKE_LINKER
CMAKE_AR
CMAKE_RANLIB
CMAKE_NM
CMAKE_STRIP

Linux uses CMAKE_SYSROOT. Darwin uses CMAKE_OSX_SYSROOT and CMAKE_OSX_ARCHITECTURES.

When a sysroot is active, CMake target lookup uses:

CMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER
CMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY
CMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY
CMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY

Dependency roots remain owned by x/cmake.Use. It accumulates the current CMAKE_FIND_ROOT_PATH, and x/cmake.Configure passes that value to CMake. The generated target file does not copy or cache the dependency list.

Configure Scripts

For a command whose basename is configure, c.Target supplies missing environment values:

CC CXX LD AR RANLIB NM STRIP

CC, CXX, and LD contain shell-quoted prepared command slices. Existing Formula values remain unchanged.

Before appending --host, the target reads the actual configure script:

  1. If the Formula already passed --host, preserve it and append nothing.
  2. If the script contains the literal --host, append the target Autoconf host tuple.
  3. If the script does not contain the option, run it with only the tool environment.

The target does not inject --build. The Autoconf host tuple is build-system input and is not forwarded to Clang as its target.

pkg-config

When a sysroot is active, pkg-config receives missing values for:

PKG_CONFIG_SYSROOT_DIR=<sysroot>
PKG_CONFIG_LIBDIR=<dependency paths plus target sysroot paths>

Dependency paths come from the current PKG_CONFIG_PATH, populated by x/pkgconfig.Use and existing build helpers. The C target adds:

<sysroot>/usr/lib64/pkgconfig
<sysroot>/usr/lib/pkgconfig
<sysroot>/usr/share/pkgconfig

An explicit PKG_CONFIG_LIBDIR remains unchanged. This prevents target builds from falling back to host .pc files.

Direct C Tools

Bare generic tool names are projected to prepared target commands:

Formula command Prepared command
cc, gcc, clang Toolchain.CC()
c++, g++, clang++ Toolchain.CXX()
ld, ld.lld, ld64.lld Toolchain.Linker()
ar, llvm-ar Toolchain.Archiver()
ranlib, llvm-ranlib Toolchain.Ranlib()
nm, llvm-nm Toolchain.NM()
strip, llvm-strip Toolchain.Strip()

The executable replaces the generic name, and mandatory target arguments are prepended before Formula arguments. A command containing an explicit tool path is not rewritten. Configure scripts are the exception because the resolved path is required for option inspection.

3.6 Precedence And Failure Behavior

Condition Behavior
Native target Use the existing native build path without target injection
Unsupported cross OS/architecture Leave the selected matrix to the Formula; do not create a built-in C target
Linux matrix contains libc Do not add the default glibc Formula
Sysroot Formula cannot be loaded or built Return the existing load or build error
Sysroot metadata has no sysroot Fail before building consumers
Required LLVM command is absent Fail while preparing the LLVM toolchain
Formula supplies a CMake toolchain Preserve it and do not append LLAR's toolchain file
Formula supplies a configure environment value Preserve the Formula value
Formula supplies --host Preserve it and do not append another value
Configure script does not declare --host Do not append --host
Formula uses an explicit C tool path Do not rewrite that command

4. Detailed Considerations

4.1 Runtime libc And SDK Semantics

The sysroot is a build input, not a runtime dependency that LLAR distributes with every library.

For GNU/Linux, the sysroot constrains available headers, startup objects, libraries, loader conventions, and glibc symbol versions. The resulting ELF continues to name the standard loader and libc.so.6; the target system provides their runtime implementations.

For Darwin, the SDK supplies headers, frameworks, target declarations, and link stubs. The resulting Mach-O continues to bind through dyld and target system install names such as /usr/lib/libSystem.B.dylib.

LLAR does not inject a sysroot workspace path as RPATH or RUNPATH.

4.2 Formula Compatibility Testing

Formula compatibility is tested on the corresponding target machine. The llarhub GitHub Actions matrix runs:

llar test <formula> --os <runner-os> --arch <runner-arch>

on Linux and Darwin amd64 and arm64 runners. Each runner builds the Formula for its own platform and executes its onTest consumer there.

Each job is native to its runner; no artifact is transferred between platforms. Target-machine llar test establishes that the Formula and its consumer are valid for that platform. Cross compilation is expected to produce equivalent target semantics because LLAR projects the selected OS, architecture, ABI, SDK/sysroot, and build-system facts through its injection path. Representative cross-build experiments validate that projection.

The two test responsibilities remain separate:

Test Responsibility
Target-machine llar test Verify the Formula builds and its installed result is usable on that platform
Cross-compilation tests and experiments Verify matrix selection, sysroot preparation, LLVM commands, command projection, and absence of host-path leakage

4.3 Build-System Coverage

Target projection is checked independently for each supported command shape:

  • CMake configure receives a lazily generated toolchain file, while build and install commands do not.
  • GNU Autoconf-compatible scripts receive --host.
  • custom configure scripts such as zlib do not receive unsupported options.
  • pkg-config searches target dependency and sysroot paths rather than host defaults.
  • direct C compiler, linker, archiver, and inspection commands resolve to the prepared target tools.
  • explicit Formula configuration remains unchanged in every path.

The runtime experiments additionally inspect ELF or Mach-O architecture, dynamic dependencies, symbol bindings, runtime search paths, and build workspace path leakage before running a target consumer.

5. Module Specification

5.1 internal/build

The build module exposes a language-neutral command transformation contract:

package build

type Command struct {
    Name string
    Args []string
    Env  []string
    Dir  string
}

type Patch struct {
    Name       string
    PrependArg []string
    AppendArg  []string
    Env        []string
}

type Target interface {
    Use(Command) Patch
}

type Options struct {
    // existing build options
    Target Target
}

Boundary:

  • Owns applying one target patch to one brokered command and scoping that target to a Builder.Build call.
  • Does not select a target, know C, select a sysroot, parse C metadata, or construct LLVM commands.
  • Does not accept an external command middleware; callers provide only the target contract.

5.2 internal/build/c

The C module defines prepared C-family commands and implements build.Target:

package c

type Toolchain struct {
    // prepared command data
}

func NewToolchain(
    cc, cxx, linker []string,
    archiver, ranlib, nm, strip string,
) Toolchain

func (t Toolchain) CC() []string
func (t Toolchain) CXX() []string
func (t Toolchain) Linker() []string
func (t Toolchain) Archiver() string
func (t Toolchain) Ranlib() string
func (t Toolchain) NM() string
func (t Toolchain) Strip() string

type Config struct {
    Matrix    string
    Toolchain Toolchain
    Sysroot   string
}

func Sysroot(targetOS, targetArch string) (module.Version, bool)
func NewTarget(config Config) (*Target, error)
func (t *Target) Use(cmd build.Command) build.Patch

Boundary:

  • Toolchain owns only prepared C-family command data.
  • Target owns CMake, configure, pkg-config, and direct C-tool projection.
  • Sysroot owns the fixed built-in C sysroot selection table.
  • The module does not load or build Formulas, resolve LLVM executables, derive LLVM target arguments, execute commands, or install middleware.

5.3 internal/build/c/llvm

The LLVM module constructs a C toolchain from target facts:

package llvm

type Config struct {
    OS      string
    Arch    string
    Sysroot string
}

type Toolchain struct {
    c.Toolchain
}

func New(config Config) (*Toolchain, error)

Boundary:

  • Owns LLVM target triples, target compiler arguments, linker selection, sysroot argument syntax, and LLVM executable lookup.
  • Returns prepared C commands consumed by c.NewTarget.
  • Does not select or build a sysroot Formula, interpret matrix conventions, understand CMake or configure scripts, or depend on build.Builder.

5.4 internal/modules

The module loader accepts generic additional root requirements:

package modules

type Options struct {
    FormulaStore repo.Store
    Matrix       formula.Matrix
    Roots        []module.Version
}

func Load(
    ctx context.Context,
    main module.Version,
    opts Options,
) ([]*Module, error)

Boundary:

  • Owns Formula loading, matrix injection, dependency resolution, and version conflict resolution.
  • Treats Roots as ordinary requirements in the same MVS graph.
  • Does not know which root is a libc or sysroot, parse build metadata, or import the C module.

5.5 cmd/llar/internal

The command entrypoint remains the composition root:

func buildModule(
    ctx context.Context,
    store repo.Store,
    modPath, version string,
    matrix formula.Matrix,
    runTest bool,
) error

Boundary:

  • Owns native-versus-cross selection, the libc matrix convention, default sysroot composition, sysroot metadata parsing, LLVM and C target creation, and the order of calls to modules.Load and Builder.Build.
  • Passes only generic roots to modules and only build.Target to build.
  • Does not implement CMake, configure, pkg-config, compiler, or linker rewriting itself.

6. User Stories

6.1 Cross-Build A CMake Formula For Linux arm64

The user runs llar make for Linux arm64 on another platform. The Formula uses its existing x/cmake build path and contains no cross-compilation branch.

sequenceDiagram
  participant User
  participant CLI as "cmd/llar"
  participant C as "internal/build/c"
  participant Modules as "internal/modules"
  participant LLVM as "internal/build/c/llvm"
  participant Build as "internal/build"
  participant Metadata as "x/metadata/cc"
  participant CMake as "x/cmake"
  participant Broker as "execbroker"

  User->>CLI: "llar make <module>@<version> --os linux --arch arm64"
  CLI->>C: "Sysroot(linux, arm64)"
  C-->>CLI: "bminor/glibc@glibc-2.17"
  CLI->>Modules: "Load Formula with sysroot Root"
  Modules-->>CLI: "one resolved module graph"
  CLI->>LLVM: "New(linux, arm64, no sysroot)"
  LLVM-->>CLI: "bootstrap compiler and linker commands"
  CLI->>C: "NewTarget(bootstrap Toolchain)"
  CLI->>Build: "Build(selected glibc Formula)"
  Build-->>CLI: "sysroot Result"
  CLI->>Metadata: "Parse(Result.Metadata)"
  Metadata-->>CLI: "sysroot path"
  CLI->>LLVM: "New(linux, arm64, sysroot path)"
  LLVM-->>CLI: "final compiler and linker commands"
  CLI->>C: "NewTarget(final Toolchain, sysroot path)"
  CLI->>Build: "Build(resolved graph with C Target)"
  Build->>Broker: "open scoped target middleware"
  Build->>CMake: "run existing Formula onBuild"
  CMake->>Broker: "cmake configure"
  Broker->>C: "Target.Use(cmake command)"
  C-->>Broker: "append generated toolchain file"
  Broker->>CMake: "execute target-configured CMake"
  CMake->>Broker: "cmake build and install"
  C-->>Broker: "no configure patch"
  Build-->>CLI: "target build Results"
  CLI-->>User: "module result"
Loading

Dependency roots accumulated by x/cmake.Use remain in the configure environment. The C target contributes target compilers, the sysroot, and find root modes without replacing those dependency roots.

6.2 Cross-Build An Autoconf Formula For Darwin arm64

The Formula calls its existing configure helper. The actual source script declares --host, so LLAR supplies both the prepared LLVM commands and the Autoconf target tuple.

sequenceDiagram
  participant User
  participant CLI as "cmd/llar"
  participant Modules as "internal/modules"
  participant LLVM as "internal/build/c/llvm"
  participant C as "internal/build/c"
  participant Build as "internal/build"
  participant Auto as "x/autotools"
  participant Broker as "execbroker"

  User->>CLI: "llar make <module>@<version> --os darwin --arch arm64"
  CLI->>C: "Sysroot(darwin, arm64)"
  C-->>CLI: "joseluisq/macosx-sdks@14.5"
  CLI->>Modules: "resolve Formula and SDK in one graph"
  CLI->>LLVM: "prepare bootstrap commands"
  CLI->>C: "prepare bootstrap C Target"
  CLI->>Build: "build or restore SDK Formula"
  Build-->>CLI: "SDK sysroot metadata"
  CLI->>LLVM: "New(darwin, arm64, SDK path)"
  LLVM-->>CLI: "Clang and ld64.lld commands"
  CLI->>C: "NewTarget(final Toolchain, SDK path)"
  CLI->>Build: "Build(graph with C Target)"
  Build->>Broker: "open scoped target middleware"
  Auto->>Broker: "run resolved source configure script"
  Broker->>C: "Target.Use(Command with Name, Args, Env, Dir)"
  C->>C: "preserve explicit values and inspect script"
  C-->>Broker: "set missing tools and append --host"
  Broker->>Auto: "execute configure in cross mode"
  Auto->>Broker: "build and install"
  Build-->>CLI: "Darwin arm64 result"
  CLI-->>User: "module result"
Loading

The Autoconf host tuple makes the script enter cross mode. Clang receives its separate target and SDK arguments from llvm.Toolchain; --host is not used as a compiler argument.

6.3 Cross-Build A Formula With A Custom Configure Script

zlib uses a custom script that is named configure but does not accept the Autoconf --host option.

sequenceDiagram
  participant Formula
  participant Auto as "x/autotools"
  participant Broker as "execbroker"
  participant C as "c.Target"
  participant Script as "custom configure"

  Formula->>Auto: "run existing Configure call"
  Auto->>Broker: "execute source/configure"
  Broker->>C: "Target.Use(Command)"
  C->>C: "add missing CC, CXX, LD, AR, RANLIB, NM, STRIP"
  C->>C: "read script: CHOST exists, literal --host does not"
  C-->>Broker: "Patch with tool environment only"
  Broker->>Script: "execute without --host"
Loading

The Formula remains unchanged. The prepared compiler command still selects the target, while LLAR avoids imposing an unsupported build-system option.

6.4 Let A Formula Own libc Selection

A Linux Formula that declares a libc requirement owns its libc variants. LLAR preserves the matrix and does not add its fixed glibc Formula.

sequenceDiagram
  participant User
  participant CLI as "cmd/llar"
  participant Modules as "internal/modules"
  participant LLVM as "internal/build/c/llvm"
  participant C as "internal/build/c"
  participant Build as "internal/build"

  User->>CLI: "llar make <module>@<version> --os linux --arch arm64 --require libc=<value>"
  CLI->>CLI: "observe Matrix.Require contains libc"
  CLI->>CLI: "skip the fixed glibc Root"
  CLI->>Modules: "Load Formula with unchanged Matrix"
  Modules->>Modules: "resolve Formula-declared dependencies"
  Modules-->>CLI: "resolved graph"
  CLI->>LLVM: "prepare Linux arm64 commands"
  CLI->>C: "NewTarget(prepared Toolchain)"
  CLI->>Build: "Build Formula graph"
  Build-->>CLI: "target result"
  CLI-->>User: "module result"
Loading

LLAR does not interpret <value> or replace the Formula's selected libc with the built-in default.

6.5 Verify A Formula On Its Target Platform

GitHub Actions verifies each Formula on the corresponding native runner. This is the compatibility test for the Formula and its installed consumer; it is not a transfer test for an artifact produced by another runner.

sequenceDiagram
  participant CI as "GitHub Actions target runner"
  participant CLI as "llar test"
  participant Modules as "internal/modules"
  participant Build as "internal/build"
  participant Formula

  CI->>CLI: "llar test <formula> --os <runner OS> --arch <runner arch>"
  CLI->>Modules: "load Formula and dependencies"
  Modules-->>CLI: "resolved native graph"
  CLI->>Build: "Build(graph, RunTest: true)"
  Build->>Formula: "run onBuild on cache miss"
  Build->>Formula: "run root onTest"
  Formula-->>Build: "consumer result"
  Build-->>CLI: "success or test error"
  CLI-->>CI: "job result"
Loading

The target runner proves that the Formula is valid for that OS and architecture. LLAR's cross-compilation tests separately prove that the same target facts are injected when the build host differs.

Clone this wiki locally