Skip to content
Merged
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
81 changes: 51 additions & 30 deletions cmd/llar/internal/make.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ var newRemoteStore = func() (repo.Store, error) {
}

var makeCmd = &cobra.Command{
Use: "make [module@version]",
Short: "Build a module to FormulaDir",
Long: `Make downloads and builds a module to FormulaDir.`,
Args: cobra.ExactArgs(1),
Use: "make [module@version]",
Short: "Build a module to FormulaDir",
Long: `Make downloads and builds a module to FormulaDir.`,
Args: cobra.ExactArgs(1),
FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
RunE: runMake,
}
Expand Down Expand Up @@ -141,28 +141,16 @@ func buildModule(ctx context.Context, store repo.Store, modPath, version, matrix
return fmt.Errorf("failed to load modules: %w", err)
}

// Handle verbose output
var savedStdout, savedStderr *os.File
if !makeVerbose {
for _, mod := range mods {
mod.SetStdout(io.Discard)
mod.SetStderr(io.Discard)
}

savedStdout = os.Stdout
savedStderr = os.Stderr
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
return fmt.Errorf("failed to open devnull: %w", err)
}
os.Stdout = devNull
os.Stderr = devNull
defer func() {
devNull.Close()
os.Stdout = savedStdout
os.Stderr = savedStderr
}()
restoreOutput, err := redirectBuildOutput(mods)
if err != nil {
return err
}
outputRestored := false
defer func() {
if !outputRestored {
restoreOutput()
}
}()

