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}