blob: 6c062e27914ffc9f7ba9259428817edacd5d1448 [file] [log] [blame]
Dan Willemsen0043c0e2016-09-18 20:27:41 -07001// 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).
42package main
43
44import (
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)
64
65var (
66 race = false
67 verbose = false
68
69 goToolDir = filepath.Join(runtime.GOROOT(), "pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH)
Dan Willemsenfde85342017-02-22 22:03:04 -080070 goVersion = findGoVersion()
Dan Willemsen0043c0e2016-09-18 20:27:41 -070071)
72
Dan Willemsenfde85342017-02-22 22:03:04 -080073func 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
Dan Willemsen0043c0e2016-09-18 20:27:41 -070086type GoPackage struct {
87 Name string
88
89 // Inputs
Jeff Gaston1ae73a62017-03-31 14:08:00 -070090 directDeps []*GoPackage // specified directly by the module
91 allDeps []*GoPackage // direct dependencies and transitive dependencies
92 files []string
Dan Willemsen0043c0e2016-09-18 20:27:41 -070093
94 // Outputs
95 pkgDir string
96 output string
97 hashResult []byte
98
99 // Status
100 mutex sync.Mutex
101 compiled bool
102 failed error
103 rebuilt bool
104}
105
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700106// LinkedHashMap<string, GoPackage>
107type linkedDepSet struct {
108 packageSet map[string](*GoPackage)
109 packageList []*GoPackage
110}
111
112func newDepSet() *linkedDepSet {
113 return &linkedDepSet{packageSet: make(map[string]*GoPackage)}
114}
115func (s *linkedDepSet) tryGetByName(name string) (*GoPackage, bool) {
116 pkg, contained := s.packageSet[name]
117 return pkg, contained
118}
119func (s *linkedDepSet) getByName(name string) *GoPackage {
120 pkg, _ := s.tryGetByName(name)
121 return pkg
122}
123func (s *linkedDepSet) add(name string, goPackage *GoPackage) {
124 s.packageSet[name] = goPackage
125 s.packageList = append(s.packageList, goPackage)
126}
127func (s *linkedDepSet) ignore(name string) {
128 s.packageSet[name] = nil
129}
130
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700131// FindDeps searches all applicable go files in `path`, parses all of them
132// for import dependencies that exist in pkgMap, then recursively does the
133// same for all of those dependencies.
134func (p *GoPackage) FindDeps(path string, pkgMap *pkgPathMapping) error {
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700135 depSet := newDepSet()
136 err := p.findDeps(path, pkgMap, depSet)
137 if err != nil {
138 return err
139 }
140 p.allDeps = depSet.packageList
141 return nil
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700142}
143
144// findDeps is the recursive version of FindDeps. allPackages is the map of
145// all locally defined packages so that the same dependency of two different
146// packages is only resolved once.
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700147func (p *GoPackage) findDeps(path string, pkgMap *pkgPathMapping, allPackages *linkedDepSet) error {
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700148 // If this ever becomes too slow, we can look at reading the files once instead of twice
149 // But that just complicates things today, and we're already really fast.
150 foundPkgs, err := parser.ParseDir(token.NewFileSet(), path, func(fi os.FileInfo) bool {
151 name := fi.Name()
152 if fi.IsDir() || strings.HasSuffix(name, "_test.go") || name[0] == '.' || name[0] == '_' {
153 return false
154 }
155 if runtime.GOOS != "darwin" && strings.HasSuffix(name, "_darwin.go") {
156 return false
157 }
158 if runtime.GOOS != "linux" && strings.HasSuffix(name, "_linux.go") {
159 return false
160 }
161 return true
162 }, parser.ImportsOnly)
163 if err != nil {
164 return fmt.Errorf("Error parsing directory %q: %v", path, err)
165 }
166
167 var foundPkg *ast.Package
168 // foundPkgs is a map[string]*ast.Package, but we only want one package
169 if len(foundPkgs) != 1 {
170 return fmt.Errorf("Expected one package in %q, got %d", path, len(foundPkgs))
171 }
172 // Extract the first (and only) entry from the map.
173 for _, pkg := range foundPkgs {
174 foundPkg = pkg
175 }
176
177 var deps []string
178 localDeps := make(map[string]bool)
179
180 for filename, astFile := range foundPkg.Files {
181 p.files = append(p.files, filename)
182
183 for _, importSpec := range astFile.Imports {
184 name, err := strconv.Unquote(importSpec.Path.Value)
185 if err != nil {
186 return fmt.Errorf("%s: invalid quoted string: <%s> %v", filename, importSpec.Path.Value, err)
187 }
188
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700189 if pkg, ok := allPackages.tryGetByName(name); ok {
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700190 if pkg != nil {
191 if _, ok := localDeps[name]; !ok {
192 deps = append(deps, name)
193 localDeps[name] = true
194 }
195 }
196 continue
197 }
198
199 var pkgPath string
200 if path, ok, err := pkgMap.Path(name); err != nil {
201 return err
202 } else if !ok {
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700203 // Probably in the stdlib, but if not, then the compiler will fail with a reasonable error message
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700204 // Mark it as such so that we don't try to decode its path again.
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700205 allPackages.ignore(name)
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700206 continue
207 } else {
208 pkgPath = path
209 }
210
211 pkg := &GoPackage{
212 Name: name,
213 }
214 deps = append(deps, name)
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700215 allPackages.add(name, pkg)
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700216 localDeps[name] = true
217
218 if err := pkg.findDeps(pkgPath, pkgMap, allPackages); err != nil {
219 return err
220 }
221 }
222 }
223
224 sort.Strings(p.files)
225
226 if verbose {
227 fmt.Fprintf(os.Stderr, "Package %q depends on %v\n", p.Name, deps)
228 }
229
230 for _, dep := range deps {
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700231 p.directDeps = append(p.directDeps, allPackages.getByName(dep))
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700232 }
233
234 return nil
235}
236
237func (p *GoPackage) Compile(outDir, trimPath string) error {
238 p.mutex.Lock()
239 defer p.mutex.Unlock()
240 if p.compiled {
241 return p.failed
242 }
243 p.compiled = true
244
245 // Build all dependencies in parallel, then fail if any of them failed.
246 var wg sync.WaitGroup
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700247 for _, dep := range p.directDeps {
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700248 wg.Add(1)
249 go func(dep *GoPackage) {
250 defer wg.Done()
251 dep.Compile(outDir, trimPath)
252 }(dep)
253 }
254 wg.Wait()
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700255 for _, dep := range p.directDeps {
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700256 if dep.failed != nil {
257 p.failed = dep.failed
258 return p.failed
259 }
260 }
261
262 p.pkgDir = filepath.Join(outDir, p.Name)
263 p.output = filepath.Join(p.pkgDir, p.Name) + ".a"
264 shaFile := p.output + ".hash"
265
266 hash := sha1.New()
Dan Willemsenfde85342017-02-22 22:03:04 -0800267 fmt.Fprintln(hash, runtime.GOOS, runtime.GOARCH, goVersion)
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700268
269 cmd := exec.Command(filepath.Join(goToolDir, "compile"),
270 "-o", p.output,
271 "-p", p.Name,
272 "-complete", "-pack", "-nolocalimports")
273 if race {
274 cmd.Args = append(cmd.Args, "-race")
275 fmt.Fprintln(hash, "-race")
276 }
277 if trimPath != "" {
278 cmd.Args = append(cmd.Args, "-trimpath", trimPath)
279 fmt.Fprintln(hash, trimPath)
280 }
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700281 for _, dep := range p.directDeps {
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700282 cmd.Args = append(cmd.Args, "-I", dep.pkgDir)
283 hash.Write(dep.hashResult)
284 }
285 for _, filename := range p.files {
286 cmd.Args = append(cmd.Args, filename)
287 fmt.Fprintln(hash, filename)
288
289 // Hash the contents of the input files
290 f, err := os.Open(filename)
291 if err != nil {
292 f.Close()
293 err = fmt.Errorf("%s: %v", filename, err)
294 p.failed = err
295 return err
296 }
297 _, err = io.Copy(hash, f)
298 if err != nil {
299 f.Close()
300 err = fmt.Errorf("%s: %v", filename, err)
301 p.failed = err
302 return err
303 }
304 f.Close()
305 }
306 p.hashResult = hash.Sum(nil)
307
308 var rebuild bool
309 if _, err := os.Stat(p.output); err != nil {
310 rebuild = true
311 }
312 if !rebuild {
313 if oldSha, err := ioutil.ReadFile(shaFile); err == nil {
314 rebuild = !bytes.Equal(oldSha, p.hashResult)
315 } else {
316 rebuild = true
317 }
318 }
319
320 if !rebuild {
321 return nil
322 }
323
324 err := os.RemoveAll(p.pkgDir)
325 if err != nil {
326 err = fmt.Errorf("%s: %v", p.Name, err)
327 p.failed = err
328 return err
329 }
330
331 err = os.MkdirAll(filepath.Dir(p.output), 0777)
332 if err != nil {
333 err = fmt.Errorf("%s: %v", p.Name, err)
334 p.failed = err
335 return err
336 }
337
338 cmd.Stdin = nil
339 cmd.Stdout = os.Stdout
340 cmd.Stderr = os.Stderr
341 if verbose {
342 fmt.Fprintln(os.Stderr, cmd.Args)
343 }
344 err = cmd.Run()
345 if err != nil {
346 err = fmt.Errorf("%s: %v", p.Name, err)
347 p.failed = err
348 return err
349 }
350
351 err = ioutil.WriteFile(shaFile, p.hashResult, 0666)
352 if err != nil {
353 err = fmt.Errorf("%s: %v", p.Name, err)
354 p.failed = err
355 return err
356 }
357
358 p.rebuilt = true
359
360 return nil
361}
362
363func (p *GoPackage) Link(out string) error {
364 if p.Name != "main" {
365 return fmt.Errorf("Can only link main package")
366 }
367
368 shaFile := filepath.Join(filepath.Dir(out), "."+filepath.Base(out)+"_hash")
369
370 if !p.rebuilt {
371 if _, err := os.Stat(out); err != nil {
372 p.rebuilt = true
373 } else if oldSha, err := ioutil.ReadFile(shaFile); err != nil {
374 p.rebuilt = true
375 } else {
376 p.rebuilt = !bytes.Equal(oldSha, p.hashResult)
377 }
378 }
379 if !p.rebuilt {
380 return nil
381 }
382
383 err := os.Remove(shaFile)
384 if err != nil && !os.IsNotExist(err) {
385 return err
386 }
387 err = os.Remove(out)
388 if err != nil && !os.IsNotExist(err) {
389 return err
390 }
391
392 cmd := exec.Command(filepath.Join(goToolDir, "link"), "-o", out)
393 if race {
394 cmd.Args = append(cmd.Args, "-race")
395 }
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700396 for _, dep := range p.allDeps {
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700397 cmd.Args = append(cmd.Args, "-L", dep.pkgDir)
398 }
399 cmd.Args = append(cmd.Args, p.output)
400 cmd.Stdin = nil
401 cmd.Stdout = os.Stdout
402 cmd.Stderr = os.Stderr
403 if verbose {
404 fmt.Fprintln(os.Stderr, cmd.Args)
405 }
406 err = cmd.Run()
407 if err != nil {
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700408 return fmt.Errorf("command %s failed with error %v", cmd.Args, err)
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700409 }
410
411 return ioutil.WriteFile(shaFile, p.hashResult, 0666)
412}
413
414// rebuildMicrofactory checks to see if microfactory itself needs to be rebuilt,
415// and if does, it will launch a new copy instead of returning.
416func rebuildMicrofactory(mybin, mysrc string, pkgMap *pkgPathMapping) {
417 intermediates := filepath.Join(filepath.Dir(mybin), "."+filepath.Base(mybin)+"_intermediates")
418
419 err := os.MkdirAll(intermediates, 0777)
420 if err != nil {
421 fmt.Fprintln(os.Stderr, "Failed to create intermediates directory: %v", err)
422 os.Exit(1)
423 }
424
425 pkg := &GoPackage{
426 Name: "main",
427 }
428
429 if err := pkg.FindDeps(mysrc, pkgMap); err != nil {
430 fmt.Fprintln(os.Stderr, err)
431 os.Exit(1)
432 }
433
434 if err := pkg.Compile(intermediates, mysrc); err != nil {
435 fmt.Fprintln(os.Stderr, err)
436 os.Exit(1)
437 }
438
439 if err := pkg.Link(mybin); err != nil {
440 fmt.Fprintln(os.Stderr, err)
441 os.Exit(1)
442 }
443
444 if !pkg.rebuilt {
445 return
446 }
447
448 cmd := exec.Command(mybin, os.Args[1:]...)
449 cmd.Stdin = os.Stdin
450 cmd.Stdout = os.Stdout
451 cmd.Stderr = os.Stderr
452 if err := cmd.Run(); err == nil {
453 os.Exit(0)
454 } else if e, ok := err.(*exec.ExitError); ok {
455 os.Exit(e.ProcessState.Sys().(syscall.WaitStatus).ExitStatus())
456 }
457 os.Exit(1)
458}
459
460func main() {
461 var output, mysrc, mybin, trimPath string
462 var pkgMap pkgPathMapping
463
464 flags := flag.NewFlagSet("", flag.ExitOnError)
465 flags.BoolVar(&race, "race", false, "enable data race detection.")
466 flags.BoolVar(&verbose, "v", false, "Verbose")
467 flags.StringVar(&output, "o", "", "Output file")
468 flags.StringVar(&mysrc, "s", "", "Microfactory source directory (for rebuilding microfactory if necessary)")
469 flags.StringVar(&mybin, "b", "", "Microfactory binary location")
470 flags.StringVar(&trimPath, "trimpath", "", "remove prefix from recorded source file paths")
471 flags.Var(&pkgMap, "pkg-path", "Mapping of package prefixes to file paths")
472 err := flags.Parse(os.Args[1:])
473
474 if err == flag.ErrHelp || flags.NArg() != 1 || output == "" {
475 fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], "-o out/binary <main-package>")
476 flags.PrintDefaults()
477 os.Exit(1)
478 }
479
480 if mybin != "" && mysrc != "" {
481 rebuildMicrofactory(mybin, mysrc, &pkgMap)
482 }
483
484 mainPackage := &GoPackage{
485 Name: "main",
486 }
487
488 if path, ok, err := pkgMap.Path(flags.Arg(0)); err != nil {
489 fmt.Fprintln(os.Stderr, "Error finding main path:", err)
490 os.Exit(1)
491 } else if !ok {
492 fmt.Fprintln(os.Stderr, "Cannot find path for", flags.Arg(0))
493 } else {
494 if err := mainPackage.FindDeps(path, &pkgMap); err != nil {
495 fmt.Fprintln(os.Stderr, err)
496 os.Exit(1)
497 }
498 }
499
500 intermediates := filepath.Join(filepath.Dir(output), "."+filepath.Base(output)+"_intermediates")
501
502 err = os.MkdirAll(intermediates, 0777)
503 if err != nil {
504 fmt.Fprintln(os.Stderr, "Failed to create intermediates directory: %ve", err)
505 os.Exit(1)
506 }
507
508 err = mainPackage.Compile(intermediates, trimPath)
509 if err != nil {
510 fmt.Fprintln(os.Stderr, "Failed to compile:", err)
511 os.Exit(1)
512 }
513
514 err = mainPackage.Link(output)
515 if err != nil {
Jeff Gaston1ae73a62017-03-31 14:08:00 -0700516 fmt.Fprintln(os.Stderr, "microfactory.go failed to link:", err)
Dan Willemsen0043c0e2016-09-18 20:27:41 -0700517 os.Exit(1)
518 }
519}
520
521// pkgPathMapping can be used with flag.Var to parse -pkg-path arguments of
522// <package-prefix>=<path-prefix> mappings.
523type pkgPathMapping struct {
524 pkgs []string
525
526 paths map[string]string
527}
528
529func (pkgPathMapping) String() string {
530 return "<package-prefix>=<path-prefix>"
531}
532
533func (p *pkgPathMapping) Set(value string) error {
534 equalPos := strings.Index(value, "=")
535 if equalPos == -1 {
536 return fmt.Errorf("Argument must be in the form of: %q", p.String())
537 }
538
539 pkgPrefix := strings.TrimSuffix(value[:equalPos], "/")
540 pathPrefix := strings.TrimSuffix(value[equalPos+1:], "/")
541
542 if p.paths == nil {
543 p.paths = make(map[string]string)
544 }
545 if _, ok := p.paths[pkgPrefix]; ok {
546 return fmt.Errorf("Duplicate package prefix: %q", pkgPrefix)
547 }
548
549 p.pkgs = append(p.pkgs, pkgPrefix)
550 p.paths[pkgPrefix] = pathPrefix
551
552 return nil
553}
554
555// Path takes a package name, applies the path mappings and returns the resulting path.
556//
557// If the package isn't mapped, we'll return false to prevent compilation attempts.
558func (p *pkgPathMapping) Path(pkg string) (string, bool, error) {
559 if p.paths == nil {
560 return "", false, fmt.Errorf("No package mappings")
561 }
562
563 for _, pkgPrefix := range p.pkgs {
564 if pkg == pkgPrefix {
565 return p.paths[pkgPrefix], true, nil
566 } else if strings.HasPrefix(pkg, pkgPrefix+"/") {
567 return filepath.Join(p.paths[pkgPrefix], strings.TrimPrefix(pkg, pkgPrefix+"/")), true, nil
568 }
569 }
570
571 return "", false, nil
572}