Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1ccd372
feat(crosscompile): add Linux phase 1 MVP
MeteorsLiu Aug 7, 2026
dbfbad1
feat(crosscompile): align C target build flow
MeteorsLiu Aug 9, 2026
539a2ba
docs(crosscompile): mark multi-language extension points
MeteorsLiu Aug 10, 2026
8a882e5
refactor(crosscompile): keep target preparation inline
MeteorsLiu Aug 10, 2026
dbfd95c
feat(crosscompile): add Darwin C targets
MeteorsLiu Aug 10, 2026
dbcab0c
refactor(crosscompile): prepare target commands in LLVM
MeteorsLiu Aug 10, 2026
de94629
fix(test): reject cross-target execution
MeteorsLiu Aug 10, 2026
e0c3567
refactor(crosscompile): hide default sysroot graph
MeteorsLiu Aug 10, 2026
9dd83fe
feat(crosscompile): support formula-owned sysroots
MeteorsLiu Aug 10, 2026
0904f9c
fix(crosscompile): detect configure host support
MeteorsLiu Aug 10, 2026
97d4fcf
docs(crosscompile): explain sysroot formula loading
MeteorsLiu Aug 10, 2026
55a5064
test(crosscompile): add cross-runner zlib e2e
MeteorsLiu Aug 10, 2026
ab1e140
fix(crosscompile): use Bootlin glibc sysroot
MeteorsLiu Aug 10, 2026
3c230a8
test(crosscompile): exercise zlib file IO
MeteorsLiu Aug 10, 2026
03332bd
ci(crosscompile): show build and target platforms
MeteorsLiu Aug 10, 2026
3e9cf75
ci(crosscompile): verify target system libraries
MeteorsLiu Aug 10, 2026
7bf94fc
ci(crosscompile): inspect target dynamic libraries
MeteorsLiu Aug 10, 2026
d087d02
ci(crosscompile): use amd64 Darwin build host
MeteorsLiu Aug 10, 2026
1fcb201
ci(crosscompile): link consumers on target runners
MeteorsLiu Aug 10, 2026
127fbff
ci(crosscompile): reuse macOS SDK archive
MeteorsLiu Aug 10, 2026
b546669
ci(crosscompile): inspect target zlib artifacts
MeteorsLiu Aug 10, 2026
2b385c8
refactor(build): own cross compile orchestration
MeteorsLiu Aug 11, 2026
187a240
refactor(build): reuse graph build closure for sysroot
MeteorsLiu Aug 11, 2026
a752078
revert: keep sysroot orchestration outside build
MeteorsLiu Aug 11, 2026
122e22d
refactor(crosscompile): share target loading
MeteorsLiu Aug 11, 2026
34f2f08
test(crosscompile): improve coverage
MeteorsLiu Aug 11, 2026
104a966
ci(crosscompile): test formula-managed libc
MeteorsLiu Aug 11, 2026
f20f1f0
ci(crosscompile): distinguish custom libc sysroot
MeteorsLiu Aug 11, 2026
26e6389
ci(crosscompile): install Bootlin sysroots directly
MeteorsLiu Aug 11, 2026
1a815f1
feat(crosscompile): select Linux environment from sysroot
MeteorsLiu Aug 11, 2026
04b50e9
test(crosscompile): verify musl libuv ABI
MeteorsLiu Aug 11, 2026
e368283
test(crosscompile): use musl-compatible libuv
MeteorsLiu Aug 11, 2026
54d130f
test(crosscompile): show libuv compile commands
MeteorsLiu Aug 11, 2026
89e76b9
test(crosscompile): clarify libuv ABI assertions
MeteorsLiu Aug 11, 2026
443823b
test(llard): cover cross compilation
MeteorsLiu Aug 11, 2026
ef0cef6
fix(crosscompile): pass pkg-config env to build systems
MeteorsLiu Aug 11, 2026
d19de85
fix(crosscompile): isolate pkg-config dependency lookup
MeteorsLiu Aug 11, 2026
308d18c
fix(autotools): remove unused strings import
MeteorsLiu Aug 11, 2026
77d00dd
fix(autotools): defer pkg-config environment to target
MeteorsLiu Aug 11, 2026
61f5bf8
fix(build): preserve main behavior after rebase
MeteorsLiu Aug 27, 2026
0478562
fix(build): adapt target middleware errors
MeteorsLiu Aug 27, 2026
4a502b0
fix(pkgconfig): return errors from use
MeteorsLiu Aug 27, 2026
8d801a7
fix(build): keep dependency environment scoped
MeteorsLiu Aug 27, 2026
b661e83
fix(pkgconfig): keep use best effort
MeteorsLiu Aug 27, 2026
50b603b
ci: run cross-compile builds on Qiniu
MeteorsLiu Sep 3, 2026
335a870
ci: install CMake for cross-compile E2E
MeteorsLiu Sep 3, 2026
4c6592b
Merge branch 'main' into feat/crosscompile-wiki-mvp
luoliwoshang Sep 3, 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
430 changes: 430 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 @@ -42,6 +42,12 @@ jobs:
with:
go-version: 1.25.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 @@ -55,18 +61,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 @@ -52,6 +53,7 @@ type Options struct {
Stderr io.Writer
WorkspaceDir string
Cache cache.Cache
Target Target
}

func runFormulaHook(fn func()) (err error) {
Expand Down Expand Up @@ -111,6 +113,7 @@ func NewBuilder(opts Options) (*Builder, error) {
stderr: stderr,
workspaceDir: workspaceDir,
cache: c,
target: opts.Target,
newRepo: vcs.NewRepo,
}, nil
}
Expand Down Expand Up @@ -244,6 +247,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 @@ -307,10 +315,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