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 }