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