external.go raw

   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  
   5  package packages
   6  
   7  // This file defines the protocol that enables an external "driver"
   8  // tool to supply package metadata in place of 'go list'.
   9  
  10  import (
  11  	"bytes"
  12  	"encoding/json"
  13  	"fmt"
  14  	"os"
  15  	"os/exec"
  16  	"slices"
  17  	"strings"
  18  )
  19  
  20  // DriverRequest defines the schema of a request for package metadata
  21  // from an external driver program. The JSON-encoded DriverRequest
  22  // message is provided to the driver program's standard input. The
  23  // query patterns are provided as command-line arguments.
  24  //
  25  // See the package documentation for an overview.
  26  type DriverRequest struct {
  27  	Mode LoadMode `json:"mode"`
  28  
  29  	// Env specifies the environment the underlying build system should be run in.
  30  	Env []string `json:"env"`
  31  
  32  	// BuildFlags are flags that should be passed to the underlying build system.
  33  	BuildFlags []string `json:"build_flags"`
  34  
  35  	// Tests specifies whether the patterns should also return test packages.
  36  	Tests bool `json:"tests"`
  37  
  38  	// Overlay maps file paths (relative to the driver's working directory)
  39  	// to the contents of overlay files (see Config.Overlay).
  40  	Overlay map[string][]byte `json:"overlay"`
  41  }
  42  
  43  // DriverResponse defines the schema of a response from an external
  44  // driver program, providing the results of a query for package
  45  // metadata. The driver program must write a JSON-encoded
  46  // DriverResponse message to its standard output.
  47  //
  48  // See the package documentation for an overview.
  49  type DriverResponse struct {
  50  	// NotHandled is returned if the request can't be handled by the current
  51  	// driver. If an external driver returns a response with NotHandled, the
  52  	// rest of the DriverResponse is ignored, and go/packages will fallback
  53  	// to the next driver. If go/packages is extended in the future to support
  54  	// lists of multiple drivers, go/packages will fall back to the next driver.
  55  	NotHandled bool
  56  
  57  	// Compiler and Arch are the arguments pass of types.SizesFor
  58  	// to get a types.Sizes to use when type checking.
  59  	Compiler string
  60  	Arch     string
  61  
  62  	// Roots is the set of package IDs that make up the root packages.
  63  	// We have to encode this separately because when we encode a single package
  64  	// we cannot know if it is one of the roots as that requires knowledge of the
  65  	// graph it is part of.
  66  	Roots []string `json:",omitempty"`
  67  
  68  	// Packages is the full set of packages in the graph.
  69  	// The packages are not connected into a graph.
  70  	// The Imports if populated will be stubs that only have their ID set.
  71  	// Imports will be connected and then type and syntax information added in a
  72  	// later pass (see refine).
  73  	Packages []*Package
  74  
  75  	// GoVersion is the minor version number used by the driver
  76  	// (e.g. the go command on the PATH) when selecting .go files.
  77  	// Zero means unknown.
  78  	GoVersion int
  79  }
  80  
  81  // driver is the type for functions that query the build system for the
  82  // packages named by the patterns.
  83  type driver func(cfg *Config, patterns []string) (*DriverResponse, error)
  84  
  85  // findExternalDriver returns the file path of a tool that supplies
  86  // the build system package structure, or "" if not found.
  87  // If GOPACKAGESDRIVER is set in the environment findExternalTool returns its
  88  // value, otherwise it searches for a binary named gopackagesdriver on the PATH.
  89  func findExternalDriver(cfg *Config) driver {
  90  	const toolPrefix = "GOPACKAGESDRIVER="
  91  	tool := ""
  92  	for _, env := range cfg.Env {
  93  		if val, ok := strings.CutPrefix(env, toolPrefix); ok {
  94  			tool = val
  95  		}
  96  	}
  97  	if tool != "" && tool == "off" {
  98  		return nil
  99  	}
 100  	if tool == "" {
 101  		var err error
 102  		tool, err = exec.LookPath("gopackagesdriver")
 103  		if err != nil {
 104  			return nil
 105  		}
 106  	}
 107  	return func(cfg *Config, patterns []string) (*DriverResponse, error) {
 108  		req, err := json.Marshal(DriverRequest{
 109  			Mode:       cfg.Mode,
 110  			Env:        cfg.Env,
 111  			BuildFlags: cfg.BuildFlags,
 112  			Tests:      cfg.Tests,
 113  			Overlay:    cfg.Overlay,
 114  		})
 115  		if err != nil {
 116  			return nil, fmt.Errorf("failed to encode message to driver tool: %v", err)
 117  		}
 118  
 119  		buf := new(bytes.Buffer)
 120  		stderr := new(bytes.Buffer)
 121  		cmd := exec.CommandContext(cfg.Context, tool, patterns...)
 122  		cmd.Dir = cfg.Dir
 123  		// The cwd gets resolved to the real path. On Darwin, where
 124  		// /tmp is a symlink, this breaks anything that expects the
 125  		// working directory to keep the original path, including the
 126  		// go command when dealing with modules.
 127  		//
 128  		// os.Getwd stdlib has a special feature where if the
 129  		// cwd and the PWD are the same node then it trusts
 130  		// the PWD, so by setting it in the env for the child
 131  		// process we fix up all the paths returned by the go
 132  		// command.
 133  		//
 134  		// (See similar trick in Invocation.run in ../../internal/gocommand/invoke.go)
 135  		cmd.Env = append(slices.Clip(cfg.Env), "PWD="+cfg.Dir)
 136  		cmd.Stdin = bytes.NewReader(req)
 137  		cmd.Stdout = buf
 138  		cmd.Stderr = stderr
 139  
 140  		if err := cmd.Run(); err != nil {
 141  			return nil, fmt.Errorf("%v: %v: %s", tool, err, cmd.Stderr)
 142  		}
 143  		if len(stderr.Bytes()) != 0 && os.Getenv("GOPACKAGESPRINTDRIVERERRORS") != "" {
 144  			fmt.Fprintf(os.Stderr, "%s stderr: <<%s>>\n", cmdDebugStr(cmd), stderr)
 145  		}
 146  
 147  		var response DriverResponse
 148  		if err := json.Unmarshal(buf.Bytes(), &response); err != nil {
 149  			return nil, err
 150  		}
 151  		return &response, nil
 152  	}
 153  }
 154