Skip to content

Commit 982a3fe

Browse files
authored
feat(make): support local formula path and improve test coverage (#104)
* feat(make): support local file patterns for llar make Add support for local filesystem patterns (., ./path, ./path@ver) in `llar make`, enabling CI workflows within the llarhub repository to build local formulas while resolving dependencies from the remote store. Key changes: - Extract Store interface from concrete struct in repo package - Add overlayStore to serve local modules with remote fallback - Add modlocal package for local module discovery via versions.json - Update parseModuleArg to detect and validate local patterns - Split runMake into local/remote branches with overlay composition * docs: add local formula build examples to README Add documentation examples demonstrating how to build local formulas using `llar make` command with local paths. This clarifies the usage of the tool for building formulas from local directories. * fix(overlay): implement LockModule with local lock file for local modules * docs: clarify unsupported parent directory module patterns Add documentation note explaining that `..` and `../path` patterns are not supported for local module imports. Users should use `.` instead, which automatically walks up from the current directory to find the nearest `versions.json` file, eliminating the need for manual parent directory navigation. * test(make): add local libpng e2e fixtures and test * test(make): switch local-remote e2e to cjson with remote zlib * test(make): switch mixed-source e2e to local demo repo + remote zlib * test(make): log captured llar output in mixed-source e2e * test(make): clarify in-process stdout capture rationale * test(make): expand parseModuleArg path edge cases * fix(make): disable local ./... pattern for now * fix(repo): unify overlay locking by module path * fix(make): enforce local pattern validity rules * refactor(modlocal): move pattern validation into Resolve * test(modlocal): expand local pattern edge-case coverage * modules: validate remote module paths in load pipeline * feat(make): align local patterns with Go-style paths
1 parent 0b1b75a commit 982a3fe

20 files changed

Lines changed: 1573 additions & 72 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ llar make -o ./output madler/zlib@v1.3.1
3333

3434
# Build and export as a zip archive
3535
llar make -o zlib.zip madler/zlib@v1.3.1
36+
37+
# Build a local formula
38+
llar make ./@1.0.0
39+
llar make ./madler/zlib@v1.3.1
3640
```
3741

3842
### Commands

cmd/llar/internal/make.go

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"archive/zip"
55
"context"
66
"fmt"
7+
stdbuild "go/build"
78
"io"
89
"os"
910
"path/filepath"
@@ -14,6 +15,7 @@ import (
1415
"github.com/goplus/llar/internal/build"
1516
"github.com/goplus/llar/internal/formula/repo"
1617
"github.com/goplus/llar/internal/modules"
18+
"github.com/goplus/llar/internal/modules/modlocal"
1719
"github.com/goplus/llar/internal/vcs"
1820
"github.com/goplus/llar/mod/module"
1921
"github.com/spf13/cobra"
@@ -22,6 +24,19 @@ import (
2224
var makeVerbose bool
2325
var makeOutput string
2426

27+
// newRemoteStore creates the remote formula store. Overridable for testing.
28+
var newRemoteStore = func() (repo.Store, error) {
29+
formulaDir, err := repo.DefaultDir()
30+
if err != nil {
31+
return nil, fmt.Errorf("failed to get formula dir: %w", err)
32+
}
33+
formulaRepo, err := vcs.NewRepo("github.com/goplus/llarhub")
34+
if err != nil {
35+
return nil, err
36+
}
37+
return repo.New(formulaDir, formulaRepo), nil
38+
}
39+
2540
var makeCmd = &cobra.Command{
2641
Use: "make [module@version]",
2742
Short: "Build a module to FormulaDir",
@@ -37,38 +52,79 @@ func init() {
3752
}
3853

3954
func runMake(cmd *cobra.Command, args []string) error {
40-
modPath, version := parseModuleArg(args[0])
55+
pattern, version, isLocal, err := parseModuleArg(args[0])
56+
if err != nil {
57+
return err
58+
}
4159

4260
ctx := context.Background()
4361

44-
// Set up formula store
45-
formulaDir, err := repo.DefaultDir()
62+
// Resolve output path to absolute before build (build may change cwd)
63+
if makeOutput != "" {
64+
abs, err := filepath.Abs(makeOutput)
65+
if err != nil {
66+
return fmt.Errorf("failed to resolve output path: %w", err)
67+
}
68+
makeOutput = abs
69+
}
70+
71+
matrix := formula.Matrix{
72+
Require: map[string][]string{
73+
"os": {runtime.GOOS},
74+
"arch": {runtime.GOARCH},
75+
},
76+
}
77+
matrixStr := matrix.Combinations()[0]
78+
79+
// Set up remote formula store (always needed for deps)
80+
remoteStore, err := newRemoteStore()
4681
if err != nil {
47-
return fmt.Errorf("failed to get formula dir: %w", err)
82+
return err
4883
}
49-
formulaRepo, err := vcs.NewRepo("github.com/goplus/llarhub")
84+
85+
if !isLocal {
86+
return buildModule(ctx, remoteStore, pattern, version, matrixStr)
87+
}
88+
89+
// Resolve local pattern
90+
cwd, err := os.Getwd()
91+
if err != nil {
92+
return fmt.Errorf("failed to get working directory: %w", err)
93+
}
94+
95+
localMods, err := modlocal.Resolve(cwd, pattern)
5096
if err != nil {
5197
return err
5298
}
53-
store := repo.New(formulaDir, formulaRepo)
5499

55-
// Load modules
100+
// Build overlay: local modules from disk, deps from remote
101+
locals := make(map[string]string, len(localMods))
102+
for _, m := range localMods {
103+
locals[m.Path] = m.Dir
104+
}
105+
store := repo.NewOverlayStore(remoteStore, locals)
106+
107+
for _, m := range localMods {
108+
ver := m.Version
109+
if ver == "" {
110+
ver = version // global @version from arg
111+
}
112+
if err := buildModule(ctx, store, m.Path, ver, matrixStr); err != nil {
113+
return err
114+
}
115+
}
116+
return nil
117+
}
118+
119+
// buildModule loads and builds a single module.
120+
func buildModule(ctx context.Context, store repo.Store, modPath, version, matrixStr string) error {
56121
mods, err := modules.Load(ctx, module.Version{Path: modPath, Version: version}, modules.Options{
57122
FormulaStore: store,
58123
})
59124
if err != nil {
60125
return fmt.Errorf("failed to load modules: %w", err)
61126
}
62127

63-
// Resolve output path to absolute before build (build may change cwd)
64-
if makeOutput != "" {
65-
abs, err := filepath.Abs(makeOutput)
66-
if err != nil {
67-
return fmt.Errorf("failed to resolve output path: %w", err)
68-
}
69-
makeOutput = abs
70-
}
71-
72128
// Handle verbose output
73129
var savedStdout, savedStderr *os.File
74130
if !makeVerbose {
@@ -77,7 +133,6 @@ func runMake(cmd *cobra.Command, args []string) error {
77133
mod.SetStderr(io.Discard)
78134
}
79135

80-
// Redirect os.Stdout/os.Stderr so subprocess output (cmake, etc.) is also silenced
81136
savedStdout = os.Stdout
82137
savedStderr = os.Stderr
83138
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
@@ -93,15 +148,6 @@ func runMake(cmd *cobra.Command, args []string) error {
93148
}()
94149
}
95150

96-
matrix := formula.Matrix{
97-
Require: map[string][]string{
98-
"os": {runtime.GOOS},
99-
"arch": {runtime.GOARCH},
100-
},
101-
}
102-
matrixStr := matrix.Combinations()[0]
103-
104-
// When -o is specified, use a temp workspace so we don't pollute the cache
105151
buildOpts := build.Options{
106152
Store: store,
107153
MatrixStr: matrixStr,
@@ -131,14 +177,11 @@ func runMake(cmd *cobra.Command, args []string) error {
131177
os.Stderr = savedStderr
132178
}
133179

134-
// Print metadata for main module (last in build order)
135180
if len(results) > 0 {
136181
main := results[len(results)-1]
137182
if main.Metadata != "" {
138183
fmt.Println(main.Metadata)
139184
}
140-
141-
// Output build artifacts if -o specified
142185
if makeOutput != "" {
143186
if err := outputResult(main.OutputDir, makeOutput); err != nil {
144187
return fmt.Errorf("failed to write output: %w", err)
@@ -149,14 +192,31 @@ func runMake(cmd *cobra.Command, args []string) error {
149192
return nil
150193
}
151194

152-
// parseModuleArg parses a module argument in the form "owner/repo@version" or "owner/repo".
153-
func parseModuleArg(arg string) (modPath, version string) {
154-
for i := len(arg) - 1; i >= 0; i-- {
155-
if arg[i] == '@' {
156-
return arg[:i], arg[i+1:]
195+
// parseModuleArg parses a module argument and detects local filesystem patterns.
196+
// Local patterns follow Go-style local import forms (., .., ./x, ../x, absolute path).
197+
// Returns an error for invalid patterns like ".@version" (use "./@version" instead).
198+
func parseModuleArg(arg string) (pattern, version string, isLocal bool, err error) {
199+
if strings.HasPrefix(arg, ".@") {
200+
return "", "", false, fmt.Errorf("invalid local pattern %q: use \"./@version\" instead of \".@version\"", arg)
201+
}
202+
203+
pattern = arg
204+
for i := len(pattern) - 1; i >= 0; i-- {
205+
if pattern[i] == '@' {
206+
version = pattern[i+1:]
207+
pattern = pattern[:i]
208+
break
209+
}
210+
}
211+
212+
if stdbuild.IsLocalImport(pattern) || filepath.IsAbs(pattern) {
213+
isLocal = true
214+
pattern = filepath.Clean(pattern)
215+
if pattern == "." {
216+
pattern = ""
157217
}
158218
}
159-
return arg, ""
219+
return
160220
}
161221

162222
// outputResult writes the build output to dest.

0 commit comments

Comments
 (0)