Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e2b690e
feat(crosscompile): add Linux phase 1 MVP
MeteorsLiu Aug 7, 2026
8d9c899
feat(crosscompile): align C target build flow
MeteorsLiu Aug 9, 2026
6323bf7
docs(crosscompile): mark multi-language extension points
MeteorsLiu Aug 10, 2026
889f850
refactor(crosscompile): keep target preparation inline
MeteorsLiu Aug 10, 2026
3c21345
feat(crosscompile): add Darwin C targets
MeteorsLiu Aug 10, 2026
61a4c80
refactor(crosscompile): prepare target commands in LLVM
MeteorsLiu Aug 10, 2026
cb88b29
fix(test): reject cross-target execution
MeteorsLiu Aug 10, 2026
3a41331
refactor(crosscompile): hide default sysroot graph
MeteorsLiu Aug 10, 2026
ee7e86b
feat(crosscompile): support formula-owned sysroots
MeteorsLiu Aug 10, 2026
7ea2eed
fix(crosscompile): detect configure host support
MeteorsLiu Aug 10, 2026
257fecb
docs(crosscompile): explain sysroot formula loading
MeteorsLiu Aug 10, 2026
35a3d64
test(crosscompile): add cross-runner zlib e2e
MeteorsLiu Aug 10, 2026
15dc468
fix(crosscompile): use Bootlin glibc sysroot
MeteorsLiu Aug 10, 2026
13617a6
test(crosscompile): exercise zlib file IO
MeteorsLiu Aug 10, 2026
53bb76b
ci(crosscompile): show build and target platforms
MeteorsLiu Aug 10, 2026
60563af
ci(crosscompile): verify target system libraries
MeteorsLiu Aug 10, 2026
a8ddf3d
ci(crosscompile): inspect target dynamic libraries
MeteorsLiu Aug 10, 2026
27e750d
ci(crosscompile): use amd64 Darwin build host
MeteorsLiu Aug 10, 2026
ba6ef79
ci(crosscompile): link consumers on target runners
MeteorsLiu Aug 10, 2026
50e3472
ci(crosscompile): reuse macOS SDK archive
MeteorsLiu Aug 10, 2026
c496e09
ci(crosscompile): inspect target zlib artifacts
MeteorsLiu Aug 10, 2026
fbbae81
refactor(build): own cross compile orchestration
MeteorsLiu Aug 11, 2026
2edfa65
refactor(build): reuse graph build closure for sysroot
MeteorsLiu Aug 11, 2026
6ade7fe
revert: keep sysroot orchestration outside build
MeteorsLiu Aug 11, 2026
f5128bf
refactor(crosscompile): share target loading
MeteorsLiu Aug 11, 2026
e7a32e1
test(crosscompile): improve coverage
MeteorsLiu Aug 11, 2026
53d723f
ci(crosscompile): test formula-managed libc
MeteorsLiu Aug 11, 2026
48f39a4
ci(crosscompile): distinguish custom libc sysroot
MeteorsLiu Aug 11, 2026
bd50f4e
ci(crosscompile): install Bootlin sysroots directly
MeteorsLiu Aug 11, 2026
b529172
feat(crosscompile): select Linux environment from sysroot
MeteorsLiu Aug 11, 2026
efa3997
test(crosscompile): verify musl libuv ABI
MeteorsLiu Aug 11, 2026
424633b
test(crosscompile): use musl-compatible libuv
MeteorsLiu Aug 11, 2026
3567e75
test(crosscompile): show libuv compile commands
MeteorsLiu Aug 11, 2026
9b27abd
test(crosscompile): clarify libuv ABI assertions
MeteorsLiu Aug 11, 2026
7e097f9
test(llard): cover cross compilation
MeteorsLiu Aug 11, 2026
e510874
fix(crosscompile): pass pkg-config env to build systems
MeteorsLiu Aug 11, 2026
96558f8
fix(crosscompile): isolate pkg-config dependency lookup
MeteorsLiu Aug 11, 2026
9dd851d
fix(autotools): remove unused strings import
MeteorsLiu Aug 11, 2026
d7738d3
fix(autotools): defer pkg-config environment to target
MeteorsLiu Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
415 changes: 415 additions & 0 deletions .github/workflows/crosscompile-e2e.yml

