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
24 changes: 24 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ jobs:
mkdir -p dist
go build -ldflags "-X main.Version=$VERSION" -o dist/compiledb ./cmd/compiledb
./dist/compiledb --help
mkdir -p dist/smoke
printf 'int main(void) { return 0; }\n' > dist/smoke/main.c
printf 'cc -c main.c -o main.o\n' > dist/smoke/build.log
(cd dist/smoke && ../compiledb --parse build.log)
grep -F '"file": "main.c"' dist/smoke/compile_commands.json
rm -rf dist/smoke
tar cJf dist/compiledb-linux-amd64.txz -C dist compiledb

- uses: actions/upload-artifact@v4
Expand All @@ -100,6 +106,12 @@ jobs:
mkdir -p dist
go build -ldflags "-X main.Version=$VERSION" -o dist/compiledb ./cmd/compiledb
./dist/compiledb --help
mkdir -p dist/smoke
printf 'int main(void) { return 0; }\n' > dist/smoke/main.c
printf 'cc -c main.c -o main.o\n' > dist/smoke/build.log
(cd dist/smoke && ../compiledb --parse build.log)
grep -F '"file": "main.c"' dist/smoke/compile_commands.json
rm -rf dist/smoke
tar cJf dist/compiledb-linux-arm64.txz -C dist compiledb

- uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -128,6 +140,12 @@ jobs:
mkdir -p dist
go build -ldflags "-X main.Version=$VERSION" -o dist/compiledb.exe ./cmd/compiledb
./dist/compiledb.exe --help
mkdir -p dist/smoke
printf 'int main(void) { return 0; }\n' > dist/smoke/main.c
printf 'cc -c main.c -o main.o\n' > dist/smoke/build.log
(cd dist/smoke && ../compiledb.exe --parse build.log)
grep -F '"file": "main.c"' dist/smoke/compile_commands.json
rm -rf dist/smoke
(cd dist && 7z a compiledb-windows-amd64.zip compiledb.exe)

- uses: actions/upload-artifact@v4
Expand All @@ -153,6 +171,12 @@ jobs:
mkdir -p dist
go build -ldflags "-X main.Version=$VERSION" -o dist/compiledb ./cmd/compiledb
./dist/compiledb --help
mkdir -p dist/smoke
printf 'int main(void) { return 0; }\n' > dist/smoke/main.c
printf 'cc -c main.c -o main.o\n' > dist/smoke/build.log
(cd dist/smoke && ../compiledb --parse build.log)
grep -F '"file": "main.c"' dist/smoke/compile_commands.json
rm -rf dist/smoke
tar cJf dist/compiledb-darwin-arm64.txz -C dist compiledb

- uses: actions/upload-artifact@v4
Expand Down
22 changes: 2 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,40 +364,22 @@ response files remain opaque.
The parser applies fixed limits to individual physical lines and response-file
expansion:

- A physical build-log line is limited to 100 MiB.
- A physical build-log line is limited to 100 MiB. An oversized line is
reported and skipped without stopping later lines from being parsed.
- One response file is limited to 8 MiB.
- Aggregate response-file input for one compiler command is limited to 32 MiB.
- Response-file nesting is limited to 32 levels and 256 files.
- Expanded compiler arguments are limited to 100000 elements.
- Expanded arguments and their copies across multi-source entries have a
64 MiB budget.

These are not whole-process resource limits. The total build-log size and the
output produced by programs in backtick expressions do not have fixed limits;
large inputs or unbounded command output can consume substantial memory.

## Related Projects

- [nickdiego/compiledb][python-compiledb] is the Python project that originally
inspired this Go rewrite.
- [clangd][clangd] and [clang-tidy][clang-tidy] are common consumers of the
generated `compile_commands.json` file.

## Development

Run the full local verification suite with:

```sh
test -z "$(gofmt -l .)"
go vet ./...
go test ./...
go test -race ./...
go build -o /tmp/compiledb-go ./cmd/compiledb
```

Some tests modify the current directory, standard streams, and process-global
state, so those tests must not run in parallel.

## License

