compilepkg: skip duplicate cgo compile (#4645)
Coverage+cgo packages currently spend extra CPU in compilepkg because
nogo needs original cgo-generated Go sources, not the
coverage-instrumented files passed to the compiler. The old path called
cgo2 a second time to produce those sources. That second call also
recompiled every generated and user C-family source, linked the cgo
probe, and ran dynimport even though compilepkg discarded the object
outputs.
Split cgo source generation from the compile/link phase and add a
generate-only path for the nogo copy. Normal cgo compilation still uses
the full path, while the coverage+nogo pass writes generated Go sources
from a separate cgo_nogo work directory and reuses the real
_cgo_imports.go when available.
diff --git a/go/tools/builders/cgo2.go b/go/tools/builders/cgo2.go
index 345851e..580ba6c 100644
--- a/go/tools/builders/cgo2.go
+++ b/go/tools/builders/cgo2.go
@@ -62,164 +62,22 @@
}
defer cleanup()
- // cgo2 will gather sources into a single temporary directory, since nogo
- // scanners might want to include or exclude these sources we need to ensure
- // that a fragment of the path is stable and human friendly enough to be
- // referenced in nogo configuration.
- workDir = filepath.Join(workDir, "cgo", packagePath)
- if err := os.MkdirAll(workDir, 0700); err != nil {
- return "", nil, nil, err
- }
-
- // Filter out -lstdc++ and -lc++ from ldflags if we don't have C++ sources,
- // and set CGO_LDFLAGS. These flags get written as special comments into cgo
- // generated sources. The compiler encodes those flags in the compiled .a
- // file, and the linker passes them on to the external linker.
- haveCxx := len(cxxSrcs)+len(objcxxSrcs) > 0
- if !haveCxx {
- for _, f := range ldFlags {
- if strings.HasSuffix(f, ".a") {
- // These flags come from cdeps options. Assume C++.
- haveCxx = true
- break
- }
- }
- }
- var combinedLdFlags []string
- if haveCxx {
- combinedLdFlags = append(combinedLdFlags, ldFlags...)
- } else {
- for _, f := range ldFlags {
- if f != "-lc++" && f != "-lstdc++" {
- combinedLdFlags = append(combinedLdFlags, f)
- }
- }
- }
- combinedLdFlags = append(combinedLdFlags, defaultLdFlags()...)
-
- // go 1.23+ supports ldflags file.
- // https://go-review.googlesource.com/c/go/+/584655
- canUseLdflagsFile, err := onVersionOrHigher(23)
+ gen, err := generateCgoSources(goenv, workDir, "cgo", cgoSrcs, cSrcs, cxxSrcs, objcSrcs, objcxxSrcs, hSrcs, packagePath, cppFlags, cFlags, ldFlags, cgoExportHPath)
if err != nil {
return "", nil, nil, err
}
-
- var ldflagsFile *os.File
- combinedLdFlagsStr := strings.Join(combinedLdFlags, " ")
- if canUseLdflagsFile {
- // Write linker flags to a temporary file instead of pasing via env variable.
- // This avoids "argument list too long" error with extremely large CGO_LDFLAGS
- // that can exceed system limits.
- // Future versions of `go` may remove support for CGO_LDFLAGS entirely, so
- // use file even for small ldflags.
- // https://go-review.googlesource.com/c/go/+/596615
- ldflagsFile, err = os.CreateTemp("", "cgo-ldflags-*.txt")
- if err != nil {
- return "", nil, nil, fmt.Errorf("failed to create temporary file for ldflags: %w", err)
- }
- defer os.Remove(ldflagsFile.Name())
- if _, err := ldflagsFile.WriteString(formatLdFlagsFileContent(combinedLdFlagsStr)); err != nil {
- ldflagsFile.Close()
- return "", nil, nil, fmt.Errorf("failed to write ldflags to temporary file: %w", err)
- }
- if err := ldflagsFile.Close(); err != nil {
- return "", nil, nil, fmt.Errorf("failed to close temporary ldflags file: %w", err)
- }
- } else {
- // Fallback to env variable for backwards compatibility with older `go` versions.
- os.Setenv("CGO_LDFLAGS", combinedLdFlagsStr)
- }
-
- // If cgo sources are in different directories, gather them into a temporary
- // directory so we can use -srcdir.
- srcDir = filepath.Dir(cgoSrcs[0])
- srcsInSingleDir := true
- for _, src := range cgoSrcs[1:] {
- if filepath.Dir(src) != srcDir {
- srcsInSingleDir = false
- break
- }
- }
-
- if srcsInSingleDir {
- for i := range cgoSrcs {
- cgoSrcs[i] = filepath.Base(cgoSrcs[i])
- }
- } else {
- srcDir = filepath.Join(workDir, "cgosrcs")
- if err := os.Mkdir(srcDir, 0777); err != nil {
- return "", nil, nil, err
- }
- copiedSrcs, err := gatherSrcs(srcDir, cgoSrcs)
- if err != nil {
- return "", nil, nil, err
- }
- cgoSrcs = copiedSrcs
- }
-
- // Generate Go and C code.
- hdrDirs := map[string]bool{}
- var hdrIncludes []string
- for _, hdr := range hSrcs {
- hdrDir := filepath.Dir(hdr)
- if !hdrDirs[hdrDir] {
- hdrDirs[hdrDir] = true
- hdrIncludes = append(hdrIncludes, "-iquote", hdrDir)
- }
- }
- hdrIncludes = append(hdrIncludes, "-iquote", workDir) // for _cgo_export.h
-
- // Trim the path in //line comments emitted by cgo.
- trimPath, err := createTrimPath()
- if err != nil {
- return "", nil, nil, err
- }
- args := goenv.goTool("cgo", "-srcdir", srcDir, "-objdir", workDir, "-trimpath", trimPath)
- if ldflagsFile != nil {
- // The "@" prefix tells cgo to read arguments from the file.
- args = append(args, "-ldflags", "@"+ldflagsFile.Name())
- }
- if packagePath != "" {
- args = append(args, "-importpath", packagePath)
- }
- args = append(args, "--")
- args = append(args, cppFlags...)
- args = append(args, hdrIncludes...)
- args = append(args, cFlags...)
- args = append(args, cgoSrcs...)
- if err := goenv.runCommand(args); err != nil {
- return "", nil, nil, err
- }
-
- if cgoExportHPath != "" {
- if err := copyFile(filepath.Join(workDir, "_cgo_export.h"), cgoExportHPath); err != nil {
- return "", nil, nil, err
- }
- }
- // Note: The tools/gopackagesdriver will break if the naming convention
- // changes for genGoSrcs files. Changes here should be reflected
- // there.
- genGoSrcs := make([]string, 1+len(cgoSrcs))
- genGoSrcs[0] = filepath.Join(workDir, "_cgo_gotypes.go")
- genCSrcs := make([]string, 1+len(cgoSrcs))
- genCSrcs[0] = filepath.Join(workDir, "_cgo_export.c")
- for i, src := range cgoSrcs {
- stem := strings.TrimSuffix(filepath.Base(src), ".go")
- genGoSrcs[i+1] = filepath.Join(workDir, stem+".cgo1.go")
- genCSrcs[i+1] = filepath.Join(workDir, stem+".cgo2.c")
- }
- cgoMainC := filepath.Join(workDir, "_cgo_main.c")
+ workDir = gen.workDir
// Compile C, C++, Objective-C/C++, and assembly code.
defaultCFlags := defaultCFlags(workDir)
- combinedCFlags := combineFlags(cppFlags, hdrIncludes, cFlags, defaultCFlags)
+ combinedCFlags := combineFlags(cppFlags, gen.hdrIncludes, cFlags, defaultCFlags)
asmCFlags := combineFlags(cppFlags, cFlags, defaultCFlags)
for _, lang := range []struct{ srcs, flags []string }{
- {genCSrcs, combinedCFlags},
+ {gen.genCSrcs, combinedCFlags},
{cSrcs, combinedCFlags},
- {cxxSrcs, combineFlags(cppFlags, hdrIncludes, cxxFlags, defaultCFlags)},
- {objcSrcs, combineFlags(cppFlags, hdrIncludes, objcFlags, defaultCFlags)},
- {objcxxSrcs, combineFlags(cppFlags, hdrIncludes, objcxxFlags, defaultCFlags)},
+ {cxxSrcs, combineFlags(cppFlags, gen.hdrIncludes, cxxFlags, defaultCFlags)},
+ {objcSrcs, combineFlags(cppFlags, gen.hdrIncludes, objcFlags, defaultCFlags)},
+ {objcxxSrcs, combineFlags(cppFlags, gen.hdrIncludes, objcxxFlags, defaultCFlags)},
{sSrcs, asmCFlags},
} {
for _, src := range lang.srcs {
@@ -232,14 +90,14 @@
}
mainObj := filepath.Join(workDir, "_cgo_main.o")
- if err := cCompile(goenv, cgoMainC, cc, combinedCFlags, mainObj); err != nil {
+ if err := cCompile(goenv, gen.cgoMainC, cc, combinedCFlags, mainObj); err != nil {
return "", nil, nil, err
}
// Link cgo binary and use the symbols to generate _cgo_import.go.
mainBin := filepath.Join(workDir, "_cgo_.o") // .o is a lie; it's an executable
- args = append([]string{cc, "-o", mainBin, mainObj}, cObjs...)
- args = append(args, combinedLdFlags...)
+ args := append([]string{cc, "-o", mainBin, mainObj}, cObjs...)
+ args = append(args, gen.combinedLdFlags...)
var originalErrBuf bytes.Buffer
if err := goenv.runCommandToFile(os.Stdout, &originalErrBuf, args); err != nil {
// If linking the binary for cgo fails, this is usually because the
@@ -280,12 +138,10 @@
if err := goenv.runCommand(args); err != nil {
return "", nil, nil, err
}
- genGoSrcs = append(genGoSrcs, cgoImportsGo)
+ gen.genGoSrcs = append(gen.genGoSrcs, cgoImportsGo)
if cgoGoSrcsPath != "" {
- for _, src := range genGoSrcs {
- if err := copyFile(src, filepath.Join(cgoGoSrcsPath, filepath.Base(src))); err != nil {
- return "", nil, nil, err
- }
+ if err := copyGeneratedGoSrcs(gen.genGoSrcs, cgoGoSrcsPath); err != nil {
+ return "", nil, nil, err
}
}
@@ -296,14 +152,220 @@
return "", nil, nil, err
}
- allGoSrcs = make([]string, len(goSrcs)+len(genGoSrcs))
+ allGoSrcs = make([]string, len(goSrcs)+len(gen.genGoSrcs))
for i := range goSrcs {
allGoSrcs[i] = filepath.Join(workDir, goBases[i])
}
- copy(allGoSrcs[len(goSrcs):], genGoSrcs)
+ copy(allGoSrcs[len(goSrcs):], gen.genGoSrcs)
return workDir, allGoSrcs, cObjs, nil
}
+func cgo2GeneratedGoSrcsForNogo(goenv *env, cgoSrcs, cSrcs, cxxSrcs, objcSrcs, objcxxSrcs, hSrcs []string, packagePath string, cc string, cppFlags, cFlags, ldFlags []string, cgoGoSrcsPath string, cgoImportsSrc string) error {
+ if cc == "" {
+ err := cgoError(cgoSrcs[:])
+ err = append(err, cSrcs...)
+ err = append(err, cxxSrcs...)
+ err = append(err, objcSrcs...)
+ err = append(err, objcxxSrcs...)
+ return err
+ }
+ if len(cgoSrcs) == 0 {
+ return nil
+ }
+
+ workDir, cleanup, err := goenv.workDir()
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ gen, err := generateCgoSources(goenv, workDir, "cgo_nogo", cgoSrcs, cSrcs, cxxSrcs, objcSrcs, objcxxSrcs, hSrcs, packagePath, cppFlags, cFlags, ldFlags, "")
+ if err != nil {
+ return err
+ }
+ if err := os.MkdirAll(cgoGoSrcsPath, 0777); err != nil {
+ return err
+ }
+ gen.genGoSrcs = append(gen.genGoSrcs, cgoImportsSrc)
+ return copyGeneratedGoSrcs(gen.genGoSrcs, cgoGoSrcsPath)
+}
+
+type cgoGenResult struct {
+ workDir string
+ hdrIncludes []string
+ combinedLdFlags []string
+ genGoSrcs []string
+ genCSrcs []string
+ cgoMainC string
+}
+
+func generateCgoSources(goenv *env, baseWorkDir, subdir string, cgoSrcs, cSrcs, cxxSrcs, objcSrcs, objcxxSrcs, hSrcs []string, packagePath string, cppFlags, cFlags, ldFlags []string, cgoExportHPath string) (*cgoGenResult, error) {
+ // cgo2 will gather sources into a single temporary directory, since nogo
+ // scanners might want to include or exclude these sources we need to ensure
+ // that a fragment of the path is stable and human friendly enough to be
+ // referenced in nogo configuration.
+ workDir := filepath.Join(baseWorkDir, subdir, packagePath)
+ if err := os.MkdirAll(workDir, 0700); err != nil {
+ return nil, err
+ }
+
+ // Filter out -lstdc++ and -lc++ from ldflags if we don't have C++ sources,
+ // and set CGO_LDFLAGS. These flags get written as special comments into cgo
+ // generated sources. The compiler encodes those flags in the compiled .a
+ // file, and the linker passes them on to the external linker.
+ haveCxx := len(cxxSrcs)+len(objcxxSrcs) > 0
+ if !haveCxx {
+ for _, f := range ldFlags {
+ if strings.HasSuffix(f, ".a") {
+ // These flags come from cdeps options. Assume C++.
+ haveCxx = true
+ break
+ }
+ }
+ }
+ var combinedLdFlags []string
+ if haveCxx {
+ combinedLdFlags = append(combinedLdFlags, ldFlags...)
+ } else {
+ for _, f := range ldFlags {
+ if f != "-lc++" && f != "-lstdc++" {
+ combinedLdFlags = append(combinedLdFlags, f)
+ }
+ }
+ }
+ combinedLdFlags = append(combinedLdFlags, defaultLdFlags()...)
+
+ // go 1.23+ supports ldflags file.
+ // https://go-review.googlesource.com/c/go/+/584655
+ canUseLdflagsFile, err := onVersionOrHigher(23)
+ if err != nil {
+ return nil, err
+ }
+
+ var ldflagsFile *os.File
+ combinedLdFlagsStr := strings.Join(combinedLdFlags, " ")
+ if canUseLdflagsFile {
+ // Write linker flags to a temporary file instead of pasing via env variable.
+ // This avoids "argument list too long" error with extremely large CGO_LDFLAGS
+ // that can exceed system limits.
+ // Future versions of `go` may remove support for CGO_LDFLAGS entirely, so
+ // use file even for small ldflags.
+ // https://go-review.googlesource.com/c/go/+/596615
+ ldflagsFile, err = os.CreateTemp("", "cgo-ldflags-*.txt")
+ if err != nil {
+ return nil, fmt.Errorf("failed to create temporary file for ldflags: %w", err)
+ }
+ defer os.Remove(ldflagsFile.Name())
+ if _, err := ldflagsFile.WriteString(formatLdFlagsFileContent(combinedLdFlagsStr)); err != nil {
+ ldflagsFile.Close()
+ return nil, fmt.Errorf("failed to write ldflags to temporary file: %w", err)
+ }
+ if err := ldflagsFile.Close(); err != nil {
+ return nil, fmt.Errorf("failed to close temporary ldflags file: %w", err)
+ }
+ } else {
+ // Fallback to env variable for backwards compatibility with older `go` versions.
+ os.Setenv("CGO_LDFLAGS", combinedLdFlagsStr)
+ }
+
+ // If cgo sources are in different directories, gather them into a temporary
+ // directory so we can use -srcdir.
+ srcDir := filepath.Dir(cgoSrcs[0])
+ srcsInSingleDir := true
+ for _, src := range cgoSrcs[1:] {
+ if filepath.Dir(src) != srcDir {
+ srcsInSingleDir = false
+ break
+ }
+ }
+
+ if srcsInSingleDir {
+ for i := range cgoSrcs {
+ cgoSrcs[i] = filepath.Base(cgoSrcs[i])
+ }
+ } else {
+ srcDir = filepath.Join(workDir, "cgosrcs")
+ if err := os.Mkdir(srcDir, 0777); err != nil {
+ return nil, err
+ }
+ copiedSrcs, err := gatherSrcs(srcDir, cgoSrcs)
+ if err != nil {
+ return nil, err
+ }
+ cgoSrcs = copiedSrcs
+ }
+
+ // Generate Go and C code.
+ hdrDirs := map[string]bool{}
+ var hdrIncludes []string
+ for _, hdr := range hSrcs {
+ hdrDir := filepath.Dir(hdr)
+ if !hdrDirs[hdrDir] {
+ hdrDirs[hdrDir] = true
+ hdrIncludes = append(hdrIncludes, "-iquote", hdrDir)
+ }
+ }
+ hdrIncludes = append(hdrIncludes, "-iquote", workDir) // for _cgo_export.h
+
+ // Trim the path in //line comments emitted by cgo.
+ trimPath, err := createTrimPath()
+ if err != nil {
+ return nil, err
+ }
+ args := goenv.goTool("cgo", "-srcdir", srcDir, "-objdir", workDir, "-trimpath", trimPath)
+ if ldflagsFile != nil {
+ // The "@" prefix tells cgo to read arguments from the file.
+ args = append(args, "-ldflags", "@"+ldflagsFile.Name())
+ }
+ if packagePath != "" {
+ args = append(args, "-importpath", packagePath)
+ }
+ args = append(args, "--")
+ args = append(args, cppFlags...)
+ args = append(args, hdrIncludes...)
+ args = append(args, cFlags...)
+ args = append(args, cgoSrcs...)
+ if err := goenv.runCommand(args); err != nil {
+ return nil, err
+ }
+
+ if cgoExportHPath != "" {
+ if err := copyFile(filepath.Join(workDir, "_cgo_export.h"), cgoExportHPath); err != nil {
+ return nil, err
+ }
+ }
+ // Note: The tools/gopackagesdriver will break if the naming convention
+ // changes for genGoSrcs files. Changes here should be reflected
+ // there.
+ genGoSrcs := make([]string, 1+len(cgoSrcs))
+ genGoSrcs[0] = filepath.Join(workDir, "_cgo_gotypes.go")
+ genCSrcs := make([]string, 1+len(cgoSrcs))
+ genCSrcs[0] = filepath.Join(workDir, "_cgo_export.c")
+ for i, src := range cgoSrcs {
+ stem := strings.TrimSuffix(filepath.Base(src), ".go")
+ genGoSrcs[i+1] = filepath.Join(workDir, stem+".cgo1.go")
+ genCSrcs[i+1] = filepath.Join(workDir, stem+".cgo2.c")
+ }
+ cgoMainC := filepath.Join(workDir, "_cgo_main.c")
+ return &cgoGenResult{
+ workDir: workDir,
+ hdrIncludes: hdrIncludes,
+ combinedLdFlags: combinedLdFlags,
+ genGoSrcs: genGoSrcs,
+ genCSrcs: genCSrcs,
+ cgoMainC: cgoMainC,
+ }, nil
+}
+
+func copyGeneratedGoSrcs(srcs []string, outDir string) error {
+ for _, src := range srcs {
+ if err := copyFile(src, filepath.Join(outDir, filepath.Base(src))); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// compileCSources compiles a list of C, C++, Objective-C, Objective-C++,
// and assembly sources into .o files to be packed into the archive.
// It does not run cgo. This is used for packages with "cgo = True" but
diff --git a/go/tools/builders/compilepkg.go b/go/tools/builders/compilepkg.go
index e2b0be5..a1782d4 100644
--- a/go/tools/builders/compilepkg.go
+++ b/go/tools/builders/compilepkg.go
@@ -243,9 +243,8 @@
// When coverage is set, source files will be modified during instrumentation. We should only run static analysis
// over original source files and not the modified ones.
- // goSrcsNogo and cgoSrcsNogo are copies of the original source files for nogo to run static analysis.
+ // cgoSrcsNogo is a copy of the original cgo source files for nogo to run static analysis.
// TODO: Use slices.Clone when 1.21 is the minimal supported version.
- goSrcsNogo := append([]string{}, goSrcs...)
cgoSrcsNogo := append([]string{}, cgoSrcs...)
// Instrument source files in a package for coverage.
@@ -339,10 +338,9 @@
return err
}
// Also run cgo on original source files, not coverage instrumented, if using nogo.
- // The compilation outputs are only used to run cgo, but the generated sources are
- // passed to the separate nogo action via cgoGoSrcsForNogoPath.
- _, _, _, err = cgo2(goenv, goSrcsNogo, cgoSrcsNogo, cSrcs, cxxSrcs, objcSrcs, objcxxSrcs, sSrcs, hSrcs, packagePath, packageName, cc, cppFlags, cFlags, cxxFlags, objcFlags, objcxxFlags, ldFlags, "", cgoGoSrcsForNogoPath)
- if err != nil {
+ // Only the generated Go sources are passed to the separate nogo action via
+ // cgoGoSrcsForNogoPath, so skip the duplicate C compilation and cgo probe link.
+ if err := cgo2GeneratedGoSrcsForNogo(goenv, cgoSrcsNogo, cSrcs, cxxSrcs, objcSrcs, objcxxSrcs, hSrcs, packagePath, cc, cppFlags, cFlags, ldFlags, cgoGoSrcsForNogoPath, filepath.Join(srcDir, "_cgo_imports.go")); err != nil {
return err
}
} else {