golist.go

  1// Copyright 2018 The Go Authors. All rights reserved.
  2// Use of this source code is governed by a BSD-style
  3// license that can be found in the LICENSE file.
  4
  5package packages
  6
  7import (
  8	"bytes"
  9	"encoding/json"
 10	"fmt"
 11	"log"
 12	"os"
 13	"os/exec"
 14	"path/filepath"
 15	"strings"
 16)
 17
 18// A goTooOldError reports that the go command
 19// found by exec.LookPath is too old to use the new go list behavior.
 20type goTooOldError struct {
 21	error
 22}
 23
 24// goListDriver uses the go list command to interpret the patterns and produce
 25// the build system package structure.
 26// See driver for more details.
 27func goListDriver(cfg *Config, patterns ...string) (*driverResponse, error) {
 28	// Determine files requested in contains patterns
 29	var containFiles []string
 30	restPatterns := make([]string, 0, len(patterns))
 31	for _, pattern := range patterns {
 32		if strings.HasPrefix(pattern, "contains:") {
 33			containFile := strings.TrimPrefix(pattern, "contains:")
 34			containFiles = append(containFiles, containFile)
 35		} else {
 36			restPatterns = append(restPatterns, pattern)
 37		}
 38	}
 39	containFiles = absJoin(cfg.Dir, containFiles)
 40	patterns = restPatterns
 41
 42	// TODO(matloob): Remove the definition of listfunc and just use golistPackages once go1.12 is released.
 43	var listfunc driver
 44	listfunc = func(cfg *Config, words ...string) (*driverResponse, error) {
 45		response, err := golistDriverCurrent(cfg, patterns...)
 46		if _, ok := err.(goTooOldError); ok {
 47			listfunc = golistDriverFallback
 48			return listfunc(cfg, patterns...)
 49		}
 50		listfunc = golistDriverCurrent
 51		return response, err
 52	}
 53
 54	var response *driverResponse
 55	var err error
 56
 57	// see if we have any patterns to pass through to go list.
 58	if len(patterns) > 0 {
 59		response, err = listfunc(cfg, patterns...)
 60		if err != nil {
 61			return nil, err
 62		}
 63	} else {
 64		response = &driverResponse{}
 65	}
 66
 67	// Run go list for contains: patterns.
 68	seenPkgs := make(map[string]*Package) // for deduplication. different containing queries could produce same packages
 69	if len(containFiles) > 0 {
 70		for _, pkg := range response.Packages {
 71			seenPkgs[pkg.ID] = pkg
 72		}
 73	}
 74	for _, f := range containFiles {
 75		// TODO(matloob): Do only one query per directory.
 76		fdir := filepath.Dir(f)
 77		cfg.Dir = fdir
 78		dirResponse, err := listfunc(cfg, ".")
 79		if err != nil {
 80			return nil, err
 81		}
 82		isRoot := make(map[string]bool, len(dirResponse.Roots))
 83		for _, root := range dirResponse.Roots {
 84			isRoot[root] = true
 85		}
 86		for _, pkg := range dirResponse.Packages {
 87			// Add any new packages to the main set
 88			// We don't bother to filter packages that will be dropped by the changes of roots,
 89			// that will happen anyway during graph construction outside this function.
 90			// Over-reporting packages is not a problem.
 91			if _, ok := seenPkgs[pkg.ID]; !ok {
 92				// it is a new package, just add it
 93				seenPkgs[pkg.ID] = pkg
 94				response.Packages = append(response.Packages, pkg)
 95			}
 96			// if the package was not a root one, it cannot have the file
 97			if !isRoot[pkg.ID] {
 98				continue
 99			}
100			for _, pkgFile := range pkg.GoFiles {
101				if filepath.Base(f) == filepath.Base(pkgFile) {
102					response.Roots = append(response.Roots, pkg.ID)
103					break
104				}
105			}
106		}
107	}
108	return response, nil
109}
110
111// Fields must match go list;
112// see $GOROOT/src/cmd/go/internal/load/pkg.go.
113type jsonPackage struct {
114	ImportPath      string
115	Dir             string
116	Name            string
117	Export          string
118	GoFiles         []string
119	CompiledGoFiles []string
120	CFiles          []string
121	CgoFiles        []string
122	CXXFiles        []string
123	MFiles          []string
124	HFiles          []string
125	FFiles          []string
126	SFiles          []string
127	SwigFiles       []string
128	SwigCXXFiles    []string
129	SysoFiles       []string
130	Imports         []string
131	ImportMap       map[string]string
132	Deps            []string
133	TestGoFiles     []string
134	TestImports     []string
135	XTestGoFiles    []string
136	XTestImports    []string
137	ForTest         string // q in a "p [q.test]" package, else ""
138	DepOnly         bool
139}
140
141func otherFiles(p *jsonPackage) [][]string {
142	return [][]string{p.CFiles, p.CXXFiles, p.MFiles, p.HFiles, p.FFiles, p.SFiles, p.SwigFiles, p.SwigCXXFiles, p.SysoFiles}
143}
144
145// golistDriverCurrent uses the "go list" command to expand the
146// pattern words and return metadata for the specified packages.
147// dir may be "" and env may be nil, as per os/exec.Command.
148func golistDriverCurrent(cfg *Config, words ...string) (*driverResponse, error) {
149	// go list uses the following identifiers in ImportPath and Imports:
150	//
151	// 	"p"			-- importable package or main (command)
152	//      "q.test"		-- q's test executable
153	// 	"p [q.test]"		-- variant of p as built for q's test executable
154	//	"q_test [q.test]"	-- q's external test package
155	//
156	// The packages p that are built differently for a test q.test
157	// are q itself, plus any helpers used by the external test q_test,
158	// typically including "testing" and all its dependencies.
159
160	// Run "go list" for complete
161	// information on the specified packages.
162	buf, err := golist(cfg, golistargs(cfg, words))
163	if err != nil {
164		return nil, err
165	}
166	// Decode the JSON and convert it to Package form.
167	var response driverResponse
168	for dec := json.NewDecoder(buf); dec.More(); {
169		p := new(jsonPackage)
170		if err := dec.Decode(p); err != nil {
171			return nil, fmt.Errorf("JSON decoding failed: %v", err)
172		}
173
174		// Bad package?
175		if p.Name == "" {
176			// This could be due to:
177			// - no such package
178			// - package directory contains no Go source files
179			// - all package declarations are mangled
180			// - and possibly other things.
181			//
182			// For now, we throw it away and let later
183			// stages rediscover the problem, but this
184			// discards the error message computed by go list
185			// and computes a new one---by different logic:
186			// if only one of the package declarations is
187			// bad, for example, should we report an error
188			// in Metadata mode?
189			// Unless we parse and typecheck, we might not
190			// notice there's a problem.
191			//
192			// Perhaps we should save a map of PackageID to
193			// errors for such cases.
194			continue
195		}
196
197		id := p.ImportPath
198
199		// Extract the PkgPath from the package's ID.
200		pkgpath := id
201		if i := strings.IndexByte(id, ' '); i >= 0 {
202			pkgpath = id[:i]
203		}
204
205		if pkgpath == "unsafe" {
206			p.GoFiles = nil // ignore fake unsafe.go file
207		}
208
209		// Assume go list emits only absolute paths for Dir.
210		if !filepath.IsAbs(p.Dir) {
211			log.Fatalf("internal error: go list returned non-absolute Package.Dir: %s", p.Dir)
212		}
213
214		export := p.Export
215		if export != "" && !filepath.IsAbs(export) {
216			export = filepath.Join(p.Dir, export)
217		}
218
219		// imports
220		//
221		// Imports contains the IDs of all imported packages.
222		// ImportsMap records (path, ID) only where they differ.
223		ids := make(map[string]bool)
224		for _, id := range p.Imports {
225			ids[id] = true
226		}
227		imports := make(map[string]*Package)
228		for path, id := range p.ImportMap {
229			imports[path] = &Package{ID: id} // non-identity import
230			delete(ids, id)
231		}
232		for id := range ids {
233			if id == "C" {
234				continue
235			}
236
237			imports[id] = &Package{ID: id} // identity import
238		}
239		if !p.DepOnly {
240			response.Roots = append(response.Roots, id)
241		}
242		pkg := &Package{
243			ID:              id,
244			Name:            p.Name,
245			PkgPath:         pkgpath,
246			GoFiles:         absJoin(p.Dir, p.GoFiles, p.CgoFiles),
247			CompiledGoFiles: absJoin(p.Dir, p.CompiledGoFiles),
248			OtherFiles:      absJoin(p.Dir, otherFiles(p)...),
249			Imports:         imports,
250			ExportFile:      export,
251		}
252		// TODO(matloob): Temporary hack since CompiledGoFiles isn't always set.
253		if len(pkg.CompiledGoFiles) == 0 {
254			pkg.CompiledGoFiles = pkg.GoFiles
255		}
256		response.Packages = append(response.Packages, pkg)
257	}
258
259	return &response, nil
260}
261
262// absJoin absolutizes and flattens the lists of files.
263func absJoin(dir string, fileses ...[]string) (res []string) {
264	for _, files := range fileses {
265		for _, file := range files {
266			if !filepath.IsAbs(file) {
267				file = filepath.Join(dir, file)
268			}
269			res = append(res, file)
270		}
271	}
272	return res
273}
274
275func golistargs(cfg *Config, words []string) []string {
276	fullargs := []string{
277		"list", "-e", "-json", "-compiled",
278		fmt.Sprintf("-test=%t", cfg.Tests),
279		fmt.Sprintf("-export=%t", usesExportData(cfg)),
280		fmt.Sprintf("-deps=%t", cfg.Mode >= LoadImports),
281	}
282	fullargs = append(fullargs, cfg.Flags...)
283	fullargs = append(fullargs, "--")
284	fullargs = append(fullargs, words...)
285	return fullargs
286}
287
288// golist returns the JSON-encoded result of a "go list args..." query.
289func golist(cfg *Config, args []string) (*bytes.Buffer, error) {
290	out := new(bytes.Buffer)
291	cmd := exec.CommandContext(cfg.Context, "go", args...)
292	cmd.Env = cfg.Env
293	cmd.Dir = cfg.Dir
294	cmd.Stdout = out
295	cmd.Stderr = new(bytes.Buffer)
296	if err := cmd.Run(); err != nil {
297		exitErr, ok := err.(*exec.ExitError)
298		if !ok {
299			// Catastrophic error:
300			// - executable not found
301			// - context cancellation
302			return nil, fmt.Errorf("couldn't exec 'go list': %s %T", err, err)
303		}
304
305		// Old go list?
306		if strings.Contains(fmt.Sprint(cmd.Stderr), "flag provided but not defined") {
307			return nil, goTooOldError{fmt.Errorf("unsupported version of go list: %s: %s", exitErr, cmd.Stderr)}
308		}
309
310		// Export mode entails a build.
311		// If that build fails, errors appear on stderr
312		// (despite the -e flag) and the Export field is blank.
313		// Do not fail in that case.
314		if !usesExportData(cfg) {
315			return nil, fmt.Errorf("go list: %s: %s", exitErr, cmd.Stderr)
316		}
317	}
318
319	// Print standard error output from "go list".
320	// Due to the -e flag, this should be empty.
321	// However, in -export mode it contains build errors.
322	// Should go list save build errors in the Package.Error JSON field?
323	// See https://github.com/golang/go/issues/26319.
324	// If so, then we should continue to print stderr as go list
325	// will be silent unless something unexpected happened.
326	// If not, perhaps we should suppress it to reduce noise.
327	if stderr := fmt.Sprint(cmd.Stderr); stderr != "" {
328		fmt.Fprintf(os.Stderr, "go list stderr <<%s>>\n", stderr)
329	}
330
331	// debugging
332	if false {
333		fmt.Fprintln(os.Stderr, out)
334	}
335
336	return out, nil
337}