1// Copyright 2017 Google Inc. All rights reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Microfactory is a tool to incrementally compile a go program. It's similar 16// to `go install`, but doesn't require a GOPATH. A package->path mapping can 17// be specified as command line options: 18// 19// -pkg-path android/soong=build/soong 20// -pkg-path github.com/google/blueprint=build/blueprint 21// 22// The paths can be relative to the current working directory, or an absolute 23// path. Both packages and paths are compared with full directory names, so the 24// android/soong-test package wouldn't be mapped in the above case. 25// 26// Microfactory will ignore *_test.go files, and limits *_darwin.go and 27// *_linux.go files to MacOS and Linux respectively. It does not support build 28// tags or any other suffixes. 29// 30// Builds are incremental by package. All input files are hashed, and if the 31// hash of an input or dependency changes, the package is rebuilt. 32// 33// It also exposes the -trimpath option from go's compiler so that embedded 34// path names (such as in log.Llongfile) are relative paths instead of absolute 35// paths. 36// 37// If you don't have a previously built version of Microfactory, when used with 38// -b <microfactory_bin_file>, Microfactory can rebuild itself as necessary. 39// Combined with a shell script like microfactory.bash that uses `go run` to 40// run Microfactory for the first time, go programs can be quickly bootstrapped 41// entirely from source (and a standard go distribution). 42package microfactory 43 44import ( 45 "bytes" 46 "crypto/sha1" 47 "flag" 48 "fmt" 49 "go/ast" 50 "go/build" 51 "go/parser" 52 "go/token" 53 "io" 54 "io/ioutil" 55 "os" 56 "os/exec" 57 "path/filepath" 58 "runtime" 59 "sort" 60 "strconv" 61 "strings" 62 "sync" 63 "syscall" 64 "time" 65) 66 67var ( 68 goToolDir = filepath.Join(runtime.GOROOT(), "pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH) 69 goVersion = findGoVersion() 70 isGo18 = strings.Contains(goVersion, "go1.8") 71) 72 73func findGoVersion() string { 74 if version, err := ioutil.ReadFile(filepath.Join(runtime.GOROOT(), "VERSION")); err == nil { 75 return string(version) 76 } 77 78 cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), "version") 79 if version, err := cmd.Output(); err == nil { 80 return string(version) 81 } else { 82 panic(fmt.Sprintf("Unable to discover go version: %v", err)) 83 } 84} 85 86type Config struct { 87 Race bool 88 Verbose bool 89 90 TrimPath string 91 92 TraceFunc func(name string) func() 93 94 pkgs []string 95 paths map[string]string 96} 97 98func (c *Config) Map(pkgPrefix, pathPrefix string) error { 99 if c.paths == nil { 100 c.paths = make(map[string]string) 101 } 102 if _, ok := c.paths[pkgPrefix]; ok { 103 return fmt.Errorf("Duplicate package prefix: %q", pkgPrefix) 104 } 105 106 c.pkgs = append(c.pkgs, pkgPrefix) 107 c.paths[pkgPrefix] = pathPrefix 108 109 return nil 110} 111 112// Path takes a package name, applies the path mappings and returns the resulting path. 113// 114// If the package isn't mapped, we'll return false to prevent compilation attempts. 115func (c *Config) Path(pkg string) (string, bool, error) { 116 if c == nil || c.paths == nil { 117 return "", false, fmt.Errorf("No package mappings") 118 } 119 120 for _, pkgPrefix := range c.pkgs { 121 if pkg == pkgPrefix { 122 return c.paths[pkgPrefix], true, nil 123 } else if strings.HasPrefix(pkg, pkgPrefix+"/") { 124 return filepath.Join(c.paths[pkgPrefix], strings.TrimPrefix(pkg, pkgPrefix+"/")), true, nil 125 } 126 } 127 128 return "", false, nil 129} 130 131func (c *Config) trace(format string, a ...interface{}) func() { 132 if c.TraceFunc == nil { 133 return func() {} 134 } 135 s := strings.TrimSpace(fmt.Sprintf(format, a...)) 136 return c.TraceFunc(s) 137} 138 139func un(f func()) { 140 f() 141} 142 143type GoPackage struct { 144 Name string 145 146 // Inputs 147 directDeps []*GoPackage // specified directly by the module 148 allDeps []*GoPackage // direct dependencies and transitive dependencies 149 files []string 150 151 // Outputs 152 pkgDir string 153 output string 154 hashResult []byte 155 156 // Status 157 mutex sync.Mutex 158 compiled bool 159 failed error 160 rebuilt bool 161} 162 163// LinkedHashMap<string, GoPackage> 164type linkedDepSet struct { 165 packageSet map[string](*GoPackage) 166 packageList []*GoPackage 167} 168 169func newDepSet() *linkedDepSet { 170 return &linkedDepSet{packageSet: make(map[string]*GoPackage)} 171} 172func (s *linkedDepSet) tryGetByName(name string) (*GoPackage, bool) { 173 pkg, contained := s.packageSet[name] 174 return pkg, contained 175} 176func (s *linkedDepSet) getByName(name string) *GoPackage { 177 pkg, _ := s.tryGetByName(name) 178 return pkg 179} 180func (s *linkedDepSet) add(name string, goPackage *GoPackage) { 181 s.packageSet[name] = goPackage 182 s.packageList = append(s.packageList, goPackage) 183} 184func (s *linkedDepSet) ignore(name string) { 185 s.packageSet[name] = nil 186} 187 188// FindDeps searches all applicable go files in `path`, parses all of them 189// for import dependencies that exist in pkgMap, then recursively does the 190// same for all of those dependencies. 191func (p *GoPackage) FindDeps(config *Config, path string) error { 192 defer un(config.trace("findDeps")) 193 194 depSet := newDepSet() 195 err := p.findDeps(config, path, depSet) 196 if err != nil { 197 return err 198 } 199 p.allDeps = depSet.packageList 200 return nil 201} 202 203// Roughly equivalent to go/build.Context.match 204func matchBuildTag(name string) bool { 205 if name == "" { 206 return false 207 } 208 if i := strings.Index(name, ","); i >= 0 { 209 ok1 := matchBuildTag(name[:i]) 210 ok2 := matchBuildTag(name[i+1:]) 211 return ok1 && ok2 212 } 213 if strings.HasPrefix(name, "!!") { 214 return false 215 } 216 if strings.HasPrefix(name, "!") { 217 return len(name) > 1 && !matchBuildTag(name[1:]) 218 } 219 220 if name == runtime.GOOS || name == runtime.GOARCH || name == "gc" { 221 return true 222 } 223 for _, tag := range build.Default.BuildTags { 224 if tag == name { 225 return true 226 } 227 } 228 for _, tag := range build.Default.ReleaseTags { 229 if tag == name { 230 return true 231 } 232 } 233 234 return false 235} 236 237func parseBuildComment(comment string) (matches, ok bool) { 238 if !strings.HasPrefix(comment, "//") { 239 return false, false 240 } 241 for i, c := range comment { 242 if i < 2 || c == ' ' || c == '\t' { 243 continue 244 } else if c == '+' { 245 f := strings.Fields(comment[i:]) 246 if f[0] == "+build" { 247 matches = false 248 for _, tok := range f[1:] { 249 matches = matches || matchBuildTag(tok) 250 } 251 return matches, true 252 } 253 } 254 break 255 } 256 return false, false 257} 258 259// findDeps is the recursive version of FindDeps. allPackages is the map of 260// all locally defined packages so that the same dependency of two different 261// packages is only resolved once. 262func (p *GoPackage) findDeps(config *Config, path string, allPackages *linkedDepSet) error { 263 // If this ever becomes too slow, we can look at reading the files once instead of twice 264 // But that just complicates things today, and we're already really fast. 265 foundPkgs, err := parser.ParseDir(token.NewFileSet(), path, func(fi os.FileInfo) bool { 266 name := fi.Name() 267 if fi.IsDir() || strings.HasSuffix(name, "_test.go") || name[0] == '.' || name[0] == '_' { 268 return false 269 } 270 if runtime.GOOS != "darwin" && strings.HasSuffix(name, "_darwin.go") { 271 return false 272 } 273 if runtime.GOOS != "linux" && strings.HasSuffix(name, "_linux.go") { 274 return false 275 } 276 return true 277 }, parser.ImportsOnly|parser.ParseComments) 278 if err != nil { 279 return fmt.Errorf("Error parsing directory %q: %v", path, err) 280 } 281 282 var foundPkg *ast.Package 283 // foundPkgs is a map[string]*ast.Package, but we only want one package 284 if len(foundPkgs) != 1 { 285 return fmt.Errorf("Expected one package in %q, got %d", path, len(foundPkgs)) 286 } 287 // Extract the first (and only) entry from the map. 288 for _, pkg := range foundPkgs { 289 foundPkg = pkg 290 } 291 292 var deps []string 293 localDeps := make(map[string]bool) 294 295 for filename, astFile := range foundPkg.Files { 296 ignore := false 297 for _, commentGroup := range astFile.Comments { 298 for _, comment := range commentGroup.List { 299 if matches, ok := parseBuildComment(comment.Text); ok && !matches { 300 ignore = true 301 } 302 } 303 } 304 if ignore { 305 continue 306 } 307 308 p.files = append(p.files, filename) 309 310 for _, importSpec := range astFile.Imports { 311 name, err := strconv.Unquote(importSpec.Path.Value) 312 if err != nil { 313 return fmt.Errorf("%s: invalid quoted string: <%s> %v", filename, importSpec.Path.Value, err) 314 } 315 316 if pkg, ok := allPackages.tryGetByName(name); ok { 317 if pkg != nil { 318 if _, ok := localDeps[name]; !ok { 319 deps = append(deps, name) 320 localDeps[name] = true 321 } 322 } 323 continue 324 } 325 326 var pkgPath string 327 if path, ok, err := config.Path(name); err != nil { 328 return err 329 } else if !ok { 330 // Probably in the stdlib, but if not, then the compiler will fail with a reasonable error message 331 // Mark it as such so that we don't try to decode its path again. 332 allPackages.ignore(name) 333 continue 334 } else { 335 pkgPath = path 336 } 337 338 pkg := &GoPackage{ 339 Name: name, 340 } 341 deps = append(deps, name) 342 allPackages.add(name, pkg) 343 localDeps[name] = true 344 345 if err := pkg.findDeps(config, pkgPath, allPackages); err != nil { 346 return err 347 } 348 } 349 } 350 351 sort.Strings(p.files) 352 353 if config.Verbose { 354 fmt.Fprintf(os.Stderr, "Package %q depends on %v\n", p.Name, deps) 355 } 356 357 sort.Strings(deps) 358 for _, dep := range deps { 359 p.directDeps = append(p.directDeps, allPackages.getByName(dep)) 360 } 361 362 return nil 363} 364 365func (p *GoPackage) Compile(config *Config, outDir string) error { 366 p.mutex.Lock() 367 defer p.mutex.Unlock() 368 if p.compiled { 369 return p.failed 370 } 371 p.compiled = true 372 373 // Build all dependencies in parallel, then fail if any of them failed. 374 var wg sync.WaitGroup 375 for _, dep := range p.directDeps { 376 wg.Add(1) 377 go func(dep *GoPackage) { 378 defer wg.Done() 379 dep.Compile(config, outDir) 380 }(dep) 381 } 382 wg.Wait() 383 for _, dep := range p.directDeps { 384 if dep.failed != nil { 385 p.failed = dep.failed 386 return p.failed 387 } 388 } 389 390 endTrace := config.trace("check compile %s", p.Name) 391 392 p.pkgDir = filepath.Join(outDir, strings.Replace(p.Name, "/", "-", -1)) 393 p.output = filepath.Join(p.pkgDir, p.Name) + ".a" 394 shaFile := p.output + ".hash" 395 396 hash := sha1.New() 397 fmt.Fprintln(hash, runtime.GOOS, runtime.GOARCH, goVersion) 398 399 cmd := exec.Command(filepath.Join(goToolDir, "compile"), 400 "-N", "-l", // Disable optimization and inlining so that debugging works better 401 "-o", p.output, 402 "-p", p.Name, 403 "-complete", "-pack", "-nolocalimports") 404 if !isGo18 && !config.Race { 405 cmd.Args = append(cmd.Args, "-c", fmt.Sprintf("%d", runtime.NumCPU())) 406 } 407 if config.Race { 408 cmd.Args = append(cmd.Args, "-race") 409 fmt.Fprintln(hash, "-race") 410 } 411 if config.TrimPath != "" { 412 cmd.Args = append(cmd.Args, "-trimpath", config.TrimPath) 413 fmt.Fprintln(hash, config.TrimPath) 414 } 415 for _, dep := range p.directDeps { 416 cmd.Args = append(cmd.Args, "-I", dep.pkgDir) 417 hash.Write(dep.hashResult) 418 } 419 for _, filename := range p.files { 420 cmd.Args = append(cmd.Args, filename) 421 fmt.Fprintln(hash, filename) 422 423 // Hash the contents of the input files 424 f, err := os.Open(filename) 425 if err != nil { 426 f.Close() 427 err = fmt.Errorf("%s: %v", filename, err) 428 p.failed = err 429 return err 430 } 431 _, err = io.Copy(hash, f) 432 if err != nil { 433 f.Close() 434 err = fmt.Errorf("%s: %v", filename, err) 435 p.failed = err 436 return err 437 } 438 f.Close() 439 } 440 p.hashResult = hash.Sum(nil) 441 442 var rebuild bool 443 if _, err := os.Stat(p.output); err != nil { 444 rebuild = true 445 } 446 if !rebuild { 447 if oldSha, err := ioutil.ReadFile(shaFile); err == nil { 448 rebuild = !bytes.Equal(oldSha, p.hashResult) 449 } else { 450 rebuild = true 451 } 452 } 453 454 endTrace() 455 if !rebuild { 456 return nil 457 } 458 defer un(config.trace("compile %s", p.Name)) 459 460 err := os.RemoveAll(p.pkgDir) 461 if err != nil { 462 err = fmt.Errorf("%s: %v", p.Name, err) 463 p.failed = err 464 return err 465 } 466 467 err = os.MkdirAll(filepath.Dir(p.output), 0777) 468 if err != nil { 469 err = fmt.Errorf("%s: %v", p.Name, err) 470 p.failed = err 471 return err 472 } 473 474 cmd.Stdin = nil 475 cmd.Stdout = os.Stdout 476 cmd.Stderr = os.Stderr 477 if config.Verbose { 478 fmt.Fprintln(os.Stderr, cmd.Args) 479 } 480 err = cmd.Run() 481 if err != nil { 482 commandText := strings.Join(cmd.Args, " ") 483 err = fmt.Errorf("%q: %v", commandText, err) 484 p.failed = err 485 return err 486 } 487 488 err = ioutil.WriteFile(shaFile, p.hashResult, 0666) 489 if err != nil { 490 err = fmt.Errorf("%s: %v", p.Name, err) 491 p.failed = err 492 return err 493 } 494 495 p.rebuilt = true 496 497 return nil 498} 499 500func (p *GoPackage) Link(config *Config, out string) error { 501 if p.Name != "main" { 502 return fmt.Errorf("Can only link main package") 503 } 504 endTrace := config.trace("check link %s", p.Name) 505 506 shaFile := filepath.Join(filepath.Dir(out), "."+filepath.Base(out)+"_hash") 507 508 if !p.rebuilt { 509 if _, err := os.Stat(out); err != nil { 510 p.rebuilt = true 511 } else if oldSha, err := ioutil.ReadFile(shaFile); err != nil { 512 p.rebuilt = true 513 } else { 514 p.rebuilt = !bytes.Equal(oldSha, p.hashResult) 515 } 516 } 517 endTrace() 518 if !p.rebuilt { 519 return nil 520 } 521 defer un(config.trace("link %s", p.Name)) 522 523 err := os.Remove(shaFile) 524 if err != nil && !os.IsNotExist(err) { 525 return err 526 } 527 err = os.Remove(out) 528 if err != nil && !os.IsNotExist(err) { 529 return err 530 } 531 532 cmd := exec.Command(filepath.Join(goToolDir, "link"), "-o", out) 533 if config.Race { 534 cmd.Args = append(cmd.Args, "-race") 535 } 536 for _, dep := range p.allDeps { 537 cmd.Args = append(cmd.Args, "-L", dep.pkgDir) 538 } 539 cmd.Args = append(cmd.Args, p.output) 540 cmd.Stdin = nil 541 cmd.Stdout = os.Stdout 542 cmd.Stderr = os.Stderr 543 if config.Verbose { 544 fmt.Fprintln(os.Stderr, cmd.Args) 545 } 546 err = cmd.Run() 547 if err != nil { 548 return fmt.Errorf("command %s failed with error %v", cmd.Args, err) 549 } 550 551 return ioutil.WriteFile(shaFile, p.hashResult, 0666) 552} 553 554func Build(config *Config, out, pkg string) (*GoPackage, error) { 555 p := &GoPackage{ 556 Name: "main", 557 } 558 559 lockFileName := filepath.Join(filepath.Dir(out), "."+filepath.Base(out)+".lock") 560 lockFile, err := os.OpenFile(lockFileName, os.O_RDWR|os.O_CREATE, 0666) 561 if err != nil { 562 return nil, fmt.Errorf("Error creating lock file (%q): %v", lockFileName, err) 563 } 564 defer lockFile.Close() 565 566 err = syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX) 567 if err != nil { 568 return nil, fmt.Errorf("Error locking file (%q): %v", lockFileName, err) 569 } 570 571 path, ok, err := config.Path(pkg) 572 if err != nil { 573 return nil, fmt.Errorf("Error finding package %q for main: %v", pkg, err) 574 } 575 if !ok { 576 return nil, fmt.Errorf("Could not find package %q", pkg) 577 } 578 579 intermediates := filepath.Join(filepath.Dir(out), "."+filepath.Base(out)+"_intermediates") 580 if err := os.MkdirAll(intermediates, 0777); err != nil { 581 return nil, fmt.Errorf("Failed to create intermediates directory: %v", err) 582 } 583 584 if err := p.FindDeps(config, path); err != nil { 585 return nil, fmt.Errorf("Failed to find deps of %v: %v", pkg, err) 586 } 587 if err := p.Compile(config, intermediates); err != nil { 588 return nil, fmt.Errorf("Failed to compile %v: %v", pkg, err) 589 } 590 if err := p.Link(config, out); err != nil { 591 return nil, fmt.Errorf("Failed to link %v: %v", pkg, err) 592 } 593 return p, nil 594} 595 596// rebuildMicrofactory checks to see if microfactory itself needs to be rebuilt, 597// and if does, it will launch a new copy and return true. Otherwise it will return 598// false to continue executing. 599func rebuildMicrofactory(config *Config, mybin string) bool { 600 if pkg, err := Build(config, mybin, "github.com/google/blueprint/microfactory/main"); err != nil { 601 fmt.Fprintln(os.Stderr, err) 602 os.Exit(1) 603 } else if !pkg.rebuilt { 604 return false 605 } 606 607 cmd := exec.Command(mybin, os.Args[1:]...) 608 cmd.Stdin = os.Stdin 609 cmd.Stdout = os.Stdout 610 cmd.Stderr = os.Stderr 611 if err := cmd.Run(); err == nil { 612 return true 613 } else if e, ok := err.(*exec.ExitError); ok { 614 os.Exit(e.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()) 615 } 616 os.Exit(1) 617 return true 618} 619 620// microfactory.bash will make a copy of this file renamed into the main package for use with `go run` 621func main() { Main() } 622func Main() { 623 var output, mybin string 624 var config Config 625 pkgMap := pkgPathMappingVar{&config} 626 627 flags := flag.NewFlagSet("", flag.ExitOnError) 628 flags.BoolVar(&config.Race, "race", false, "enable data race detection.") 629 flags.BoolVar(&config.Verbose, "v", false, "Verbose") 630 flags.StringVar(&output, "o", "", "Output file") 631 flags.StringVar(&mybin, "b", "", "Microfactory binary location") 632 flags.StringVar(&config.TrimPath, "trimpath", "", "remove prefix from recorded source file paths") 633 flags.Var(&pkgMap, "pkg-path", "Mapping of package prefixes to file paths") 634 err := flags.Parse(os.Args[1:]) 635 636 if err == flag.ErrHelp || flags.NArg() != 1 || output == "" { 637 fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], "-o out/binary <main-package>") 638 flags.PrintDefaults() 639 os.Exit(1) 640 } 641 642 tracePath := filepath.Join(filepath.Dir(output), "."+filepath.Base(output)+".trace") 643 if traceFile, err := os.OpenFile(tracePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666); err == nil { 644 defer traceFile.Close() 645 config.TraceFunc = func(name string) func() { 646 fmt.Fprintf(traceFile, "%d B %s\n", time.Now().UnixNano()/1000, name) 647 return func() { 648 fmt.Fprintf(traceFile, "%d E %s\n", time.Now().UnixNano()/1000, name) 649 } 650 } 651 } 652 if executable, err := os.Executable(); err == nil { 653 defer un(config.trace("microfactory %s", executable)) 654 } else { 655 defer un(config.trace("microfactory <unknown>")) 656 } 657 658 if mybin != "" { 659 if rebuildMicrofactory(&config, mybin) { 660 return 661 } 662 } 663 664 if _, err := Build(&config, output, flags.Arg(0)); err != nil { 665 fmt.Fprintln(os.Stderr, err) 666 os.Exit(1) 667 } 668} 669 670// pkgPathMapping can be used with flag.Var to parse -pkg-path arguments of 671// <package-prefix>=<path-prefix> mappings. 672type pkgPathMappingVar struct{ *Config } 673 674func (pkgPathMappingVar) String() string { 675 return "<package-prefix>=<path-prefix>" 676} 677 678func (p *pkgPathMappingVar) Set(value string) error { 679 equalPos := strings.Index(value, "=") 680 if equalPos == -1 { 681 return fmt.Errorf("Argument must be in the form of: %q", p.String()) 682 } 683 684 pkgPrefix := strings.TrimSuffix(value[:equalPos], "/") 685 pathPrefix := strings.TrimSuffix(value[equalPos+1:], "/") 686 687 return p.Map(pkgPrefix, pathPrefix) 688} 689