Skip to content

Commit a036d01

Browse files
committed
builder: key CGo header cache by clang identity
Include the exact Clang identity in package cache keys when CGo headers are compiled. Remap generated header snippet paths before linking so __FILE__ and debug metadata are stable without changing file-based quoted-include lookup.
1 parent 25aebcc commit a036d01

2 files changed

Lines changed: 85 additions & 19 deletions

File tree

builder/build.go

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,18 @@ type BuildResult struct {
8080
// key, avoiding the need for recompiling all dependencies when only the
8181
// implementation of an imported package changes.
8282
type packageAction struct {
83-
ImportPath string
84-
CompilerBuildID string
85-
TinyGoVersion string
86-
LLVMVersion string
87-
Config *compiler.Config
88-
CFlags []string
89-
FileHashes map[string]string // hash of every file that's part of the package
90-
EmbeddedFiles map[string]string // hash of all the //go:embed files in the package
91-
Imports map[string]string // map from imported package to action ID hash
92-
OptLevel string // LLVM optimization level (O0, O1, O2, Os, Oz)
93-
UndefinedGlobals []string // globals that are left as external globals (no initializer)
83+
ImportPath string
84+
CompilerBuildID string
85+
TinyGoVersion string
86+
LLVMVersion string
87+
CCompilerIdentity string
88+
Config *compiler.Config
89+
CFlags []string
90+
FileHashes map[string]string // hash of every file that's part of the package
91+
EmbeddedFiles map[string]string // hash of all the //go:embed files in the package
92+
Imports map[string]string // map from imported package to action ID hash
93+
OptLevel string // LLVM optimization level (O0, O1, O2, Os, Oz)
94+
UndefinedGlobals []string // globals that are left as external globals (no initializer)
9495
}
9596

9697
// Build performs a single package to executable Go build. It takes in a package
@@ -344,6 +345,13 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
344345
OptLevel: optLevel,
345346
UndefinedGlobals: undefinedGlobals,
346347
}
348+
if len(pkg.CGoHeaders) != 0 {
349+
compilerID, err := clangCompilerIdentity()
350+
if err != nil {
351+
return err
352+
}
353+
actionID.CCompilerIdentity = compilerID
354+
}
347355
for filePath, hash := range pkg.FileHashes {
348356
actionID.FileHashes[filePath] = hex.EncodeToString(hash)
349357
}
@@ -398,9 +406,6 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
398406
}
399407

400408
// Load bitcode of CGo headers and join the modules together.
401-
// This may seem vulnerable to cache problems, but this is not
402-
// the case: the Go code that was just compiled already tracks
403-
// all C files that are read and hashes them.
404409
// These headers could be compiled in parallel but the benefit
405410
// is so small that it's probably not worth parallelizing.
406411
// Packages are compiled independently anyway.
@@ -410,14 +415,16 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
410415
if err != nil {
411416
return err
412417
}
413-
_, err = f.Write([]byte(cgoHeader))
414-
if err != nil {
418+
if _, err := f.Write([]byte(cgoHeader)); err != nil {
415419
return err
416420
}
417-
f.Close()
421+
if err := f.Close(); err != nil {
422+
return err
423+
}
424+
output := f.Name() + ".bc"
418425

419426
// Compile the code (if there is any) to bitcode.
420-
flags := append([]string{"-c", "-emit-llvm", "-o", f.Name() + ".bc", f.Name()}, pkg.CFlags...)
427+
flags := cgoHeaderCompileArgs(f.Name(), output, pkg.CFlags)
421428
if config.Options.PrintCommands != nil {
422429
config.Options.PrintCommands("clang", flags...)
423430
}
@@ -431,7 +438,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
431438
// in the header together with the Go code. In particular,
432439
// this allows inlining. It also ensures there is only one
433440
// file per package to cache.
434-
headerMod, err := mod.Context().ParseBitcodeFile(f.Name() + ".bc")
441+
headerMod, err := mod.Context().ParseBitcodeFile(output)
435442
if err != nil {
436443
return fmt.Errorf("failed to load bitcode file: %w", err)
437444
}
@@ -1074,6 +1081,15 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
10741081
return result, nil
10751082
}
10761083

1084+
func cgoHeaderCompileArgs(source, output string, cflags []string) []string {
1085+
flags := append([]string{"-c", "-emit-llvm", "-o", output, source}, cflags...)
1086+
flags = append(flags,
1087+
"-ffile-prefix-map="+source+"=tinygo-cgo.c",
1088+
"-fdebug-prefix-map="+source+"=tinygo-cgo.c",
1089+
)
1090+
return appendCacheStableCFlags(flags)
1091+
}
1092+
10771093
// createEmbedObjectFile creates a new object file with the given contents, for
10781094
// the embed package.
10791095
func createEmbedObjectFile(data, hexSum, sourceFile, sourceDir, tmpdir string, compilerConfig *compiler.Config) (string, error) {

builder/build_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package builder
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"tinygo.org/x/go-llvm"
10+
)
11+
12+
func TestCGoHeaderCompileIsPathIndependent(t *testing.T) {
13+
var outputs [][]byte
14+
for _, dirName := range []string{"first", "second"} {
15+
dir := filepath.Join(t.TempDir(), dirName)
16+
if err := os.Mkdir(dir, 0o777); err != nil {
17+
t.Fatal(err)
18+
}
19+
source := filepath.Join(dir, "snippet.c")
20+
output := filepath.Join(dir, "snippet.bc")
21+
if err := os.WriteFile(source, []byte("const char *sourceName = __FILE__;\n"), 0o666); err != nil {
22+
t.Fatal(err)
23+
}
24+
flags := cgoHeaderCompileArgs(source, output, []string{
25+
"-gdwarf-4",
26+
"--target=x86_64-unknown-linux-gnu",
27+
})
28+
if err := runCCompiler(flags...); err != nil {
29+
t.Fatal(err)
30+
}
31+
32+
ctx := llvm.NewContext()
33+
mod := ctx.NewModule("package")
34+
headerMod, err := ctx.ParseBitcodeFile(output)
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
if err := llvm.LinkModules(mod, headerMod); err != nil {
39+
t.Fatal(err)
40+
}
41+
buf := llvm.WriteBitcodeToMemoryBuffer(mod)
42+
outputs = append(outputs, bytes.Clone(buf.Bytes()))
43+
buf.Dispose()
44+
mod.Dispose()
45+
ctx.Dispose()
46+
}
47+
if !bytes.Equal(outputs[0], outputs[1]) {
48+
t.Fatal("CGo header bitcode depends on its temporary source path")
49+
}
50+
}

0 commit comments

Comments
 (0)