-
Notifications
You must be signed in to change notification settings - Fork 4
Cross compile Design
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.
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:
-
cmd/llarreads the selected target fromformula.Matrix. - It either prepares the built-in sysroot as a hidden Formula or preserves a
libcrequirement so the requested Formula can select its own sysroot. -
llvm.Newprepares target-specific compiler and linker commands. -
c.NewTargetprojects those commands into the C build mechanisms used by the Formula. -
build.Builderapplies the target while the existing Formula hooks run.
These are two distinct sysroot modes. Without a libc key, LLAR supplies the
built-in sysroot without exposing it as a package dependency. With a libc
key, LLAR does not add that default: the Formula resolves its selected libc or
SDK through the normal dependency graph and passes the resulting directory to
its build helper.
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.
The design was derived from existing package and build systems, then checked with Linux ELF, Darwin Mach-O, Autoconf, and real zlib experiments.
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:
- Conan cross building
- Conan toolchain packages
- Xmake toolchain configuration
- Nixpkgs cross compilation
- Bazel toolchains
- Bazel C++ toolchain configuration
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.
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.
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
libSystemconventions.
The experiment validates command and runtime behavior. It does not define how a macOS SDK is acquired or distributed.
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
--hostonly when the script contains that literal option; -
CHOSTalone does not match; - a Formula-provided
--hostis preserved.
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.
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.
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 |
The two modes differ in both sysroot ownership and injection:
Default injection: no libc key |
Custom injection: libc key exists |
|
|---|---|---|
| Sysroot selection |
cmd/llar selects the built-in sysroot Formula for the target OS and architecture. |
The Formula interprets the libc value and maps it to an ordinary dependency. The key itself does not select or inject a sysroot. |
| Module graph | The sysroot is loaded and built as a hidden, independent graph. | The selected libc or SDK belongs to the requested Formula's normal dependency graph. |
| Path source | The sysroot Formula publishes --sysroot=<path> in C metadata; cmd/llar parses that metadata. |
The Formula obtains the dependency output with ctx.outputDir(dep) and decides which directory is the sysroot. cmd/llar does not parse the dependency's C metadata. |
| Target preparation |
cmd/llar passes the path to llvm.New and c.NewTarget, so the resulting build.Target contains the default sysroot. |
cmd/llar creates llvm.New and c.NewTarget without a sysroot. The target contains only the selected compiler and binary-tool commands. |
| Injection point |
c.Target applies the prepared compiler, build-system, and pkg-config defaults at the command boundary. The Formula does not call Sysroot. |
The Formula explicitly passes the path to cmake.Sysroot or autotools.Sysroot. That helper injects the path into its own CMake or Autotools build. |
| Dependency visibility | Hidden from Project.Deps, build results, and artifact dependencies. |
Visible as an ordinary dependency. The preserved libc matrix value distinguishes the artifact identity. |
The two injection paths are therefore:
default: sysroot metadata -> cmd/llar -> llvm.New(Sysroot) + c.NewTarget(Sysroot)
custom: dependency output -> Formula -> cmake.Sysroot or autotools.Sysroot
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.
In the default mode, the 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. This metadata path is used only for the hidden default
sysroot.
The presence of Matrix.Require["libc"] switches to the custom mode. It only
prevents cmd/llar from selecting the built-in sysroot; it does not make the
matrix value a path, add a dependency, or inject compiler flags. The Formula
must perform those steps through its existing dependency and build-helper APIs.
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:
-
cmd/llarcompares the matrix with the Linux amd64 host and activates cross compilation. -
c.Sysroot("linux", "arm64")selectsbminor/glibc@glibc-2.17. -
modules.Loadresolves the requested zlib graph. A secondmodules.Loadloads the default glibc as an independent root so itsfromVerFormula is selected without adding glibc to zlib's graph or artifact dependencies. -
llvm.Newfirst prepares a bootstrap command without a sysroot:<clang> --target=aarch64-linux-gnu -fuse-ld=lldc.NewTargetprojects this command into a bootstrapbuild.Target. -
Builder.Buildbuilds or restores the hidden glibc graph with that bootstrap target. Its C metadata returns the actual directory:--sysroot=<glibc output directory> -
llvm.Newprepares the final compiler command:<clang> --target=aarch64-linux-gnu -fuse-ld=lld \ --sysroot=<glibc output directory>c.NewTargetreceives the same path. The finalbuild.Targettherefore owns both the prepared LLVM commands and the default sysroot. -
Builder.Buildruns the unchanged zlib Formula. When the Formula launches the source tree'sconfigure,c.Targetsupplies missing values such asCC,CXX,LD,AR,RANLIB,NM, andSTRIP. TheCCandCXXcommands already contain--sysroot=<glibc output directory>. For CMake andpkg-config, the same target supplies their sysroot lookup settings. The Formula does not callcmake.Sysrootorautotools.Sysroot. -
zlib 1.3.1 has a custom
configurescript. The script containsCHOSTbut does not declare the literal--host, so LLAR does not append an unsupported--hostoption. The preparedCCcommand still makes its compile and link probes target Linux arm64. -
The resulting zlib artifact is recorded for
arm64-linux. The hidden glibc Formula does not appear in zlib'sProject.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 Section 4.5.
When the matrix contains libc, steps 2 through 6 are replaced by the
Formula-owned sysroot flow described in Section 4.4.
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.
Assume example/custom-libc@v1.0.0 must build for Linux arm64 with glibc 2.24
instead of LLAR's built-in sysroot. The libc key only disables default
injection. The Formula must separately map its value to a dependency, obtain
that dependency's output directory, and pass the directory to its build helper:
id "example/custom-libc"
fromVer "v1.0.0"
onRequire (proj, deps) => {
deps.require "bminor/glibc", target.require["libc"][0]
}
onBuild ctx => {
root := ctx.outputDir(ctx.Proj.Deps[0])
c := cmake.new(ctx.SourceDir, ctx.SourceDir+"/_build", ctx.outputDir)
c.sysroot root
c.configure
c.build
c.install
}The Linux amd64 worker receives this request:
llar make example/custom-libc@v1.0.0 --os linux --arch arm64 \
--require libc=glibc-2.24
The selected matrix contains libc=glibc-2.24. LLAR executes the following
steps:
-
cmd/llarcompares the matrix with the Linux amd64 host and activates cross compilation. -
The
libckey is present, socmd/llardoes not select or load the built-in glibc Formula as a hidden root. -
modules.Loadresolvesexample/custom-libcwith the unchanged matrix. ItsonRequiremapsglibc-2.24tobminor/glibc@glibc-2.24, producing one ordinary graph that contains both modules. -
llvm.Newprepares the target command without a sysroot:<clang> --target=aarch64-linux-gnu -fuse-ld=lldc.NewTargetprojects that toolchain into thebuild.Targetused for the graph. Unlike the default mode, neither call receives the glibc output path. -
Builder.Buildbuilds or restores the glibc dependency first, then runs the requested Formula after the dependency output is available. There is no separate sysroot build call. -
The Formula obtains the selected dependency directory through
ctx.outputDir(ctx.Proj.Deps[0]). This is the custom sysroot path; it does not come fromx/metadata/cc. -
The Formula calls
c.sysroot(root).cmake.Sysrootturns that path intoCMAKE_SYSROOT,CMAKE_OSX_SYSROOT, and the CMake find-root modes beforec.configureruns.c.Targetindependently supplies the Linux arm64 compiler, linker, and binary tools, but does not add a sysroot. -
The resulting artifact is recorded with the
libc=glibc-2.24matrix value. The selected glibc remains inProject.Deps, build results, and artifact dependencies because the Formula declared it as an ordinary dependency.
An Autotools Formula performs the same explicit handoff with
a.sysroot(root). autotools.Sysroot adds the sysroot to compiler and linker
flags and configures pkg-config lookup before a.configure runs. If the
Formula invokes a compiler directly instead of using the CMake or Autotools
helper, it must pass the custom sysroot argument to that command itself; the
libc key does not rewrite direct commands with the custom path.
This mode has no hidden second modules.Load and no CLI-owned sysroot build.
The libc value participates in the requested artifact's matrix, the selected
glibc remains an ordinary visible dependency, and the Formula decides that its
output is the sysroot. LLAR does not interpret glibc-2.24, parse that
dependency's sysroot metadata, or replace it with the built-in default. It
continues to own only the Linux arm64 compiler and binary-tool commands.
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.
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.
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.
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:
- If the Formula already passed
--host, preserve it and append nothing. - If the script contains the literal
--host, append the target Autoconf host tuple. - 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.
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.
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.
| 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 |
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.
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 |
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-configsearches 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.
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.Buildcall. - 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.
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.PatchBoundary:
-
Toolchainowns only prepared C-family command data. -
Targetowns CMake, configure,pkg-config, and direct C-tool projection. -
Sysrootowns 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.
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.
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.
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.
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.
The command entrypoint remains the composition root:
func buildModule(
ctx context.Context,
store repo.Store,
modPath, version string,
matrix formula.Matrix,
runTest bool,
) errorBoundary:
- Owns native-versus-cross selection, the
libcmatrix convention, default sysroot composition, sysroot metadata parsing, LLVM and C target creation, and the order of calls tomodules.LoadandBuilder.Build. - Loads the requested Formula and hidden default sysroot as separate graphs,
then passes only
build.Targetto the requested Formula build. - Does not implement CMake, configure,
pkg-config, compiler, or linker rewriting itself.
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"
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"
sequenceDiagram
participant CLI as "cmd/llar"
participant LLVM as "internal/build/c/llvm"
participant C as "internal/build/c"
participant Build as "internal/build"
participant Metadata as "x/metadata/cc"
CLI->>LLVM: "New(linux, arm64)"
LLVM-->>CLI: "bootstrap toolchain"
CLI->>C: "NewTarget(bootstrap toolchain)"
CLI->>Build: "Build(hidden glibc graph)"
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)"
sequenceDiagram
participant Build as "internal/build"
participant Zlib as "zlib Formula"
participant Broker as "execbroker"
participant C as "internal/build/c"
participant Configure as "zlib configure"
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"
Zlib-->>Build: "onBuild complete"
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.
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 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"
CLI->>C: "Sysroot(darwin, arm64)"
C-->>CLI: "joseluisq/macosx-sdks@14.5"
CLI->>Modules: "Load(requested Formula)"
CLI->>Modules: "Load(hidden SDK)"
CLI->>LLVM: "New(darwin, arm64)"
CLI->>C: "NewTarget(bootstrap toolchain)"
CLI->>Build: "Build(hidden SDK graph)"
Build-->>CLI: "SDK sysroot metadata"
CLI->>LLVM: "New(darwin, arm64, SDK path)"
CLI->>C: "NewTarget(final toolchain, SDK path)"
sequenceDiagram
participant Build as "internal/build"
participant Auto as "x/autotools"
participant Broker as "execbroker"
participant C as "internal/build/c"
Build->>Auto: "run existing onBuild"
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"
Auto-->>Build: "onBuild complete"
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.
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 Build as "internal/build"
participant Formula
CI->>CLI: "llar test <formula> --os <runner OS> --arch <runner arch>"
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"
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.