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