[GNU General Public License v3.0](LICENSE).
Expand Down
22 changes: 5 additions & 17 deletions internal/build_log_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ func BenchmarkBuildLogScan(b *testing.B) {
b.ResetTimer()

for range b.N {
lines, err := scanBuildLog(data)
if err != nil {
b.Fatal(err)
}
lines := scanBuildLog(data)
runtime.KeepAlive(lines)
}
})
Expand All @@ -47,21 +44,15 @@ func BenchmarkBuildLogMergeLogicalLines(b *testing.B) {
for _, size := range benchmarkBuildLogSizes {
b.Run(size.name, func(b *testing.B) {
data := buildLogBenchmarkData(size.size, chunk)
lines, err := scanBuildLog(data)
if err != nil {
b.Fatal(err)
}
lines := scanBuildLog(data)
data = nil
runtime.GC()

b.SetBytes(size.size)
b.ReportAllocs()
b.ResetTimer()
for range b.N {
logicalLines, issues := mergeLogicalLines(lines)
if len(issues) != 0 {
b.Fatalf("unexpected logical line issues: %#v", issues)
}
logicalLines := mergeLogicalLines(lines)
runtime.KeepAlive(logicalLines)
}
})
Expand Down Expand Up @@ -127,11 +118,8 @@ func benchmarkDiscoveryOutput(b *testing.B, size int64, chunk []byte) {
if err := writeBuildLogBenchmarkData(&stdout, size, chunk); err != nil {
b.Fatal(err)
}
buildLog, err := scanBuildLog(stdout.Bytes())
if err != nil {
b.Fatal(err)
}
tool.Parse(buildLog)
buildLog := scanBuildLog(stdout.Bytes())
tool.parseBuildLog(buildLog)
if tool.StatusCode != 0 {
b.Fatalf("parser status: %d", tool.StatusCode)
}
Expand Down
50 changes: 38 additions & 12 deletions internal/init.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package internal

import (
"bufio"
"bytes"
"context"
"encoding/json"
Expand Down Expand Up @@ -40,6 +39,7 @@ type Tool struct {
predefinedMacros map[string][]string
compilerCommand func(name string, arg ...string) *exec.Cmd
compilerCommandContext func(context.Context, string, ...string) *exec.Cmd
buildLogLineLimit int
makeDirectoryMarkers bool
}

Expand All @@ -60,6 +60,13 @@ func (t *Tool) operationContext() context.Context {
return context.Background()
}

func (t *Tool) physicalLineLimit() int {
if t.buildLogLineLimit > 0 {
return t.buildLogLineLimit
}
return maxBuildLogLineSize
}

func (t *Tool) compilationDatabaseBuildDir() string {
if t.Config.BuildDir != "" {
return compilationDatabaseBuildDir(t.Config.BuildDir)
Expand Down Expand Up @@ -368,23 +375,42 @@ func (t *Tool) Generate() {
t.Logger.Debugf("Build from stdin")
}

buildLog, err := scanBuildLog(data)
if err != nil {
t.Logger.Fatalf("read build log failed: %v", err)
if t.operationContext().Err() != nil {
t.StatusCode = contextExitCode(t.operationContext())
return
}
buildLog := scanBuildLogWithLimit(data, t.physicalLineLimit())
if t.operationContext().Err() != nil {
t.StatusCode = contextExitCode(t.operationContext())
return
}
t.Parse(buildLog)
t.parseBuildLog(buildLog)
}

func scanBuildLog(data []byte) ([]string, error) {
var lines []string
scanner := bufio.NewScanner(bytes.NewReader(data))
scanner.Buffer(make([]byte, 1024*1024), maxBuildLogLineSize)
for scanner.Scan() {
lines = append(lines, scanner.Text())
func scanBuildLog(data []byte) []buildLogLine {
return scanBuildLogWithLimit(data, maxBuildLogLineSize)
}

func scanBuildLogWithLimit(data []byte, limit int) []buildLogLine {
lineCount := bytes.Count(data, []byte{'\n'})
if len(data) > 0 && data[len(data)-1] != '\n' {
lineCount++
}
lines := make([]buildLogLine, 0, lineCount)
for len(data) > 0 {
line := data
if newline := bytes.IndexByte(data, '\n'); newline >= 0 {
line = data[:newline]
data = data[newline+1:]
} else {
data = nil
}
line = bytes.TrimSuffix(line, []byte{'\r'})
if len(line) > limit {
lines = append(lines, buildLogLine{raw: line, oversized: true, limit: limit})
continue
}
lines = append(lines, buildLogLine{text: string(line)})
}
return lines, scanner.Err()
return lines
}
61 changes: 61 additions & 0 deletions internal/init_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package internal

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -54,6 +55,66 @@ func TestGenerateFromStdinDoesNotPanic(t *testing.T) {
tool.Generate()
}

func TestScanBuildLogPhysicalLineLimit(t *testing.T) {
if reason := buildLogLineLimitReason(maxBuildLogLineSize); reason != "physical line exceeds 100 MiB limit" {
t.Fatalf("unexpected production line-limit diagnostic: %q", reason)
}

const limit = 8
for name, input := range map[string]string{
"LF": strings.Repeat("x", limit) + "\n",
"CRLF": strings.Repeat("x", limit) + "\r\n",
"EOF": strings.Repeat("x", limit),
} {
t.Run(name, func(t *testing.T) {
lines := scanBuildLogWithLimit([]byte(input), limit)
if len(lines) != 1 || lines[0].oversized || lines[0].text != strings.Repeat("x", limit) {
t.Fatalf("line at limit was not preserved: %#v", lines)
}
})
}

lines := scanBuildLogWithLimit([]byte(strings.Repeat("x", limit+1)+"\r\nok\n"), limit)
if len(lines) != 2 || !lines[0].oversized || lines[0].limit != limit || lines[0].text != "" ||
lines[1].oversized || lines[1].text != "ok" {
t.Fatalf("oversized line was not discarded cleanly: %#v", lines)
}
}

func TestGenerateSkipsOversizedPhysicalLine(t *testing.T) {
projectDir := t.TempDir()
buildLog := filepath.Join(projectDir, "build.log")
outputFile := filepath.Join(projectDir, "compile_commands.json")
contents := strings.Repeat("x", 32) + "\\\ncc -c joined.c\ncc -c valid.c\n"
if err := os.WriteFile(buildLog, []byte(contents), 0o644); err != nil {
t.Fatalf("write build log failed: %v", err)
}

var logs bytes.Buffer
tool := newTestTool(t, Config{
InputFile: buildLog,
OutputFile: outputFile,
RegexCompile: RegexCompile,
RegexFile: RegexFile,
NoStrict: true,
})
tool.buildLogLineLimit = 32
tool.Logger.SetLevel(log.ErrorLevel)
tool.Logger.SetOutput(&logs)
tool.Generate()

commands := readCompilerTestCommands(t, outputFile)
if tool.StatusCode != 0 || len(commands) != 1 || commands[0].File != "valid.c" {
t.Fatalf("oversized build-log line stopped Generate: status=%d commands=%#v", tool.StatusCode, commands)
}
diagnostic := logs.String()
for _, want := range []string{"build log line 1", "cwd", "physical line exceeds 32 byte limit", "at byte 32"} {
if !strings.Contains(diagnostic, want) {
t.Fatalf("physical line diagnostic lacks %q: %q", want, diagnostic)
}
}
}

func TestGenerateResolvesResponseFileFromBuildLogDirectory(t *testing.T) {
projectDir := t.TempDir()
buildLog := filepath.Join(projectDir, "build.log")
Expand Down
8 changes: 1 addition & 7 deletions internal/make_wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,18 +290,12 @@ func (t *Tool) runDiscoveryMake(cmd *exec.Cmd) discoveryResult {
return result
}

buildLog, scanErr := scanBuildLog(stdoutBuf.Bytes())
if scanErr != nil {
result.status = 1
t.Logger.Errorf("read dry-run output failed: %v", scanErr)
return result
}
clone := *t
clone.makeDirectoryMarkers = true
if !t.Config.NoBuild {
clone.Logger = loggerAtLevel(t.Logger, logrus.ErrorLevel)
}
clone.Parse(buildLog)
clone.parseBuildLog(scanBuildLogWithLimit(stdoutBuf.Bytes(), clone.physicalLineLimit()))
result.parserStatus = clone.StatusCode
return result
}
Expand Down
Loading
Loading