Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 3 additions & 4 deletions cmd/llar/internal/make.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,10 @@ func buildModule(ctx context.Context, store repo.Store, modPath, version string,
}
}()

matrixStr := matrix.Combinations()[0]
buildOpts := build.Options{
Store: store,
MatrixStr: matrixStr,
RunTest: runTest,
Store: store,
Target: matrix,
RunTest: runTest,
}
if makeOutput != "" {
tmpDir, err := os.MkdirTemp("", "llar-make-*")
Expand Down
57 changes: 47 additions & 10 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io/fs"
"maps"
"os"
"path/filepath"
"strings"
Expand All @@ -19,7 +20,7 @@ import (

type Builder struct {
store repo.Store
matrix string
target classfile.Matrix
runTest bool
workspaceDir string
cache cache.Cache
Expand All @@ -32,8 +33,8 @@ type Result struct {
}

type Options struct {
Store repo.Store
MatrixStr string
Store repo.Store
Target classfile.Matrix
// RunTest, when true, causes Build to invoke OnTest on the root target
// after OnBuild (or after reusing cached build metadata). The build
// cache is consulted as usual: on a cache hit the root's OnBuild is
Expand Down Expand Up @@ -75,14 +76,27 @@ func NewBuilder(opts Options) (*Builder, error) {
}
return &Builder{
store: opts.Store,
matrix: opts.MatrixStr,
target: opts.Target,
runTest: opts.RunTest,
workspaceDir: workspaceDir,
cache: c,
newRepo: vcs.NewRepo,
}, nil
}

func intersect(values, keys map[string][]string) map[string][]string {
intersection := make(map[string][]string)
for key, value := range values {
if _, ok := keys[key]; ok {
intersection[key] = value
}
}
if len(intersection) == 0 {
return nil
}
return intersection
}

// constructBuildList reorders the MVS build list into a valid build order
// using DFS post-order traversal: leaves (modules with no deps) come first,
// the main module (root) comes last.
Expand Down Expand Up @@ -213,6 +227,16 @@ func (b *Builder) Build(ctx context.Context, targets []*modules.Module) ([]Resul
}

build := func(mod *modules.Module) (Result, error) {
modVer := module.Version{Path: mod.Path, Version: mod.Version}
target := classfile.Matrix{
Require: maps.Clone(b.target.Require),
Options: intersect(b.target.Options, mod.Formula.Matrix.Options),
}
combinations := target.Combinations()
if len(combinations) == 0 {
return Result{}, fmt.Errorf("%s@%s has no matrix dimensions matching target", mod.Path, mod.Version)
}
targetStr := combinations[0]
isRoot := mod.Path == rootID.Path && mod.Version == rootID.Version
testThisMod := b.runTest && isRoot && mod.OnTest != nil

Expand All @@ -222,17 +246,16 @@ func (b *Builder) Build(ctx context.Context, targets []*modules.Module) ([]Resul
}
defer unlock()

installDir, err := b.installDir(mod.Path, mod.Version)
installDir, err := b.installDir(mod.Path, mod.Version, targetStr)
if err != nil {
return Result{}, err
}
deps := b.resolveModTransitiveDeps(targets, mod)
modVer := module.Version{Path: mod.Path, Version: mod.Version}

// Consult the build cache. A hit means we already have the
// module's build metadata and its installDir is populated from a
// previous successful build.
entry, cacheHit, err := b.cache.Get(ctx, cache.Key{Module: modVer, Matrix: b.matrix})
entry, cacheHit, err := b.cache.Get(ctx, cache.Key{Module: modVer, Matrix: targetStr})
if err != nil {
return Result{}, err
}
Expand Down Expand Up @@ -270,9 +293,23 @@ func (b *Builder) Build(ctx context.Context, targets []*modules.Module) ([]Resul
}

getOutputDir := func(_ string, m module.Version) (string, error) {
return b.installDir(m.Path, m.Version)
for _, dep := range targets {
if dep.Path != m.Path || dep.Version != m.Version {
continue
}
target := classfile.Matrix{
Require: maps.Clone(b.target.Require),
Options: intersect(b.target.Options, dep.Formula.Matrix.Options),
}
combinations := target.Combinations()
if len(combinations) == 0 {
return "", fmt.Errorf("%s@%s has no matrix dimensions matching target", m.Path, m.Version)
}
return b.installDir(m.Path, m.Version, combinations[0])
}
return "", fmt.Errorf("target not found for %s@%s", m.Path, m.Version)
}
buildContext := classfile.NewContext(tmpSourceDir, installDir, b.matrix, getOutputDir)
buildContext := classfile.NewContext(tmpSourceDir, installDir, targetStr, getOutputDir)

// Inject results of already-built dependencies
for modVer, result := range builtResults {
Expand Down Expand Up @@ -313,7 +350,7 @@ func (b *Builder) Build(ctx context.Context, targets []*modules.Module) ([]Resul
// Save cache only on cache miss. A cache hit means the entry is
// already present and current; OnTest does not modify metadata.
if !cacheHit {
entry, err := b.cache.Put(ctx, cache.Key{Module: modVer, Matrix: b.matrix}, os.DirFS(installDir), cache.Entry{
entry, err := b.cache.Put(ctx, cache.Key{Module: modVer, Matrix: targetStr}, os.DirFS(installDir), cache.Entry{
Metadata: metadata,
Deps: deps,
})
Expand Down
116 changes: 108 additions & 8 deletions internal/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import (
"io/fs"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
"time"

classfile "github.com/goplus/llar/formula"
"github.com/goplus/llar/internal/build/cache"
internalformula "github.com/goplus/llar/internal/formula"
"github.com/goplus/llar/internal/formula/repo"
"github.com/goplus/llar/internal/modules"
"github.com/goplus/llar/internal/vcs"
Expand Down Expand Up @@ -305,8 +307,10 @@ func setupBuilder(t *testing.T, store repo.Store, matrix string) *Builder {
t.Helper()
workspaceDir := t.TempDir()
return &Builder{
store: store,
matrix: matrix,
store: store,
target: classfile.Matrix{Require: map[string][]string{
"matrix": {matrix},
}},
workspaceDir: workspaceDir,
cache: &localCache{workspaceDir: workspaceDir},
newRepo: func(repoPath string) (vcs.Repo, error) {
Expand Down Expand Up @@ -351,10 +355,12 @@ type cachePut struct {

type recordingCache struct {
hits map[module.Version]cache.Entry
gets []cache.Key
puts []cachePut
}

func (c *recordingCache) Get(ctx context.Context, key cache.Key) (cache.Entry, bool, error) {
c.gets = append(c.gets, key)
if c.hits == nil {
return cache.Entry{}, false, nil
}
Expand All @@ -380,8 +386,11 @@ func TestNewBuilder(t *testing.T) {
tmpDir := t.TempDir()
store := setupTestStore(t)
b, err := NewBuilder(Options{
Store: store,
MatrixStr: "amd64-linux",
Store: store,
Target: classfile.Matrix{Require: map[string][]string{
"arch": {"amd64"},
"os": {"linux"},
}},
WorkspaceDir: tmpDir,
})
if err != nil {
Expand All @@ -390,8 +399,8 @@ func TestNewBuilder(t *testing.T) {
if b.workspaceDir != tmpDir {
t.Errorf("workspaceDir = %q, want %q", b.workspaceDir, tmpDir)
}
if b.matrix != "amd64-linux" {
t.Errorf("matrix = %q, want %q", b.matrix, "amd64-linux")
if got := b.target.Combinations()[0]; got != "amd64-linux" {
t.Errorf("target = %q, want %q", got, "amd64-linux")
}
if b.store != store {
t.Error("store not set correctly")
Expand All @@ -403,7 +412,10 @@ func TestNewBuilder(t *testing.T) {

t.Run("default workspace dir", func(t *testing.T) {
b, err := NewBuilder(Options{
MatrixStr: "arm64-darwin",
Target: classfile.Matrix{Require: map[string][]string{
"arch": {"arm64"},
"os": {"darwin"},
}},
})
if err != nil {
t.Fatalf("NewBuilder() error = %v", err)
Expand All @@ -421,6 +433,94 @@ func TestNewBuilder(t *testing.T) {
})
}

func TestIntersect(t *testing.T) {
values := map[string][]string{
"debug": {"ON"},
"shared": {"OFF"},
"ssl": {"openssl"},
}
keys := map[string][]string{
"debug": nil,
"ssl": nil,
}

got := intersect(values, keys)
want := map[string][]string{
"debug": {"ON"},
"ssl": {"openssl"},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("intersect() = %#v, want %#v", got, want)
}
}

func TestIntersect_NoMatch(t *testing.T) {
got := intersect(map[string][]string{"shared": {"OFF"}}, nil)
if got != nil {
t.Fatalf("intersect() = %#v, want nil", got)
}
}

func TestBuild_UsesTargetIntersectionForCache(t *testing.T) {
store := setupTestStore(t)
modVer := module.Version{Path: "test/liba", Version: "1.0.0"}
recording := &recordingCache{
hits: map[module.Version]cache.Entry{modVer: {Metadata: "-lA"}},
}
b := &Builder{
store: store,
target: classfile.Matrix{
Require: map[string][]string{
"arch": {"amd64"},
"os": {"linux"},
},
Options: map[string][]string{
"debug": {"ON"},
"shared": {"OFF"},
},
},
workspaceDir: t.TempDir(),
cache: recording,
}
targets := []*modules.Module{{
Formula: &internalformula.Formula{Matrix: classfile.Matrix{
Options: map[string][]string{"debug": nil},
}},
Path: modVer.Path,
Version: modVer.Version,
}}

if _, err := b.Build(context.Background(), targets); err != nil {
t.Fatalf("Build() error = %v", err)
}
if len(recording.gets) != 1 {
t.Fatalf("cache gets = %d, want 1", len(recording.gets))
}
if got, want := recording.gets[0].Matrix, "amd64-linux|ON"; got != want {
t.Fatalf("cache matrix = %q, want %q", got, want)
}
}

func TestBuild_RejectsTargetWithoutIntersection(t *testing.T) {
b := &Builder{
target: classfile.Matrix{
Options: map[string][]string{"shared": {"OFF"}},
},
}
targets := []*modules.Module{{
Formula: &internalformula.Formula{Matrix: classfile.Matrix{
Options: map[string][]string{"debug": nil},
}},
Path: "test/liba",
Version: "1.0.0",
}}

_, err := b.Build(context.Background(), targets)
if err == nil {
t.Fatal("Build() error = nil, want target intersection error")
}
}

// ---------------------------------------------------------------------------
// Build error path tests
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -964,7 +1064,7 @@ func TestBuild_InstallDirConvention(t *testing.T) {
main := module.Version{Path: "test/liba", Version: "1.0.0"}
loadAndBuild(t, b, store, main)

installDir, _ := b.installDir("test/liba", "1.0.0")
installDir, _ := b.installDir("test/liba", "1.0.0", "amd64-linux")

// Verify the path follows workspace/<escaped>@<version>-<matrix>
rel, err := filepath.Rel(b.workspaceDir, installDir)
Expand Down
4 changes: 2 additions & 2 deletions internal/build/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ func (c *localCache) cacheDir(modPath string) (string, error) {
}

// installDir returns the build output directory: workspaceDir/<escapedPath>@<version>-<matrix>.
func (b *Builder) installDir(modPath, version string) (string, error) {
func (b *Builder) installDir(modPath, version, target string) (string, error) {
escaped, err := module.EscapePath(modPath)
if err != nil {
return "", err
}
return filepath.Join(b.workspaceDir, fmt.Sprintf("%s@%s-%s", escaped, version, b.matrix)), nil
return filepath.Join(b.workspaceDir, fmt.Sprintf("%s@%s-%s", escaped, version, target)), nil
}

// loadCache reads the cache file for a module from the workspace directory.
Expand Down
Loading
Loading