【BUG】Build command output is fully buffered before being printed
Summary
runCommandCopyOutput() reads the entire build command stdout into memory before writing it to the terminal. For verbose build commands, this can cause unnecessarily high memory usage and delayed output.
Affected Code
In runner/engine.go, runCommandCopyOutput() reads all stdout at once:
stdoutBytes, _ := io.ReadAll(stdout)
_, _ = io.Copy(os.Stdout, strings.NewReader(string(stdoutBytes)))This path is used by building() when running e.config.Build.Cmd.
Safe Validation Evidence
Tested locally against commit b487a2c59460eea59cee389dfb36152eab5b6eb0.
A local package test used a build command that produced large stdout. The test confirmed that the output is accumulated in full before being copied to stdout:
=== RUN TestReproH7H8_UnboundedCommandOutput
repro_high_test.go:121: H7/H8 = TRUE POSITIVE: seq 1 100000 = 588895 bytes. engine.go:687 io.ReadAll(stdout) reads ALL with NO LimitReader
--- PASS: TestReproH7H8_UnboundedCommandOutput (0.00s)
PASS
ok github.com/air-verse/air/runner (cached)The current master branch still contains the same code path at runner/engine.go.
Expected Behavior
Build command output should be streamed or bounded so Air does not need to keep the entire stdout content in memory.
Actual Behavior
Air reads the complete stdout stream into a byte slice before printing it.
Impact
Very verbose build commands can cause high memory usage and delayed terminal output during rebuilds.
Suggested Fix
Stream stdout directly while preserving enough output for the build failure message, or use a bounded buffer for the retained output.
Source: air-verse/air