// 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/. 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) } }