-
Notifications
You must be signed in to change notification settings - Fork 4
Cross compile Design
LLAR supports C-family cross compilation by composing three independent pieces:
-
cmd/llarselects and prepares the target sysroot; -
llvm.Toolchainprepares target-specific compiler and linker commands; -
c.Targetprojects those commands into CMake, Autotools,pkg-config, and direct C/C++ tool invocations.
The build module remains language-neutral. It receives a build.Target and
applies that target to commands created while a Formula build hook is running.
It does not know whether the target represents C, LLVM, a sysroot, or cross
compilation.
A sysroot remains an ordinary Formula. cmd/llar adds the default sysroot to
module resolution, builds or restores it before its consumers, parses its
published C metadata, and then prepares the final C target. Existing Formulas
do not need a cross-compilation API: their existing CMake, Autotools,
pkg-config, and direct compiler commands pass through the same command
broker and receive target defaults automatically.
The design covers Linux and Darwin targets. Native builds keep their current behavior and do not receive a sysroot or command rewrite.
The design follows four rules:
- Target selection, toolchain construction, and command rewriting are separate responsibilities.
-
cmd/llarowns composition because it knows the selected matrix, resolved modules, and build results. -
buildaccepts only the language-neutralbuild.Targetcontract. - Formula configuration takes precedence over LLAR-provided defaults.
The resulting dependency direction is:
cmd/llar/internal
|-- internal/modules
|-- internal/build
|-- internal/build/c
`-- internal/build/c/llvm
internal/build/c/llvm --prepares--> c.Toolchain
internal/build/c --implements-> build.Target
internal/build --adapts------> execbroker.Middleware
Neither build nor internal/modules imports the C or LLVM packages.
For a supported cross target, cmd/llar performs the following sequence:
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->>CLI: "read target OS and arch from formula.Matrix"
CLI->>C: "Sysroot(targetOS, targetArch)"
C-->>CLI: "fixed sysroot module.Version"
CLI->>Modules: "Load(root, Options{Matrix, Roots: [sysroot]})"
Modules-->>CLI: "MVS-resolved module graph"
CLI->>LLVM: "New(Config{OS, Arch})"
LLVM-->>CLI: "bootstrap c.Toolchain"
CLI->>C: "NewTarget(Config{Matrix, Toolchain})"
C-->>CLI: "bootstrap build.Target"
CLI->>Build: "Build(selected sysroot)"
Build-->>CLI: "sysroot build Result"
CLI->>Metadata: "Parse(result.Metadata)"
Metadata-->>CLI: "sysroot path"
CLI->>LLVM: "New(Config{OS, Arch, Sysroot})"
LLVM-->>CLI: "configured c.Toolchain"
CLI->>C: "NewTarget(Config{Matrix, Toolchain, Sysroot})"
C-->>CLI: "configured build.Target"
CLI->>Build: "Build(resolved graph, Target)"
Build-->>CLI: "target build results"
The first target contains the compiler target and linker selection but no
sysroot flag. It is used to build or restore the sysroot Formula. The second
target contains the resolved sysroot path and is used for the requested module
graph. Both build calls use the existing build.Builder; there is no separate
cross-compilation build implementation.
Cross compilation is active when the selected target OS or architecture differs from the host. The supported C target policy is:
| Target OS | Target arch | 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 |
c.Sysroot returns only the fixed module.Version. It does not load the
Formula, access a cache, build the module, or parse its metadata.
The returned module is passed through modules.Options.Roots. It therefore
participates in the same MVS resolution as Formula-declared dependencies. The
module loader sees an ordinary root requirement and has no sysroot or libc
semantics.
Linux Formulas may define a libc requirement in their matrix. The existence
of the libc key means that the Formula owns libc selection, so cmd/llar
does not add the fixed glibc Formula. LLAR does not normalize or interpret the
value; the matrix key is the convention that allows the Formula to constrain
its own variants.
When the requested module is the default sysroot Formula itself, cmd/llar
does not add that Formula as its own root requirement.
If the target is native, LLAR skips c.Sysroot, does not prepare a
build.Target, and follows the existing native build path. If c.Sysroot
does not recognize a cross-target OS and architecture, LLAR leaves the matrix
to the Formula and does not inject C target behavior.
A sysroot Formula publishes the actual sysroot directory in its existing C/C++ metadata:
--sysroot=<absolute output path>
The path may be the Formula output root or a directory below it. cmd/llar
parses the build result with x/metadata/cc and uses Metadata.Sysroot(); it
does not infer the path from the module installation directory.
Linux sysroots provide target headers, startup objects, the dynamic loader ABI, and link-time libraries. Darwin sysroots provide the macOS SDK headers, frameworks, and link stubs.
The sysroot is prepared in two stages because its path does not exist until the Formula has been built or restored:
- Prepare an LLVM toolchain with target OS and architecture but without a sysroot.
- Build or restore the selected sysroot Formula with that bootstrap target.
- Parse the sysroot path from the returned metadata.
- Prepare the final LLVM toolchain and C target with the parsed path.
- Build the resolved module graph. The sysroot entry produced by step 2 is reused from the existing build cache.
This keeps sysroot knowledge out of build while retaining one build
implementation.
c.Toolchain is prepared command data. CC, CXX, and Linker are
[]string because a command includes both an executable and mandatory target
arguments. For example:
CC = ["/usr/bin/clang", "--target=aarch64-linux-gnu", "-fuse-ld=lld", "--sysroot=/sdk"]
c.Toolchain does not derive target triples, choose a linker, select a
sysroot, or interpret CMake and Autotools commands. Those facts are supplied
when the toolchain is constructed.
llvm.New owns LLVM-specific decisions:
| OS/arch | Clang target | 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> |
It resolves clang, clang++, the selected linker, llvm-ar,
llvm-ranlib, llvm-nm, and llvm-strip from PATH. Compiler-driver
commands include -fuse-ld=lld; raw linker selection is exposed separately
through c.Toolchain.Linker().
Target triples, Darwin deployment versions encoded in those triples, LLVM
linker names, and LLVM sysroot flag syntax remain inside the LLVM package.
c.Target receives only prepared commands.
c.Target owns C build-system semantics. It converts a prepared
c.Toolchain, a target matrix string, and an optional sysroot path into
build.Patch values.
The target portion is the matrix text before the first |. It determines only
build-system facts:
| Target | CMake system | CMake processor | Autoconf host |
|---|---|---|---|
amd64-linux |
Linux |
x86_64 |
x86_64-linux-gnu |
arm64-linux |
Linux |
aarch64 |
aarch64-linux-gnu |
amd64-darwin |
Darwin |
x86_64 |
x86_64-apple-darwin |
arm64-darwin |
Darwin |
arm64 |
aarch64-apple-darwin |
These values describe CMake and Autoconf conventions. They are not passed to LLVM to construct compiler commands.
On a CMake configure command, c.Target appends a generated toolchain file
unless the Formula already supplied CMAKE_TOOLCHAIN_FILE or --toolchain.
The file is created lazily on the first matching configure command and removed
when c.Target.Close runs. CMake build and install commands do not create or
receive the file.
The generated file sets missing values only:
CMAKE_SYSTEM_NAME
CMAKE_SYSTEM_PROCESSOR
CMAKE_C_COMPILER
CMAKE_CXX_COMPILER
CMAKE_LINKER
CMAKE_AR
CMAKE_RANLIB
CMAKE_NM
CMAKE_STRIP
Linux sysroots use CMAKE_SYSROOT. Darwin sysroots use
CMAKE_OSX_SYSROOT and also set CMAKE_OSX_ARCHITECTURES.
When a sysroot is active, 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
CMAKE_FIND_ROOT_PATH in the environment, and x/cmake.Configure passes the
current value to CMake. The generated target file does not copy or cache that
dynamic dependency list.
For a command whose basename is configure, c.Target supplies missing
environment values:
CC CXX LD AR RANLIB NM STRIP
CC and CXX contain the complete shell-quoted command arrays, including
LLVM target, linker, and sysroot arguments. Formula-provided environment
values are preserved.
--host is not accepted by every script named configure, so it is not added
unconditionally. The command already contains the resolved source path and
working directory. c.Target reads that script and appends the target's
Autoconf host tuple only when:
- the Formula did not already pass
--host; and - the script contains the literal
--hostoption.
A script containing CHOST but not --host does not receive the option. LLAR
does not inject --build. The Autoconf host tuple controls configure-time
cross-build behavior; it is not forwarded to Clang as the compiler target.
When a sysroot is active, pkg-config receives missing values for:
PKG_CONFIG_SYSROOT_DIR=<sysroot>
PKG_CONFIG_LIBDIR=<dependency pkg-config paths plus sysroot pkg-config paths>
Dependency paths come from the current PKG_CONFIG_PATH, which is populated
by x/pkgconfig.Use and by build helpers such as x/autotools.Use. The C
target appends the sysroot's usr/lib64/pkgconfig, usr/lib/pkgconfig, and
usr/share/pkgconfig directories. An explicit PKG_CONFIG_LIBDIR remains
unchanged.
Bare generic tool names are rewritten as follows:
| 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 compiler or linker
arguments are prepended before Formula arguments. A command containing an
explicit path is not rewritten. configure is the exception because its
source path is required for option detection.
| Condition | Behavior |
|---|---|
| Native target | Use the existing native build path |
| Unsupported cross OS/arch | Do not create a C target; leave the matrix to the Formula |
Linux matrix contains libc
|
Do not add the default glibc Formula |
| Sysroot Formula cannot be resolved or built | Return the module load or build error |
| Sysroot metadata is invalid or has no sysroot | Fail before building consumers |
Required LLVM command is absent from PATH
|
Fail while preparing llvm.Toolchain
|
| Formula supplies a CMake toolchain | Do not append LLAR's generated toolchain file |
| Formula supplies an Autotools environment value | Keep the Formula value |
Formula supplies --host
|
Do not append another --host
|
Configure script does not declare --host
|
Do not append --host
|
| Configure script cannot be read | Fail before executing that configure command |
| Formula uses an explicit tool path | Do not rewrite the command |
The build module exposes one target-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 fields
Target Target
}Builder adapts Target.Use to the existing execbroker.Middleware once per
Build call. Formula build commands run inside the existing
execbroker.Scope, so CMake helpers, Autotools helpers, gsh commands, and
direct brokered commands all receive the same target.
internal/build owns applying Patch to one command and scoping it to a build.
It does not select a target, know C or LLVM, locate a sysroot, parse C metadata,
or construct a toolchain. External callers cannot inject command middleware
through build.Options; they can supply only a build.Target.
The C module defines prepared toolchain data and implements the build target:
package c
type Toolchain struct {
// prepared commands
}
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
func (t *Target) Close() errorc.Toolchain owns only prepared C-family commands. c.Target owns CMake,
configure, pkg-config, and direct C-tool projection. c.Sysroot owns the
fixed C sysroot selection policy.
The C module does not load or build Formulas, resolve LLVM executables, decide
LLVM triples or linker drivers, execute commands, or install middleware.
Close cleans up only configuration generated by that target, currently the
lazily created CMake toolchain file.
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)The LLVM module owns target triples, target-specific compiler arguments,
linker selection, and LLVM executable lookup. The returned object embeds the
prepared c.Toolchain consumed by c.NewTarget.
It does not select or build a sysroot Formula, interpret matrix conventions,
understand CMake or Autotools, rewrite commands, or depend on build.Builder.
The existing command entrypoint remains the composition root:
func buildModule(
ctx context.Context,
store repo.Store,
modPath, version string,
matrix formula.Matrix,
runTest bool,
) errorIt owns the complete orchestration order:
- determine native versus cross build from the selected matrix and host;
- apply the
libcmatrix convention; - ask
c.Sysrootfor the default module version; - add that version to module loading;
- build or restore the selected sysroot;
- parse its metadata;
- construct the bootstrap and final LLVM toolchains and C targets;
- pass the resulting
build.Targettobuild.NewBuilder; - close the concrete C targets that it created.
It does not implement command rewriting or duplicate CMake, Autotools, pkg-config, Clang, or linker policy.
Module loading exposes a generic caller-supplied root mechanism:
package modules
type Options struct {
FormulaStore repo.Store
Matrix formula.Matrix
Roots []module.Version
}
func Load(
ctx context.Context,
main module.Version,
opts Options,
) ([]*Module, error)Roots are ordinary requirements appended before MVS resolution. The module
loader owns Formula loading, matrix injection, dependency resolution, and
version conflict resolution. It does not know which root is a sysroot, select
a libc, parse build metadata, or import the C package.
The user runs llar make <module>@<version> --os linux --arch arm64 on a
non-arm64 host. The Formula itself uses the existing x/cmake workflow and
contains no cross-compilation setup.
sequenceDiagram
participant User
participant CLI as "cmd/llar"
participant C as "c"
participant Modules as "modules"
participant LLVM as "llvm"
participant Build as "build"
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 module + sysroot root with selected matrix"
Modules-->>CLI: "MVS-resolved graph"
CLI->>LLVM: "New(linux, arm64, no sysroot)"
LLVM-->>CLI: "clang --target=aarch64-linux-gnu ..."
CLI->>C: "NewTarget(bootstrap toolchain)"
CLI->>Build: "build/restore glibc Formula"
Build-->>CLI: "metadata containing --sysroot=<path>"
CLI->>LLVM: "New(linux, arm64, <path>)"
LLVM-->>CLI: "compiler and linker commands with sysroot"
CLI->>C: "NewTarget(configured toolchain, <path>)"
CLI->>Build: "Build(resolved graph, C target)"
Build->>Broker: "open scoped target middleware"
Build->>CMake: "run Formula onBuild"
CMake->>Broker: "cmake -S ... -B ..."
Broker->>C: "Use(cmake configure command)"
C-->>Broker: "append lazy CMake toolchain file"
Broker->>CMake: "execute target-configured cmake"
CMake->>Broker: "cmake --build / --install"
Broker->>C: "Use(non-configure commands)"
C-->>Broker: "no toolchain-file patch"
Build-->>CLI: "target build result"
CLI-->>User: "module result"
If the Formula called cmake.Use(depRoot) for a dependency, that helper's
current CMAKE_FIND_ROOT_PATH reaches the configure command. The generated
target file supplies compiler, sysroot, and find-root modes without replacing
those dependency roots.
The user selects Darwin arm64 on another platform. The Formula calls the
existing x/autotools.Configure, whose resolved source script advertises
--host.
sequenceDiagram
participant User
participant CLI as "cmd/llar"
participant Modules as "modules"
participant LLVM as "llvm"
participant C as "c"
participant Build as "build"
participant Auto as "x/autotools"
participant Broker as "execbroker"
User->>CLI: "llar make <module>@<version> --os darwin --arch arm64"
CLI->>C: "Sysroot(darwin, arm64)"
C-->>CLI: "joseluisq/macosx-sdks@14.5"
CLI->>Modules: "resolve module graph with SDK root"
CLI->>LLVM: "prepare bootstrap target commands"
CLI->>C: "prepare bootstrap C target"
CLI->>Build: "build/restore SDK Formula"
Build-->>CLI: "SDK sysroot metadata"
CLI->>LLVM: "New(darwin, arm64, SDK path)"
LLVM-->>CLI: "clang --target=arm64-apple-macos11.0, ld64.lld, -isysroot"
CLI->>C: "NewTarget(final toolchain, SDK path)"
CLI->>Build: "Build(graph, C target)"
Build->>Broker: "open scoped target middleware"
Auto->>Broker: "run <source>/configure"
Broker->>C: "Use(configure command and working directory)"
C->>C: "read configure source and find literal --host"
C-->>Broker: "set missing tools and append --host=aarch64-apple-darwin"
Broker->>Auto: "execute configured command"
Auto->>Broker: "make and make install"
Broker->>C: "rewrite generic compiler/linker tools used by the build"
Build-->>CLI: "Darwin arm64 result"
CLI-->>User: "module result"
The Autoconf host tuple makes the configure script enter cross-build mode.
Clang receives its separate LLVM target through the prepared CC command; the
--host value is not treated as a compiler argument.
Some projects, including zlib, use a script named configure that is not GNU
Autoconf. Such a script may mention CHOST while rejecting --host.
sequenceDiagram
participant Formula
participant Auto as "x/autotools"
participant Broker as "execbroker"
participant C as "c.Target"
participant Script as "custom configure"
Formula->>Auto: "Configure(existing Formula arguments)"
Auto->>Broker: "run resolved <source>/configure"
Broker->>C: "Use(Command{Name, Args, Env, Dir})"
C->>C: "preserve Formula environment"
C->>C: "add missing CC/CXX/LD/AR/RANLIB/NM/STRIP"
C->>C: "inspect script: CHOST exists, literal --host does not"
C-->>Broker: "Patch{Env: configured environment}"
Broker->>Script: "execute without --host"
The Formula remains non-invasive: it does not need to identify which kind of
configure script it uses. If another Formula explicitly passes
--host=custom, c.Target preserves it and does not append a second value.
A Formula that supports multiple libc variants declares a libc requirement
in its matrix and resolves the selected libc through its own Formula logic.
sequenceDiagram
participant User
participant CLI as "cmd/llar"
participant C as "c"
participant Modules as "modules"
participant LLVM as "llvm"
participant Build as "build"
User->>CLI: "llar make <module>@<version> --os linux --arch arm64 --require libc=<value>"
CLI->>CLI: "observe that matrix.Require contains libc"
CLI->>C: "Sysroot(linux, arm64)"
C-->>CLI: "default glibc version"
CLI->>CLI: "do not add the default glibc root"
CLI->>Modules: "Load Formula with the unchanged matrix"
Modules->>Modules: "Formula resolves its declared libc requirements"
Modules-->>CLI: "resolved Formula graph"
CLI->>LLVM: "prepare Linux arm64 target commands without a default sysroot"
CLI->>C: "NewTarget(target commands, no default sysroot)"
CLI->>Build: "Build Formula graph with C target"
Build-->>CLI: "target result"
CLI-->>User: "module result"
LLAR does not interpret <value> or replace the Formula's libc with its fixed
default. The Formula's matrix key controls its own selection semantics, while
the LLVM toolchain still supplies the target compiler and linker commands.