Home | History | Annotate | Download | only in microfactory
      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