Large diffs are not rendered by default.

32 changes: 30 additions & 2 deletions .github/workflows/llard-cluster-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ jobs:
with:
go-version: 1.24.x

- name: Install LLVM
run: |
sudo apt-get update
sudo apt-get install --yes clang-18 lld-18 llvm-18
echo "/usr/lib/llvm-18/bin" >> "$GITHUB_PATH"

- name: Join private network
uses: tailscale/github-action@v4
with:
Expand All @@ -47,18 +53,40 @@ jobs:
go build -ldflags="-checklinkname=0" -o "$RUNNER_TEMP/llard" ./cmd/llard
go build -o "$RUNNER_TEMP/llard-e2e-control" ./testdata/llard-e2e/control

- name: Prepare formula repository
- name: Download Bootlin glibc arm64 toolchain
run: |
archive="$RUNNER_TEMP/aarch64--glibc--stable-2017.05-toolchains-1-1.tar.bz2"
curl --fail --location --retry 3 \
"https://toolchains.bootlin.com/downloads/releases/toolchains/aarch64/tarballs/aarch64--glibc--stable-2017.05-toolchains-1-1.tar.bz2" \
--output "$archive"
echo "530137589c4588599ebd115c12630c75e79c935ac4f34b0941404036f5e25026 $archive" | sha256sum --check

- name: Prepare formula and source repositories
run: |
formula_repo="$RUNNER_TEMP/llarhub"
mkdir -p "$formula_repo"
source_repo="$RUNNER_TEMP/glibc-source"
mkdir -p "$formula_repo" "$source_repo"
cp -R testdata/kodo-e2e/formulas/. "$formula_repo/"
cp -R testdata/crosscompile-e2e/formulas/bminor "$formula_repo/"
git -C "$formula_repo" init --initial-branch=main
git -C "$formula_repo" config user.email llard-e2e@example.com
git -C "$formula_repo" config user.name "LLARD E2E"
git -C "$formula_repo" add .
git -C "$formula_repo" commit -m "LLARD E2E formulas"

cp testdata/crosscompile-e2e/install-bootlin-sysroot "$source_repo/install-sysroot"
cp "$RUNNER_TEMP/aarch64--glibc--stable-2017.05-toolchains-1-1.tar.bz2" "$source_repo/"
chmod +x "$source_repo/install-sysroot"
git -C "$source_repo" init --initial-branch=main
git -C "$source_repo" config user.email llard-e2e@example.com
git -C "$source_repo" config user.name "LLARD E2E"
git -C "$source_repo" add .
git -C "$source_repo" commit --quiet -m "Add glibc 2.24 toolchain"
git -C "$source_repo" tag glibc-2.24

git config --global protocol.file.allow always
git config --global url."file://$formula_repo".insteadOf "https://github.com/goplus/llarhub.git"
git config --global url."file://$source_repo".insteadOf "https://github.com/bminor/glibc.git"

