Skip to content

Cross compile Design

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

Cross Compilation Proposal

Summary

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:

  1. build or restore the sysroot with a C target that has the target toolchain but no sysroot;
  2. 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.

Implementation

Target Selection

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 Matrix Convention

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.

Default Sysroot Resolution

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.

Two-Phase C Target Preparation

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 Command Scope

build.Options.Target accepts a prepared build.Target. Build adapts Target.Use to its scoped execbroker middleware internally:

  1. convert the pending command into build.Command;
  2. call Target.Use;
  3. apply the returned build.Patch;
  4. 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.

Formula Precedence

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.

CMake

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.

Autotools

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.

pkg-config

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.

Direct Commands

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

Failure Behavior

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

Module Specification

internal/build

Boundary

Owns:

  • module build ordering, cache use, formula execution, and command scope;
  • the language-neutral target contract;
  • adapting target patches to internal execbroker middleware.

Does not own:

  • C or other language target policy;
  • compiler toolchain preparation;
  • libc or sysroot selection;
  • target-specific command arguments and environment values.

API

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.

internal/build/c

Boundary

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.Sysroot is 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.

API

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() error

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

internal/build/c/llvm

Boundary

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.

API

package llvm

type Toolchain struct {
	c.Toolchain
}

type Config struct {
	OS      string
	Arch    string
	Sysroot string
}

func New(config Config) (*Toolchain, error)

User Stories

1. Build a C Library for Another Linux Architecture

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:

  1. read os and arch from Matrix.Require;
  2. call c.Sysroot and add the exact result to modules.Load.Options.Roots;
  3. resolve the main module graph through ordinary MVS;
  4. prepare a bootstrap llvm.Toolchain with the selected OS and architecture;
  5. create a bootstrap C target with c.NewTarget;
  6. call Builder.Build for the selected sysroot using the bootstrap target;
  7. parse the result metadata into a sysroot directory;
  8. prepare another llvm.Toolchain with the selected OS, architecture, and sysroot;
  9. create a configured C target with another call to c.NewTarget;
  10. call Builder.Build for 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.

2. Build a C Library for macOS

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.

3. Reuse a Cached Sysroot

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.

4. Build With Formula-Selected libc

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.

5. Preserve Explicit Formula 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.

6. Keep Native and Unknown Targets Unchanged

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.

7. Add Another Language Target

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.

Clone this wiki locally