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