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