Skip to content

Cross compile Design

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

Cross Compilation

1. Motivation

Cross-compiling a C library is not merely changing its target architecture. The compiler, linker, target headers and libraries, build-system conventions, and runtime ABI must all describe the same target. This makes C cross-compilation substantially more complex than a native build.

That complexity would not need to be handled if LLAR could always build on a machine matching the target. LLAR is a cloud build package manager, however, and must produce packages for platforms that differ from the available build workers. It therefore has two ways to cover a target platform:

Approach Tradeoff
Build on a machine that matches the target The build environment naturally matches the target, but every platform requires corresponding build capacity. Purchasing and maintaining that capacity is expensive, and machines based on specialized chips may not be deployable in a conventional data center.
Cross-compile on an available build machine The build platform no longer needs to match the target, but LLAR must correctly prepare and inject the target toolchain, sysroot, and build-system settings.

Building on matching machines avoids cross-compilation complexity, but its cost grows with every supported platform. It also cannot cover specialized target hardware that cannot be deployed in the build service's data center. Cross-compilation removes that hardware requirement. Although it introduces the C-specific complexity described above, the research and experiments in this proposal show that the complexity is technically tractable.

LLAR therefore uses cross-compilation to expand platform coverage without requiring matching build capacity for every target. Because this is a property of the build service rather than an individual library, LLAR owns target selection and injection. Formula authors continue to describe only the library's normal build process.

LLAR is also a multi-language package manager. Centralizing C cross-compilation must not turn C concepts into requirements for every language: another language may represent its target, runtime, and toolchain differently and may not use a C sysroot at all. The shared build path therefore accepts only the language-neutral build.Target contract. C, LLVM, libc, and sysroot semantics remain inside the C target implementation, leaving other languages free to provide their own target behavior without changing build.Builder.

2. 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.

For example, assume a Linux amd64 worker runs:

llar make madler/zlib@v1.3.1 --os linux --arch arm64

The zlib Formula still runs its normal configure, make, and install steps. LLAR separately prepares the default Linux arm64 sysroot, turns the selected target into Clang and LLVM tool commands, and supplies those commands when zlib's existing build steps execute. The Formula does not contain a host-to-target branch.

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.

3. 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.

3.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.

3.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.

3.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.

3.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.

3.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.

4. Proposed Design

4.1 Non-Invasive Rule

Cross compilation is activated by the selected matrix, not by Formula code. For example, the zlib Formula continues to run the source project's normal commands:

./configure --prefix=<install directory>
make
make install

When the selected OS or architecture differs from the host, LLAR supplies the target toolchain and sysroot to those same commands. CMake Formulas likewise continue to run configure, build, and install without 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.

4.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 default sysroot is an ordinary Formula and module.Version, but it is a hidden input owned by cmd/llar. The CLI loads it as the root of a separate module graph. It does not add the sysroot to the requested Formula's MVS graph, Project.Deps, dependency build results, or artifact dependencies. The module loader handles two ordinary load operations and has no sysroot-specific API.

The presence of Matrix.Require["libc"] is the convention for a Formula-owned libc or SDK selection. If the key exists, cmd/llar does not add the built-in sysroot Formula for that C target. LLAR does not normalize or interpret the value, load another sysroot graph, or inspect the custom dependency's metadata. The Formula resolves that dependency through its normal graph and passes the selected directory to its build helper's Sysroot method.

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.

4.3 Default Sysroot Flow: zlib For Linux arm64

This example defines the default-sysroot behavior. Assume the build worker is Linux amd64 and the user requests Linux arm64:

llar make madler/zlib@v1.3.1 --os linux --arch arm64