buildOpts := build.Options{
Store: store,
Expand All @@ -188,11 +176,9 @@ func buildModule(ctx context.Context, store repo.Store, modPath, version, matrix
return fmt.Errorf("failed to build %s@%s: %w", modPath, version, err)
}

// Restore stdout before printing results
if !makeVerbose {
os.Stdout = savedStdout
os.Stderr = savedStderr
}
// Restore stdout before printing results.
restoreOutput()
outputRestored = true

if len(results) > 0 {
main := results[len(results)-1]
Expand All @@ -209,6 +195,41 @@ func buildModule(ctx context.Context, store repo.Store, modPath, version, matrix
return nil
}

Comment thread
MeteorsLiu marked this conversation as resolved.
// redirectBuildOutput reserves command stdout for final metadata. In verbose
// mode, build stdout is redirected to stderr; in silent mode, build output is
// discarded until the metadata line is printed.
func redirectBuildOutput(mods []*modules.Module) (func(), error) {
if !makeVerbose {
for _, mod := range mods {
mod.SetStdout(io.Discard)
mod.SetStderr(io.Discard)
}

savedStdout := os.Stdout
savedStderr := os.Stderr
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
return nil, fmt.Errorf("failed to open devnull: %w", err)
}
os.Stdout = devNull
os.Stderr = devNull
return func() {
os.Stdout = savedStdout
os.Stderr = savedStderr
_ = devNull.Close()
}, nil
Comment thread
MeteorsLiu marked this conversation as resolved.
}

savedStdout := os.Stdout
os.Stdout = os.Stderr
for _, mod := range mods {
mod.SetStdout(os.Stderr)
}
return func() {
os.Stdout = savedStdout
}, nil
}

// parseModuleArg parses a module argument and detects local filesystem patterns.
// Local patterns follow Go-style local import forms (., .., ./x, ../x, absolute path).
// Returns an error for invalid patterns like ".@version" (use "./@version" instead).
Expand Down
130 changes: 115 additions & 15 deletions cmd/llar/internal/make_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (

"github.com/goplus/llar/formula"
"github.com/goplus/llar/internal/formula/repo"
"github.com/goplus/llar/internal/modules"
"github.com/goplus/llar/mod/module"
)

func TestParseModuleArg(t *testing.T) {
Expand Down Expand Up @@ -311,7 +313,7 @@ func TestOutputResult_NestedDirs(t *testing.T) {
// Integration tests that run the real `llar make` command.
// Requires network, git, and cmake.

func runMakeCmd(t *testing.T, args ...string) (string, error) {
func runMakeCmdStreams(t *testing.T, args ...string) (string, string, error) {
t.Helper()

// Save and restore cwd — builder.Build may os.Chdir during build
Expand All @@ -331,29 +333,88 @@ func runMakeCmd(t *testing.T, args ...string) (string, error) {
defer func() { os.Args = origArgs }()

// Execute rootCmd in-process to keep test coverage. Because build output
// flows through process-wide os.Stdout (including nested cmake commands),
// redirect to a pipe and drain concurrently to avoid blocking on full pipe buffers.
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
defer func() { os.Stdout = old }()

var buf bytes.Buffer
copyDone := make(chan error, 1)
// flows through process-wide os.Stdout/os.Stderr (including nested build
Comment thread
MeteorsLiu marked this conversation as resolved.
// commands), redirect to pipes and drain concurrently to avoid blocking on
// full pipe buffers.
oldStdout := os.Stdout
stdoutR, stdoutW, _ := os.Pipe()
Comment thread
MeteorsLiu marked this conversation as resolved.
os.Stdout = stdoutW
defer func() { os.Stdout = oldStdout }()

oldStderr := os.Stderr
stderrR, stderrW, _ := os.Pipe()
os.Stderr = stderrW
defer func() { os.Stderr = oldStderr }()
Comment thread
MeteorsLiu marked this conversation as resolved.
Comment thread
MeteorsLiu marked this conversation as resolved.

var stdoutBuf, stderrBuf bytes.Buffer
stdoutDone := make(chan error, 1)
go func() {
_, copyErr := io.Copy(&buf, r)
copyDone <- copyErr
_, copyErr := io.Copy(&stdoutBuf, stdoutR)
stdoutDone <- copyErr
}()
stderrDone := make(chan error, 1)
go func() {
_, copyErr := io.Copy(&stderrBuf, stderrR)
stderrDone <- copyErr
}()

cmd := rootCmd
cmd.SetArgs(append([]string{"make"}, args...))
err = cmd.Execute()

_ = w.Close()
if copyErr := <-copyDone; copyErr != nil {
_ = stdoutW.Close()
if copyErr := <-stdoutDone; copyErr != nil {
t.Fatalf("failed to capture stdout: %v", copyErr)
}
return buf.String(), err
_ = stderrW.Close()
if copyErr := <-stderrDone; copyErr != nil {
t.Fatalf("failed to capture stderr: %v", copyErr)
}
return stdoutBuf.String(), stderrBuf.String(), err
}

func runMakeCmd(t *testing.T, args ...string) (string, error) {
t.Helper()
stdout, _, err := runMakeCmdStreams(t, args...)
return stdout, err
}

func captureProcessStreams(t *testing.T) (*bytes.Buffer, *bytes.Buffer, func()) {
t.Helper()

oldStdout := os.Stdout
stdoutR, stdoutW, _ := os.Pipe()
os.Stdout = stdoutW

oldStderr := os.Stderr
stderrR, stderrW, _ := os.Pipe()
os.Stderr = stderrW

var stdoutBuf, stderrBuf bytes.Buffer
stdoutDone := make(chan error, 1)
go func() {
_, copyErr := io.Copy(&stdoutBuf, stdoutR)
stdoutDone <- copyErr
}()
stderrDone := make(chan error, 1)
go func() {
_, copyErr := io.Copy(&stderrBuf, stderrR)
stderrDone <- copyErr
}()

return &stdoutBuf, &stderrBuf, func() {
_ = stdoutW.Close()
if copyErr := <-stdoutDone; copyErr != nil {
t.Fatalf("failed to capture stdout: %v", copyErr)
}
os.Stdout = oldStdout

_ = stderrW.Close()
if copyErr := <-stderrDone; copyErr != nil {
t.Fatalf("failed to capture stderr: %v", copyErr)
}
os.Stderr = oldStderr
}
Comment thread
MeteorsLiu marked this conversation as resolved.
}

func TestMakeReal_Verbose(t *testing.T) {
Expand Down Expand Up @@ -682,6 +743,45 @@ func TestMakeLocal_BuildSuccess(t *testing.T) {
}
}

func TestMakeLocal_VerboseWritesBuildOutputToStderr(t *testing.T) {
formulaDir := setupLocalFormulas(t)
store := repo.New(formulaDir, &noopVCSRepo{})
ctx := context.Background()
mods, err := modules.Load(ctx, module.Version{Path: "test/liba", Version: "1.0.0"}, modules.Options{
FormulaStore: store,
})
if err != nil {
t.Fatalf("modules.Load() failed: %v", err)
}

savedVerbose := makeVerbose
makeVerbose = true
Comment thread
MeteorsLiu marked this conversation as resolved.
Comment thread
MeteorsLiu marked this conversation as resolved.
t.Cleanup(func() { makeVerbose = savedVerbose })

stdout, stderr, restore := captureProcessStreams(t)
restoreBuildOutput, err := redirectBuildOutput(mods)
if err != nil {
t.Fatalf("redirectBuildOutput() failed: %v", err)
}

var out formula.BuildResult
mods[0].OnBuild(nil, nil, &out)

restoreBuildOutput()
restore()

if out.Metadata() != "-lA" {
t.Fatalf("metadata = %q, want %q", out.Metadata(), "-lA")
}

if got := strings.TrimSpace(stdout.String()); got != "" {
t.Fatalf("stdout = %q, want no build output", got)
}
if !strings.Contains(stderr.String(), "verbose build output") {
t.Fatalf("stderr = %q, want verbose build output", stderr.String())
}
}

func TestBuildModule_SilentSuccess(t *testing.T) {
formulaDir := setupLocalFormulas(t)
store := repo.NewOverlayStore(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ id "test/liba"
fromVer "1.0.0"

onBuild (ctx, proj, out) => {
println "verbose build output"
out.setMetadata "-lA"
}
Loading