github.com/gsquire/gb@v0.4.4-0.20161112235727-3982dc872064/context.go (about)

     1  package gb
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"runtime"
    11  	"sort"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/constabulary/gb/internal/debug"
    17  	"github.com/constabulary/gb/internal/importer"
    18  	"github.com/pkg/errors"
    19  )
    20  
    21  // Importer resolves package import paths to *importer.Packages.
    22  type Importer interface {
    23  
    24  	// Import attempts to resolve the package import path, path,
    25  	// to an *importer.Package.
    26  	Import(path string) (*importer.Package, error)
    27  }
    28  
    29  // Context represents an execution of one or more Targets inside a Project.
    30  type Context struct {
    31  	Project
    32  
    33  	importer Importer
    34  
    35  	pkgs map[string]*Package // map of package paths to resolved packages
    36  
    37  	workdir string
    38  
    39  	tc Toolchain
    40  
    41  	gohostos, gohostarch     string // GOOS and GOARCH for this host
    42  	gotargetos, gotargetarch string // GOOS and GOARCH for the target
    43  
    44  	Statistics
    45  
    46  	Force   bool // force rebuild of packages
    47  	Install bool // copy packages into $PROJECT/pkg
    48  	Verbose bool // verbose output
    49  	Nope    bool // command specfic flag, under test it skips the execute action.
    50  	race    bool // race detector requested
    51  
    52  	gcflags []string // flags passed to the compiler
    53  	ldflags []string // flags passed to the linker
    54  
    55  	linkmode, buildmode string // link and build modes
    56  
    57  	buildtags []string // build tags
    58  }
    59  
    60  // GOOS configures the Context to use goos as the target os.
    61  func GOOS(goos string) func(*Context) error {
    62  	return func(c *Context) error {
    63  		if goos == "" {
    64  			return fmt.Errorf("GOOS cannot be blank")
    65  		}
    66  		c.gotargetos = goos
    67  		return nil
    68  	}
    69  }
    70  
    71  // GOARCH configures the Context to use goarch as the target arch.
    72  func GOARCH(goarch string) func(*Context) error {
    73  	return func(c *Context) error {
    74  		if goarch == "" {
    75  			return fmt.Errorf("GOARCH cannot be blank")
    76  		}
    77  		c.gotargetarch = goarch
    78  		return nil
    79  	}
    80  }
    81  
    82  // Tags configured the context to use these additional build tags
    83  func Tags(tags ...string) func(*Context) error {
    84  	return func(c *Context) error {
    85  		c.buildtags = append(c.buildtags, tags...)
    86  		return nil
    87  	}
    88  }
    89  
    90  // Gcflags appends flags to the list passed to the compiler.
    91  func Gcflags(flags ...string) func(*Context) error {
    92  	return func(c *Context) error {
    93  		c.gcflags = append(c.gcflags, flags...)
    94  		return nil
    95  	}
    96  }
    97  
    98  // Ldflags appends flags to the list passed to the linker.
    99  func Ldflags(flags ...string) func(*Context) error {
   100  	return func(c *Context) error {
   101  		c.ldflags = append(c.ldflags, flags...)
   102  		return nil
   103  	}
   104  }
   105  
   106  // WithRace enables the race detector and adds the tag "race" to
   107  // the Context build tags.
   108  func WithRace(c *Context) error {
   109  	c.race = true
   110  	Tags("race")(c)
   111  	Gcflags("-race")(c)
   112  	Ldflags("-race")(c)
   113  	return nil
   114  }
   115  
   116  // NewContext returns a new build context from this project.
   117  // By default this context will use the gc toolchain with the
   118  // host's GOOS and GOARCH values.
   119  func NewContext(p Project, opts ...func(*Context) error) (*Context, error) {
   120  	envOr := func(key, def string) string {
   121  		if v := os.Getenv(key); v != "" {
   122  			return v
   123  		} else {
   124  			return def
   125  		}
   126  	}
   127  
   128  	defaults := []func(*Context) error{
   129  		// must come before GcToolchain()
   130  		func(c *Context) error {
   131  			c.gohostos = runtime.GOOS
   132  			c.gohostarch = runtime.GOARCH
   133  			c.gotargetos = envOr("GOOS", runtime.GOOS)
   134  			c.gotargetarch = envOr("GOARCH", runtime.GOARCH)
   135  			return nil
   136  		},
   137  		GcToolchain(),
   138  	}
   139  	workdir, err := ioutil.TempDir("", "gb")
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	ctx := Context{
   145  		Project:   p,
   146  		workdir:   workdir,
   147  		buildmode: "exe",
   148  		pkgs:      make(map[string]*Package),
   149  	}
   150  
   151  	for _, opt := range append(defaults, opts...) {
   152  		err := opt(&ctx)
   153  		if err != nil {
   154  			return nil, err
   155  		}
   156  	}
   157  
   158  	// sort build tags to ensure the ctxSring and Suffix is stable
   159  	sort.Strings(ctx.buildtags)
   160  
   161  	ic := importer.Context{
   162  		GOOS:        ctx.gotargetos,
   163  		GOARCH:      ctx.gotargetarch,
   164  		CgoEnabled:  cgoEnabled(ctx.gohostos, ctx.gohostarch, ctx.gotargetos, ctx.gotargetarch),
   165  		ReleaseTags: releaseTags, // from go/build, see gb.go
   166  		BuildTags:   ctx.buildtags,
   167  	}
   168  
   169  	i, err := buildImporter(&ic, &ctx)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	ctx.importer = i
   175  
   176  	// C and unsafe are fake packages synthesised by the compiler.
   177  	// Insert fake packages into the package cache.
   178  	for _, name := range []string{"C", "unsafe"} {
   179  		pkg, err := newPackage(&ctx, &importer.Package{
   180  			Name:       name,
   181  			ImportPath: name,
   182  			Standard:   true,
   183  			Dir:        name, // fake, but helps diagnostics
   184  		})
   185  		if err != nil {
   186  			return nil, err
   187  		}
   188  		pkg.Stale = false
   189  		ctx.pkgs[pkg.ImportPath] = pkg
   190  	}
   191  
   192  	return &ctx, err
   193  }
   194  
   195  // IncludePaths returns the include paths visible in this context.
   196  func (c *Context) IncludePaths() []string {
   197  	return []string{
   198  		c.workdir,
   199  		c.Pkgdir(),
   200  	}
   201  }
   202  
   203  // NewPackage creates a resolved Package for p.
   204  func (c *Context) NewPackage(p *importer.Package) (*Package, error) {
   205  	pkg, err := newPackage(c, p)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  	pkg.Stale = isStale(pkg)
   210  	return pkg, nil
   211  }
   212  
   213  // Pkgdir returns the path to precompiled packages.
   214  func (c *Context) Pkgdir() string {
   215  	return filepath.Join(c.Project.Pkgdir(), c.ctxString())
   216  }
   217  
   218  // Suffix returns the suffix (if any) for binaries produced
   219  // by this context.
   220  func (c *Context) Suffix() string {
   221  	suffix := c.ctxString()
   222  	if suffix != "" {
   223  		suffix = "-" + suffix
   224  	}
   225  	return suffix
   226  }
   227  
   228  // Workdir returns the path to this Context's working directory.
   229  func (c *Context) Workdir() string { return c.workdir }
   230  
   231  // ResolvePackage resolves the package at path using the current context.
   232  func (c *Context) ResolvePackage(path string) (*Package, error) {
   233  	if path == "." {
   234  		return nil, errors.Errorf("%q is not a package", filepath.Join(c.Projectdir(), "src"))
   235  	}
   236  	path, err := relImportPath(filepath.Join(c.Projectdir(), "src"), path)
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  	if path == "." || path == ".." || strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../") {
   241  		return nil, errors.Errorf("import %q: relative import not supported", path)
   242  	}
   243  	return c.loadPackage(nil, path)
   244  }
   245  
   246  // loadPackage recursively resolves path as a package. If successful loadPackage
   247  // records the package in the Context's internal package cache.
   248  func (c *Context) loadPackage(stack []string, path string) (*Package, error) {
   249  	if pkg, ok := c.pkgs[path]; ok {
   250  		// already loaded, just return
   251  		return pkg, nil
   252  	}
   253  
   254  	p, err := c.importer.Import(path)
   255  	if err != nil {
   256  		return nil, err
   257  	}
   258  
   259  	stack = append(stack, p.ImportPath)
   260  	var stale bool
   261  	for i, im := range p.Imports {
   262  		for _, p := range stack {
   263  			if p == im {
   264  				return nil, fmt.Errorf("import cycle detected: %s", strings.Join(append(stack, im), " -> "))
   265  			}
   266  		}
   267  		pkg, err := c.loadPackage(stack, im)
   268  		if err != nil {
   269  			return nil, err
   270  		}
   271  
   272  		// update the import path as the import may have been discovered via vendoring.
   273  		p.Imports[i] = pkg.ImportPath
   274  		stale = stale || pkg.Stale
   275  	}
   276  
   277  	pkg, err := newPackage(c, p)
   278  	if err != nil {
   279  		return nil, errors.Wrapf(err, "loadPackage(%q)", path)
   280  	}
   281  	pkg.Stale = stale || isStale(pkg)
   282  	c.pkgs[p.ImportPath] = pkg
   283  	return pkg, nil
   284  }
   285  
   286  // Destroy removes the temporary working files of this context.
   287  func (c *Context) Destroy() error {
   288  	debug.Debugf("removing work directory: %v", c.workdir)
   289  	return os.RemoveAll(c.workdir)
   290  }
   291  
   292  // ctxString returns a string representation of the unique properties
   293  // of the context.
   294  func (c *Context) ctxString() string {
   295  	v := []string{
   296  		c.gotargetos,
   297  		c.gotargetarch,
   298  	}
   299  	v = append(v, c.buildtags...)
   300  	return strings.Join(v, "-")
   301  }
   302  
   303  func runOut(output io.Writer, dir string, env []string, command string, args ...string) error {
   304  	cmd := exec.Command(command, args...)
   305  	cmd.Dir = dir
   306  	cmd.Stdout = output
   307  	cmd.Stderr = os.Stderr
   308  	cmd.Env = mergeEnvLists(env, envForDir(cmd.Dir))
   309  	debug.Debugf("cd %s; %s", cmd.Dir, cmd.Args)
   310  	err := cmd.Run()
   311  	return err
   312  }
   313  
   314  // Statistics records the various Durations
   315  type Statistics struct {
   316  	sync.Mutex
   317  	stats map[string]time.Duration
   318  }
   319  
   320  func (s *Statistics) Record(name string, d time.Duration) {
   321  	s.Lock()
   322  	defer s.Unlock()
   323  	if s.stats == nil {
   324  		s.stats = make(map[string]time.Duration)
   325  	}
   326  	s.stats[name] += d
   327  }
   328  
   329  func (s *Statistics) Total() time.Duration {
   330  	s.Lock()
   331  	defer s.Unlock()
   332  	var d time.Duration
   333  	for _, v := range s.stats {
   334  		d += v
   335  	}
   336  	return d
   337  }
   338  
   339  func (s *Statistics) String() string {
   340  	s.Lock()
   341  	defer s.Unlock()
   342  	return fmt.Sprintf("%v", s.stats)
   343  }
   344  
   345  func (c *Context) isCrossCompile() bool {
   346  	return c.gohostos != c.gotargetos || c.gohostarch != c.gotargetarch
   347  }
   348  
   349  // envForDir returns a copy of the environment
   350  // suitable for running in the given directory.
   351  // The environment is the current process's environment
   352  // but with an updated $PWD, so that an os.Getwd in the
   353  // child will be faster.
   354  func envForDir(dir string) []string {
   355  	env := os.Environ()
   356  	// Internally we only use rooted paths, so dir is rooted.
   357  	// Even if dir is not rooted, no harm done.
   358  	return mergeEnvLists([]string{"PWD=" + dir}, env)
   359  }
   360  
   361  // mergeEnvLists merges the two environment lists such that
   362  // variables with the same name in "in" replace those in "out".
   363  func mergeEnvLists(in, out []string) []string {
   364  NextVar:
   365  	for _, inkv := range in {
   366  		k := strings.SplitAfterN(inkv, "=", 2)[0]
   367  		for i, outkv := range out {
   368  			if strings.HasPrefix(outkv, k) {
   369  				out[i] = inkv
   370  				continue NextVar
   371  			}
   372  		}
   373  		out = append(out, inkv)
   374  	}
   375  	return out
   376  }
   377  
   378  func cgoEnabled(gohostos, gohostarch, gotargetos, gotargetarch string) bool {
   379  	switch os.Getenv("CGO_ENABLED") {
   380  	case "1":
   381  		return true
   382  	case "0":
   383  		return false
   384  	default:
   385  		// cgo must be explicitly enabled for cross compilation builds
   386  		if gohostos == gotargetos && gohostarch == gotargetarch {
   387  			switch gotargetos + "/" + gotargetarch {
   388  			case "darwin/386", "darwin/amd64", "darwin/arm", "darwin/arm64":
   389  				return true
   390  			case "dragonfly/amd64":
   391  				return true
   392  			case "freebsd/386", "freebsd/amd64", "freebsd/arm":
   393  				return true
   394  			case "linux/386", "linux/amd64", "linux/arm", "linux/arm64", "linux/ppc64le":
   395  				return true
   396  			case "android/386", "android/amd64", "android/arm":
   397  				return true
   398  			case "netbsd/386", "netbsd/amd64", "netbsd/arm":
   399  				return true
   400  			case "openbsd/386", "openbsd/amd64":
   401  				return true
   402  			case "solaris/amd64":
   403  				return true
   404  			case "windows/386", "windows/amd64":
   405  				return true
   406  			default:
   407  				return false
   408  			}
   409  		}
   410  		return false
   411  	}
   412  }
   413  
   414  func buildImporter(ic *importer.Context, ctx *Context) (Importer, error) {
   415  	i, err := addDepfileDeps(ic, ctx)
   416  	if err != nil {
   417  		return nil, err
   418  	}
   419  
   420  	// construct importer stack in reverse order, vendor at the bottom, GOROOT on the top.
   421  	i = &_importer{
   422  		Importer: i,
   423  		im: importer.Importer{
   424  			Context: ic,
   425  			Root:    filepath.Join(ctx.Projectdir(), "vendor"),
   426  		},
   427  	}
   428  
   429  	i = &srcImporter{
   430  		i,
   431  		importer.Importer{
   432  			Context: ic,
   433  			Root:    ctx.Projectdir(),
   434  		},
   435  	}
   436  
   437  	i = &_importer{
   438  		i,
   439  		importer.Importer{
   440  			Context: ic,
   441  			Root:    runtime.GOROOT(),
   442  		},
   443  	}
   444  
   445  	i = &fixupImporter{
   446  		Importer: i,
   447  	}
   448  
   449  	return i, nil
   450  }