github.com/kekek/gb@v0.4.5-0.20170222120241-d4ba64b0b297/context.go (about)

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