458 lines
15 KiB
Go
458 lines
15 KiB
Go
// Copyright 2018 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package packages
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"go/build"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/internal/cgo"
|
|
)
|
|
|
|
// TODO(matloob): Delete this file once Go 1.12 is released.
|
|
|
|
// This file provides backwards compatibility support for
|
|
// loading for versions of Go earlier than 1.10.4. This support is meant to
|
|
// assist with migration to the Package API until there's
|
|
// widespread adoption of these newer Go versions.
|
|
// This support will be removed once Go 1.12 is released
|
|
// in Q1 2019.
|
|
|
|
func golistDriverFallback(cfg *Config, words ...string) (*driverResponse, error) {
|
|
// Turn absolute paths into GOROOT and GOPATH-relative paths to provide to go list.
|
|
// This will have surprising behavior if GOROOT or GOPATH contain multiple packages with the same
|
|
// path and a user provides an absolute path to a directory that's shadowed by an earlier
|
|
// directory in GOROOT or GOPATH with the same package path.
|
|
words = cleanAbsPaths(cfg, words)
|
|
|
|
original, deps, err := getDeps(cfg, words...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var tmpdir string // used for generated cgo files
|
|
var needsTestVariant []struct {
|
|
pkg, xtestPkg *Package
|
|
}
|
|
|
|
var response driverResponse
|
|
allPkgs := make(map[string]bool)
|
|
addPackage := func(p *jsonPackage) {
|
|
id := p.ImportPath
|
|
|
|
if allPkgs[id] {
|
|
return
|
|
}
|
|
allPkgs[id] = true
|
|
|
|
isRoot := original[id] != nil
|
|
pkgpath := id
|
|
|
|
if pkgpath == "unsafe" {
|
|
p.GoFiles = nil // ignore fake unsafe.go file
|
|
}
|
|
|
|
importMap := func(importlist []string) map[string]*Package {
|
|
importMap := make(map[string]*Package)
|
|
for _, id := range importlist {
|
|
|
|
if id == "C" {
|
|
for _, path := range []string{"unsafe", "syscall", "runtime/cgo"} {
|
|
if pkgpath != path && importMap[path] == nil {
|
|
importMap[path] = &Package{ID: path}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
importMap[vendorlessPath(id)] = &Package{ID: id}
|
|
}
|
|
return importMap
|
|
}
|
|
compiledGoFiles := absJoin(p.Dir, p.GoFiles)
|
|
// Use a function to simplify control flow. It's just a bunch of gotos.
|
|
var cgoErrors []error
|
|
var outdir string
|
|
getOutdir := func() (string, error) {
|
|
if outdir != "" {
|
|
return outdir, nil
|
|
}
|
|
if tmpdir == "" {
|
|
if tmpdir, err = ioutil.TempDir("", "gopackages"); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
// Add a "go-build" component to the path to make the tests think the files are in the cache.
|
|
// This allows the same test to test the pre- and post-Go 1.11 go list logic because the Go 1.11
|
|
// go list generates test mains in the cache, and the test code knows not to rely on paths in the
|
|
// cache to stay stable.
|
|
outdir = filepath.Join(tmpdir, "go-build", strings.Replace(p.ImportPath, "/", "_", -1))
|
|
if err := os.MkdirAll(outdir, 0755); err != nil {
|
|
outdir = ""
|
|
return "", err
|
|
}
|
|
return outdir, nil
|
|
}
|
|
processCgo := func() bool {
|
|
// Suppress any cgo errors. Any relevant errors will show up in typechecking.
|
|
// TODO(matloob): Skip running cgo if Mode < LoadTypes.
|
|
outdir, err := getOutdir()
|
|
if err != nil {
|
|
cgoErrors = append(cgoErrors, err)
|
|
return false
|
|
}
|
|
files, _, err := runCgo(p.Dir, outdir, cfg.Env)
|
|
if err != nil {
|
|
cgoErrors = append(cgoErrors, err)
|
|
return false
|
|
}
|
|
compiledGoFiles = append(compiledGoFiles, files...)
|
|
return true
|
|
}
|
|
if len(p.CgoFiles) == 0 || !processCgo() {
|
|
compiledGoFiles = append(compiledGoFiles, absJoin(p.Dir, p.CgoFiles)...) // Punt to typechecker.
|
|
}
|
|
if isRoot {
|
|
response.Roots = append(response.Roots, id)
|
|
}
|
|
pkg := &Package{
|
|
ID: id,
|
|
Name: p.Name,
|
|
GoFiles: absJoin(p.Dir, p.GoFiles, p.CgoFiles),
|
|
CompiledGoFiles: compiledGoFiles,
|
|
OtherFiles: absJoin(p.Dir, otherFiles(p)...),
|
|
PkgPath: pkgpath,
|
|
Imports: importMap(p.Imports),
|
|
// TODO(matloob): set errors on the Package to cgoErrors
|
|
}
|
|
if p.Error != nil {
|
|
pkg.Errors = append(pkg.Errors, Error{
|
|
Pos: p.Error.Pos,
|
|
Msg: p.Error.Err,
|
|
})
|
|
}
|
|
response.Packages = append(response.Packages, pkg)
|
|
if cfg.Tests && isRoot {
|
|
testID := fmt.Sprintf("%s [%s.test]", id, id)
|
|
if len(p.TestGoFiles) > 0 || len(p.XTestGoFiles) > 0 {
|
|
response.Roots = append(response.Roots, testID)
|
|
testPkg := &Package{
|
|
ID: testID,
|
|
Name: p.Name,
|
|
GoFiles: absJoin(p.Dir, p.GoFiles, p.CgoFiles, p.TestGoFiles),
|
|
CompiledGoFiles: append(compiledGoFiles, absJoin(p.Dir, p.TestGoFiles)...),
|
|
OtherFiles: absJoin(p.Dir, otherFiles(p)...),
|
|
PkgPath: pkgpath,
|
|
Imports: importMap(append(p.Imports, p.TestImports...)),
|
|
// TODO(matloob): set errors on the Package to cgoErrors
|
|
}
|
|
response.Packages = append(response.Packages, testPkg)
|
|
var xtestPkg *Package
|
|
if len(p.XTestGoFiles) > 0 {
|
|
xtestID := fmt.Sprintf("%s_test [%s.test]", id, id)
|
|
response.Roots = append(response.Roots, xtestID)
|
|
// Generate test variants for all packages q where a path exists
|
|
// such that xtestPkg -> ... -> q -> ... -> p (where p is the package under test)
|
|
// and rewrite all import map entries of p to point to testPkg (the test variant of
|
|
// p), and of each q to point to the test variant of that q.
|
|
xtestPkg = &Package{
|
|
ID: xtestID,
|
|
Name: p.Name + "_test",
|
|
GoFiles: absJoin(p.Dir, p.XTestGoFiles),
|
|
CompiledGoFiles: absJoin(p.Dir, p.XTestGoFiles),
|
|
PkgPath: pkgpath + "_test",
|
|
Imports: importMap(p.XTestImports),
|
|
}
|
|
// Add to list of packages we need to rewrite imports for to refer to test variants.
|
|
// We may need to create a test variant of a package that hasn't been loaded yet, so
|
|
// the test variants need to be created later.
|
|
needsTestVariant = append(needsTestVariant, struct{ pkg, xtestPkg *Package }{pkg, xtestPkg})
|
|
response.Packages = append(response.Packages, xtestPkg)
|
|
}
|
|
// testmain package
|
|
testmainID := id + ".test"
|
|
response.Roots = append(response.Roots, testmainID)
|
|
imports := map[string]*Package{}
|
|
imports[testPkg.PkgPath] = &Package{ID: testPkg.ID}
|
|
if xtestPkg != nil {
|
|
imports[xtestPkg.PkgPath] = &Package{ID: xtestPkg.ID}
|
|
}
|
|
testmainPkg := &Package{
|
|
ID: testmainID,
|
|
Name: "main",
|
|
PkgPath: testmainID,
|
|
Imports: imports,
|
|
}
|
|
response.Packages = append(response.Packages, testmainPkg)
|
|
outdir, err := getOutdir()
|
|
if err != nil {
|
|
testmainPkg.Errors = append(testmainPkg.Errors, Error{
|
|
Pos: "-",
|
|
Msg: fmt.Sprintf("failed to generate testmain: %v", err),
|
|
Kind: ListError,
|
|
})
|
|
return
|
|
}
|
|
testmain := filepath.Join(outdir, "testmain.go")
|
|
extraimports, extradeps, err := generateTestmain(testmain, testPkg, xtestPkg)
|
|
if err != nil {
|
|
testmainPkg.Errors = append(testmainPkg.Errors, Error{
|
|
Pos: "-",
|
|
Msg: fmt.Sprintf("failed to generate testmain: %v", err),
|
|
Kind: ListError,
|
|
})
|
|
}
|
|
deps = append(deps, extradeps...)
|
|
for _, imp := range extraimports { // testing, testing/internal/testdeps, and maybe os
|
|
imports[imp] = &Package{ID: imp}
|
|
}
|
|
testmainPkg.GoFiles = []string{testmain}
|
|
testmainPkg.CompiledGoFiles = []string{testmain}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, pkg := range original {
|
|
addPackage(pkg)
|
|
}
|
|
if cfg.Mode < LoadImports || len(deps) == 0 {
|
|
return &response, nil
|
|
}
|
|
|
|
buf, err := invokeGo(cfg, golistArgsFallback(cfg, deps)...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Decode the JSON and convert it to Package form.
|
|
for dec := json.NewDecoder(buf); dec.More(); {
|
|
p := new(jsonPackage)
|
|
if err := dec.Decode(p); err != nil {
|
|
return nil, fmt.Errorf("JSON decoding failed: %v", err)
|
|
}
|
|
|
|
addPackage(p)
|
|
}
|
|
|
|
for _, v := range needsTestVariant {
|
|
createTestVariants(&response, v.pkg, v.xtestPkg)
|
|
}
|
|
|
|
// TODO(matloob): Is this the right ordering?
|
|
sort.SliceStable(response.Packages, func(i, j int) bool {
|
|
return response.Packages[i].PkgPath < response.Packages[j].PkgPath
|
|
})
|
|
|
|
return &response, nil
|
|
}
|
|
|
|
func createTestVariants(response *driverResponse, pkgUnderTest, xtestPkg *Package) {
|
|
allPkgs := make(map[string]*Package)
|
|
for _, pkg := range response.Packages {
|
|
allPkgs[pkg.ID] = pkg
|
|
}
|
|
needsTestVariant := make(map[string]bool)
|
|
needsTestVariant[pkgUnderTest.ID] = true
|
|
var needsVariantRec func(p *Package) bool
|
|
needsVariantRec = func(p *Package) bool {
|
|
if needsTestVariant[p.ID] {
|
|
return true
|
|
}
|
|
for _, imp := range p.Imports {
|
|
if needsVariantRec(allPkgs[imp.ID]) {
|
|
// Don't break because we want to make sure all dependencies
|
|
// have been processed, and all required test variants of our dependencies
|
|
// exist.
|
|
needsTestVariant[p.ID] = true
|
|
}
|
|
}
|
|
if !needsTestVariant[p.ID] {
|
|
return false
|
|
}
|
|
// Create a clone of the package. It will share the same strings and lists of source files,
|
|
// but that's okay. It's only necessary for the Imports map to have a separate identity.
|
|
testVariant := *p
|
|
testVariant.ID = fmt.Sprintf("%s [%s.test]", p.ID, pkgUnderTest.ID)
|
|
testVariant.Imports = make(map[string]*Package)
|
|
for imp, pkg := range p.Imports {
|
|
testVariant.Imports[imp] = pkg
|
|
if needsTestVariant[pkg.ID] {
|
|
testVariant.Imports[imp] = &Package{ID: fmt.Sprintf("%s [%s.test]", pkg.ID, pkgUnderTest.ID)}
|
|
}
|
|
}
|
|
response.Packages = append(response.Packages, &testVariant)
|
|
return needsTestVariant[p.ID]
|
|
}
|
|
// finally, update the xtest package's imports
|
|
for imp, pkg := range xtestPkg.Imports {
|
|
if allPkgs[pkg.ID] == nil {
|
|
fmt.Printf("for %s: package %s doesn't exist\n", xtestPkg.ID, pkg.ID)
|
|
}
|
|
if needsVariantRec(allPkgs[pkg.ID]) {
|
|
xtestPkg.Imports[imp] = &Package{ID: fmt.Sprintf("%s [%s.test]", pkg.ID, pkgUnderTest.ID)}
|
|
}
|
|
}
|
|
}
|
|
|
|
// cleanAbsPaths replaces all absolute paths with GOPATH- and GOROOT-relative
|
|
// paths. If an absolute path is not GOPATH- or GOROOT- relative, it is left as an
|
|
// absolute path so an error can be returned later.
|
|
func cleanAbsPaths(cfg *Config, words []string) []string {
|
|
var searchpaths []string
|
|
var cleaned = make([]string, len(words))
|
|
for i := range cleaned {
|
|
cleaned[i] = words[i]
|
|
// Ignore relative directory paths (they must already be goroot-relative) and Go source files
|
|
// (absolute source files are already allowed for ad-hoc packages).
|
|
// TODO(matloob): Can there be non-.go files in ad-hoc packages.
|
|
if !filepath.IsAbs(cleaned[i]) || strings.HasSuffix(cleaned[i], ".go") {
|
|
continue
|
|
}
|
|
// otherwise, it's an absolute path. Search GOPATH and GOROOT to find it.
|
|
if searchpaths == nil {
|
|
cmd := exec.Command("go", "env", "GOPATH", "GOROOT")
|
|
cmd.Env = cfg.Env
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
searchpaths = []string{}
|
|
continue // suppress the error, it will show up again when running go list
|
|
}
|
|
lines := strings.Split(string(out), "\n")
|
|
if len(lines) != 3 || lines[0] == "" || lines[1] == "" || lines[2] != "" {
|
|
continue // suppress error
|
|
}
|
|
// first line is GOPATH
|
|
for _, path := range filepath.SplitList(lines[0]) {
|
|
searchpaths = append(searchpaths, filepath.Join(path, "src"))
|
|
}
|
|
// second line is GOROOT
|
|
searchpaths = append(searchpaths, filepath.Join(lines[1], "src"))
|
|
}
|
|
for _, sp := range searchpaths {
|
|
if strings.HasPrefix(cleaned[i], sp) {
|
|
cleaned[i] = strings.TrimPrefix(cleaned[i], sp)
|
|
cleaned[i] = strings.TrimLeft(cleaned[i], string(filepath.Separator))
|
|
}
|
|
}
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
// vendorlessPath returns the devendorized version of the import path ipath.
|
|
// For example, VendorlessPath("foo/bar/vendor/a/b") returns "a/b".
|
|
// Copied from golang.org/x/tools/imports/fix.go.
|
|
func vendorlessPath(ipath string) string {
|
|
// Devendorize for use in import statement.
|
|
if i := strings.LastIndex(ipath, "/vendor/"); i >= 0 {
|
|
return ipath[i+len("/vendor/"):]
|
|
}
|
|
if strings.HasPrefix(ipath, "vendor/") {
|
|
return ipath[len("vendor/"):]
|
|
}
|
|
return ipath
|
|
}
|
|
|
|
// getDeps runs an initial go list to determine all the dependency packages.
|
|
func getDeps(cfg *Config, words ...string) (originalSet map[string]*jsonPackage, deps []string, err error) {
|
|
buf, err := invokeGo(cfg, golistArgsFallback(cfg, words)...)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
depsSet := make(map[string]bool)
|
|
originalSet = make(map[string]*jsonPackage)
|
|
var testImports []string
|
|
|
|
// Extract deps from the JSON.
|
|
for dec := json.NewDecoder(buf); dec.More(); {
|
|
p := new(jsonPackage)
|
|
if err := dec.Decode(p); err != nil {
|
|
return nil, nil, fmt.Errorf("JSON decoding failed: %v", err)
|
|
}
|
|
|
|
originalSet[p.ImportPath] = p
|
|
for _, dep := range p.Deps {
|
|
depsSet[dep] = true
|
|
}
|
|
if cfg.Tests {
|
|
// collect the additional imports of the test packages.
|
|
pkgTestImports := append(p.TestImports, p.XTestImports...)
|
|
for _, imp := range pkgTestImports {
|
|
if depsSet[imp] {
|
|
continue
|
|
}
|
|
depsSet[imp] = true
|
|
testImports = append(testImports, imp)
|
|
}
|
|
}
|
|
}
|
|
// Get the deps of the packages imported by tests.
|
|
if len(testImports) > 0 {
|
|
buf, err = invokeGo(cfg, golistArgsFallback(cfg, testImports)...)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
// Extract deps from the JSON.
|
|
for dec := json.NewDecoder(buf); dec.More(); {
|
|
p := new(jsonPackage)
|
|
if err := dec.Decode(p); err != nil {
|
|
return nil, nil, fmt.Errorf("JSON decoding failed: %v", err)
|
|
}
|
|
for _, dep := range p.Deps {
|
|
depsSet[dep] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
for orig := range originalSet {
|
|
delete(depsSet, orig)
|
|
}
|
|
|
|
deps = make([]string, 0, len(depsSet))
|
|
for dep := range depsSet {
|
|
deps = append(deps, dep)
|
|
}
|
|
sort.Strings(deps) // ensure output is deterministic
|
|
return originalSet, deps, nil
|
|
}
|
|
|
|
func golistArgsFallback(cfg *Config, words []string) []string {
|
|
fullargs := []string{"list", "-e", "-json"}
|
|
fullargs = append(fullargs, cfg.BuildFlags...)
|
|
fullargs = append(fullargs, "--")
|
|
fullargs = append(fullargs, words...)
|
|
return fullargs
|
|
}
|
|
|
|
func runCgo(pkgdir, tmpdir string, env []string) (files, displayfiles []string, err error) {
|
|
// Use go/build to open cgo files and determine the cgo flags, etc, from them.
|
|
// This is tricky so it's best to avoid reimplementing as much as we can, and
|
|
// we plan to delete this support once Go 1.12 is released anyways.
|
|
// TODO(matloob): This isn't completely correct because we're using the Default
|
|
// context. Perhaps we should more accurately fill in the context.
|
|
bp, err := build.ImportDir(pkgdir, build.ImportMode(0))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, ev := range env {
|
|
if v := strings.TrimPrefix(ev, "CGO_CPPFLAGS"); v != ev {
|
|
bp.CgoCPPFLAGS = append(bp.CgoCPPFLAGS, strings.Fields(v)...)
|
|
} else if v := strings.TrimPrefix(ev, "CGO_CFLAGS"); v != ev {
|
|
bp.CgoCFLAGS = append(bp.CgoCFLAGS, strings.Fields(v)...)
|
|
} else if v := strings.TrimPrefix(ev, "CGO_CXXFLAGS"); v != ev {
|
|
bp.CgoCXXFLAGS = append(bp.CgoCXXFLAGS, strings.Fields(v)...)
|
|
} else if v := strings.TrimPrefix(ev, "CGO_LDFLAGS"); v != ev {
|
|
bp.CgoLDFLAGS = append(bp.CgoLDFLAGS, strings.Fields(v)...)
|
|
}
|
|
}
|
|
return cgo.Run(bp, pkgdir, tmpdir, true)
|
|
}
|