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