- name: Run llard
env:
Expand Down
36 changes: 33 additions & 3 deletions cmd/llar/internal/make.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/goplus/llar/formula"
"github.com/goplus/llar/internal/build"
"github.com/goplus/llar/internal/crosscompile"
"github.com/goplus/llar/internal/formula/repo"
"github.com/goplus/llar/internal/modules"
"github.com/goplus/llar/internal/modules/modlocal"
Expand Down Expand Up @@ -129,10 +130,23 @@ func hostMatrix() formula.Matrix {
// hooks triggered — each dependency is verified by its own
// `llar test <dep>` invocation.
func buildModule(ctx context.Context, store repo.Store, modPath, version string, matrix formula.Matrix, runTest bool) error {
mods, err := modules.Load(ctx, module.Version{Path: modPath, Version: version}, modules.Options{
root := module.Version{Path: modPath, Version: version}
var targetOS, targetArch string
if values := matrix.Require["os"]; len(values) > 0 {
targetOS = values[0]
}
if values := matrix.Require["arch"]; len(values) > 0 {
targetArch = values[0]
}
crossCompile := targetOS != runtime.GOOS || targetArch != runtime.GOARCH
Comment thread
MeteorsLiu marked this conversation as resolved.
if runTest && crossCompile {
return fmt.Errorf("llar test cannot run %s/%s target on %s/%s host", targetOS, targetArch, runtime.GOOS, runtime.GOARCH)
}
loadOpts := modules.Options{
FormulaStore: store,
Matrix: matrix,
})
}
mods, err := modules.Load(ctx, root, loadOpts)
if err != nil {
return fmt.Errorf("failed to load modules: %w", err)
}
Expand All @@ -158,7 +172,23 @@ func buildModule(ctx context.Context, store repo.Store, modPath, version string,
defer os.RemoveAll(tmpDir)
buildOpts.WorkspaceDir = tmpDir
}

target, err := crosscompile.Load(ctx, root, crosscompile.Config{
Store: store,
Matrix: matrix,
Stdout: buildOpts.Stdout,
Stderr: buildOpts.Stderr,
WorkspaceDir: buildOpts.WorkspaceDir,
Cache: buildOpts.Cache,
})
if err != nil {
return err
}
if target != nil {
if closer, ok := target.(io.Closer); ok {
defer closer.Close()
}
buildOpts.Target = target
}
builder, err := build.NewBuilder(buildOpts)
if err != nil {
return fmt.Errorf("failed to create builder: %w", err)
Expand Down
17 changes: 17 additions & 0 deletions cmd/llar/internal/make_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,23 @@ func TestRunMakeReturnsMatrixErrorBeforeStore(t *testing.T) {
}
}

func TestBuildModuleRejectsCrossTargetTest(t *testing.T) {
targetOS, targetArch := "linux", "amd64"
if runtime.GOOS == targetOS && runtime.GOARCH == targetArch {
targetArch = "arm64"
}
matrix := formula.Matrix{Require: map[string][]string{
"os": {targetOS},
"arch": {targetArch},
}}

err := buildModule(context.Background(), nil, "owner/repo", "v1.0.0", matrix, true)
want := fmt.Sprintf("llar test cannot run %s/%s target on %s/%s host", targetOS, targetArch, runtime.GOOS, runtime.GOARCH)
if err == nil || err.Error() != want {
t.Fatalf("buildModule error = %v, want %q", err, want)
}
}

func TestNewRemoteStore(t *testing.T) {
isolatedWorkspaceDir(t)
store, err := newRemoteStore()
Expand Down
17 changes: 13 additions & 4 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Builder struct {
stderr io.Writer
workspaceDir string
cache cache.Cache
target Target
newRepo func(repoPath string) (vcs.Repo, error) // defaults to vcs.NewRepo
}

Expand All @@ -51,6 +52,7 @@ type Options struct {
Stderr io.Writer
WorkspaceDir string
Cache cache.Cache
Target Target
}

func runFormulaHook(fn func()) (err error) {
Expand Down Expand Up @@ -110,6 +112,7 @@ func NewBuilder(opts Options) (*Builder, error) {
stderr: stderr,
workspaceDir: workspaceDir,
cache: c,
target: opts.Target,
newRepo: vcs.NewRepo,
}, nil
}
Expand Down Expand Up @@ -243,6 +246,11 @@ func (b *Builder) Build(ctx context.Context, targets []*modules.Module) ([]Resul
rootID = module.Version{Path: targets[0].Path, Version: targets[0].Version}
}

var commandMiddleware execbroker.Middleware
if b.target != nil {
commandMiddleware = targetMiddleware(b.target)
}

build := func(mod *modules.Module) (Result, error) {
isRoot := mod.Path == rootID.Path && mod.Version == rootID.Version
testThisMod := b.runTest && isRoot && mod.OnTest != nil
Expand Down Expand Up @@ -306,10 +314,11 @@ func (b *Builder) Build(ctx context.Context, targets []*modules.Module) ([]Resul

var metadata string
if err := execbroker.Do(execbroker.Scope{
Dir: tmpSourceDir,
Stdin: os.Stdin,
Stdout: b.stdout,
Stderr: b.stderr,
Dir: tmpSourceDir,
Stdin: os.Stdin,
Stdout: b.stdout,
Stderr: b.stderr,
Middleware: commandMiddleware,
}, func() error {
// Run OnBuild only on cache miss; reuse cached metadata otherwise.
if cacheHit {
Expand Down
28 changes: 24 additions & 4 deletions internal/build/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/goplus/llar/internal/artifact"
"github.com/goplus/llar/internal/build"
"github.com/goplus/llar/internal/build/cache"
"github.com/goplus/llar/internal/crosscompile"
"github.com/goplus/llar/internal/formula/repo"
"github.com/goplus/llar/internal/modules"
"github.com/goplus/llar/mod/module"
Expand Down Expand Up @@ -120,25 +121,44 @@ func (h *handler) do(ctx context.Context, req request, info io.Writer) (result,
}

func (h *handler) build(ctx context.Context, req request, info io.Writer) (result, error) {
mods, err := modules.Load(ctx, module.Version{Path: req.module, Version: req.version}, modules.Options{
root := module.Version{Path: req.module, Version: req.version}
mods, err := modules.Load(ctx, root, modules.Options{
FormulaStore: h.formulaStore,
Matrix: req.matrix,
})
if err != nil {
return result{}, err
}

builder, err := build.NewBuilder(build.Options{
buildOpts := build.Options{
Store: h.formulaStore,
MatrixStr: req.matrixStr,
Stdout: info,
Stderr: info,
WorkspaceDir: h.workspaceDir,
Cache: h.cache,
}
target, err := crosscompile.Load(ctx, root, crosscompile.Config{
Store: h.formulaStore,
Matrix: req.matrix,
Stdout: info,
Stderr: info,
WorkspaceDir: h.workspaceDir,
Cache: h.cache,
})
if err != nil {
return result{}, err
}
if target != nil {
if closer, ok := target.(io.Closer); ok {
defer closer.Close()
}
buildOpts.Target = target
}
builder, err := build.NewBuilder(buildOpts)
if err != nil {
return result{}, err
}
if _, err := builder.Build(ctx, mods); err != nil {
return result{}, err
}
Expand Down Expand Up @@ -200,9 +220,9 @@ func parseRequest(r *http.Request) (request, error) {
if len(values) != 1 || values[0] == "" {
return request{}, fmt.Errorf("matrix %q requires exactly one value", key)
}
// Requests carry a flat matrix. Platform dimensions propagate to
// Requests carry a flat matrix. Target requirements propagate to
// dependencies; all other dimensions are package build options.
if key == "os" || key == "arch" {
if key == "os" || key == "arch" || key == "libc" {
if require == nil {
require = make(map[string][]string)
}
Expand Down
69 changes: 60 additions & 9 deletions internal/build/http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"io/fs"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"runtime"
Expand Down Expand Up @@ -53,11 +54,11 @@ func TestParseRequest(t *testing.T) {
},
{
name: "require and options",
target: "/v1/artifacts/madler/zlib@v1.3.1?os=linux&arch=amd64&debug=OFF&shared=ON",
target: "/v1/artifacts/madler/zlib@v1.3.1?os=linux&arch=amd64&libc=glibc-2.13&debug=OFF&shared=ON",
wantModule: "madler/zlib",
wantVer: "v1.3.1",
wantMatrix: "amd64-linux|OFF-ON",
wantRequire: map[string][]string{"arch": {"amd64"}, "os": {"linux"}},
wantMatrix: "amd64-glibc-2.13-linux|OFF-ON",
wantRequire: map[string][]string{"arch": {"amd64"}, "libc": {"glibc-2.13"}, "os": {"linux"}},
wantOptions: map[string][]string{"debug": {"OFF"}, "shared": {"ON"}},
},
{name: "wrong path", target: "/v1/modules/madler/zlib?os=linux", wantErr: "artifact path not found"},
Expand Down Expand Up @@ -94,7 +95,7 @@ func TestParseRequest(t *testing.T) {
for key, values := range req.query {
values[0] = "changed"
matrixValues := req.matrix.Options[key]
if key == "os" || key == "arch" {
if key == "os" || key == "arch" || key == "libc" {
matrixValues = req.matrix.Require[key]
}
if matrixValues[0] == "changed" {
Expand Down Expand Up @@ -163,7 +164,8 @@ func TestServeHTTPSingleflight(t *testing.T) {
Artifacts: artifacts,
WorkspaceDir: t.TempDir(),
}).(*handler)
const target = "/v1/artifacts/DaveGamble/cJSON@v1.7.18?os=linux&arch=amd64"
query := "arch=" + runtime.GOARCH + "&os=" + runtime.GOOS
target := "/v1/artifacts/DaveGamble/cJSON@v1.7.18?" + query

var recorders [2]*httptest.ResponseRecorder
var wg sync.WaitGroup
Expand All @@ -182,7 +184,7 @@ func TestServeHTTPSingleflight(t *testing.T) {

wg.Add(1)
go serve(1)
key := "DaveGamble/cJSON@v1.7.18?arch=amd64&os=linux"
key := "DaveGamble/cJSON@v1.7.18?" + query
waitFor(t, func() bool {
stream, ok := h.infos.Load(key)
if !ok {
Expand All @@ -206,10 +208,11 @@ func TestServeHTTPSingleflight(t *testing.T) {
t.Fatalf("artifact Get calls = %d, want 2", got)
}

jsonQuery := strings.ReplaceAll(query, "&", `\u0026`)
wantLines := []string{
`info "resolving DaveGamble/cJSON@v1.7.18?arch=amd64\u0026os=linux"`,
`artifact {"id":"madler/zlib@v1.3.1?arch=amd64\u0026os=linux","type":"tar.gz","url":"https://artifacts.example/madler/zlib"}`,
`artifact {"id":"DaveGamble/cJSON@v1.7.18?arch=amd64\u0026os=linux","type":"tar.gz","url":"https://artifacts.example/DaveGamble/cJSON","deps":["madler/zlib@v1.3.1?arch=amd64\u0026os=linux"]}`,
`info "resolving DaveGamble/cJSON@v1.7.18?` + jsonQuery + `"`,
`artifact {"id":"madler/zlib@v1.3.1?` + jsonQuery + `","type":"tar.gz","url":"https://artifacts.example/madler/zlib"}`,
`artifact {"id":"DaveGamble/cJSON@v1.7.18?` + jsonQuery + `","type":"tar.gz","url":"https://artifacts.example/DaveGamble/cJSON","deps":["madler/zlib@v1.3.1?` + jsonQuery + `"]}`,
}
gotLines := strings.Split(strings.TrimSpace(recorders[0].Body.String()), "\n")
if len(gotLines) != len(wantLines) {
Expand Down Expand Up @@ -262,6 +265,54 @@ func TestServeHTTPBuildErrors(t *testing.T) {
}
}

func TestBuildCrossCompileTarget(t *testing.T) {
targetArch := "amd64"
if runtime.GOOS == "linux" && runtime.GOARCH == targetArch {
targetArch = "arm64"
}
target := "/v1/artifacts/madler/zlib@v1.3.1?arch=" + targetArch + "&libc=custom&os=linux"
req, err := parseRequest(httptest.NewRequest(http.MethodGet, target, nil))
if err != nil {
t.Fatal(err)
}

t.Run("success", func(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{"clang", "clang++", "ld.lld", "llvm-ar", "llvm-ranlib", "llvm-nm", "llvm-strip"} {
if err := os.WriteFile(filepath.Join(dir, name), []byte("tool"), 0o755); err != nil {
t.Fatal(err)
}
}
t.Setenv("PATH", dir)
h := New(Options{
FormulaStore: localFormulas(t),
Cache: &testCache{},
Artifacts: &testArtifactStore{},
WorkspaceDir: t.TempDir(),
}).(*handler)
result, err := h.build(context.Background(), req, io.Discard)
if err != nil {
t.Fatal(err)
}
if len(result.artifacts) != 1 || !strings.HasPrefix(result.artifacts[0].ID, "madler/zlib@v1.3.1?") {
t.Fatalf("build artifacts = %+v", result.artifacts)
}
})

t.Run("toolchain error", func(t *testing.T) {
t.Setenv("PATH", t.TempDir())
h := New(Options{
FormulaStore: localFormulas(t),
Cache: &testCache{},
WorkspaceDir: t.TempDir(),
}).(*handler)
_, err := h.build(context.Background(), req, io.Discard)
if err == nil || !strings.Contains(err.Error(), "prepare C toolchain") {
t.Fatalf("build error = %v, want toolchain error", err)
}
})
}

func TestDoReturnsCanceledContext(t *testing.T) {
store := &blockingFormulaStore{
started: make(chan struct{}),
Expand Down
Loading
Loading