blob: 80043e467b4bb8afa9afb085473498539e5adb5c [file] [log] [blame]
// Copyright 2019 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// cgo2.go provides new cgo functionality for use by the GoCompilePkg action.
// We can't use the functionality in cgo.go, since it relies too heavily
// on logic in cgo.bzl. Ideally, we'd be able to replace cgo.go with this
// file eventually, but not until Bazel gives us enough toolchain information
// to compile ObjC files.
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
)
// cgo2 processes a set of mixed source files with cgo.
func cgo2(goenv *env, goSrcs, cgoSrcs, cSrcs, cxxSrcs, objcSrcs, objcxxSrcs, sSrcs, hSrcs []string, packagePath, packageName string, cc string, cppFlags, cFlags, cxxFlags, objcFlags, objcxxFlags, ldFlags []string, cgoExportHPath string) (srcDir string, allGoSrcs, cObjs []string, err error) {
// Report an error if the C/C++ toolchain wasn't configured.
if cc == "" {
err := cgoError(cgoSrcs[:])
err = append(err, cSrcs...)
err = append(err, cxxSrcs...)
err = append(err, objcSrcs...)
err = append(err, objcxxSrcs...)
err = append(err, sSrcs...)
return "", nil, nil, err
}
// If we only have C/C++ sources without cgo, just compile and pack them
// without generating code. The Go command forbids this, but we've
// historically allowed it.
// TODO(jayconrod): this doesn't write CGO_LDFLAGS into the archive. We
// might miss dependencies like -lstdc++ if they aren't referenced in
// some other way.
if len(cgoSrcs) == 0 {
cObjs, err = compileCSources(goenv, cSrcs, cxxSrcs, objcSrcs, objcxxSrcs, sSrcs, hSrcs, cc, cppFlags, cFlags, cxxFlags, objcFlags, objcxxFlags)
return ".", nil, cObjs, err
}
workDir, cleanup, err := goenv.workDir()
if err != nil {
return "", nil, nil, err
}
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()...)
os.Setenv("CGO_LDFLAGS", strings.Join(combinedLdFlags, " "))
// 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
execRoot, err := bazelExecRoot()
if err != nil {
return "", nil, nil, err
}
// Trim the execroot from the //line comments emitted by cgo.
args := goenv.goTool("cgo", "-srcdir", srcDir, "-objdir", workDir, "-trimpath", execRoot)
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
}
}
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")
// Compile C, C++, Objective-C/C++, and assembly code.
defaultCFlags := defaultCFlags(workDir)
combinedCFlags := combineFlags(cppFlags, hdrIncludes, cFlags, defaultCFlags)
for _, lang := range []struct{ srcs, flags []string }{
{genCSrcs, combinedCFlags},
{cSrcs, combinedCFlags},
{cxxSrcs, combineFlags(cppFlags, hdrIncludes, cxxFlags, defaultCFlags)},
{objcSrcs, combineFlags(cppFlags, hdrIncludes, objcFlags, defaultCFlags)},
{objcxxSrcs, combineFlags(cppFlags, hdrIncludes, objcxxFlags, defaultCFlags)},
{sSrcs, nil},
} {
for _, src := range lang.srcs {
obj := filepath.Join(workDir, fmt.Sprintf("_x%d.o", len(cObjs)))
cObjs = append(cObjs, obj)
if err := cCompile(goenv, src, cc, lang.flags, obj); err != nil {
return "", nil, nil, err
}
}
}
mainObj := filepath.Join(workDir, "_cgo_main.o")
if err := cCompile(goenv, 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...)
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
// object files reference external symbols that can't be resolved yet.
// Since the binary is only produced to have its symbols read by the cgo
// command, there is no harm in trying to build it allowing unresolved
// symbols - the real link that happens at the end will fail if they
// rightfully can't be resolved.
var allowUnresolvedSymbolsLdFlag string
switch os.Getenv("GOOS") {
case "windows":
// MinGW's linker doesn't seem to support --unresolved-symbols
// and MSVC isn't supported at all.
return "", nil, nil, err
case "darwin", "ios":
allowUnresolvedSymbolsLdFlag = "-Wl,-undefined,dynamic_lookup"
default:
allowUnresolvedSymbolsLdFlag = "-Wl,--unresolved-symbols=ignore-all"
}
// Print and return the original error if we can't link the binary with
// the additional linker flags as they may simply be incorrect for the
// particular compiler/linker pair and would obscure the true reason for
// the failure of the original command.
if err2 := goenv.runCommandToFile(
os.Stdout,
ioutil.Discard,
append(args, allowUnresolvedSymbolsLdFlag),
); err2 != nil {
os.Stderr.Write(relativizePaths(originalErrBuf.Bytes()))
return "", nil, nil, err
}
// Do not print the original error - rerunning the command with the
// additional linker flag fixed it.
}
cgoImportsGo := filepath.Join(workDir, "_cgo_imports.go")
args = goenv.goTool("cgo", "-dynpackage", packageName, "-dynimport", mainBin, "-dynout", cgoImportsGo)
if err := goenv.runCommand(args); err != nil {
return "", nil, nil, err
}
genGoSrcs = append(genGoSrcs, cgoImportsGo)
// Copy regular Go source files into the work directory so that we can
// use -trimpath=workDir.
goBases, err := gatherSrcs(workDir, goSrcs)
if err != nil {
return "", nil, nil, err
}
allGoSrcs = make([]string, len(goSrcs)+len(genGoSrcs))
for i := range goSrcs {
allGoSrcs[i] = filepath.Join(workDir, goBases[i])
}
copy(allGoSrcs[len(goSrcs):], genGoSrcs)
return workDir, allGoSrcs, cObjs, 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
// without any .go files that import "C". The Go command forbids this,
// but we have historically allowed it.
func compileCSources(goenv *env, cSrcs, cxxSrcs, objcSrcs, objcxxSrcs, sSrcs, hSrcs []string, cc string, cppFlags, cFlags, cxxFlags, objcFlags, objcxxFlags []string) (cObjs []string, err error) {
workDir, cleanup, err := goenv.workDir()
if err != nil {
return nil, err
}
defer cleanup()
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)
}
}
defaultCFlags := defaultCFlags(workDir)
for _, lang := range []struct{ srcs, flags []string }{
{cSrcs, combineFlags(cppFlags, hdrIncludes, cFlags, defaultCFlags)},
{cxxSrcs, combineFlags(cppFlags, hdrIncludes, cxxFlags, defaultCFlags)},
{objcSrcs, combineFlags(cppFlags, hdrIncludes, objcFlags, defaultCFlags)},
{objcxxSrcs, combineFlags(cppFlags, hdrIncludes, objcxxFlags, defaultCFlags)},
{sSrcs, nil},
} {
for _, src := range lang.srcs {
obj := filepath.Join(workDir, fmt.Sprintf("_x%d.o", len(cObjs)))
cObjs = append(cObjs, obj)
if err := cCompile(goenv, src, cc, lang.flags, obj); err != nil {
return nil, err
}
}
}
return cObjs, nil
}
func combineFlags(lists ...[]string) []string {
n := 0
for _, list := range lists {
n += len(list)
}
flags := make([]string, 0, n)
for _, list := range lists {
flags = append(flags, list...)
}
return flags
}
func cCompile(goenv *env, src, cc string, flags []string, out string) error {
args := []string{cc}
args = append(args, flags...)
args = append(args, "-c", src, "-o", out)
return goenv.runCommand(args)
}
func defaultCFlags(workDir string) []string {
flags := []string{
"-fdebug-prefix-map=" + abs(".") + "=.",
"-fdebug-prefix-map=" + workDir + "=.",
}
goos, goarch := os.Getenv("GOOS"), os.Getenv("GOARCH")
switch {
case goos == "darwin" || goos == "ios":
return flags
case goos == "windows" && goarch == "amd64":
return append(flags, "-mthreads")
default:
return append(flags, "-pthread")
}
}
func defaultLdFlags() []string {
goos, goarch := os.Getenv("GOOS"), os.Getenv("GOARCH")
switch {
case goos == "android":
return []string{"-llog", "-ldl"}
case goos == "darwin" || goos == "ios":
return nil
case goos == "windows" && goarch == "amd64":
return []string{"-mthreads"}
default:
return []string{"-pthread"}
}
}
// gatherSrcs copies or links files listed in srcs into dir. This is needed
// to effectively use -trimpath with generated sources. It's also needed by cgo.
//
// gatherSrcs returns the basenames of copied files in the directory.
func gatherSrcs(dir string, srcs []string) ([]string, error) {
copiedBases := make([]string, len(srcs))
for i, src := range srcs {
base := filepath.Base(src)
ext := filepath.Ext(base)
stem := base[:len(base)-len(ext)]
var err error
for j := 1; j < 10000; j++ {
if err = copyOrLinkFile(src, filepath.Join(dir, base)); err == nil {
break
} else if !os.IsExist(err) {
return nil, err
} else {
base = fmt.Sprintf("%s_%d%s", stem, j, ext)
}
}
if err != nil {
return nil, fmt.Errorf("could not find unique name for file %s", src)
}
copiedBases[i] = base
}
return copiedBases, nil
}
func bazelExecRoot() (string, error) {
// Bazel executes the builder with a working directory of the form
// .../execroot/<workspace name>. By stripping the last segment, we obtain a
// prefix of all possible source files, even when contained in external
// repositories.
cwd, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Dir(cwd), nil
}
type cgoError []string
func (e cgoError) Error() string {
b := &bytes.Buffer{}
fmt.Fprint(b, "CC is not set and files need to be processed with cgo:\n")
for _, f := range e {
fmt.Fprintf(b, "\t%s\n", f)
}
fmt.Fprintf(b, "Ensure that 'cgo = True' is set and the C/C++ toolchain is configured.")
return b.String()
}
func copyFile(inPath, outPath string) error {
inFile, err := os.Open(inPath)
if err != nil {
return err
}
defer inFile.Close()
outFile, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
return err
}
defer outFile.Close()
_, err = io.Copy(outFile, inFile)
return err
}
func linkFile(inPath, outPath string) error {
inPath, err := filepath.Abs(inPath)
if err != nil {
return err
}
return os.Symlink(inPath, outPath)
}
func copyOrLinkFile(inPath, outPath string) error {
if runtime.GOOS == "windows" {
return copyFile(inPath, outPath)
} else {
return linkFile(inPath, outPath)
}
}