The selected matrix is arm64-linux and contains no libc key. LLAR executes the following steps:

  1. cmd/llar compares the matrix with the Linux amd64 host and activates cross compilation.

  2. c.Sysroot("linux", "arm64") selects bminor/glibc@glibc-2.17.

  3. modules.Load resolves the requested zlib graph. A second modules.Load loads the default glibc as an independent root so its fromVer Formula is selected without adding glibc to zlib's graph or artifact dependencies.

  4. llvm.New first prepares a bootstrap command without a sysroot:

    <clang> --target=aarch64-linux-gnu -fuse-ld=lld
    

    c.NewTarget projects this command into a bootstrap build.Target.

  5. Builder.Build builds or restores the hidden glibc graph with that bootstrap target. Its C metadata returns the actual directory:

    --sysroot=<glibc output directory>
    
  6. llvm.New prepares the final compiler command:

    <clang> --target=aarch64-linux-gnu -fuse-ld=lld \
      --sysroot=<glibc output directory>
    

    c.NewTarget wraps the final toolchain and sysroot as the target used for zlib.

  7. Builder.Build runs the unchanged zlib Formula. When the Formula launches the source tree's configure, c.Target supplies missing values such as CC, CXX, LD, AR, RANLIB, NM, and STRIP.

  8. zlib 1.3.1 has a custom configure script. The script contains CHOST but does not declare the literal --host, so LLAR does not append an unsupported --host option. The prepared CC command still makes its compile and link probes target Linux arm64.

  9. The resulting zlib artifact is recorded for arm64-linux. The hidden glibc Formula does not appear in zlib's Project.Deps, build results, or artifact dependencies.

The same preparation order applies to other built-in C targets. Darwin changes the LLVM triple, linker, and sysroot spelling to the values in the next section. When the matrix contains libc, steps 2 through 6 are replaced by the Formula-owned sysroot flow described in Section 7.3.

This is the proposed execution flow for the fixed glibc 2.17 Formula. The Linux arm64 zlib experiment in Section 3.5 used a Bootlin glibc 2.24 sysroot and validates the command and artifact path, not that fixed Formula version.

4.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.

4.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.

For a Formula-owned sysroot, x/cmake.Sysroot(root) sets both CMAKE_SYSROOT and CMAKE_OSX_SYSROOT. CMake ignores the Apple variable on non-Apple targets; on Apple targets it emits -isysroot in addition to the generic sysroot setting. The helper applies the same find-root modes shown below, so the Formula-owned path replaces the sysroot part of the generated target file without requiring another file.

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.

For a Formula-owned sysroot, x/autotools.Sysroot(root) adds both --sysroot=<root> and -isysroot<root> to CPPFLAGS, CFLAGS, CXXFLAGS, and LDFLAGS. It also sets the missing pkg-config sysroot values used by the Autotools workflow. LLAR still supplies the target compiler and tool commands; the Formula supplies only its selected root.

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.

4.6 Precedence And Failure Behavior

Condition Behavior
Native target Use the existing native build path without target injection
llar test target differs from the host Fail before module loading; OnTest must run on the target platform
Unsupported cross OS/architecture Leave the selected matrix to the Formula; do not create a built-in C target
C target matrix contains libc Do not add the built-in sysroot 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

5. Detailed Considerations

5.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.

5.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.

llar test is valid only when the selected target matches the host. If either the OS or architecture differs, LLAR fails before module loading. It does not cross-build and then skip OnTest, because that would report a successful test without running the target consumer.

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

5.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.

6. Module Specification

6.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.

6.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.

6.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.

6.4 x/cmake

The CMake helper accepts a Formula-owned sysroot directory:

package cmake

func (c *CMake) Sysroot(root string)

Boundary:

  • Owns CMake sysroot definitions and CMake find-root modes for the supplied directory.
  • Does not select or resolve the directory, inspect the target matrix, prepare compiler commands, or configure pkg-config.

6.5 x/autotools

The Autotools helper accepts the same Formula-owned sysroot directory:

package autotools

func (a *AutoTools) Sysroot(root string)

Boundary:

  • Owns Autotools compiler, preprocessor, linker, and pkg-config environment for the supplied directory.
  • Does not select or resolve the directory, inspect the target matrix, or prepare compiler and tool commands.

6.6 internal/modules

The module loader resolves one requested root and its dependencies:

package modules

type Options struct {
    FormulaStore repo.Store
    Matrix       formula.Matrix
}

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

