-
Notifications
You must be signed in to change notification settings - Fork 4
Cross compile Design
LLAR will derive target behavior from the selected matrix and apply a prepared target configuration to commands issued by a formula. Formula authors continue to use CMake, Autotools, pkg-config, compilers, and binutils normally; they do not add a separate cross-compilation API to their formulas.
Build consumes one language-neutral contract:
type Target interface {
Use(Command) Patch
}The C implementation lives in internal/build/c. It combines target facts, a
prepared C toolchain, and an optional sysroot. There is no generic target
package or language registry. Other languages can provide their own
build.Target implementation when one is needed without adding their rules to
the C target.
The built-in C policies cover Linux amd64/arm64 and Darwin amd64/arm64. Linux uses a fixed glibc compatibility sysroot. Darwin uses a fixed macOS SDK.
A built-in C sysroot is an ordinary formula. Preparing it requires two builds:
- build or restore the sysroot with a C target that has the target toolchain but no sysroot;
- create another C target with the resulting sysroot and build the requested module graph.
Both phases call the existing Builder.Build implementation. The target
objects differ, but module loading, cache restoration, source builds, locking,
and formula execution are not duplicated.
The C layout is language-first:
internal/build/c
internal/build/c/llvm
llvm.Toolchain embeds the common C toolchain. build does not know about C,
LLVM, libc, sysroots, CMake, or Autotools, and command middleware remains an
internal build implementation detail.
The command reads the target OS and architecture from the existing
formula.Matrix.Require:
target.require["os"]
target.require["arch"]The structured matrix is used while selecting built-in policy. The selected OS
and architecture are passed to LLVM toolchain preparation and form
targetMatrix for C target preparation. The existing encoded full matrix
remains the cache and formula input.
| Condition | Behavior |
|---|---|
| Target equals the build host | Native build; do not create an automatic target |
| Target has no built-in policy | Pass the matrix to the formula unchanged |
| Supported cross C target | Prepare an LLVM toolchain from the selected OS and architecture, then create a C target from it |
Target-specific policy is implemented by c, not by the module loader or
generic build package.
| Matrix | Sysroot Formula | LLVM Triple |
|---|---|---|
amd64-linux |
bminor/glibc@glibc-2.17 |
x86_64-linux-gnu |
arm64-linux |
bminor/glibc@glibc-2.17 |
aarch64-linux-gnu |
amd64-darwin |
joseluisq/macosx-sdks@14.5 |
x86_64-apple-macos10.13 |
arm64-darwin |
joseluisq/macosx-sdks@14.5 |
arm64-apple-macos11.0 |
libc is a reserved Matrix.Require key following the os and arch
convention:
target.require["libc"]LLAR checks only whether the key exists. It does not normalize, parse, or validate the value.
| Linux target condition | Default libc injection |
|---|---|
| Native target | Do not inject |
| Target architecture without built-in Linux policy | Do not inject |
Supported cross target with a libc key |
Do not inject |
Supported cross target without a libc key |
Inject bminor/glibc@glibc-2.17
|
For a Linux target, when the key exists, the formula owns libc selection and explicit sysroot configuration. LLAR can still supply target and toolchain defaults, but it does not select or inject the default glibc sysroot.
This convention controls Linux libc selection only. It does not suppress the macOS SDK selected for a Darwin target.
The key is a target requirement, not a package option. Command and service
request parsing classify it with os and arch.
c.Sysroot maps a supported target to an exact sysroot module.Version. The
version is fixed compatibility policy and is never resolved through latest.
For supported cross Linux without a libc key, and for supported cross Darwin,
the composition root adds the returned version to modules.Load through
Options.Roots. The sysroot therefore participates in ordinary MVS without
teaching internal/modules about libc, SDKs, C targets, or sysroots.
The sysroot formula installs target development files and publishes C/C++ metadata containing a parseable sysroot flag:
onBuild ctx => {
sysrootDir := ctx.outputDir()
ctx.setMetadata "--sysroot=" + sysrootDir
}x/metadata/cc parses the formula result and returns the sysroot directory.
llvm.Config.Sysroot receives that directory for compiler preparation.
c.Config.Sysroot receives the same directory for build-system-native sysroot
projection, not as raw formula metadata. The exact sysroot version remains
part of the ordinary resolved module graph.
Adding the sysroot to Options.Roots is the only dependency injection. LLAR
does not append it manually to every module's direct dependencies.
The composition root first prepares the LLVM toolchain with the selected OS and architecture and no sysroot:
bootstrapToolchain, err := llvm.New(llvm.Config{
OS: targetOS,
Arch: targetArch,
})It then creates a bootstrap C target without a sysroot:
bootstrapTarget, err := c.NewTarget(c.Config{
Matrix: targetMatrix,
Toolchain: bootstrapToolchain.Toolchain,
})The first Builder.Build call builds or restores the selected sysroot formula
using this target. The bootstrap toolchain supplies a prepared target compiler
and binutils without injecting a sysroot. The C target projects those prepared
commands and the build-system target settings.
After the result metadata is parsed, the command creates a configured LLVM toolchain and a second C target:
configuredToolchain, err := llvm.New(llvm.Config{
OS: targetOS,
Arch: targetArch,
Sysroot: sysrootDir,
})
configuredTarget, err := c.NewTarget(c.Config{
Matrix: targetMatrix,
Toolchain: configuredToolchain.Toolchain,
Sysroot: sysrootDir,
})The second Builder.Build call builds the full resolved module graph using the
configured target. The sysroot formula is already present in the graph and its
artifact was prepared by the first build, so this build restores it from the
same cache instead of rebuilding its source.
The two target values are immutable for their respective build calls. The
build.Target interface does not expose sysroot preparation, build results, or
state mutation.
build.Options.Target accepts a prepared build.Target. Build adapts
Target.Use to its scoped execbroker middleware internally:
- convert the pending command into
build.Command; - call
Target.Use; - apply the returned
build.Patch; - execute the command inside the existing formula scope.
The caller never passes an execbroker.Middleware. A target returns command
changes but does not execute commands or install middleware.
Generated configuration belongs to the concrete C target. The composition root retains each concrete target for the duration of its corresponding build and closes it afterward.
Automatic target values are defaults. Formula configuration wins when it already specifies the same fact.
The following are preserved:
- explicit compiler and binutils command paths;
- explicit target and sysroot flags;
- explicit Autotools tool variables and
--host; - an explicit CMake toolchain file;
- explicit pkg-config environment values.
Absolute command paths and target-prefixed custom commands are not rewritten.
For a CMake configure command without an explicit toolchain file, c
passes a generated file through CMAKE_TOOLCHAIN_FILE.
Every C target file contains the target system, processor, prepared compiler, linker, and binutils commands. A configured target additionally contains its build-system-native sysroot value.
set(CMAKE_SYSTEM_NAME <target-system>)
set(CMAKE_SYSTEM_PROCESSOR <target-processor>)
set(CMAKE_C_COMPILER <compiler-and-fixed-arguments>)
set(CMAKE_CXX_COMPILER <compiler-and-fixed-arguments>)
set(CMAKE_LINKER <linker-command>)
set(CMAKE_AR <archiver>)
set(CMAKE_RANLIB <ranlib>)
set(CMAKE_NM <nm>)
set(CMAKE_STRIP <strip>)When c.Config.Sysroot is non-empty, the generated file also carries the
platform-specific sysroot and target-root lookup policy. A bootstrap target
does not emit sysroot-dependent CMake values.
Linux uses CMAKE_SYSROOT. Darwin uses the macOS-specific fields:
set(CMAKE_SYSTEM_NAME Darwin)
set(CMAKE_OSX_ARCHITECTURES <x86_64-or-arm64>)
set(CMAKE_OSX_SYSROOT <sdk>)If the formula supplies CMAKE_TOOLCHAIN_FILE, LLAR does not add another one.
Formula dependency roots remain owned by CMake.Use, not by the generated
target file. CMake.Use supplies current dependency roots when constructing
the configure command so target preparation does not retain stale dependency
state.
For a generic configure command, the C target supplies missing tool variables:
CC=<prepared-compiler-command>
CXX=<prepared-compiler-command>
LD=<prepared-linker-command>
AR=<archiver>
RANLIB=<ranlib>
NM=<nm>
STRIP=<strip>
It adds --host=<autotools-host> when absent. Target, sysroot, and compiler
driver linker selection are already part of the prepared compiler commands.
The prepared LLVM compiler commands use --sysroot for Linux. Darwin uses a
versioned Clang triple and -isysroot. Both select LLD through the prepared
compiler command. LLVM supplies ld.lld for Linux and ld64.lld for Darwin
when a build system invokes the linker directly.
These are Autotools conventions. A project can ignore or replace them, so an explicit formula remains authoritative.
When the C target has a sysroot, it supplies missing target pkg-config isolation:
PKG_CONFIG_SYSROOT_DIR=<sysroot>
PKG_CONFIG_LIBDIR=<target pkg-config directories>
PKG_CONFIG_SYSROOT_DIR relocates returned paths through the target sysroot.
PKG_CONFIG_LIBDIR prevents fallback to the host database. Dependency
directories prepared through x/pkgconfig remain separate inputs to the target
lookup path.
A bootstrap target without a sysroot does not inject sysroot-specific pkg-config values.
Generic C and C++ compiler names receive the prepared compiler command, including its fixed target and sysroot arguments. Generic binutils names receive the corresponding prepared tool path.
Compiler commands include the selected toolchain's compiler-driver linker selection. Generic linker names receive the prepared target linker command.
cc gcc clang
c++ g++ clang++
ar llvm-ar
ranlib llvm-ranlib
nm llvm-nm
strip llvm-strip
| Condition | Result |
|---|---|
| Required LLVM command is unavailable |
llvm.New returns an error |
| Default sysroot cannot be resolved, restored, or built | Return the ordinary module or build error |
| Sysroot result has missing or malformed metadata | Fail before creating the configured target |
| C target configuration is invalid |
c.NewTarget returns an error before the corresponding build |
Owns:
- module build ordering, cache use, formula execution, and command scope;
- the language-neutral target contract;
- adapting target patches to internal
execbrokermiddleware.
Does not own:
- C or other language target policy;
- compiler toolchain preparation;
- libc or sysroot selection;
- target-specific command arguments and environment values.
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
}There is no generic target package, target registry, or language dispatch in
build. The consumer-owned Target interface is the complete convention.
Owns:
- prepared C and C++ compiler and linker commands, and archiver, ranlib, nm, and strip command paths;
- supported C target facts, including CMake processors and Autotools hosts;
- platform-specific build-system sysroot projection;
- fixed default C sysroot formula policy;
- CMake, Autotools, pkg-config, compiler, and binutils target projection;
- optional sysroot injection when
Config.Sysrootis non-empty; - generated CMake target file lifetime.
Does not own:
- module loading, MVS, or calls to
Builder.Build; - cache restoration or artifact storage;
- discovery or installation of concrete toolchains;
- command execution or middleware installation;
- formula dependency discovery.
package c
type Toolchain struct {
// internal state
}
func NewToolchain(
cc []string,
cxx []string,
linker []string,
archiver string,
ranlib string,
nm string,
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
}
type Target struct {
// internal state
}
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() errorSysroot is a pure C target policy lookup. It performs no module loading,
network access, cache access, or build work. NewTarget derives one immutable
target configuration from its input. An empty Config.Sysroot creates the
bootstrap configuration; a non-empty value creates the configuration used for
consumers.
Owns discovery and preparation of LLVM commands, Clang target and sysroot arguments, and target linker selection required by the C toolchain. It embeds the common C toolchain and does not own sysroot formula policy, build-system configuration, or command rewriting.
package llvm
type Toolchain struct {
c.Toolchain
}
type Config struct {
OS string
Arch string
Sysroot string
}
func New(config Config) (*Toolchain, error)A user selects a target through the existing CLI matrix flags:
llar make madler/zlib@v1.3.1 --os linux --arch arm64
On a build host other than Linux ARM64, and without a libc key, LLAR performs
the following workflow:
- read
osandarchfromMatrix.Require; - call
c.Sysrootand add the exact result tomodules.Load.Options.Roots; - resolve the main module graph through ordinary MVS;
- prepare a bootstrap
llvm.Toolchainwith the selected OS and architecture; - create a bootstrap C target with
c.NewTarget; - call
Builder.Buildfor the selected sysroot using the bootstrap target; - parse the result metadata into a sysroot directory;
- prepare another
llvm.Toolchainwith the selected OS, architecture, and sysroot; - create a configured C target with another call to
c.NewTarget; - call
Builder.Buildfor the full graph using the configured target.
The formula does not contain cross-compilation setup. Both builds use the same workspace and cache, so the sysroot is restored rather than rebuilt during the second build.
A user selects a Darwin target through the same matrix flags:
llar make madler/zlib@v1.3.1 --os darwin --arch amd64
On a build host other than Darwin amd64, LLAR selects
joseluisq/macosx-sdks@14.5, builds or restores it through the same first
Builder.Build call, and creates the configured C target from its metadata.
The LLVM toolchain supplies x86_64-apple-macos10.13, -isysroot,
-fuse-ld=lld, and ld64.lld. An arm64 Darwin target instead uses
arm64-apple-macos11.0.
The formula continues to call CMake, Autotools, or compiler commands normally. It does not locate an SDK or add Darwin-specific flags.
A user runs the same target build after the sysroot was produced locally or downloaded into the build cache.
The command follows the same two-phase workflow. The first Builder.Build
returns the cached sysroot metadata without running its formula build hook. The
configured C target is created from the same metadata, so source builds and
cache restores have identical target behavior.
A formula supports libc selection through a required matrix key:
llar make <module>@<version> --os linux --arch arm64 \
--require libc=glibc-2.13
Because the libc key exists, LLAR does not add its default sysroot formula.
The value remains opaque to LLAR. The C target can still provide target and
toolchain defaults without an automatic sysroot, while the formula owns libc
selection and explicit sysroot configuration.
A formula supplies one or more target facts explicitly, such as a CMake target file, compiler path, target or sysroot flag, Autotools variable, or pkg-config environment value.
build still calls Target.Use for the formula command. The C target preserves
the explicit value and supplies only missing defaults. The formula does not need
to disable all automatic target behavior to override one setting.
For a native target, the command does not prepare a build.Target; the existing
single-build path remains unchanged.
For a target without built-in policy, LLAR passes the matrix to the formula and does not guess a toolchain, sysroot formula, or target command policy.
A maintainer adding target behavior for another language implements
build.Target.Use in that language's own module. The implementation does not
modify c, and build does not gain a language switch or registry.
Language-specific preparation requirements are not added to the build.Target
interface. The interface remains the command-target contract consumed by
Builder.