From d64bb2210273eee5fc32b21591c02fbbb21c7f1e Mon Sep 17 00:00:00 2001 From: Rick Guo Date: Fri, 21 Aug 2026 10:57:59 +0800 Subject: [PATCH 1/3] fix(modules): preserve matrix on loaded formulas --- internal/modules/load.go | 1 + internal/modules/load_test.go | 43 +++++++++++++++++++ .../towner/targetreq/1.0.0/Targetreq_llar.gox | 5 ++- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/modules/load.go b/internal/modules/load.go index 185ba01f..1a6aaf42 100644 --- a/internal/modules/load.go +++ b/internal/modules/load.go @@ -146,6 +146,7 @@ func (c *formulaContext) convertToModules(ctx context.Context, modList []module. if err != nil { return nil, err } + injectMatrix(f, c.matrix) module := &Module{ Formula: f, FS: thisMod.fsys, diff --git a/internal/modules/load_test.go b/internal/modules/load_test.go index 49a77272..5be425e6 100644 --- a/internal/modules/load_test.go +++ b/internal/modules/load_test.go @@ -6,6 +6,7 @@ import ( "fmt" "io/fs" "os" + "reflect" "slices" "strings" "testing" @@ -455,6 +456,48 @@ func TestLoad_InjectsTargetBeforeFilterAndOnRequire(t *testing.T) { if findModule(mods, "towner/depmod") == nil { t.Fatalf("missing towner/depmod in build list") } + + mainMod := findModule(mods, main.Path) + if mainMod == nil || mainMod.Formula == nil { + t.Fatalf("missing formula for %s", main.Path) + } + if mainMod.Formula.OnBuild == nil { + t.Fatal("targetreq OnBuild is nil") + } + buildCtx := classfile.NewContext(&classfile.Project{}, "", "", "", nil) + mainMod.Formula.OnBuild(buildCtx) + if got := buildCtx.Out.Metadata(); got != "-ltargetreq-linux-openssl" { + t.Fatalf("OnBuild metadata = %q, want %q", got, "-ltargetreq-linux-openssl") + } +} + +func TestConvertToModules_InjectsSelectedMatrix(t *testing.T) { + store := setupTestStore(t, "testdata/load") + matrix := classfile.Matrix{ + Require: map[string][]string{"os": {"linux"}}, + Options: map[string][]string{"ssl": {"securetransport"}}, + } + formulaCtx := newFormulaContext(store.ModuleFS, matrix) + + mods, err := formulaCtx.convertToModules(context.Background(), []module.Version{ + {Path: "towner/targetreq", Version: "1.0.0"}, + }) + if err != nil { + t.Fatalf("convertToModules failed: %v", err) + } + if len(mods) != 1 { + t.Fatalf("modules len = %d, want 1", len(mods)) + } + + formulaElem := reflect.ValueOf(mods[0].Formula).Elem() + structElem := valueOf(formulaElem, "structElem").(reflect.Value) + target := valueOf(structElem, "target").(classfile.Matrix) + if got := target.Require["os"]; !slices.Equal(got, []string{"linux"}) { + t.Errorf("target.require[os] = %v, want [linux]", got) + } + if got := target.Options["ssl"]; !slices.Equal(got, []string{"securetransport"}) { + t.Errorf("target.options[ssl] = %v, want [securetransport]", got) + } } func TestLoad_FilterRejectsSelectedMatrix(t *testing.T) { diff --git a/internal/modules/testdata/load/towner/targetreq/1.0.0/Targetreq_llar.gox b/internal/modules/testdata/load/towner/targetreq/1.0.0/Targetreq_llar.gox index 747d5a22..c9186386 100644 --- a/internal/modules/testdata/load/towner/targetreq/1.0.0/Targetreq_llar.gox +++ b/internal/modules/testdata/load/towner/targetreq/1.0.0/Targetreq_llar.gox @@ -21,5 +21,8 @@ onRequire (proj, deps) => { } onBuild ctx => { - ctx.setMetadata "-ltargetreq" + if slices.contains(target.require["os"], "linux") && + slices.contains(target.options["ssl"], "openssl") { + ctx.setMetadata "-ltargetreq-linux-openssl" + } } From e308d7a43fab236c3b023ddfc98f9eaf5ff74251 Mon Sep 17 00:00:00 2001 From: Rick Guo Date: Fri, 21 Aug 2026 11:36:04 +0800 Subject: [PATCH 2/3] refactor(modules): bind matrix to formula context --- internal/modules/load.go | 11 +++----- internal/modules/load_coverage_test.go | 9 +++---- internal/modules/load_test.go | 36 +++++++++++++++++++++----- internal/modules/source.go | 10 +++++-- 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/internal/modules/load.go b/internal/modules/load.go index 1a6aaf42..c3485013 100644 --- a/internal/modules/load.go +++ b/internal/modules/load.go @@ -116,6 +116,7 @@ func (c *formulaContext) moduleOf(ctx context.Context, modPath string) (*formula return nil, err } fm := newFormulaModule(fs, modPath) + fm.matrix = c.matrix actual, _ := c.moduleCache.LoadOrStore(modPath, fm) return actual.(*formulaModule), nil } @@ -130,7 +131,7 @@ func (c *formulaContext) loadDeps(ctx context.Context, mod module.Version) (deps if err != nil { return nil, err } - return resolveDeps(mod, thisMod.fsys.(fs.ReadFileFS), f, c.matrix) + return resolveDeps(mod, thisMod.fsys.(fs.ReadFileFS), f) } // convertToModules converts a list of module.Version into loaded Module structs. @@ -146,7 +147,6 @@ func (c *formulaContext) convertToModules(ctx context.Context, modList []module. if err != nil { return nil, err } - injectMatrix(f, c.matrix) module := &Module{ Formula: f, FS: thisMod.fsys, @@ -193,7 +193,7 @@ func Load(ctx context.Context, main module.Version, opts Options) ([]*Module, er if err != nil { return nil, err } - mainDeps, err := resolveDeps(main, mainMod.fsys.(fs.ReadFileFS), mainFormula, context.matrix) + mainDeps, err := resolveDeps(main, mainMod.fsys.(fs.ReadFileFS), mainFormula) if err != nil { return nil, err } @@ -288,14 +288,11 @@ func runFormulaHook(fn func()) (err error) { // resolveDeps resolves the dependencies for a formula. // It first tries to get dependencies from the OnRequire callback, // then falls back to parsing versions.json if no dependencies are found. -func resolveDeps(mod module.Version, modFS fs.ReadFileFS, frla *formula.Formula, matrix classfile.Matrix) ([]module.Version, error) { +func resolveDeps(mod module.Version, modFS fs.ReadFileFS, frla *formula.Formula) ([]module.Version, error) { if err := validateModulePath(mod.Path); err != nil { return nil, err } - // XGo formulas read the selected matrix through target.require/options. - // Inject before filter and onRequire so both hooks see the same target. - injectMatrix(frla, matrix) if frla.Filter != nil { var supported bool if err := runFormulaHook(func() { diff --git a/internal/modules/load_coverage_test.go b/internal/modules/load_coverage_test.go index ac780072..c2a03509 100644 --- a/internal/modules/load_coverage_test.go +++ b/internal/modules/load_coverage_test.go @@ -11,7 +11,6 @@ import ( "strings" "testing" - classfile "github.com/goplus/llar/formula" "github.com/goplus/llar/internal/formula" "github.com/goplus/llar/internal/vcs" "github.com/goplus/llar/mod/module" @@ -123,7 +122,7 @@ func TestResolveDeps_InvalidModulePath(t *testing.T) { mod := module.Version{Path: "", Version: "1.0.0"} frla := &formula.Formula{ModPath: "", FromVer: "1.0.0"} - _, err := resolveDeps(mod, modFS, frla, classfile.Matrix{}) + _, err := resolveDeps(mod, modFS, frla) if err == nil { t.Fatal("expected error for invalid module path") } @@ -215,7 +214,7 @@ func TestResolveDeps_InvalidDependencyPathFromVersions(t *testing.T) { mod := module.Version{Path: "towner/main", Version: "1.0.0"} frla := loadTestFormula(t, "testdata/load/towner/standalone", "towner/standalone", "1.0.0") - _, err := resolveDeps(mod, modFS, frla, classfile.Matrix{}) + _, err := resolveDeps(mod, modFS, frla) if err == nil { t.Fatal("expected error for invalid dependency path") } @@ -229,7 +228,7 @@ func TestResolveDeps_MissingVersionsFile(t *testing.T) { mod := module.Version{Path: "towner/badcmp", Version: "1.0.0"} frla := loadTestFormula(t, "testdata/load/towner/standalone", "towner/standalone", "1.0.0") - _, err := resolveDeps(mod, modFS, frla, classfile.Matrix{}) + _, err := resolveDeps(mod, modFS, frla) if err == nil { t.Fatal("expected error for missing versions.json") } @@ -278,7 +277,7 @@ func TestResolveDeps_OnRequireMkdirTempError(t *testing.T) { modFS := os.DirFS("testdata/load/towner/withreq").(fs.ReadFileFS) mod := module.Version{Path: "towner/withreq", Version: "1.0.0"} - _, err := resolveDeps(mod, modFS, frla, classfile.Matrix{}) + _, err := resolveDeps(mod, modFS, frla) if err == nil { t.Fatal("expected MkdirTemp error") } diff --git a/internal/modules/load_test.go b/internal/modules/load_test.go index 5be425e6..ebe435c2 100644 --- a/internal/modules/load_test.go +++ b/internal/modules/load_test.go @@ -91,7 +91,7 @@ func TestResolveDeps_NoOnRequire_DepsFromVersionsJson(t *testing.T) { frla := loadTestFormula(t, "testdata/load/towner/mainmod", "towner/mainmod", "1.0.0") mod := module.Version{Path: "towner/mainmod", Version: "1.0.0"} - deps, err := resolveDeps(mod, modFS, frla, classfile.Matrix{}) + deps, err := resolveDeps(mod, modFS, frla) if err != nil { t.Fatalf("resolveDeps failed: %v", err) } @@ -108,7 +108,7 @@ func TestResolveDeps_NoOnRequire_NoDeps(t *testing.T) { frla := loadTestFormula(t, "testdata/load/towner/leafmod", "towner/leafmod", "1.0.0") mod := module.Version{Path: "towner/leafmod", Version: "1.0.0"} - deps, err := resolveDeps(mod, modFS, frla, classfile.Matrix{}) + deps, err := resolveDeps(mod, modFS, frla) if err != nil { t.Fatalf("resolveDeps failed: %v", err) } @@ -161,7 +161,7 @@ func TestResolveDeps_VersionNotInDepsTable(t *testing.T) { // Version 9.9.9 doesn't exist in versions.json deps table mod := module.Version{Path: "towner/mainmod", Version: "9.9.9"} - deps, err := resolveDeps(mod, modFS, frla, classfile.Matrix{}) + deps, err := resolveDeps(mod, modFS, frla) if err != nil { t.Fatalf("resolveDeps failed: %v", err) } @@ -175,7 +175,7 @@ func TestResolveDeps_WithOnRequire_EchoOnly_FallbackToVersionsJson(t *testing.T) modFS := os.DirFS("testdata/load/towner/withreq").(fs.ReadFileFS) mod := module.Version{Path: "towner/withreq", Version: "1.0.0"} - deps, err := resolveDeps(mod, modFS, frla, classfile.Matrix{}) + deps, err := resolveDeps(mod, modFS, frla) if err != nil { t.Fatalf("resolveDeps failed: %v", err) } @@ -193,7 +193,7 @@ func TestResolveDeps_WithOnRequire_AddsDeps(t *testing.T) { modFS := os.DirFS("testdata/load/towner/withdeps").(fs.ReadFileFS) mod := module.Version{Path: "towner/withdeps", Version: "1.0.0"} - deps, err := resolveDeps(mod, modFS, frla, classfile.Matrix{}) + deps, err := resolveDeps(mod, modFS, frla) if err != nil { t.Fatalf("resolveDeps failed: %v", err) } @@ -212,7 +212,7 @@ func TestResolveDeps_WithOnRequire_EmptyVersionFallback(t *testing.T) { modFS := os.DirFS("testdata/load/towner/reqnover").(fs.ReadFileFS) mod := module.Version{Path: "towner/reqnover", Version: "1.0.0"} - deps, err := resolveDeps(mod, modFS, frla, classfile.Matrix{}) + deps, err := resolveDeps(mod, modFS, frla) if err != nil { t.Fatalf("resolveDeps failed: %v", err) } @@ -237,7 +237,7 @@ func TestResolveDeps_WithOnRequire_UnknownDepDropped(t *testing.T) { modFS := os.DirFS("testdata/load/towner/reqdrop").(fs.ReadFileFS) mod := module.Version{Path: "towner/reqdrop", Version: "1.0.0"} - deps, err := resolveDeps(mod, modFS, frla, classfile.Matrix{}) + deps, err := resolveDeps(mod, modFS, frla) if err != nil { t.Fatalf("resolveDeps failed: %v", err) } @@ -471,6 +471,28 @@ func TestLoad_InjectsTargetBeforeFilterAndOnRequire(t *testing.T) { } } +func TestFormulaContext_AtInjectsMatrix(t *testing.T) { + store := setupTestStore(t, "testdata/load") + formulaCtx := newFormulaContext(store.ModuleFS, classfile.Matrix{ + Require: map[string][]string{"os": {"linux"}}, + }) + + mod, err := formulaCtx.moduleOf(context.Background(), "towner/targetreq") + if err != nil { + t.Fatalf("moduleOf failed: %v", err) + } + f, err := mod.at("1.0.0") + if err != nil { + t.Fatalf("at failed: %v", err) + } + + var deps classfile.ModuleDeps + f.OnRequire(&classfile.Project{}, &deps) + if got := len(deps.Deps()); got != 1 { + t.Fatalf("OnRequire deps = %d, want 1", got) + } +} + func TestConvertToModules_InjectsSelectedMatrix(t *testing.T) { store := setupTestStore(t, "testdata/load") matrix := classfile.Matrix{ diff --git a/internal/modules/source.go b/internal/modules/source.go index 461bde20..84a1bfc5 100644 --- a/internal/modules/source.go +++ b/internal/modules/source.go @@ -12,6 +12,7 @@ import ( "sync" "github.com/goplus/ixgo/xgobuild" + classfile "github.com/goplus/llar/formula" "github.com/goplus/llar/internal/formula" "github.com/goplus/llar/mod/module" "github.com/goplus/llar/x/gnu" @@ -28,6 +29,7 @@ const defaultComparatorSuffix = "_cmp.gox" type formulaModule struct { fsys fs.FS modPath string + matrix classfile.Matrix comparator func() (func(v1, v2 module.Version) int, error) mu sync.Mutex @@ -82,7 +84,9 @@ func (m *formulaModule) at(version string) (*formula.Formula, error) { defer m.mu.Unlock() if f, ok := m.formulas[fromVer]; ok { - return formula.Clone(f), nil + clone := formula.Clone(f) + injectMatrix(clone, m.matrix) + return clone, nil } f, err := formula.LoadFS(m.fsys.(fs.ReadFileFS), formulaPath) @@ -90,7 +94,9 @@ func (m *formulaModule) at(version string) (*formula.Formula, error) { return nil, err } m.formulas[fromVer] = f - return formula.Clone(f), nil + clone := formula.Clone(f) + injectMatrix(clone, m.matrix) + return clone, nil } // findMaxFromVer finds the formula file with the highest fromVer that is <= the target version. From 2761624d82a604d610f552f1bbf080eedc6bc4ff Mon Sep 17 00:00:00 2001 From: Rick Guo Date: Fri, 21 Aug 2026 11:42:45 +0800 Subject: [PATCH 3/3] refactor(modules): initialize formula module matrix --- internal/modules/load.go | 3 +-- internal/modules/load_test.go | 2 +- internal/modules/source.go | 3 ++- internal/modules/source_test.go | 24 ++++++++++++------------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/modules/load.go b/internal/modules/load.go index c3485013..40e0e9f2 100644 --- a/internal/modules/load.go +++ b/internal/modules/load.go @@ -115,8 +115,7 @@ func (c *formulaContext) moduleOf(ctx context.Context, modPath string) (*formula if err != nil { return nil, err } - fm := newFormulaModule(fs, modPath) - fm.matrix = c.matrix + fm := newFormulaModule(fs, modPath, c.matrix) actual, _ := c.moduleCache.LoadOrStore(modPath, fm) return actual.(*formulaModule), nil } diff --git a/internal/modules/load_test.go b/internal/modules/load_test.go index ebe435c2..b61d5eaa 100644 --- a/internal/modules/load_test.go +++ b/internal/modules/load_test.go @@ -54,7 +54,7 @@ func setupTestStore(t *testing.T, testdataDir string) repo.Store { func loadTestFormula(t *testing.T, moduleDir, modPath, version string) *formula.Formula { t.Helper() fsys := os.DirFS(moduleDir) - mod := newFormulaModule(fsys, modPath) + mod := newFormulaModule(fsys, modPath, classfile.Matrix{}) f, err := mod.at(version) if err != nil { t.Fatalf("failed to load formula for %s@%s: %v", modPath, version, err) diff --git a/internal/modules/source.go b/internal/modules/source.go index 84a1bfc5..973a3dcb 100644 --- a/internal/modules/source.go +++ b/internal/modules/source.go @@ -39,10 +39,11 @@ type formulaModule struct { // newFormulaModule creates a new formulaModule for the given module. // The fsys should be rooted at the module's directory (already positioned by the caller). // The modPath is used for constructing module.Version in version comparisons. -func newFormulaModule(fsys fs.FS, modPath string) *formulaModule { +func newFormulaModule(fsys fs.FS, modPath string, matrix classfile.Matrix) *formulaModule { m := &formulaModule{ fsys: fsys, modPath: modPath, + matrix: matrix, formulas: make(map[string]*formula.Formula), } m.comparator = sync.OnceValues(func() (func(v1, v2 module.Version) int, error) { diff --git a/internal/modules/source_test.go b/internal/modules/source_test.go index ca1da05d..1ded6955 100644 --- a/internal/modules/source_test.go +++ b/internal/modules/source_test.go @@ -20,7 +20,7 @@ import ( func TestNewFormulaModule(t *testing.T) { fsys := os.DirFS("testdata/DaveGamble/cJSON") - mod := newFormulaModule(fsys, "DaveGamble/cJSON") + mod := newFormulaModule(fsys, "DaveGamble/cJSON", classfile.Matrix{}) if mod == nil { t.Fatal("newFormulaModule returned nil") @@ -38,7 +38,7 @@ func TestNewFormulaModule(t *testing.T) { func TestFormulaModule_Comparator(t *testing.T) { fsys := os.DirFS("testdata/DaveGamble/cJSON") - mod := newFormulaModule(fsys, "DaveGamble/cJSON") + mod := newFormulaModule(fsys, "DaveGamble/cJSON", classfile.Matrix{}) // First call should load comparator cmp, err := mod.comparator() @@ -76,7 +76,7 @@ func TestFormulaModule_Comparator(t *testing.T) { func TestFormulaModule_ComparatorDefaultFallback(t *testing.T) { fsys := os.DirFS("testdata/madler/zlib") // madler/zlib has no comparator file, should use default - mod := newFormulaModule(fsys, "madler/zlib") + mod := newFormulaModule(fsys, "madler/zlib", classfile.Matrix{}) cmp, err := mod.comparator() if err != nil { @@ -96,7 +96,7 @@ func TestFormulaModule_ComparatorDefaultFallback(t *testing.T) { func TestFormulaModule_At(t *testing.T) { fsys := os.DirFS("testdata/DaveGamble/cJSON") - mod := newFormulaModule(fsys, "DaveGamble/cJSON") + mod := newFormulaModule(fsys, "DaveGamble/cJSON", classfile.Matrix{}) // Test getting formula for version v1.7.18 (should match fromVer v1.5.0) f, err := mod.at("v1.7.18") @@ -124,7 +124,7 @@ func TestFormulaModule_At(t *testing.T) { } func TestFormulaModule_AtReturnsIsolatedClasses(t *testing.T) { - mod := newFormulaModule(os.DirFS("testdata/load/towner/targetreq"), "towner/targetreq") + mod := newFormulaModule(os.DirFS("testdata/load/towner/targetreq"), "towner/targetreq", classfile.Matrix{}) const workers = 32 errs := make(chan error, workers) @@ -177,7 +177,7 @@ func TestFormulaModule_AtReturnsIsolatedClasses(t *testing.T) { func TestFormulaModule_AtVersionMatching(t *testing.T) { fsys := os.DirFS("testdata/DaveGamble/cJSON") - mod := newFormulaModule(fsys, "DaveGamble/cJSON") + mod := newFormulaModule(fsys, "DaveGamble/cJSON", classfile.Matrix{}) tests := []struct { version string @@ -206,7 +206,7 @@ func TestFormulaModule_AtVersionMatching(t *testing.T) { func TestFormulaModule_AtNoFormula(t *testing.T) { fsys := os.DirFS("testdata/DaveGamble/cJSON") - mod := newFormulaModule(fsys, "DaveGamble/cJSON") + mod := newFormulaModule(fsys, "DaveGamble/cJSON", classfile.Matrix{}) // Version lower than all fromVer should fail _, err := mod.at("v0.5.0") @@ -217,7 +217,7 @@ func TestFormulaModule_AtNoFormula(t *testing.T) { func TestFormulaModule_AtNonexistentModule(t *testing.T) { fsys := os.DirFS("testdata/nonexistent") - mod := newFormulaModule(fsys, "nonexistent/module") + mod := newFormulaModule(fsys, "nonexistent/module", classfile.Matrix{}) _, err := mod.at("1.0.0") if err == nil { @@ -227,7 +227,7 @@ func TestFormulaModule_AtNonexistentModule(t *testing.T) { func TestFormulaModule_FindMaxFromVer(t *testing.T) { fsys := os.DirFS("testdata/DaveGamble/cJSON") - mod := newFormulaModule(fsys, "DaveGamble/cJSON") + mod := newFormulaModule(fsys, "DaveGamble/cJSON", classfile.Matrix{}) cmp, _ := mod.comparator() target := module.Version{Path: "DaveGamble/cJSON", Version: "v1.7.18"} @@ -448,7 +448,7 @@ func TestParseCallArg_NonStringArg(t *testing.T) { func TestIntegration_FormulaModuleToFormula(t *testing.T) { fsys := os.DirFS("testdata/DaveGamble/cJSON") - mod := newFormulaModule(fsys, "DaveGamble/cJSON") + mod := newFormulaModule(fsys, "DaveGamble/cJSON", classfile.Matrix{}) // Get formula f, err := mod.at("v1.7.18") @@ -477,7 +477,7 @@ func TestIntegration_MultipleModules(t *testing.T) { for _, m := range modules { fsys := os.DirFS(m.dir) - mod := newFormulaModule(fsys, m.path) + mod := newFormulaModule(fsys, m.path, classfile.Matrix{}) f, err := mod.at(m.version) if err != nil { @@ -513,7 +513,7 @@ func TestIntegration_RealRepo(t *testing.T) { t.Fatalf("ModuleFS() failed: %v", err) } - mod := newFormulaModule(fsys, "madler/zlib") + mod := newFormulaModule(fsys, "madler/zlib", classfile.Matrix{}) // Test comparator (should fall back to GNU version comparison) cmp, err := mod.comparator()