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 }