github.com/hugelgupf/u-root@v0.0.0-20191023214958-4807c632154c/pkg/golang/build.go (about)

     1  // Copyright 2015-2018 the u-root 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 golang is an API to the Go compiler.
     6  package golang
     7  
     8  import (
     9  	"bytes"
    10  	"encoding/json"
    11  	"fmt"
    12  	"go/build"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"strings"
    17  	"time"
    18  )
    19  
    20  type Environ struct {
    21  	build.Context
    22  
    23  	GO111MODULE string
    24  }
    25  
    26  // Default is the default build environment comprised of the default GOPATH,
    27  // GOROOT, GOOS, GOARCH, and CGO_ENABLED values.
    28  func Default() Environ {
    29  	return Environ{
    30  		Context:     build.Default,
    31  		GO111MODULE: os.Getenv("GO111MODULE"),
    32  	}
    33  }
    34  
    35  // Package matches a subset of the JSON output of the `go list -json`
    36  // command.
    37  //
    38  // See `go help list` for the full structure.
    39  //
    40  // This currently contains an incomplete list of dependencies.
    41  type Package struct {
    42  	Name       string
    43  	Dir        string
    44  	Deps       []string
    45  	GoFiles    []string
    46  	SFiles     []string
    47  	HFiles     []string
    48  	Goroot     bool
    49  	Root       string
    50  	Module     *Module
    51  	ImportPath string
    52  }
    53  
    54  type Module struct {
    55  	Path      string       // module path
    56  	Version   string       // module version
    57  	Versions  []string     // available module versions (with -versions)
    58  	Replace   *Module      // replaced by this module
    59  	Time      *time.Time   // time version was created
    60  	Update    *Module      // available update, if any (with -u)
    61  	Main      bool         // is this the main module?
    62  	Indirect  bool         // is this module only an indirect dependency of main module?
    63  	Dir       string       // directory holding files for this module, if any
    64  	GoMod     string       // path to go.mod file for this module, if any
    65  	GoVersion string       // go version used in module
    66  	Error     *ModuleError // error loading module
    67  }
    68  
    69  type ModuleError struct {
    70  	Err string
    71  }
    72  
    73  func (c Environ) goCmd(args ...string) *exec.Cmd {
    74  	cmd := exec.Command(filepath.Join(c.GOROOT, "bin", "go"), args...)
    75  	cmd.Env = append(os.Environ(), c.Env()...)
    76  	return cmd
    77  }
    78  
    79  // Version returns the Go version string that runtime.Version would return for
    80  // the Go compiler in this environ.
    81  func (c Environ) Version() (string, error) {
    82  	cmd := c.goCmd("version")
    83  	v, err := cmd.CombinedOutput()
    84  	if err != nil {
    85  		return "", err
    86  	}
    87  	s := strings.Fields(string(v))
    88  	if len(s) < 3 {
    89  		return "", fmt.Errorf("unknown go version, tool returned weird output for 'go version': %v", string(v))
    90  	}
    91  	return s[2], nil
    92  }
    93  
    94  // Find lists all dependencies of the package given by `importPath`.
    95  func (c Environ) Find(pattern string) ([]*Package, error) {
    96  	// The output of this is almost the same as build.Import, except for
    97  	// the dependencies.
    98  	cmd := c.goCmd("list", "-json", pattern)
    99  	stdout := new(bytes.Buffer)
   100  	stderr := new(bytes.Buffer)
   101  	cmd.Stdout = stdout
   102  	cmd.Stderr = stderr
   103  	if err := cmd.Run(); err != nil {
   104  		return nil, fmt.Errorf("go list -json %q: %v", pattern, stderr.String())
   105  	}
   106  
   107  	var ps []*Package
   108  	for dec := json.NewDecoder(stdout); dec.More(); {
   109  		var p Package
   110  		if err := dec.Decode(&p); err != nil {
   111  			return nil, fmt.Errorf("json unmarshal of go list -json %q: %v", pattern, err)
   112  		}
   113  		ps = append(ps, &p)
   114  	}
   115  	return ps, nil
   116  }
   117  
   118  func (c Environ) FindCmds(pattern string) ([]*Package, error) {
   119  	ps, err := c.Find(pattern)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	var cmds []*Package
   124  	for _, p := range ps {
   125  		if p.Name == "main" {
   126  			cmds = append(cmds, p)
   127  		}
   128  	}
   129  	if len(cmds) == 0 {
   130  		return nil, fmt.Errorf("pattern %q did not find commands, only packages", pattern)
   131  	}
   132  	return cmds, nil
   133  }
   134  
   135  func (c Environ) FindOne(pattern string) (*Package, error) {
   136  	ps, err := c.Find(pattern)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	if len(ps) != 1 {
   141  		return nil, fmt.Errorf("pattern %q returned %d packages, wanted one", pattern, len(ps))
   142  	}
   143  	return ps[0], nil
   144  }
   145  
   146  func (c Environ) FindOneCmd(pattern string) (*Package, error) {
   147  	ps, err := c.FindCmds(pattern)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  	if len(ps) != 1 {
   152  		return nil, fmt.Errorf("pattern %q returned %d packages, wanted one", pattern, len(ps))
   153  	}
   154  	return ps[0], nil
   155  }
   156  
   157  // Env returns all environment variables for invoking a Go command.
   158  func (c Environ) Env() []string {
   159  	var env []string
   160  	if c.GOARCH != "" {
   161  		env = append(env, fmt.Sprintf("GOARCH=%s", c.GOARCH))
   162  	}
   163  	if c.GOOS != "" {
   164  		env = append(env, fmt.Sprintf("GOOS=%s", c.GOOS))
   165  	}
   166  	if c.GOPATH != "" {
   167  		env = append(env, fmt.Sprintf("GOPATH=%s", c.GOPATH))
   168  	}
   169  	var cgo int8
   170  	if c.CgoEnabled {
   171  		cgo = 1
   172  	}
   173  	env = append(env, fmt.Sprintf("CGO_ENABLED=%d", cgo))
   174  	env = append(env, fmt.Sprintf("GO111MODULE=%s", c.GO111MODULE))
   175  
   176  	if c.GOROOT != "" {
   177  		env = append(env, fmt.Sprintf("GOROOT=%s", c.GOROOT))
   178  
   179  		// If GOROOT is set to a different version of Go, we must
   180  		// ensure that $GOROOT/bin is also in path to make the "go"
   181  		// binary available to golang.org/x/tools/packages.
   182  		env = append(env, fmt.Sprintf("PATH=%s:%s", filepath.Join(c.GOROOT, "bin"), os.Getenv("PATH")))
   183  	}
   184  	return env
   185  }
   186  
   187  // String returns all environment variables for Go invocations.
   188  func (c Environ) String() string {
   189  	return strings.Join(c.Env(), " ")
   190  }
   191  
   192  // Build compiles the package given by `importPath`, writing the build object
   193  // to `binaryPath`.
   194  func (c Environ) Build(importPath string, binaryPath string) error {
   195  	p, err := c.FindOneCmd(importPath)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	return c.BuildDir(p.Dir, binaryPath)
   200  }
   201  
   202  // BuildDir compiles the package in the directory `dirPath`, writing the build
   203  // object to `binaryPath`.
   204  func (c Environ) BuildDir(dirPath string, binaryPath string) error {
   205  	args := []string{
   206  		"build",
   207  
   208  		// Force rebuilding of packages.
   209  		"-a",
   210  
   211  		// Strip all symbols, and don't embed a Go build ID to be reproducible.
   212  		"-ldflags", "-s -w -buildid=",
   213  
   214  		"-o", binaryPath,
   215  		"-installsuffix", "uroot",
   216  	}
   217  
   218  	v, err := c.Version()
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	// Reproducible builds: Trim any GOPATHs out of the executable's
   224  	// debugging information.
   225  	//
   226  	// E.g. Trim /tmp/bb-*/ from /tmp/bb-12345567/src/github.com/...
   227  	if strings.Contains(v, "go1.13") || strings.Contains(v, "go1.14") || strings.Contains(v, "gotip") {
   228  		args = append(args, "-trimpath")
   229  	} else {
   230  		args = append(args, "-gcflags", fmt.Sprintf("-trimpath=%s", c.GOPATH))
   231  		args = append(args, "-asmflags", fmt.Sprintf("-trimpath=%s", c.GOPATH))
   232  	}
   233  
   234  	if len(c.BuildTags) > 0 {
   235  		args = append(args, []string{"-tags", strings.Join(c.BuildTags, " ")}...)
   236  	}
   237  	// We always set the working directory, so this is always '.'.
   238  	args = append(args, ".")
   239  
   240  	cmd := c.goCmd(args...)
   241  	cmd.Dir = dirPath
   242  
   243  	if o, err := cmd.CombinedOutput(); err != nil {
   244  		return fmt.Errorf("error building go package in %q: %v, %v", dirPath, string(o), err)
   245  	}
   246  	return nil
   247  }