Boundary:

  • Owns Formula loading, matrix injection, dependency resolution, and version conflict resolution.
  • Does not combine the requested Formula with LLAR's hidden default sysroot.
  • Does not know whether a separately loaded root is a libc or sysroot, parse build metadata, or import the C module.

6.7 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.
  • Loads the requested Formula and hidden default sysroot as separate graphs, then passes only build.Target to the requested Formula build.
  • Does not implement CMake, configure, pkg-config, compiler, or linker rewriting itself.

7. User Stories

7.1 Cross-Build zlib From Linux amd64 To Linux arm64

The user requests zlib 1.3.1 for Linux arm64 from a Linux amd64 worker. This story shows how the modules in this proposal are called; the Formula itself keeps zlib's normal build sequence.

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 Sysroot as "glibc Formula"
  participant Zlib as "zlib Formula"
  participant Broker as "execbroker"
  participant Configure as "zlib configure"

  User->>CLI: "llar make madler/zlib@v1.3.1 --os linux --arch arm64"
  CLI->>C: "Sysroot(linux, arm64)"
  C-->>CLI: "bminor/glibc@glibc-2.17"
  CLI->>Modules: "Load(zlib, arm64-linux)"
  Modules-->>CLI: "zlib graph"
  CLI->>Modules: "Load(glibc, arm64-linux)"
  Modules-->>CLI: "hidden glibc graph"
  CLI->>LLVM: "New(linux, arm64)"
  LLVM-->>CLI: "Clang target command without sysroot"
  CLI->>C: "NewTarget(bootstrap toolchain)"
  CLI->>Build: "Build(hidden glibc graph)"
  Build->>Sysroot: "run onBuild on cache miss"
  Build-->>CLI: "sysroot Result"
  CLI->>Metadata: "Parse(Result.Metadata)"
  Metadata-->>CLI: "glibc output path"
  CLI->>LLVM: "New(linux, arm64, glibc path)"
  LLVM-->>CLI: "final Clang and LLVM commands"
  CLI->>C: "NewTarget(final toolchain, glibc path)"
  CLI->>Build: "Build(zlib graph, final C Target)"
  Build->>Zlib: "run existing onBuild"
  Zlib->>Broker: "run source/configure"
  Broker->>C: "Target.Use(configure command)"
  C->>C: inject tools and inspect "--host" support
  C-->>Broker: tool environment without "--host"
  Broker->>Configure: "execute for Linux arm64"
  Configure-->>Zlib: "configured makefiles"
  Zlib->>Broker: "run make and install"
  Build-->>CLI: "zlib arm64 Result"
  CLI-->>User: "zlib module result"
Loading

The two modules.Load calls remain independent. The first graph defines the requested package and its visible dependencies. The second selects the default glibc Formula, including its fromVer, but does not add glibc to zlib's graph. build.Builder is unchanged: it receives a different C target for each of its two ordinary Build calls.

7.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: "load requested Formula graph"
  CLI->>Modules: "load hidden SDK graph separately"
  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.

7.3 Let A Formula Own libc Or SDK Selection

A C Formula that declares a libc requirement owns the selected libc or SDK. LLAR preserves the matrix and does not add its built-in sysroot 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"
  participant Formula
  participant CMake as "x/cmake"

  User->>CLI: "llar make <module>@<version> --os <os> --arch <arch> --require libc=<value>"
  CLI->>CLI: "observe Matrix.Require contains libc"
  CLI->>CLI: "skip the built-in sysroot Root"
  CLI->>Modules: "Load Formula with unchanged Matrix"
  Modules->>Modules: "resolve Formula-declared dependencies"
  Modules-->>CLI: "resolved graph"
  CLI->>LLVM: "prepare target commands without a sysroot"
  CLI->>C: "NewTarget(prepared Toolchain)"
  CLI->>Build: "Build Formula graph"
  Build->>Formula: "run onBuild with resolved dependency outputs"
  Formula->>CMake: "Sysroot(selected dependency output)"
  CMake->>CMake: "prepare --sysroot and Apple -isysroot settings"
  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. The Formula chooses which dependency output is the sysroot; LLAR continues to own only the target tools.

7.4 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