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