-
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 selects and prepares the target sysroot as an ordinary Formula.
-
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.
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, 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.
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.
When no libc key exists, the default sysroot path exists only after the
sysroot Formula has been built or restored. Preparation therefore uses the same
Builder.Build entrypoint twice:
- Prepare an LLVM toolchain for the target OS and architecture without a sysroot.
- Create a bootstrap C target from that toolchain.
- Build or restore the selected sysroot Formula.
- Parse the exact sysroot path from its C metadata.
- Prepare the final LLVM toolchain with the parsed sysroot.
- Create the final C target and build the resolved Formula graph.
The sysroot graph uses the normal build cache. The requested Formula graph
receives only the prepared target containing the parsed sysroot path; it never
receives the sysroot module or its build result. There is no separate
cross-compilation builder and no sysroot knowledge inside build.
When a libc key exists, the hidden preparation flow is absent. LLAR prepares
only the target tool commands, and the Formula calls Sysroot with the output
directory of its selected dependency.
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(requested Formula, Matrix)"
Modules-->>CLI: "requested Formula graph"
CLI->>Modules: "Load(default sysroot, Matrix)"
Modules-->>CLI: "hidden sysroot graph"
CLI->>LLVM: "New(OS, Arch)"
LLVM-->>CLI: "bootstrap c.Toolchain"
CLI->>C: "NewTarget(bootstrap Toolchain)"
C-->>CLI: "bootstrap build.Target"
CLI->>Build: "Build(hidden sysroot graph)"
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(requested Formula graph)"
Build-->>CLI: "target Results"
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 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 requested Formula"
Modules-->>CLI: "requested Formula graph"
CLI->>Modules: "Load default glibc separately"
Modules-->>CLI: "hidden glibc graph"
CLI->>LLVM: "New(linux, arm64, no sysroot)"
LLVM-->>CLI: "bootstrap compiler and linker commands"
CLI->>C: "NewTarget(bootstrap Toolchain)"
CLI->>Build: "Build(hidden glibc graph)"
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"
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.
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"
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.
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"
The Formula remains unchanged. The prepared compiler command still selects the target, while LLAR avoids imposing an unsupported build-system option.
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"
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.
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"
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.