Merge "Fix envDeps initialization and locking"
diff --git a/Android.bp b/Android.bp
index 4ba6959..ec3cabb 100644
--- a/Android.bp
+++ b/Android.bp
@@ -14,6 +14,7 @@
"androidmk",
"cmd/*",
"third_party/zip",
+ "ui/*",
]
bootstrap_go_package {
diff --git a/cmd/microfactory/Android.bp b/cmd/microfactory/Android.bp
new file mode 100644
index 0000000..a457f43
--- /dev/null
+++ b/cmd/microfactory/Android.bp
@@ -0,0 +1,23 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+blueprint_go_binary {
+ name: "microfactory",
+ srcs: [
+ "microfactory.go",
+ ],
+ testSrcs: [
+ "microfactory_test.go",
+ ],
+}
diff --git a/cmd/microfactory/microfactory.go b/cmd/microfactory/microfactory.go
new file mode 100644
index 0000000..3ed5a2c
--- /dev/null
+++ b/cmd/microfactory/microfactory.go
@@ -0,0 +1,526 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Microfactory is a tool to incrementally compile a go program. It's similar
+// to `go install`, but doesn't require a GOPATH. A package->path mapping can
+// be specified as command line options:
+//
+// -pkg-path android/soong=build/soong
+// -pkg-path github.com/google/blueprint=build/blueprint
+//
+// The paths can be relative to the current working directory, or an absolute
+// path. Both packages and paths are compared with full directory names, so the
+// android/soong-test package wouldn't be mapped in the above case.
+//
+// Microfactory will ignore *_test.go files, and limits *_darwin.go and
+// *_linux.go files to MacOS and Linux respectively. It does not support build
+// tags or any other suffixes.
+//
+// Builds are incremental by package. All input files are hashed, and if the
+// hash of an input or dependency changes, the package is rebuilt.
+//
+// It also exposes the -trimpath option from go's compiler so that embedded
+// path names (such as in log.Llongfile) are relative paths instead of absolute
+// paths.
+//
+// If you don't have a previously built version of Microfactory, when used with
+// -s <microfactory_src_dir> -b <microfactory_bin_file>, Microfactory can
+// rebuild itself as necessary. Combined with a shell script like soong_ui.bash
+// that uses `go run` to run Microfactory for the first time, go programs can be
+// quickly bootstrapped entirely from source (and a standard go distribution).
+package main
+
+import (
+ "bytes"
+ "crypto/sha1"
+ "flag"
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "syscall"
+)
+
+var (
+ race = false
+ verbose = false
+
+ goToolDir = filepath.Join(runtime.GOROOT(), "pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH)
+)
+
+type GoPackage struct {
+ Name string
+
+ // Inputs
+ deps []*GoPackage
+ files []string
+
+ // Outputs
+ pkgDir string
+ output string
+ hashResult []byte
+
+ // Status
+ mutex sync.Mutex
+ compiled bool
+ failed error
+ rebuilt bool
+}
+
+// FindDeps searches all applicable go files in `path`, parses all of them
+// for import dependencies that exist in pkgMap, then recursively does the
+// same for all of those dependencies.
+func (p *GoPackage) FindDeps(path string, pkgMap *pkgPathMapping) error {
+ return p.findDeps(path, pkgMap, make(map[string]*GoPackage))
+}
+
+// findDeps is the recursive version of FindDeps. allPackages is the map of
+// all locally defined packages so that the same dependency of two different
+// packages is only resolved once.
+func (p *GoPackage) findDeps(path string, pkgMap *pkgPathMapping, allPackages map[string]*GoPackage) error {
+ // If this ever becomes too slow, we can look at reading the files once instead of twice
+ // But that just complicates things today, and we're already really fast.
+ foundPkgs, err := parser.ParseDir(token.NewFileSet(), path, func(fi os.FileInfo) bool {
+ name := fi.Name()
+ if fi.IsDir() || strings.HasSuffix(name, "_test.go") || name[0] == '.' || name[0] == '_' {
+ return false
+ }
+ if runtime.GOOS != "darwin" && strings.HasSuffix(name, "_darwin.go") {
+ return false
+ }
+ if runtime.GOOS != "linux" && strings.HasSuffix(name, "_linux.go") {
+ return false
+ }
+ return true
+ }, parser.ImportsOnly)
+ if err != nil {
+ return fmt.Errorf("Error parsing directory %q: %v", path, err)
+ }
+
+ var foundPkg *ast.Package
+ // foundPkgs is a map[string]*ast.Package, but we only want one package
+ if len(foundPkgs) != 1 {
+ return fmt.Errorf("Expected one package in %q, got %d", path, len(foundPkgs))
+ }
+ // Extract the first (and only) entry from the map.
+ for _, pkg := range foundPkgs {
+ foundPkg = pkg
+ }
+
+ var deps []string
+ localDeps := make(map[string]bool)
+
+ for filename, astFile := range foundPkg.Files {
+ p.files = append(p.files, filename)
+
+ for _, importSpec := range astFile.Imports {
+ name, err := strconv.Unquote(importSpec.Path.Value)
+ if err != nil {
+ return fmt.Errorf("%s: invalid quoted string: <%s> %v", filename, importSpec.Path.Value, err)
+ }
+
+ if pkg, ok := allPackages[name]; ok && pkg != nil {
+ if pkg != nil {
+ if _, ok := localDeps[name]; !ok {
+ deps = append(deps, name)
+ localDeps[name] = true
+ }
+ }
+ continue
+ }
+
+ var pkgPath string
+ if path, ok, err := pkgMap.Path(name); err != nil {
+ return err
+ } else if !ok {
+ // Probably in the stdlib, compiler will fail we a reasonable error message otherwise.
+ // Mark it as such so that we don't try to decode its path again.
+ allPackages[name] = nil
+ continue
+ } else {
+ pkgPath = path
+ }
+
+ pkg := &GoPackage{
+ Name: name,
+ }
+ deps = append(deps, name)
+ allPackages[name] = pkg
+ localDeps[name] = true
+
+ if err := pkg.findDeps(pkgPath, pkgMap, allPackages); err != nil {
+ return err
+ }
+ }
+ }
+
+ sort.Strings(p.files)
+
+ if verbose {
+ fmt.Fprintf(os.Stderr, "Package %q depends on %v\n", p.Name, deps)
+ }
+
+ for _, dep := range deps {
+ p.deps = append(p.deps, allPackages[dep])
+ }
+
+ return nil
+}
+
+func (p *GoPackage) Compile(outDir, trimPath string) error {
+ p.mutex.Lock()
+ defer p.mutex.Unlock()
+ if p.compiled {
+ return p.failed
+ }
+ p.compiled = true
+
+ // Build all dependencies in parallel, then fail if any of them failed.
+ var wg sync.WaitGroup
+ for _, dep := range p.deps {
+ wg.Add(1)
+ go func(dep *GoPackage) {
+ defer wg.Done()
+ dep.Compile(outDir, trimPath)
+ }(dep)
+ }
+ wg.Wait()
+ for _, dep := range p.deps {
+ if dep.failed != nil {
+ p.failed = dep.failed
+ return p.failed
+ }
+ }
+
+ p.pkgDir = filepath.Join(outDir, p.Name)
+ p.output = filepath.Join(p.pkgDir, p.Name) + ".a"
+ shaFile := p.output + ".hash"
+
+ hash := sha1.New()
+ fmt.Fprintln(hash, runtime.GOOS, runtime.GOARCH, runtime.Version())
+
+ cmd := exec.Command(filepath.Join(goToolDir, "compile"),
+ "-o", p.output,
+ "-p", p.Name,
+ "-complete", "-pack", "-nolocalimports")
+ if race {
+ cmd.Args = append(cmd.Args, "-race")
+ fmt.Fprintln(hash, "-race")
+ }
+ if trimPath != "" {
+ cmd.Args = append(cmd.Args, "-trimpath", trimPath)
+ fmt.Fprintln(hash, trimPath)
+ }
+ for _, dep := range p.deps {
+ cmd.Args = append(cmd.Args, "-I", dep.pkgDir)
+ hash.Write(dep.hashResult)
+ }
+ for _, filename := range p.files {
+ cmd.Args = append(cmd.Args, filename)
+ fmt.Fprintln(hash, filename)
+
+ // Hash the contents of the input files
+ f, err := os.Open(filename)
+ if err != nil {
+ f.Close()
+ err = fmt.Errorf("%s: %v", filename, err)
+ p.failed = err
+ return err
+ }
+ _, err = io.Copy(hash, f)
+ if err != nil {
+ f.Close()
+ err = fmt.Errorf("%s: %v", filename, err)
+ p.failed = err
+ return err
+ }
+ f.Close()
+ }
+ p.hashResult = hash.Sum(nil)
+
+ var rebuild bool
+ if _, err := os.Stat(p.output); err != nil {
+ rebuild = true
+ }
+ if !rebuild {
+ if oldSha, err := ioutil.ReadFile(shaFile); err == nil {
+ rebuild = !bytes.Equal(oldSha, p.hashResult)
+ } else {
+ rebuild = true
+ }
+ }
+
+ if !rebuild {
+ return nil
+ }
+
+ err := os.RemoveAll(p.pkgDir)
+ if err != nil {
+ err = fmt.Errorf("%s: %v", p.Name, err)
+ p.failed = err
+ return err
+ }
+
+ err = os.MkdirAll(filepath.Dir(p.output), 0777)
+ if err != nil {
+ err = fmt.Errorf("%s: %v", p.Name, err)
+ p.failed = err
+ return err
+ }
+
+ cmd.Stdin = nil
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if verbose {
+ fmt.Fprintln(os.Stderr, cmd.Args)
+ }
+ err = cmd.Run()
+ if err != nil {
+ err = fmt.Errorf("%s: %v", p.Name, err)
+ p.failed = err
+ return err
+ }
+
+ err = ioutil.WriteFile(shaFile, p.hashResult, 0666)
+ if err != nil {
+ err = fmt.Errorf("%s: %v", p.Name, err)
+ p.failed = err
+ return err
+ }
+
+ p.rebuilt = true
+
+ return nil
+}
+
+func (p *GoPackage) Link(out string) error {
+ if p.Name != "main" {
+ return fmt.Errorf("Can only link main package")
+ }
+
+ shaFile := filepath.Join(filepath.Dir(out), "."+filepath.Base(out)+"_hash")
+
+ if !p.rebuilt {
+ if _, err := os.Stat(out); err != nil {
+ p.rebuilt = true
+ } else if oldSha, err := ioutil.ReadFile(shaFile); err != nil {
+ p.rebuilt = true
+ } else {
+ p.rebuilt = !bytes.Equal(oldSha, p.hashResult)
+ }
+ }
+ if !p.rebuilt {
+ return nil
+ }
+
+ err := os.Remove(shaFile)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ err = os.Remove(out)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+
+ cmd := exec.Command(filepath.Join(goToolDir, "link"), "-o", out)
+ if race {
+ cmd.Args = append(cmd.Args, "-race")
+ }
+ for _, dep := range p.deps {
+ cmd.Args = append(cmd.Args, "-L", dep.pkgDir)
+ }
+ cmd.Args = append(cmd.Args, p.output)
+ cmd.Stdin = nil
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if verbose {
+ fmt.Fprintln(os.Stderr, cmd.Args)
+ }
+ err = cmd.Run()
+ if err != nil {
+ return err
+ }
+
+ return ioutil.WriteFile(shaFile, p.hashResult, 0666)
+}
+
+// rebuildMicrofactory checks to see if microfactory itself needs to be rebuilt,
+// and if does, it will launch a new copy instead of returning.
+func rebuildMicrofactory(mybin, mysrc string, pkgMap *pkgPathMapping) {
+ intermediates := filepath.Join(filepath.Dir(mybin), "."+filepath.Base(mybin)+"_intermediates")
+
+ err := os.MkdirAll(intermediates, 0777)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Failed to create intermediates directory: %v", err)
+ os.Exit(1)
+ }
+
+ pkg := &GoPackage{
+ Name: "main",
+ }
+
+ if err := pkg.FindDeps(mysrc, pkgMap); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ if err := pkg.Compile(intermediates, mysrc); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ if err := pkg.Link(mybin); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ if !pkg.rebuilt {
+ return
+ }
+
+ cmd := exec.Command(mybin, os.Args[1:]...)
+ cmd.Stdin = os.Stdin
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if err := cmd.Run(); err == nil {
+ os.Exit(0)
+ } else if e, ok := err.(*exec.ExitError); ok {
+ os.Exit(e.ProcessState.Sys().(syscall.WaitStatus).ExitStatus())
+ }
+ os.Exit(1)
+}
+
+func main() {
+ var output, mysrc, mybin, trimPath string
+ var pkgMap pkgPathMapping
+
+ flags := flag.NewFlagSet("", flag.ExitOnError)
+ flags.BoolVar(&race, "race", false, "enable data race detection.")
+ flags.BoolVar(&verbose, "v", false, "Verbose")
+ flags.StringVar(&output, "o", "", "Output file")
+ flags.StringVar(&mysrc, "s", "", "Microfactory source directory (for rebuilding microfactory if necessary)")
+ flags.StringVar(&mybin, "b", "", "Microfactory binary location")
+ flags.StringVar(&trimPath, "trimpath", "", "remove prefix from recorded source file paths")
+ flags.Var(&pkgMap, "pkg-path", "Mapping of package prefixes to file paths")
+ err := flags.Parse(os.Args[1:])
+
+ if err == flag.ErrHelp || flags.NArg() != 1 || output == "" {
+ fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], "-o out/binary <main-package>")
+ flags.PrintDefaults()
+ os.Exit(1)
+ }
+
+ if mybin != "" && mysrc != "" {
+ rebuildMicrofactory(mybin, mysrc, &pkgMap)
+ }
+
+ mainPackage := &GoPackage{
+ Name: "main",
+ }
+
+ if path, ok, err := pkgMap.Path(flags.Arg(0)); err != nil {
+ fmt.Fprintln(os.Stderr, "Error finding main path:", err)
+ os.Exit(1)
+ } else if !ok {
+ fmt.Fprintln(os.Stderr, "Cannot find path for", flags.Arg(0))
+ } else {
+ if err := mainPackage.FindDeps(path, &pkgMap); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ }
+
+ intermediates := filepath.Join(filepath.Dir(output), "."+filepath.Base(output)+"_intermediates")
+
+ err = os.MkdirAll(intermediates, 0777)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Failed to create intermediates directory: %ve", err)
+ os.Exit(1)
+ }
+
+ err = mainPackage.Compile(intermediates, trimPath)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Failed to compile:", err)
+ os.Exit(1)
+ }
+
+ err = mainPackage.Link(output)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Failed to link:", err)
+ os.Exit(1)
+ }
+}
+
+// pkgPathMapping can be used with flag.Var to parse -pkg-path arguments of
+// <package-prefix>=<path-prefix> mappings.
+type pkgPathMapping struct {
+ pkgs []string
+
+ paths map[string]string
+}
+
+func (pkgPathMapping) String() string {
+ return "<package-prefix>=<path-prefix>"
+}
+
+func (p *pkgPathMapping) Set(value string) error {
+ equalPos := strings.Index(value, "=")
+ if equalPos == -1 {
+ return fmt.Errorf("Argument must be in the form of: %q", p.String())
+ }
+
+ pkgPrefix := strings.TrimSuffix(value[:equalPos], "/")
+ pathPrefix := strings.TrimSuffix(value[equalPos+1:], "/")
+
+ if p.paths == nil {
+ p.paths = make(map[string]string)
+ }
+ if _, ok := p.paths[pkgPrefix]; ok {
+ return fmt.Errorf("Duplicate package prefix: %q", pkgPrefix)
+ }
+
+ p.pkgs = append(p.pkgs, pkgPrefix)
+ p.paths[pkgPrefix] = pathPrefix
+
+ return nil
+}
+
+// Path takes a package name, applies the path mappings and returns the resulting path.
+//
+// If the package isn't mapped, we'll return false to prevent compilation attempts.
+func (p *pkgPathMapping) Path(pkg string) (string, bool, error) {
+ if p.paths == nil {
+ return "", false, fmt.Errorf("No package mappings")
+ }
+
+ for _, pkgPrefix := range p.pkgs {
+ if pkg == pkgPrefix {
+ return p.paths[pkgPrefix], true, nil
+ } else if strings.HasPrefix(pkg, pkgPrefix+"/") {
+ return filepath.Join(p.paths[pkgPrefix], strings.TrimPrefix(pkg, pkgPrefix+"/")), true, nil
+ }
+ }
+
+ return "", false, nil
+}
diff --git a/cmd/microfactory/microfactory_test.go b/cmd/microfactory/microfactory_test.go
new file mode 100644
index 0000000..296a844
--- /dev/null
+++ b/cmd/microfactory/microfactory_test.go
@@ -0,0 +1,422 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "flag"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "testing"
+ "time"
+)
+
+func TestSimplePackagePathMap(t *testing.T) {
+ t.Parallel()
+
+ var pkgMap pkgPathMapping
+ flags := flag.NewFlagSet("", flag.ContinueOnError)
+ flags.Var(&pkgMap, "m", "")
+ err := flags.Parse([]string{
+ "-m", "android/soong=build/soong/",
+ "-m", "github.com/google/blueprint/=build/blueprint",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ compare := func(got, want interface{}) {
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("Unexpected values in .pkgs:\nwant: %v\n got: %v",
+ want, got)
+ }
+ }
+
+ wantPkgs := []string{"android/soong", "github.com/google/blueprint"}
+ compare(pkgMap.pkgs, wantPkgs)
+ compare(pkgMap.paths[wantPkgs[0]], "build/soong")
+ compare(pkgMap.paths[wantPkgs[1]], "build/blueprint")
+
+ got, ok, err := pkgMap.Path("android/soong/ui/test")
+ if err != nil {
+ t.Error("Unexpected error in pkgMap.Path(soong):", err)
+ } else if !ok {
+ t.Error("Expected a result from pkgMap.Path(soong)")
+ } else {
+ compare(got, "build/soong/ui/test")
+ }
+
+ got, ok, err = pkgMap.Path("github.com/google/blueprint")
+ if err != nil {
+ t.Error("Unexpected error in pkgMap.Path(blueprint):", err)
+ } else if !ok {
+ t.Error("Expected a result from pkgMap.Path(blueprint)")
+ } else {
+ compare(got, "build/blueprint")
+ }
+}
+
+func TestBadPackagePathMap(t *testing.T) {
+ t.Parallel()
+
+ var pkgMap pkgPathMapping
+ if _, _, err := pkgMap.Path("testing"); err == nil {
+ t.Error("Expected error if no maps are specified")
+ }
+ if err := pkgMap.Set(""); err == nil {
+ t.Error("Expected error with blank argument, but none returned")
+ }
+ if err := pkgMap.Set("a=a"); err != nil {
+ t.Error("Unexpected error: %v", err)
+ }
+ if err := pkgMap.Set("a=b"); err == nil {
+ t.Error("Expected error with duplicate package prefix, but none returned")
+ }
+ if _, ok, err := pkgMap.Path("testing"); err != nil {
+ t.Error("Unexpected error: %v", err)
+ } else if ok {
+ t.Error("Expected testing to be consider in the stdlib")
+ }
+}
+
+// TestSingleBuild ensures that just a basic build works.
+func TestSingleBuild(t *testing.T) {
+ t.Parallel()
+
+ setupDir(t, func(dir string, loadPkg loadPkgFunc) {
+ // The output binary
+ out := filepath.Join(dir, "out", "test")
+
+ pkg := loadPkg()
+
+ if err := pkg.Compile(filepath.Join(dir, "out"), ""); err != nil {
+ t.Fatalf("Got error when compiling:", err)
+ }
+
+ if err := pkg.Link(out); err != nil {
+ t.Fatal("Got error when linking:", err)
+ }
+
+ if _, err := os.Stat(out); err != nil {
+ t.Error("Cannot stat output:", err)
+ }
+ })
+}
+
+// testBuildAgain triggers two builds, running the modify function in between
+// each build. It verifies that the second build did or did not actually need
+// to rebuild anything based on the shouldRebuild argument.
+func testBuildAgain(t *testing.T,
+ shouldRecompile, shouldRelink bool,
+ modify func(dir string, loadPkg loadPkgFunc),
+ after func(pkg *GoPackage)) {
+
+ t.Parallel()
+
+ setupDir(t, func(dir string, loadPkg loadPkgFunc) {
+ // The output binary
+ out := filepath.Join(dir, "out", "test")
+
+ pkg := loadPkg()
+
+ if err := pkg.Compile(filepath.Join(dir, "out"), ""); err != nil {
+ t.Fatal("Got error when compiling:", err)
+ }
+
+ if err := pkg.Link(out); err != nil {
+ t.Fatal("Got error when linking:", err)
+ }
+
+ var firstTime time.Time
+ if stat, err := os.Stat(out); err == nil {
+ firstTime = stat.ModTime()
+ } else {
+ t.Fatal("Failed to stat output file:", err)
+ }
+
+ // mtime on HFS+ (the filesystem on darwin) are stored with 1
+ // second granularity, so the timestamp checks will fail unless
+ // we wait at least a second. Sleeping 1.1s to be safe.
+ if runtime.GOOS == "darwin" {
+ time.Sleep(1100 * time.Millisecond)
+ }
+
+ modify(dir, loadPkg)
+
+ pkg = loadPkg()
+
+ if err := pkg.Compile(filepath.Join(dir, "out"), ""); err != nil {
+ t.Fatal("Got error when compiling:", err)
+ }
+ if shouldRecompile {
+ if !pkg.rebuilt {
+ t.Fatal("Package should have recompiled, but was not recompiled.")
+ }
+ } else {
+ if pkg.rebuilt {
+ t.Fatal("Package should not have needed to be recompiled, but was recompiled.")
+ }
+ }
+
+ if err := pkg.Link(out); err != nil {
+ t.Fatal("Got error while linking:", err)
+ }
+ if shouldRelink {
+ if !pkg.rebuilt {
+ t.Error("Package should have relinked, but was not relinked.")
+ }
+ } else {
+ if pkg.rebuilt {
+ t.Error("Package should not have needed to be relinked, but was relinked.")
+ }
+ }
+
+ if stat, err := os.Stat(out); err == nil {
+ if shouldRelink {
+ if stat.ModTime() == firstTime {
+ t.Error("Output timestamp should be different, but both were", firstTime)
+ }
+ } else {
+ if stat.ModTime() != firstTime {
+ t.Error("Output timestamp should be the same.")
+ t.Error(" first:", firstTime)
+ t.Error("second:", stat.ModTime())
+ }
+ }
+ } else {
+ t.Fatal("Failed to stat output file:", err)
+ }
+
+ after(pkg)
+ })
+}
+
+// TestRebuildAfterNoChanges ensures that we don't rebuild if nothing
+// changes
+func TestRebuildAfterNoChanges(t *testing.T) {
+ testBuildAgain(t, false, false, func(dir string, loadPkg loadPkgFunc) {}, func(pkg *GoPackage) {})
+}
+
+// TestRebuildAfterTimestamp ensures that we don't rebuild because
+// timestamps of important files have changed. We should only rebuild if the
+// content hashes are different.
+func TestRebuildAfterTimestampChange(t *testing.T) {
+ testBuildAgain(t, false, false, func(dir string, loadPkg loadPkgFunc) {
+ // Ensure that we've spent some amount of time asleep
+ time.Sleep(100 * time.Millisecond)
+
+ newTime := time.Now().Local()
+ os.Chtimes(filepath.Join(dir, "test.fact"), newTime, newTime)
+ os.Chtimes(filepath.Join(dir, "main/main.go"), newTime, newTime)
+ os.Chtimes(filepath.Join(dir, "a/a.go"), newTime, newTime)
+ os.Chtimes(filepath.Join(dir, "a/b.go"), newTime, newTime)
+ os.Chtimes(filepath.Join(dir, "b/a.go"), newTime, newTime)
+ }, func(pkg *GoPackage) {})
+}
+
+// TestRebuildAfterGoChange ensures that we rebuild after a content change
+// to a package's go file.
+func TestRebuildAfterGoChange(t *testing.T) {
+ testBuildAgain(t, true, true, func(dir string, loadPkg loadPkgFunc) {
+ if err := ioutil.WriteFile(filepath.Join(dir, "a", "a.go"), []byte(go_a_a+"\n"), 0666); err != nil {
+ t.Fatal("Error writing a/a.go:", err)
+ }
+ }, func(pkg *GoPackage) {
+ if !pkg.deps[0].rebuilt {
+ t.Fatal("android/soong/a should have rebuilt")
+ }
+ if !pkg.deps[1].rebuilt {
+ t.Fatal("android/soong/b should have rebuilt")
+ }
+ })
+}
+
+// TestRebuildAfterMainChange ensures that we don't rebuild any dependencies
+// if only the main package's go files are touched.
+func TestRebuildAfterMainChange(t *testing.T) {
+ testBuildAgain(t, true, true, func(dir string, loadPkg loadPkgFunc) {
+ if err := ioutil.WriteFile(filepath.Join(dir, "main", "main.go"), []byte(go_main_main+"\n"), 0666); err != nil {
+ t.Fatal("Error writing main/main.go:", err)
+ }
+ }, func(pkg *GoPackage) {
+ if pkg.deps[0].rebuilt {
+ t.Fatal("android/soong/a should not have rebuilt")
+ }
+ if pkg.deps[1].rebuilt {
+ t.Fatal("android/soong/b should not have rebuilt")
+ }
+ })
+}
+
+// TestRebuildAfterRemoveOut ensures that we rebuild if the output file is
+// missing, even if everything else doesn't need rebuilding.
+func TestRebuildAfterRemoveOut(t *testing.T) {
+ testBuildAgain(t, false, true, func(dir string, loadPkg loadPkgFunc) {
+ if err := os.Remove(filepath.Join(dir, "out", "test")); err != nil {
+ t.Fatal("Failed to remove output:", err)
+ }
+ }, func(pkg *GoPackage) {})
+}
+
+// TestRebuildAfterPartialBuild ensures that even if the build was interrupted
+// between the recompile and relink stages, we'll still relink when we run again.
+func TestRebuildAfterPartialBuild(t *testing.T) {
+ testBuildAgain(t, false, true, func(dir string, loadPkg loadPkgFunc) {
+ if err := ioutil.WriteFile(filepath.Join(dir, "main", "main.go"), []byte(go_main_main+"\n"), 0666); err != nil {
+ t.Fatal("Error writing main/main.go:", err)
+ }
+
+ pkg := loadPkg()
+
+ if err := pkg.Compile(filepath.Join(dir, "out"), ""); err != nil {
+ t.Fatal("Got error when compiling:", err)
+ }
+ if !pkg.rebuilt {
+ t.Fatal("Package should have recompiled, but was not recompiled.")
+ }
+ }, func(pkg *GoPackage) {})
+}
+
+// BenchmarkInitialBuild computes how long a clean build takes (for tiny test
+// inputs).
+func BenchmarkInitialBuild(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ setupDir(b, func(dir string, loadPkg loadPkgFunc) {
+ pkg := loadPkg()
+ if err := pkg.Compile(filepath.Join(dir, "out"), ""); err != nil {
+ b.Fatal("Got error when compiling:", err)
+ }
+
+ if err := pkg.Link(filepath.Join(dir, "out", "test")); err != nil {
+ b.Fatal("Got error when linking:", err)
+ }
+ })
+ }
+}
+
+// BenchmarkMinIncrementalBuild computes how long an incremental build that
+// doesn't actually need to build anything takes.
+func BenchmarkMinIncrementalBuild(b *testing.B) {
+ setupDir(b, func(dir string, loadPkg loadPkgFunc) {
+ pkg := loadPkg()
+
+ if err := pkg.Compile(filepath.Join(dir, "out"), ""); err != nil {
+ b.Fatal("Got error when compiling:", err)
+ }
+
+ if err := pkg.Link(filepath.Join(dir, "out", "test")); err != nil {
+ b.Fatal("Got error when linking:", err)
+ }
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ pkg := loadPkg()
+
+ if err := pkg.Compile(filepath.Join(dir, "out"), ""); err != nil {
+ b.Fatal("Got error when compiling:", err)
+ }
+
+ if err := pkg.Link(filepath.Join(dir, "out", "test")); err != nil {
+ b.Fatal("Got error when linking:", err)
+ }
+
+ if pkg.rebuilt {
+ b.Fatal("Should not have rebuilt anything")
+ }
+ }
+ })
+}
+
+///////////////////////////////////////////////////////
+// Templates used to create fake compilable packages //
+///////////////////////////////////////////////////////
+
+const go_main_main = `
+package main
+import (
+ "fmt"
+ "android/soong/a"
+ "android/soong/b"
+)
+func main() {
+ fmt.Println(a.Stdout, b.Stdout)
+}
+`
+
+const go_a_a = `
+package a
+import "os"
+var Stdout = os.Stdout
+`
+
+const go_a_b = `
+package a
+`
+
+const go_b_a = `
+package b
+import "android/soong/a"
+var Stdout = a.Stdout
+`
+
+type T interface {
+ Fatal(args ...interface{})
+ Fatalf(format string, args ...interface{})
+}
+
+type loadPkgFunc func() *GoPackage
+
+func setupDir(t T, test func(dir string, loadPkg loadPkgFunc)) {
+ dir, err := ioutil.TempDir("", "test")
+ if err != nil {
+ t.Fatalf("Error creating temporary directory: %#v", err)
+ }
+ defer os.RemoveAll(dir)
+
+ writeFile := func(name, contents string) {
+ if err := ioutil.WriteFile(filepath.Join(dir, name), []byte(contents), 0666); err != nil {
+ t.Fatalf("Error writing %q: %#v", name, err)
+ }
+ }
+ mkdir := func(name string) {
+ if err := os.Mkdir(filepath.Join(dir, name), 0777); err != nil {
+ t.Fatalf("Error creating %q directory: %#v", name, err)
+ }
+ }
+ mkdir("main")
+ mkdir("a")
+ mkdir("b")
+ writeFile("main/main.go", go_main_main)
+ writeFile("a/a.go", go_a_a)
+ writeFile("a/b.go", go_a_b)
+ writeFile("b/a.go", go_b_a)
+
+ loadPkg := func() *GoPackage {
+ pkg := &GoPackage{
+ Name: "main",
+ }
+ pkgMap := &pkgPathMapping{}
+ pkgMap.Set("android/soong=" + dir)
+ if err := pkg.FindDeps(filepath.Join(dir, "main"), pkgMap); err != nil {
+ t.Fatalf("Error finding deps: %v", err)
+ }
+ return pkg
+ }
+
+ test(dir, loadPkg)
+}
diff --git a/cmd/multiproduct_kati/Android.bp b/cmd/multiproduct_kati/Android.bp
new file mode 100644
index 0000000..b264c35
--- /dev/null
+++ b/cmd/multiproduct_kati/Android.bp
@@ -0,0 +1,25 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+blueprint_go_binary {
+ name: "multiproduct_kati",
+ deps: [
+ "soong-ui-build",
+ "soong-ui-logger",
+ "soong-ui-tracer",
+ ],
+ srcs: [
+ "main.go",
+ ],
+}
diff --git a/cmd/multiproduct_kati/main.go b/cmd/multiproduct_kati/main.go
new file mode 100644
index 0000000..2ff19ce
--- /dev/null
+++ b/cmd/multiproduct_kati/main.go
@@ -0,0 +1,205 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "bytes"
+ "context"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "sync"
+
+ "android/soong/ui/build"
+ "android/soong/ui/logger"
+ "android/soong/ui/tracer"
+)
+
+// We default to number of cpus / 4, which seems to be the sweet spot for my
+// system. I suspect this is mostly due to memory or disk bandwidth though, and
+// may depend on the size ofthe source tree, so this probably isn't a great
+// default.
+func detectNumJobs() int {
+ if runtime.NumCPU() < 4 {
+ return 1
+ }
+ return runtime.NumCPU() / 4
+}
+
+var numJobs = flag.Int("j", detectNumJobs(), "number of parallel kati jobs")
+
+var keep = flag.Bool("keep", false, "keep successful output files")
+
+var outDir = flag.String("out", "", "path to store output directories (defaults to tmpdir under $OUT when empty)")
+
+var onlyConfig = flag.Bool("only-config", false, "Only run product config (not Soong or Kati)")
+var onlySoong = flag.Bool("only-soong", false, "Only run product config and Soong (not Kati)")
+
+type Product struct {
+ ctx build.Context
+ config build.Config
+}
+
+func main() {
+ log := logger.New(os.Stderr)
+ defer log.Cleanup()
+
+ flag.Parse()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ trace := tracer.New(log)
+ defer trace.Close()
+
+ build.SetupSignals(log, cancel, func() {
+ trace.Close()
+ log.Cleanup()
+ })
+
+ buildCtx := build.Context{&build.ContextImpl{
+ Context: ctx,
+ Logger: log,
+ Tracer: trace,
+ StdioInterface: build.StdioImpl{},
+ }}
+
+ failed := false
+
+ config := build.NewConfig(buildCtx)
+ if *outDir == "" {
+ var err error
+ *outDir, err = ioutil.TempDir(config.OutDir(), "multiproduct")
+ if err != nil {
+ log.Fatalf("Failed to create tempdir: %v", err)
+ }
+
+ if !*keep {
+ defer func() {
+ if !failed {
+ os.RemoveAll(*outDir)
+ }
+ }()
+ }
+ }
+ config.Environment().Set("OUT_DIR", *outDir)
+ log.Println("Output directory:", *outDir)
+
+ build.SetupOutDir(buildCtx, config)
+ log.SetOutput(filepath.Join(config.OutDir(), "build.log"))
+ trace.SetOutput(filepath.Join(config.OutDir(), "build.trace"))
+
+ vars, err := build.DumpMakeVars(buildCtx, config, nil, nil, []string{"all_named_products"})
+ if err != nil {
+ log.Fatal(err)
+ }
+ products := strings.Fields(vars["all_named_products"])
+ log.Verbose("Got product list:", products)
+
+ var wg sync.WaitGroup
+ errs := make(chan error, len(products))
+ productConfigs := make(chan Product, len(products))
+
+ // Run the product config for every product in parallel
+ for _, product := range products {
+ wg.Add(1)
+ go func(product string) {
+ defer wg.Done()
+ defer logger.Recover(func(err error) {
+ errs <- fmt.Errorf("Error building %s: %v", product, err)
+ })
+
+ productOutDir := filepath.Join(config.OutDir(), product)
+
+ if err := os.MkdirAll(productOutDir, 0777); err != nil {
+ log.Fatalf("Error creating out directory: %v", err)
+ }
+
+ f, err := os.Create(filepath.Join(productOutDir, "std.log"))
+ if err != nil {
+ log.Fatalf("Error creating std.log: %v", err)
+ }
+
+ productLog := logger.New(&bytes.Buffer{})
+ productLog.SetOutput(filepath.Join(productOutDir, "build.log"))
+
+ productCtx := build.Context{&build.ContextImpl{
+ Context: ctx,
+ Logger: productLog,
+ Tracer: trace,
+ StdioInterface: build.NewCustomStdio(nil, f, f),
+ Thread: trace.NewThread(product),
+ }}
+
+ productConfig := build.NewConfig(productCtx)
+ productConfig.Environment().Set("OUT_DIR", productOutDir)
+ productConfig.Lunch(productCtx, product, "eng")
+
+ build.Build(productCtx, productConfig, build.BuildProductConfig)
+ productConfigs <- Product{productCtx, productConfig}
+ }(product)
+ }
+ go func() {
+ defer close(productConfigs)
+ wg.Wait()
+ }()
+
+ var wg2 sync.WaitGroup
+ // Then run up to numJobs worth of Soong and Kati
+ for i := 0; i < *numJobs; i++ {
+ wg2.Add(1)
+ go func() {
+ defer wg2.Done()
+ for product := range productConfigs {
+ func() {
+ defer logger.Recover(func(err error) {
+ errs <- fmt.Errorf("Error building %s: %v", product.config.TargetProduct(), err)
+ })
+
+ buildWhat := 0
+ if !*onlyConfig {
+ buildWhat |= build.BuildSoong
+ if !*onlySoong {
+ buildWhat |= build.BuildKati
+ }
+ }
+ build.Build(product.ctx, product.config, buildWhat)
+ if !*keep {
+ // TODO: kati aborts from opendir while setting up the find emulator
+ //os.RemoveAll(product.config.OutDir())
+ }
+ log.Println("Finished running for", product.config.TargetProduct())
+ }()
+ }
+ }()
+ }
+ go func() {
+ wg2.Wait()
+ close(errs)
+ }()
+
+ for err := range errs {
+ failed = true
+ log.Print(err)
+ }
+
+ if failed {
+ log.Fatalln("Failed")
+ }
+}
diff --git a/cmd/soong_ui/Android.bp b/cmd/soong_ui/Android.bp
new file mode 100644
index 0000000..f09e42e
--- /dev/null
+++ b/cmd/soong_ui/Android.bp
@@ -0,0 +1,25 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+blueprint_go_binary {
+ name: "soong_ui",
+ deps: [
+ "soong-ui-build",
+ "soong-ui-logger",
+ "soong-ui-tracer",
+ ],
+ srcs: [
+ "main.go",
+ ],
+}
diff --git a/cmd/soong_ui/main.go b/cmd/soong_ui/main.go
new file mode 100644
index 0000000..de941f4
--- /dev/null
+++ b/cmd/soong_ui/main.go
@@ -0,0 +1,87 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "android/soong/ui/build"
+ "android/soong/ui/logger"
+ "android/soong/ui/tracer"
+)
+
+func indexList(s string, list []string) int {
+ for i, l := range list {
+ if l == s {
+ return i
+ }
+ }
+
+ return -1
+}
+
+func inList(s string, list []string) bool {
+ return indexList(s, list) != -1
+}
+
+func main() {
+ log := logger.New(os.Stderr)
+ defer log.Cleanup()
+
+ if len(os.Args) < 2 || !inList("--make-mode", os.Args) {
+ log.Fatalln("The `soong` native UI is not yet available.")
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ trace := tracer.New(log)
+ defer trace.Close()
+
+ build.SetupSignals(log, cancel, func() {
+ trace.Close()
+ log.Cleanup()
+ })
+
+ buildCtx := build.Context{&build.ContextImpl{
+ Context: ctx,
+ Logger: log,
+ Tracer: trace,
+ StdioInterface: build.StdioImpl{},
+ }}
+ config := build.NewConfig(buildCtx, os.Args[1:]...)
+
+ log.SetVerbose(config.IsVerbose())
+ build.SetupOutDir(buildCtx, config)
+ log.SetOutput(filepath.Join(config.OutDir(), "build.log"))
+ trace.SetOutput(filepath.Join(config.OutDir(), "build.trace"))
+
+ if start, ok := os.LookupEnv("TRACE_BEGIN_SOONG"); ok {
+ if !strings.HasSuffix(start, "N") {
+ if start_time, err := strconv.ParseUint(start, 10, 64); err == nil {
+ log.Verbosef("Took %dms to start up.",
+ time.Since(time.Unix(0, int64(start_time))).Nanoseconds()/time.Millisecond.Nanoseconds())
+ buildCtx.CompleteTrace("startup", start_time, uint64(time.Now().UnixNano()))
+ }
+ }
+ }
+
+ build.Build(buildCtx, config, build.BuildAll)
+}
diff --git a/soong_ui.bash b/soong_ui.bash
new file mode 100755
index 0000000..724d9c5
--- /dev/null
+++ b/soong_ui.bash
@@ -0,0 +1,101 @@
+#!/bin/bash -eu
+#
+# Copyright 2017 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# To track how long we took to startup. %N isn't supported on Darwin, but
+# that's detected in the Go code, and skip calculating the startup time.
+export TRACE_BEGIN_SOONG=$(date +%s%N)
+
+# Function to find top of the source tree (if $TOP isn't set) by walking up the
+# tree.
+function gettop
+{
+ local TOPFILE=build/soong/root.bp
+ if [ -z "${TOP-}" -a -f "${TOP-}/${TOPFILE}" ] ; then
+ # The following circumlocution ensures we remove symlinks from TOP.
+ (cd $TOP; PWD= /bin/pwd)
+ else
+ if [ -f $TOPFILE ] ; then
+ # The following circumlocution (repeated below as well) ensures
+ # that we record the true directory name and not one that is
+ # faked up with symlink names.
+ PWD= /bin/pwd
+ else
+ local HERE=$PWD
+ T=
+ while [ \( ! \( -f $TOPFILE \) \) -a \( $PWD != "/" \) ]; do
+ \cd ..
+ T=`PWD= /bin/pwd -P`
+ done
+ \cd $HERE
+ if [ -f "$T/$TOPFILE" ]; then
+ echo $T
+ fi
+ fi
+ fi
+}
+
+# Bootstrap microfactory from source if necessary and use it to build the
+# soong_ui binary, then run soong_ui.
+function run_go
+{
+ # Increment when microfactory changes enough that it cannot rebuild itself.
+ # For example, if we use a new command line argument that doesn't work on older versions.
+ local mf_version=1
+
+ local mf_src="${TOP}/build/soong/cmd/microfactory"
+
+ local out_dir="${OUT_DIR:-${TOP}/out}"
+ local mf_bin="${out_dir}/microfactory_$(uname)"
+ local mf_version_file="${out_dir}/.microfactory_$(uname)_version"
+ local soong_ui_bin="${out_dir}/soong_ui"
+ local from_src=1
+
+ if [ -f "${mf_bin}" ] && [ -f "${mf_version_file}" ]; then
+ if [ "${mf_version}" -eq "$(cat "${mf_version_file}")" ]; then
+ from_src=0
+ fi
+ fi
+
+ local mf_cmd
+ if [ $from_src -eq 1 ]; then
+ mf_cmd="${GOROOT}/bin/go run ${mf_src}/microfactory.go"
+ else
+ mf_cmd="${mf_bin}"
+ fi
+
+ ${mf_cmd} -s "${mf_src}" -b "${mf_bin}" \
+ -pkg-path "android/soong=${TOP}/build/soong" -trimpath "${TOP}/build/soong" \
+ -o "${soong_ui_bin}" android/soong/cmd/soong_ui
+
+ if [ $from_src -eq 1 ]; then
+ echo "${mf_version}" >"${mf_version_file}"
+ fi
+
+ exec "${out_dir}/soong_ui" "$@"
+}
+
+export TOP=$(gettop)
+case $(uname) in
+ Linux)
+ export GOROOT="${TOP}/prebuilts/go/linux-x86/"
+ ;;
+ Darwin)
+ export GOROOT="${TOP}/prebuilts/go/darwin-x86/"
+ ;;
+ *) echo "unknown OS:" $(uname) >&2 && exit 1;;
+esac
+
+run_go "$@"
diff --git a/ui/build/Android.bp b/ui/build/Android.bp
new file mode 100644
index 0000000..e4044e1
--- /dev/null
+++ b/ui/build/Android.bp
@@ -0,0 +1,37 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+bootstrap_go_package {
+ name: "soong-ui-build",
+ pkgPath: "android/soong/ui/build",
+ deps: [
+ "soong-ui-logger",
+ "soong-ui-tracer",
+ ],
+ srcs: [
+ "build.go",
+ "config.go",
+ "context.go",
+ "environment.go",
+ "kati.go",
+ "make.go",
+ "ninja.go",
+ "signal.go",
+ "soong.go",
+ "util.go",
+ ],
+ testSrcs: [
+ "environment_test.go",
+ ],
+}
diff --git a/ui/build/build.go b/ui/build/build.go
new file mode 100644
index 0000000..506ff51
--- /dev/null
+++ b/ui/build/build.go
@@ -0,0 +1,105 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+ "os"
+ "os/exec"
+ "path/filepath"
+ "text/template"
+)
+
+// Ensures the out directory exists, and has the proper files to prevent kati
+// from recursing into it.
+func SetupOutDir(ctx Context, config Config) {
+ ensureEmptyFileExists(ctx, filepath.Join(config.OutDir(), "Android.mk"))
+ ensureEmptyFileExists(ctx, filepath.Join(config.OutDir(), "CleanSpec.mk"))
+ ensureEmptyFileExists(ctx, filepath.Join(config.SoongOutDir(), ".soong.in_make"))
+ // The ninja_build file is used by our buildbots to understand that the output
+ // can be parsed as ninja output.
+ ensureEmptyFileExists(ctx, filepath.Join(config.OutDir(), "ninja_build"))
+}
+
+var combinedBuildNinjaTemplate = template.Must(template.New("combined").Parse(`
+builddir = {{.OutDir}}
+include {{.KatiNinjaFile}}
+include {{.SoongNinjaFile}}
+build {{.CombinedNinjaFile}}: phony {{.SoongNinjaFile}}
+`))
+
+func createCombinedBuildNinjaFile(ctx Context, config Config) {
+ file, err := os.Create(config.CombinedNinjaFile())
+ if err != nil {
+ ctx.Fatalln("Failed to create combined ninja file:", err)
+ }
+ defer file.Close()
+
+ if err := combinedBuildNinjaTemplate.Execute(file, config); err != nil {
+ ctx.Fatalln("Failed to write combined ninja file:", err)
+ }
+}
+
+const (
+ BuildNone = iota
+ BuildProductConfig = 1 << iota
+ BuildSoong = 1 << iota
+ BuildKati = 1 << iota
+ BuildNinja = 1 << iota
+ BuildAll = BuildProductConfig | BuildSoong | BuildKati | BuildNinja
+)
+
+// Build the tree. The 'what' argument can be used to chose which components of
+// the build to run.
+func Build(ctx Context, config Config, what int) {
+ ctx.Verboseln("Starting build with args:", config.Arguments())
+ ctx.Verboseln("Environment:", config.Environment().Environ())
+
+ if inList("help", config.Arguments()) {
+ cmd := exec.CommandContext(ctx.Context, "make", "-f", "build/core/help.mk")
+ cmd.Env = config.Environment().Environ()
+ cmd.Stdout = ctx.Stdout()
+ cmd.Stderr = ctx.Stderr()
+ if err := cmd.Run(); err != nil {
+ ctx.Fatalln("Failed to run make:", err)
+ }
+ return
+ }
+
+ SetupOutDir(ctx, config)
+
+ if what&BuildProductConfig != 0 {
+ // Run make for product config
+ runMakeProductConfig(ctx, config)
+ }
+
+ if what&BuildSoong != 0 {
+ // Run Soong
+ runSoongBootstrap(ctx, config)
+ runSoong(ctx, config)
+ }
+
+ if what&BuildKati != 0 {
+ // Run ckati
+ runKati(ctx, config)
+ }
+
+ if what&BuildNinja != 0 {
+ // Write combined ninja file
+ createCombinedBuildNinjaFile(ctx, config)
+
+ // Run ninja
+ runNinja(ctx, config)
+ }
+}
diff --git a/ui/build/config.go b/ui/build/config.go
new file mode 100644
index 0000000..35c5213
--- /dev/null
+++ b/ui/build/config.go
@@ -0,0 +1,286 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+ "log"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+)
+
+type Config struct{ *configImpl }
+
+type configImpl struct {
+ // From the environment
+ arguments []string
+ goma bool
+ environ *Environment
+
+ // From the arguments
+ parallel int
+ keepGoing int
+ verbose bool
+
+ // From the product config
+ katiArgs []string
+ ninjaArgs []string
+ katiSuffix string
+}
+
+const srcDirFileCheck = "build/soong/root.bp"
+
+func NewConfig(ctx Context, args ...string) Config {
+ ret := &configImpl{
+ environ: OsEnvironment(),
+ }
+
+ ret.environ.Unset(
+ // We're already using it
+ "USE_SOONG_UI",
+
+ // We should never use GOROOT/GOPATH from the shell environment
+ "GOROOT",
+ "GOPATH",
+
+ // These should only come from Soong, not the environment.
+ "CLANG",
+ "CLANG_CXX",
+ "CCC_CC",
+ "CCC_CXX",
+
+ // Used by the goma compiler wrapper, but should only be set by
+ // gomacc
+ "GOMACC_PATH",
+ )
+
+ // Tell python not to spam the source tree with .pyc files.
+ ret.environ.Set("PYTHONDONTWRITEBYTECODE", "1")
+
+ // Sane default matching ninja
+ ret.parallel = runtime.NumCPU() + 2
+ ret.keepGoing = 1
+
+ // Precondition: the current directory is the top of the source tree
+ if _, err := os.Stat(srcDirFileCheck); err != nil {
+ if os.IsNotExist(err) {
+ log.Fatalf("Current working directory must be the source tree. %q not found", srcDirFileCheck)
+ }
+ log.Fatalln("Error verifying tree state:", err)
+ }
+
+ for _, arg := range args {
+ arg = strings.TrimSpace(arg)
+ if arg == "--make-mode" {
+ continue
+ } else if arg == "showcommands" {
+ ret.verbose = true
+ continue
+ }
+ if arg[0] == '-' {
+ var err error
+ if arg[1] == 'j' {
+ // TODO: handle space between j and number
+ // Unnecessary if used with makeparallel
+ ret.parallel, err = strconv.Atoi(arg[2:])
+ } else if arg[1] == 'k' {
+ // TODO: handle space between k and number
+ // Unnecessary if used with makeparallel
+ ret.keepGoing, err = strconv.Atoi(arg[2:])
+ } else {
+ ctx.Fatalln("Unknown option:", arg)
+ }
+ if err != nil {
+ ctx.Fatalln("Argument error:", err, arg)
+ }
+ } else {
+ ret.arguments = append(ret.arguments, arg)
+ }
+ }
+
+ return Config{ret}
+}
+
+// Lunch configures the environment for a specific product similarly to the
+// `lunch` bash function.
+func (c *configImpl) Lunch(ctx Context, product, variant string) {
+ if variant != "eng" && variant != "userdebug" && variant != "user" {
+ ctx.Fatalf("Invalid variant %q. Must be one of 'user', 'userdebug' or 'eng'", variant)
+ }
+
+ c.environ.Set("TARGET_PRODUCT", product)
+ c.environ.Set("TARGET_BUILD_VARIANT", variant)
+ c.environ.Set("TARGET_BUILD_TYPE", "release")
+ c.environ.Unset("TARGET_BUILD_APPS")
+}
+
+// Tapas configures the environment to build one or more unbundled apps,
+// similarly to the `tapas` bash function.
+func (c *configImpl) Tapas(ctx Context, apps []string, arch, variant string) {
+ if len(apps) == 0 {
+ apps = []string{"all"}
+ }
+ if variant == "" {
+ variant = "eng"
+ }
+
+ if variant != "eng" && variant != "userdebug" && variant != "user" {
+ ctx.Fatalf("Invalid variant %q. Must be one of 'user', 'userdebug' or 'eng'", variant)
+ }
+
+ var product string
+ switch arch {
+ case "armv5":
+ product = "generic_armv5"
+ case "arm", "":
+ product = "aosp_arm"
+ case "arm64":
+ product = "aosm_arm64"
+ case "mips":
+ product = "aosp_mips"
+ case "mips64":
+ product = "aosp_mips64"
+ case "x86":
+ product = "aosp_x86"
+ case "x86_64":
+ product = "aosp_x86_64"
+ default:
+ ctx.Fatalf("Invalid architecture: %q", arch)
+ }
+
+ c.environ.Set("TARGET_PRODUCT", product)
+ c.environ.Set("TARGET_BUILD_VARIANT", variant)
+ c.environ.Set("TARGET_BUILD_TYPE", "release")
+ c.environ.Set("TARGET_BUILD_APPS", strings.Join(apps, " "))
+}
+
+func (c *configImpl) Environment() *Environment {
+ return c.environ
+}
+
+func (c *configImpl) Arguments() []string {
+ return c.arguments
+}
+
+func (c *configImpl) OutDir() string {
+ if outDir, ok := c.environ.Get("OUT_DIR"); ok {
+ return outDir
+ }
+ return "out"
+}
+
+func (c *configImpl) NinjaArgs() []string {
+ return c.ninjaArgs
+}
+
+func (c *configImpl) SoongOutDir() string {
+ return filepath.Join(c.OutDir(), "soong")
+}
+
+func (c *configImpl) KatiSuffix() string {
+ if c.katiSuffix != "" {
+ return c.katiSuffix
+ }
+ panic("SetKatiSuffix has not been called")
+}
+
+func (c *configImpl) IsVerbose() bool {
+ return c.verbose
+}
+
+func (c *configImpl) TargetProduct() string {
+ if v, ok := c.environ.Get("TARGET_PRODUCT"); ok {
+ return v
+ }
+ panic("TARGET_PRODUCT is not defined")
+}
+
+func (c *configImpl) KatiArgs() []string {
+ return c.katiArgs
+}
+
+func (c *configImpl) Parallel() int {
+ return c.parallel
+}
+
+func (c *configImpl) UseGoma() bool {
+ if v, ok := c.environ.Get("USE_GOMA"); ok {
+ v = strings.TrimSpace(v)
+ if v != "" && v != "false" {
+ return true
+ }
+ }
+ return false
+}
+
+// RemoteParallel controls how many remote jobs (i.e., commands which contain
+// gomacc) are run in parallel. Note the paralleism of all other jobs is
+// still limited by Parallel()
+func (c *configImpl) RemoteParallel() int {
+ if v, ok := c.environ.Get("NINJA_REMOTE_NUM_JOBS"); ok {
+ if i, err := strconv.Atoi(v); err == nil {
+ return i
+ }
+ }
+ return 500
+}
+
+func (c *configImpl) SetKatiArgs(args []string) {
+ c.katiArgs = args
+}
+
+func (c *configImpl) SetNinjaArgs(args []string) {
+ c.ninjaArgs = args
+}
+
+func (c *configImpl) SetKatiSuffix(suffix string) {
+ c.katiSuffix = suffix
+}
+
+func (c *configImpl) KatiEnvFile() string {
+ return filepath.Join(c.OutDir(), "env"+c.KatiSuffix()+".sh")
+}
+
+func (c *configImpl) KatiNinjaFile() string {
+ return filepath.Join(c.OutDir(), "build"+c.KatiSuffix()+".ninja")
+}
+
+func (c *configImpl) SoongNinjaFile() string {
+ return filepath.Join(c.SoongOutDir(), "build.ninja")
+}
+
+func (c *configImpl) CombinedNinjaFile() string {
+ return filepath.Join(c.OutDir(), "combined"+c.KatiSuffix()+".ninja")
+}
+
+func (c *configImpl) SoongAndroidMk() string {
+ return filepath.Join(c.SoongOutDir(), "Android-"+c.TargetProduct()+".mk")
+}
+
+func (c *configImpl) SoongMakeVarsMk() string {
+ return filepath.Join(c.SoongOutDir(), "make_vars-"+c.TargetProduct()+".mk")
+}
+
+func (c *configImpl) HostPrebuiltTag() string {
+ if runtime.GOOS == "linux" {
+ return "linux-x86"
+ } else if runtime.GOOS == "darwin" {
+ return "darwin-x86"
+ } else {
+ panic("Unsupported OS")
+ }
+}
diff --git a/ui/build/context.go b/ui/build/context.go
new file mode 100644
index 0000000..8144e58
--- /dev/null
+++ b/ui/build/context.go
@@ -0,0 +1,97 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+ "context"
+ "io"
+ "os"
+ "time"
+
+ "android/soong/ui/logger"
+ "android/soong/ui/tracer"
+)
+
+type StdioInterface interface {
+ Stdin() io.Reader
+ Stdout() io.Writer
+ Stderr() io.Writer
+}
+
+type StdioImpl struct{}
+
+func (StdioImpl) Stdin() io.Reader { return os.Stdin }
+func (StdioImpl) Stdout() io.Writer { return os.Stdout }
+func (StdioImpl) Stderr() io.Writer { return os.Stderr }
+
+var _ StdioInterface = StdioImpl{}
+
+type customStdio struct {
+ stdin io.Reader
+ stdout io.Writer
+ stderr io.Writer
+}
+
+func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface {
+ return customStdio{stdin, stdout, stderr}
+}
+
+func (c customStdio) Stdin() io.Reader { return c.stdin }
+func (c customStdio) Stdout() io.Writer { return c.stdout }
+func (c customStdio) Stderr() io.Writer { return c.stderr }
+
+var _ StdioInterface = customStdio{}
+
+// Context combines a context.Context, logger.Logger, and StdIO redirection.
+// These all are agnostic of the current build, and may be used for multiple
+// builds, while the Config objects contain per-build information.
+type Context struct{ *ContextImpl }
+type ContextImpl struct {
+ context.Context
+ logger.Logger
+
+ StdioInterface
+
+ Thread tracer.Thread
+ Tracer tracer.Tracer
+}
+
+// BeginTrace starts a new Duration Event.
+func (c ContextImpl) BeginTrace(name string) {
+ if c.Tracer != nil {
+ c.Tracer.Begin(name, c.Thread)
+ }
+}
+
+// EndTrace finishes the last Duration Event.
+func (c ContextImpl) EndTrace() {
+ if c.Tracer != nil {
+ c.Tracer.End(c.Thread)
+ }
+}
+
+// CompleteTrace writes a trace with a beginning and end times.
+func (c ContextImpl) CompleteTrace(name string, begin, end uint64) {
+ if c.Tracer != nil {
+ c.Tracer.Complete(name, c.Thread, begin, end)
+ }
+}
+
+// ImportNinjaLog imports a .ninja_log file into the tracer.
+func (c ContextImpl) ImportNinjaLog(filename string, startOffset time.Time) {
+ if c.Tracer != nil {
+ c.Tracer.ImportNinjaLog(c.Thread, filename, startOffset)
+ }
+}
diff --git a/ui/build/environment.go b/ui/build/environment.go
new file mode 100644
index 0000000..baab101
--- /dev/null
+++ b/ui/build/environment.go
@@ -0,0 +1,152 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+)
+
+// Environment adds a number of useful manipulation functions to the list of
+// strings returned by os.Environ() and used in exec.Cmd.Env.
+type Environment []string
+
+// OsEnvironment wraps the current environment returned by os.Environ()
+func OsEnvironment() *Environment {
+ env := Environment(os.Environ())
+ return &env
+}
+
+// Get returns the value associated with the key, and whether it exists.
+// It's equivalent to the os.LookupEnv function, but with this copy of the
+// Environment.
+func (e *Environment) Get(key string) (string, bool) {
+ for _, env := range *e {
+ if k, v, ok := decodeKeyValue(env); ok && k == key {
+ return v, true
+ }
+ }
+ return "", false
+}
+
+// Set sets the value associated with the key, overwriting the current value
+// if it exists.
+func (e *Environment) Set(key, value string) {
+ e.Unset(key)
+ *e = append(*e, key+"="+value)
+}
+
+// Unset removes the specified keys from the Environment.
+func (e *Environment) Unset(keys ...string) {
+ out := (*e)[:0]
+ for _, env := range *e {
+ if key, _, ok := decodeKeyValue(env); ok && inList(key, keys) {
+ continue
+ }
+ out = append(out, env)
+ }
+ *e = out
+}
+
+// Environ returns the []string required for exec.Cmd.Env
+func (e *Environment) Environ() []string {
+ return []string(*e)
+}
+
+// Copy returns a copy of the Environment so that independent changes may be made.
+func (e *Environment) Copy() *Environment {
+ ret := Environment(make([]string, len(*e)))
+ for i, v := range *e {
+ ret[i] = v
+ }
+ return &ret
+}
+
+// IsTrue returns whether an environment variable is set to a positive value (1,y,yes,on,true)
+func (e *Environment) IsEnvTrue(key string) bool {
+ if value, ok := e.Get(key); ok {
+ return value == "1" || value == "y" || value == "yes" || value == "on" || value == "true"
+ }
+ return false
+}
+
+// IsFalse returns whether an environment variable is set to a negative value (0,n,no,off,false)
+func (e *Environment) IsFalse(key string) bool {
+ if value, ok := e.Get(key); ok {
+ return value == "0" || value == "n" || value == "no" || value == "off" || value == "false"
+ }
+ return false
+}
+
+// AppendFromKati reads a shell script written by Kati that exports or unsets
+// environment variables, and applies those to the local Environment.
+func (e *Environment) AppendFromKati(filename string) error {
+ file, err := os.Open(filename)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ return e.appendFromKati(file)
+}
+
+func (e *Environment) appendFromKati(reader io.Reader) error {
+ scanner := bufio.NewScanner(reader)
+ for scanner.Scan() {
+ text := strings.TrimSpace(scanner.Text())
+
+ if len(text) == 0 || text[0] == '#' {
+ continue
+ }
+
+ cmd := strings.SplitN(text, " ", 2)
+ if len(cmd) != 2 {
+ return fmt.Errorf("Unknown kati environment line: %q", text)
+ }
+
+ if cmd[0] == "unset" {
+ str, ok := singleUnquote(cmd[1])
+ if !ok {
+ fmt.Errorf("Failed to unquote kati line: %q", text)
+ }
+ e.Unset(str)
+ } else if cmd[0] == "export" {
+ key, value, ok := decodeKeyValue(cmd[1])
+ if !ok {
+ return fmt.Errorf("Failed to parse export: %v", cmd)
+ }
+
+ key, ok = singleUnquote(key)
+ if !ok {
+ return fmt.Errorf("Failed to unquote kati line: %q", text)
+ }
+ value, ok = singleUnquote(value)
+ if !ok {
+ return fmt.Errorf("Failed to unquote kati line: %q", text)
+ }
+
+ e.Set(key, value)
+ } else {
+ return fmt.Errorf("Unknown kati environment command: %q", text)
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/ui/build/environment_test.go b/ui/build/environment_test.go
new file mode 100644
index 0000000..0294dac
--- /dev/null
+++ b/ui/build/environment_test.go
@@ -0,0 +1,80 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func TestEnvUnset(t *testing.T) {
+ initial := &Environment{"TEST=1", "TEST2=0"}
+ initial.Unset("TEST")
+ got := initial.Environ()
+ if len(got) != 1 || got[0] != "TEST2=0" {
+ t.Errorf("Expected [TEST2=0], got: %v", got)
+ }
+}
+
+func TestEnvUnsetMissing(t *testing.T) {
+ initial := &Environment{"TEST2=0"}
+ initial.Unset("TEST")
+ got := initial.Environ()
+ if len(got) != 1 || got[0] != "TEST2=0" {
+ t.Errorf("Expected [TEST2=0], got: %v", got)
+ }
+}
+
+func TestEnvSet(t *testing.T) {
+ initial := &Environment{}
+ initial.Set("TEST", "0")
+ got := initial.Environ()
+ if len(got) != 1 || got[0] != "TEST=0" {
+ t.Errorf("Expected [TEST=0], got: %v", got)
+ }
+}
+
+func TestEnvSetDup(t *testing.T) {
+ initial := &Environment{"TEST=1"}
+ initial.Set("TEST", "0")
+ got := initial.Environ()
+ if len(got) != 1 || got[0] != "TEST=0" {
+ t.Errorf("Expected [TEST=0], got: %v", got)
+ }
+}
+
+const testKatiEnvFileContents = `#!/bin/sh
+# Generated by kati unknown
+
+unset 'CLANG'
+export 'BUILD_ID'='NYC'
+`
+
+func TestEnvAppendFromKati(t *testing.T) {
+ initial := &Environment{"CLANG=/usr/bin/clang", "TEST=0"}
+ err := initial.appendFromKati(strings.NewReader(testKatiEnvFileContents))
+ if err != nil {
+ t.Fatalf("Unexpected error from %v", err)
+ }
+
+ got := initial.Environ()
+ expected := []string{"TEST=0", "BUILD_ID=NYC"}
+ if !reflect.DeepEqual(got, expected) {
+ t.Errorf("Environment list does not match")
+ t.Errorf("expected: %v", expected)
+ t.Errorf(" got: %v", got)
+ }
+}
diff --git a/ui/build/kati.go b/ui/build/kati.go
new file mode 100644
index 0000000..423bcbc
--- /dev/null
+++ b/ui/build/kati.go
@@ -0,0 +1,107 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+ "crypto/md5"
+ "fmt"
+ "io/ioutil"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+)
+
+var spaceSlashReplacer = strings.NewReplacer("/", "_", " ", "_")
+
+// genKatiSuffix creates a suffix for kati-generated files so that we can cache
+// them based on their inputs. So this should encode all common changes to Kati
+// inputs. Currently that includes the TARGET_PRODUCT, kati-processed command
+// line arguments, and the directories specified by mm/mmm.
+func genKatiSuffix(ctx Context, config Config) {
+ katiSuffix := "-" + config.TargetProduct()
+ if args := config.KatiArgs(); len(args) > 0 {
+ katiSuffix += "-" + spaceSlashReplacer.Replace(strings.Join(args, "_"))
+ }
+ if oneShot, ok := config.Environment().Get("ONE_SHOT_MAKEFILE"); ok {
+ katiSuffix += "-" + spaceSlashReplacer.Replace(oneShot)
+ }
+
+ // If the suffix is too long, replace it with a md5 hash and write a
+ // file that contains the original suffix.
+ if len(katiSuffix) > 64 {
+ shortSuffix := "-" + fmt.Sprintf("%x", md5.Sum([]byte(katiSuffix)))
+ config.SetKatiSuffix(shortSuffix)
+
+ ctx.Verbosef("Kati ninja suffix too long: %q", katiSuffix)
+ ctx.Verbosef("Replacing with: %q", shortSuffix)
+
+ if err := ioutil.WriteFile(strings.TrimSuffix(config.KatiNinjaFile(), "ninja")+"suf", []byte(katiSuffix), 0777); err != nil {
+ ctx.Println("Error writing suffix file:", err)
+ }
+ } else {
+ config.SetKatiSuffix(katiSuffix)
+ }
+}
+
+func runKati(ctx Context, config Config) {
+ ctx.BeginTrace("kati")
+ defer ctx.EndTrace()
+
+ genKatiSuffix(ctx, config)
+
+ executable := "prebuilts/build-tools/" + config.HostPrebuiltTag() + "/bin/ckati"
+ args := []string{
+ "--ninja",
+ "--ninja_dir=" + config.OutDir(),
+ "--ninja_suffix=" + config.KatiSuffix(),
+ "--regen",
+ "--ignore_optional_include=" + filepath.Join(config.OutDir(), "%.P"),
+ "--detect_android_echo",
+ }
+
+ if !config.Environment().IsFalse("KATI_EMULATE_FIND") {
+ args = append(args, "--use_find_emulator")
+ }
+
+ // The argument order could be simplified, but currently this matches
+ // the ordering in Make
+ args = append(args, "-f", "build/core/main.mk")
+
+ args = append(args, config.KatiArgs()...)
+
+ args = append(args,
+ "--gen_all_targets",
+ "BUILDING_WITH_NINJA=true",
+ "SOONG_ANDROID_MK="+config.SoongAndroidMk(),
+ "SOONG_MAKEVARS_MK="+config.SoongMakeVarsMk())
+
+ if config.UseGoma() {
+ args = append(args, "-j"+strconv.Itoa(config.Parallel()))
+ }
+
+ cmd := exec.CommandContext(ctx.Context, executable, args...)
+ cmd.Env = config.Environment().Environ()
+ cmd.Stdout = ctx.Stdout()
+ cmd.Stderr = ctx.Stderr()
+ ctx.Verboseln(cmd.Path, cmd.Args)
+ if err := cmd.Run(); err != nil {
+ if e, ok := err.(*exec.ExitError); ok {
+ ctx.Fatalln("ckati failed with:", e.ProcessState.String())
+ } else {
+ ctx.Fatalln("Failed to run ckati:", err)
+ }
+ }
+}
diff --git a/ui/build/make.go b/ui/build/make.go
new file mode 100644
index 0000000..89e03f7
--- /dev/null
+++ b/ui/build/make.go
@@ -0,0 +1,163 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+ "fmt"
+ "os/exec"
+ "path/filepath"
+ "strings"
+)
+
+// DumpMakeVars can be used to extract the values of Make variables after the
+// product configurations are loaded. This is roughly equivalent to the
+// `get_build_var` bash function.
+//
+// goals can be used to set MAKECMDGOALS, which emulates passing arguments to
+// Make without actually building them. So all the variables based on
+// MAKECMDGOALS can be read.
+//
+// extra_targets adds real arguments to the make command, in case other targets
+// actually need to be run (like the Soong config generator).
+//
+// vars is the list of variables to read. The values will be put in the
+// returned map.
+func DumpMakeVars(ctx Context, config Config, goals, extra_targets, vars []string) (map[string]string, error) {
+ ctx.BeginTrace("dumpvars")
+ defer ctx.EndTrace()
+
+ cmd := exec.CommandContext(ctx.Context,
+ "make",
+ "--no-print-directory",
+ "-f", "build/core/config.mk",
+ "dump-many-vars",
+ "CALLED_FROM_SETUP=true",
+ "BUILD_SYSTEM=build/core",
+ "MAKECMDGOALS="+strings.Join(goals, " "),
+ "DUMP_MANY_VARS="+strings.Join(vars, " "),
+ "OUT_DIR="+config.OutDir())
+ cmd.Env = config.Environment().Environ()
+ cmd.Args = append(cmd.Args, extra_targets...)
+ // TODO: error out when Stderr contains any content
+ cmd.Stderr = ctx.Stderr()
+ ctx.Verboseln(cmd.Path, cmd.Args)
+ output, err := cmd.Output()
+ if err != nil {
+ return nil, err
+ }
+
+ ret := make(map[string]string, len(vars))
+ for _, line := range strings.Split(string(output), "\n") {
+ if len(line) == 0 {
+ continue
+ }
+
+ if key, value, ok := decodeKeyValue(line); ok {
+ if value, ok = singleUnquote(value); ok {
+ ret[key] = value
+ ctx.Verboseln(key, value)
+ } else {
+ return nil, fmt.Errorf("Failed to parse make line: %q", line)
+ }
+ } else {
+ return nil, fmt.Errorf("Failed to parse make line: %q", line)
+ }
+ }
+
+ return ret, nil
+}
+
+func runMakeProductConfig(ctx Context, config Config) {
+ // Variables to export into the environment of Kati/Ninja
+ exportEnvVars := []string{
+ // So that we can use the correct TARGET_PRODUCT if it's been
+ // modified by PRODUCT-* arguments
+ "TARGET_PRODUCT",
+
+ // compiler wrappers set up by make
+ "CC_WRAPPER",
+ "CXX_WRAPPER",
+
+ // ccache settings
+ "CCACHE_COMPILERCHECK",
+ "CCACHE_SLOPPINESS",
+ "CCACHE_BASEDIR",
+ "CCACHE_CPP2",
+ }
+
+ // Variables to print out in the top banner
+ bannerVars := []string{
+ "PLATFORM_VERSION_CODENAME",
+ "PLATFORM_VERSION",
+ "TARGET_PRODUCT",
+ "TARGET_BUILD_VARIANT",
+ "TARGET_BUILD_TYPE",
+ "TARGET_BUILD_APPS",
+ "TARGET_ARCH",
+ "TARGET_ARCH_VARIANT",
+ "TARGET_CPU_VARIANT",
+ "TARGET_2ND_ARCH",
+ "TARGET_2ND_ARCH_VARIANT",
+ "TARGET_2ND_CPU_VARIANT",
+ "HOST_ARCH",
+ "HOST_2ND_ARCH",
+ "HOST_OS",
+ "HOST_OS_EXTRA",
+ "HOST_CROSS_OS",
+ "HOST_CROSS_ARCH",
+ "HOST_CROSS_2ND_ARCH",
+ "HOST_BUILD_TYPE",
+ "BUILD_ID",
+ "OUT_DIR",
+ "AUX_OS_VARIANT_LIST",
+ "TARGET_BUILD_PDK",
+ "PDK_FUSION_PLATFORM_ZIP",
+ }
+
+ allVars := append(append([]string{
+ // Used to execute Kati and Ninja
+ "NINJA_GOALS",
+ "KATI_GOALS",
+ }, exportEnvVars...), bannerVars...)
+
+ make_vars, err := DumpMakeVars(ctx, config, config.Arguments(), []string{
+ filepath.Join(config.SoongOutDir(), "soong.variables"),
+ }, allVars)
+ if err != nil {
+ ctx.Fatalln("Error dumping make vars:", err)
+ }
+
+ // Print the banner like make does
+ fmt.Fprintln(ctx.Stdout(), "============================================")
+ for _, name := range bannerVars {
+ if make_vars[name] != "" {
+ fmt.Fprintf(ctx.Stdout(), "%s=%s\n", name, make_vars[name])
+ }
+ }
+ fmt.Fprintln(ctx.Stdout(), "============================================")
+
+ // Populate the environment
+ env := config.Environment()
+ for _, name := range exportEnvVars {
+ if make_vars[name] == "" {
+ env.Unset(name)
+ } else {
+ env.Set(name, make_vars[name])
+ }
+ }
+
+ config.SetKatiArgs(strings.Fields(make_vars["KATI_GOALS"]))
+ config.SetNinjaArgs(strings.Fields(make_vars["NINJA_GOALS"]))
+}
diff --git a/ui/build/ninja.go b/ui/build/ninja.go
new file mode 100644
index 0000000..33f9a07
--- /dev/null
+++ b/ui/build/ninja.go
@@ -0,0 +1,85 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func runNinja(ctx Context, config Config) {
+ ctx.BeginTrace("ninja")
+ defer ctx.EndTrace()
+
+ executable := "prebuilts/build-tools/" + config.HostPrebuiltTag() + "/bin/ninja"
+ args := []string{
+ "-d", "keepdepfile",
+ }
+
+ args = append(args, config.NinjaArgs()...)
+
+ var parallel int
+ if config.UseGoma() {
+ parallel = config.RemoteParallel()
+ } else {
+ parallel = config.Parallel()
+ }
+ args = append(args, "-j", strconv.Itoa(parallel))
+ if config.keepGoing != 1 {
+ args = append(args, "-k", strconv.Itoa(config.keepGoing))
+ }
+
+ args = append(args, "-f", config.CombinedNinjaFile())
+
+ if config.IsVerbose() {
+ args = append(args, "-v")
+ }
+ args = append(args, "-w", "dupbuild=err")
+
+ env := config.Environment().Copy()
+ env.AppendFromKati(config.KatiEnvFile())
+
+ // Allow both NINJA_ARGS and NINJA_EXTRA_ARGS, since both have been
+ // used in the past to specify extra ninja arguments.
+ if extra, ok := env.Get("NINJA_ARGS"); ok {
+ args = append(args, strings.Fields(extra)...)
+ }
+ if extra, ok := env.Get("NINJA_EXTRA_ARGS"); ok {
+ args = append(args, strings.Fields(extra)...)
+ }
+
+ if _, ok := env.Get("NINJA_STATUS"); !ok {
+ env.Set("NINJA_STATUS", "[%p %f/%t] ")
+ }
+
+ cmd := exec.CommandContext(ctx.Context, executable, args...)
+ cmd.Env = env.Environ()
+ cmd.Stdin = ctx.Stdin()
+ cmd.Stdout = ctx.Stdout()
+ cmd.Stderr = ctx.Stderr()
+ ctx.Verboseln(cmd.Path, cmd.Args)
+ startTime := time.Now()
+ defer ctx.ImportNinjaLog(filepath.Join(config.OutDir(), ".ninja_log"), startTime)
+ if err := cmd.Run(); err != nil {
+ if e, ok := err.(*exec.ExitError); ok {
+ ctx.Fatalln("ninja failed with:", e.ProcessState.String())
+ } else {
+ ctx.Fatalln("Failed to run ninja:", err)
+ }
+ }
+}
diff --git a/ui/build/signal.go b/ui/build/signal.go
new file mode 100644
index 0000000..3c8c8e1
--- /dev/null
+++ b/ui/build/signal.go
@@ -0,0 +1,60 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+ "os"
+ "os/signal"
+ "runtime/debug"
+ "syscall"
+
+ "android/soong/ui/logger"
+)
+
+// SetupSignals sets up signal handling to kill our children and allow us to cleanly finish
+// writing our log/trace files.
+//
+// Currently, on the first SIGINT|SIGALARM we call the cancel() function, which is usually
+// the CancelFunc returned by context.WithCancel, which will kill all the commands running
+// within that Context. Usually that's enough, and you'll run through your normal error paths.
+//
+// If another signal comes in after the first one, we'll trigger a panic with full stacktraces
+// from every goroutine so that it's possible to debug what is stuck. Just before the process
+// exits, we'll call the cleanup() function so that you can flush your log files.
+func SetupSignals(log logger.Logger, cancel, cleanup func()) {
+ signals := make(chan os.Signal, 5)
+ // TODO: Handle other signals
+ signal.Notify(signals, os.Interrupt, syscall.SIGALRM)
+ go handleSignals(signals, log, cancel, cleanup)
+}
+
+func handleSignals(signals chan os.Signal, log logger.Logger, cancel, cleanup func()) {
+ defer cleanup()
+
+ var force bool
+
+ for {
+ s := <-signals
+ if force {
+ // So that we can better see what was stuck
+ debug.SetTraceback("all")
+ log.Panicln("Second signal received:", s)
+ } else {
+ log.Println("Got signal:", s)
+ cancel()
+ force = true
+ }
+ }
+}
diff --git a/ui/build/soong.go b/ui/build/soong.go
new file mode 100644
index 0000000..d017e70
--- /dev/null
+++ b/ui/build/soong.go
@@ -0,0 +1,63 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+ "os/exec"
+ "path/filepath"
+)
+
+func runSoongBootstrap(ctx Context, config Config) {
+ ctx.BeginTrace("bootstrap soong")
+ defer ctx.EndTrace()
+
+ cmd := exec.CommandContext(ctx.Context, "./bootstrap.bash")
+ env := config.Environment().Copy()
+ env.Set("BUILDDIR", config.SoongOutDir())
+ cmd.Env = env.Environ()
+ cmd.Stdout = ctx.Stdout()
+ cmd.Stderr = ctx.Stderr()
+ ctx.Verboseln(cmd.Path, cmd.Args)
+ if err := cmd.Run(); err != nil {
+ if e, ok := err.(*exec.ExitError); ok {
+ ctx.Fatalln("soong bootstrap failed with:", e.ProcessState.String())
+ } else {
+ ctx.Fatalln("Failed to run soong bootstrap:", err)
+ }
+ }
+}
+
+func runSoong(ctx Context, config Config) {
+ ctx.BeginTrace("soong")
+ defer ctx.EndTrace()
+
+ cmd := exec.CommandContext(ctx.Context, filepath.Join(config.SoongOutDir(), "soong"), "-w", "dupbuild=err")
+ if config.IsVerbose() {
+ cmd.Args = append(cmd.Args, "-v")
+ }
+ env := config.Environment().Copy()
+ env.Set("SKIP_NINJA", "true")
+ cmd.Env = env.Environ()
+ cmd.Stdout = ctx.Stdout()
+ cmd.Stderr = ctx.Stderr()
+ ctx.Verboseln(cmd.Path, cmd.Args)
+ if err := cmd.Run(); err != nil {
+ if e, ok := err.(*exec.ExitError); ok {
+ ctx.Fatalln("soong bootstrap failed with:", e.ProcessState.String())
+ } else {
+ ctx.Fatalln("Failed to run soong bootstrap:", err)
+ }
+ }
+}
diff --git a/ui/build/util.go b/ui/build/util.go
new file mode 100644
index 0000000..ad084da
--- /dev/null
+++ b/ui/build/util.go
@@ -0,0 +1,79 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// indexList finds the index of a string in a []string
+func indexList(s string, list []string) int {
+ for i, l := range list {
+ if l == s {
+ return i
+ }
+ }
+
+ return -1
+}
+
+// inList determines whether a string is in a []string
+func inList(s string, list []string) bool {
+ return indexList(s, list) != -1
+}
+
+// ensureDirectoriesExist is a shortcut to os.MkdirAll, sending errors to the ctx logger.
+func ensureDirectoriesExist(ctx Context, dirs ...string) {
+ for _, dir := range dirs {
+ err := os.MkdirAll(dir, 0777)
+ if err != nil {
+ ctx.Fatalf("Error creating %s: %q\n", dir, err)
+ }
+ }
+}
+
+// ensureEmptyFileExists ensures that the containing directory exists, and the
+// specified file exists. If it doesn't exist, it will write an empty file.
+func ensureEmptyFileExists(ctx Context, file string) {
+ ensureDirectoriesExist(ctx, filepath.Dir(file))
+ if _, err := os.Stat(file); os.IsNotExist(err) {
+ f, err := os.Create(file)
+ if err != nil {
+ ctx.Fatalf("Error creating %s: %q\n", file, err)
+ }
+ f.Close()
+ } else if err != nil {
+ ctx.Fatalf("Error checking %s: %q\n", file, err)
+ }
+}
+
+// singleUnquote is similar to strconv.Unquote, but can handle multi-character strings inside single quotes.
+func singleUnquote(str string) (string, bool) {
+ if len(str) < 2 || str[0] != '\'' || str[len(str)-1] != '\'' {
+ return "", false
+ }
+ return str[1 : len(str)-1], true
+}
+
+// decodeKeyValue decodes a key=value string
+func decodeKeyValue(str string) (string, string, bool) {
+ idx := strings.IndexRune(str, '=')
+ if idx == -1 {
+ return "", "", false
+ }
+ return str[:idx], str[idx+1:], true
+}
diff --git a/ui/logger/Android.bp b/ui/logger/Android.bp
new file mode 100644
index 0000000..8091ef9
--- /dev/null
+++ b/ui/logger/Android.bp
@@ -0,0 +1,24 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+bootstrap_go_package {
+ name: "soong-ui-logger",
+ pkgPath: "android/soong/ui/logger",
+ srcs: [
+ "logger.go",
+ ],
+ testSrcs: [
+ "logger_test.go",
+ ],
+}
diff --git a/ui/logger/logger.go b/ui/logger/logger.go
new file mode 100644
index 0000000..db7e82a
--- /dev/null
+++ b/ui/logger/logger.go
@@ -0,0 +1,302 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package logger implements a logging package designed for command line
+// utilities. It uses the standard 'log' package and function, but splits
+// output between stderr and a rotating log file.
+//
+// In addition to the standard logger functions, Verbose[f|ln] calls only go to
+// the log file by default, unless SetVerbose(true) has been called.
+//
+// The log file also includes extended date/time/source information, which are
+// omitted from the stderr output for better readability.
+//
+// In order to better handle resource cleanup after a Fatal error, the Fatal
+// functions panic instead of calling os.Exit(). To actually do the cleanup,
+// and prevent the printing of the panic, call defer logger.Cleanup() at the
+// beginning of your main function.
+package logger
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+ "strconv"
+ "sync"
+)
+
+type Logger interface {
+ // Print* prints to both stderr and the file log.
+ // Arguments to Print are handled in the manner of fmt.Print.
+ Print(v ...interface{})
+ // Arguments to Printf are handled in the manner of fmt.Printf
+ Printf(format string, v ...interface{})
+ // Arguments to Println are handled in the manner of fmt.Println
+ Println(v ...interface{})
+
+ // Verbose* is equivalent to Print*, but skips stderr unless the
+ // logger has been configured in verbose mode.
+ Verbose(v ...interface{})
+ Verbosef(format string, v ...interface{})
+ Verboseln(v ...interface{})
+
+ // Fatal* is equivalent to Print* followed by a call to panic that
+ // can be converted to an error using Recover, or will be converted
+ // to a call to os.Exit(1) with a deferred call to Cleanup()
+ Fatal(v ...interface{})
+ Fatalf(format string, v ...interface{})
+ Fatalln(v ...interface{})
+
+ // Panic is equivalent to Print* followed by a call to panic.
+ Panic(v ...interface{})
+ Panicf(format string, v ...interface{})
+ Panicln(v ...interface{})
+
+ // Output writes the string to both stderr and the file log.
+ Output(calldepth int, str string) error
+}
+
+// fatalLog is the type used when Fatal[f|ln]
+type fatalLog error
+
+func fileRotation(from, baseName, ext string, cur, max int) error {
+ newName := baseName + "." + strconv.Itoa(cur) + ext
+
+ if _, err := os.Lstat(newName); err == nil {
+ if cur+1 <= max {
+ fileRotation(newName, baseName, ext, cur+1, max)
+ }
+ }
+
+ if err := os.Rename(from, newName); err != nil {
+ return fmt.Errorf("Failed to rotate", from, "to", newName, ".", err)
+ }
+ return nil
+}
+
+// CreateFileWithRotation returns a new os.File using os.Create, renaming any
+// existing files to <filename>.#.<ext>, keeping up to maxCount files.
+// <filename>.1.<ext> is the most recent backup, <filename>.2.<ext> is the
+// second most recent backup, etc.
+//
+// TODO: This function is not guaranteed to be atomic, if there are multiple
+// users attempting to do the same operation, the result is undefined.
+func CreateFileWithRotation(filename string, maxCount int) (*os.File, error) {
+ if _, err := os.Lstat(filename); err == nil {
+ ext := filepath.Ext(filename)
+ basename := filename[:len(filename)-len(ext)]
+ if err = fileRotation(filename, basename, ext, 1, maxCount); err != nil {
+ return nil, err
+ }
+ }
+
+ return os.Create(filename)
+}
+
+// Recover can be used with defer in a GoRoutine to convert a Fatal panics to
+// an error that can be handled.
+func Recover(fn func(err error)) {
+ p := recover()
+
+ if p == nil {
+ return
+ } else if log, ok := p.(fatalLog); ok {
+ fn(error(log))
+ } else {
+ panic(p)
+ }
+}
+
+type stdLogger struct {
+ stderr *log.Logger
+ verbose bool
+
+ fileLogger *log.Logger
+ mutex sync.Mutex
+ file *os.File
+}
+
+var _ Logger = &stdLogger{}
+
+// New creates a new Logger. The out variable sets the destination, commonly
+// os.Stderr, but it may be a buffer for tests, or a separate log file if
+// the user doesn't need to see the output.
+func New(out io.Writer) *stdLogger {
+ return &stdLogger{
+ stderr: log.New(out, "", log.Ltime),
+ fileLogger: log.New(ioutil.Discard, "", log.Ldate|log.Lmicroseconds|log.Llongfile),
+ }
+}
+
+// SetVerbose controls whether Verbose[f|ln] logs to stderr as well as the
+// file-backed log.
+func (s *stdLogger) SetVerbose(v bool) {
+ s.verbose = v
+}
+
+// SetOutput controls where the file-backed log will be saved. It will keep
+// some number of backups of old log files.
+func (s *stdLogger) SetOutput(path string) {
+ if f, err := CreateFileWithRotation(path, 5); err == nil {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ if s.file != nil {
+ s.file.Close()
+ }
+ s.file = f
+ s.fileLogger.SetOutput(f)
+ } else {
+ s.Fatal(err.Error())
+ }
+}
+
+// Close disables logging to the file and closes the file handle.
+func (s *stdLogger) Close() {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+ if s.file != nil {
+ s.fileLogger.SetOutput(ioutil.Discard)
+ s.file.Close()
+ s.file = nil
+ }
+}
+
+// Cleanup should be used with defer in your main function. It will close the
+// log file and convert any Fatal panics back to os.Exit(1)
+func (s *stdLogger) Cleanup() {
+ fatal := false
+ p := recover()
+
+ if _, ok := p.(fatalLog); ok {
+ fatal = true
+ p = nil
+ } else if p != nil {
+ s.Println(p)
+ }
+
+ s.Close()
+
+ if p != nil {
+ panic(p)
+ } else if fatal {
+ os.Exit(1)
+ }
+}
+
+// Output writes string to both stderr and the file log.
+func (s *stdLogger) Output(calldepth int, str string) error {
+ s.stderr.Output(calldepth+1, str)
+ return s.fileLogger.Output(calldepth+1, str)
+}
+
+// VerboseOutput is equivalent to Output, but only goes to the file log
+// unless SetVerbose(true) has been called.
+func (s *stdLogger) VerboseOutput(calldepth int, str string) error {
+ if s.verbose {
+ s.stderr.Output(calldepth+1, str)
+ }
+ return s.fileLogger.Output(calldepth+1, str)
+}
+
+// Print prints to both stderr and the file log.
+// Arguments are handled in the manner of fmt.Print.
+func (s *stdLogger) Print(v ...interface{}) {
+ output := fmt.Sprint(v...)
+ s.Output(2, output)
+}
+
+// Printf prints to both stderr and the file log.
+// Arguments are handled in the manner of fmt.Printf.
+func (s *stdLogger) Printf(format string, v ...interface{}) {
+ output := fmt.Sprintf(format, v...)
+ s.Output(2, output)
+}
+
+// Println prints to both stderr and the file log.
+// Arguments are handled in the manner of fmt.Println.
+func (s *stdLogger) Println(v ...interface{}) {
+ output := fmt.Sprintln(v...)
+ s.Output(2, output)
+}
+
+// Verbose is equivalent to Print, but only goes to the file log unless
+// SetVerbose(true) has been called.
+func (s *stdLogger) Verbose(v ...interface{}) {
+ output := fmt.Sprint(v...)
+ s.VerboseOutput(2, output)
+}
+
+// Verbosef is equivalent to Printf, but only goes to the file log unless
+// SetVerbose(true) has been called.
+func (s *stdLogger) Verbosef(format string, v ...interface{}) {
+ output := fmt.Sprintf(format, v...)
+ s.VerboseOutput(2, output)
+}
+
+// Verboseln is equivalent to Println, but only goes to the file log unless
+// SetVerbose(true) has been called.
+func (s *stdLogger) Verboseln(v ...interface{}) {
+ output := fmt.Sprintln(v...)
+ s.VerboseOutput(2, output)
+}
+
+// Fatal is equivalent to Print() followed by a call to panic() that
+// Cleanup will convert to a os.Exit(1).
+func (s *stdLogger) Fatal(v ...interface{}) {
+ output := fmt.Sprint(v...)
+ s.Output(2, output)
+ panic(fatalLog(errors.New(output)))
+}
+
+// Fatalf is equivalent to Printf() followed by a call to panic() that
+// Cleanup will convert to a os.Exit(1).
+func (s *stdLogger) Fatalf(format string, v ...interface{}) {
+ output := fmt.Sprintf(format, v...)
+ s.Output(2, output)
+ panic(fatalLog(errors.New(output)))
+}
+
+// Fatalln is equivalent to Println() followed by a call to panic() that
+// Cleanup will convert to a os.Exit(1).
+func (s *stdLogger) Fatalln(v ...interface{}) {
+ output := fmt.Sprintln(v...)
+ s.Output(2, output)
+ panic(fatalLog(errors.New(output)))
+}
+
+// Panic is equivalent to Print() followed by a call to panic().
+func (s *stdLogger) Panic(v ...interface{}) {
+ output := fmt.Sprint(v...)
+ s.Output(2, output)
+ panic(output)
+}
+
+// Panicf is equivalent to Printf() followed by a call to panic().
+func (s *stdLogger) Panicf(format string, v ...interface{}) {
+ output := fmt.Sprintf(format, v...)
+ s.Output(2, output)
+ panic(output)
+}
+
+// Panicln is equivalent to Println() followed by a call to panic().
+func (s *stdLogger) Panicln(v ...interface{}) {
+ output := fmt.Sprintln(v...)
+ s.Output(2, output)
+ panic(output)
+}
diff --git a/ui/logger/logger_test.go b/ui/logger/logger_test.go
new file mode 100644
index 0000000..0f88ab3
--- /dev/null
+++ b/ui/logger/logger_test.go
@@ -0,0 +1,198 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package logger
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "syscall"
+ "testing"
+)
+
+func TestCreateFileWithRotation(t *testing.T) {
+ dir, err := ioutil.TempDir("", "test-rotation")
+ if err != nil {
+ t.Fatalf("Failed to get TempDir: %v", err)
+ }
+ defer os.RemoveAll(dir)
+
+ file := filepath.Join(dir, "build.log")
+
+ writeFile := func(name string, data string) {
+ f, err := CreateFileWithRotation(name, 3)
+ if err != nil {
+ t.Fatalf("Failed to create file: %v", err)
+ }
+ if n, err := io.WriteString(f, data); err == nil && n < len(data) {
+ t.Fatalf("Short write")
+ } else if err != nil {
+ t.Fatalf("Failed to write: %v", err)
+ }
+ if err := f.Close(); err != nil {
+ t.Fatalf("Failed to close: %v", err)
+ }
+ }
+
+ writeFile(file, "a")
+ writeFile(file, "b")
+ writeFile(file, "c")
+ writeFile(file, "d")
+ writeFile(file, "e")
+
+ d, err := os.Open(dir)
+ if err != nil {
+ t.Fatalf("Failed to open dir: %v", err)
+ }
+ names, err := d.Readdirnames(0)
+ if err != nil {
+ t.Fatalf("Failed to read dir: %v", err)
+ }
+ sort.Strings(names)
+ expected := []string{"build.1.log", "build.2.log", "build.3.log", "build.log"}
+ if !reflect.DeepEqual(names, expected) {
+ t.Errorf("File list does not match.")
+ t.Errorf(" got: %v", names)
+ t.Errorf("expected: %v", expected)
+ t.FailNow()
+ }
+
+ expectFileContents := func(name, expected string) {
+ data, err := ioutil.ReadFile(filepath.Join(dir, name))
+ if err != nil {
+ t.Errorf("Error reading file: %v", err)
+ return
+ }
+ str := string(data)
+ if str != expected {
+ t.Errorf("Contents of %v does not match.", name)
+ t.Errorf(" got: %v", data)
+ t.Errorf("expected: %v", expected)
+ }
+ }
+
+ expectFileContents("build.log", "e")
+ expectFileContents("build.1.log", "d")
+ expectFileContents("build.2.log", "c")
+ expectFileContents("build.3.log", "b")
+}
+
+func TestPanic(t *testing.T) {
+ if os.Getenv("ACTUALLY_PANIC") == "1" {
+ panicValue := "foo"
+ log := New(&bytes.Buffer{})
+
+ defer func() {
+ p := recover()
+
+ if p == panicValue {
+ os.Exit(42)
+ } else {
+ fmt.Fprintln(os.Stderr, "Expected %q, got %v", panicValue, p)
+ os.Exit(3)
+ }
+ }()
+ defer log.Cleanup()
+
+ log.Panic(panicValue)
+ os.Exit(2)
+ return
+ }
+
+ // Run this in an external process so that we don't pollute stderr
+ cmd := exec.Command(os.Args[0], "-test.run=TestPanic")
+ cmd.Env = append(os.Environ(), "ACTUALLY_PANIC=1")
+ err := cmd.Run()
+ if e, ok := err.(*exec.ExitError); ok && e.Sys().(syscall.WaitStatus).ExitStatus() == 42 {
+ return
+ }
+ t.Errorf("Expected process to exit with status 42, got %v", err)
+}
+
+func TestFatal(t *testing.T) {
+ if os.Getenv("ACTUALLY_FATAL") == "1" {
+ log := New(&bytes.Buffer{})
+ defer func() {
+ // Shouldn't get here
+ os.Exit(3)
+ }()
+ defer log.Cleanup()
+ log.Fatal("Test")
+ os.Exit(0)
+ return
+ }
+
+ cmd := exec.Command(os.Args[0], "-test.run=TestFatal")
+ cmd.Env = append(os.Environ(), "ACTUALLY_FATAL=1")
+ err := cmd.Run()
+ if e, ok := err.(*exec.ExitError); ok && e.Sys().(syscall.WaitStatus).ExitStatus() == 1 {
+ return
+ }
+ t.Errorf("Expected process to exit with status 1, got %v", err)
+}
+
+func TestNonFatal(t *testing.T) {
+ if os.Getenv("ACTUAL_TEST") == "1" {
+ log := New(&bytes.Buffer{})
+ defer log.Cleanup()
+ log.Println("Test")
+ return
+ }
+
+ cmd := exec.Command(os.Args[0], "-test.run=TestNonFatal")
+ cmd.Env = append(os.Environ(), "ACTUAL_TEST=1")
+ err := cmd.Run()
+ if e, ok := err.(*exec.ExitError); ok || (ok && !e.Success()) {
+ t.Errorf("Expected process to exit cleanly, got %v", err)
+ }
+}
+
+func TestRecoverFatal(t *testing.T) {
+ log := New(&bytes.Buffer{})
+ defer func() {
+ if p := recover(); p != nil {
+ t.Errorf("Unexpected panic: %#v", p)
+ }
+ }()
+ defer Recover(func(err error) {
+ if err.Error() != "Test" {
+ t.Errorf("Expected %q, but got %q", "Test", err.Error())
+ }
+ })
+ log.Fatal("Test")
+ t.Errorf("Should not get here")
+}
+
+func TestRecoverNonFatal(t *testing.T) {
+ log := New(&bytes.Buffer{})
+ defer func() {
+ if p := recover(); p == nil {
+ t.Errorf("Panic not thrown")
+ } else if p != "Test" {
+ t.Errorf("Expected %q, but got %#v", "Test", p)
+ }
+ }()
+ defer Recover(func(err error) {
+ t.Errorf("Recover function should not be called")
+ })
+ log.Panic("Test")
+ t.Errorf("Should not get here")
+}
diff --git a/ui/tracer/Android.bp b/ui/tracer/Android.bp
new file mode 100644
index 0000000..89812a1
--- /dev/null
+++ b/ui/tracer/Android.bp
@@ -0,0 +1,23 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+bootstrap_go_package {
+ name: "soong-ui-tracer",
+ pkgPath: "android/soong/ui/tracer",
+ deps: ["soong-ui-logger"],
+ srcs: [
+ "ninja.go",
+ "tracer.go",
+ ],
+}
diff --git a/ui/tracer/ninja.go b/ui/tracer/ninja.go
new file mode 100644
index 0000000..da558f1
--- /dev/null
+++ b/ui/tracer/ninja.go
@@ -0,0 +1,130 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tracer
+
+import (
+ "bufio"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type ninjaLogEntry struct {
+ Name string
+ Begin int
+ End int
+}
+type ninjaLogEntries []*ninjaLogEntry
+
+func (n ninjaLogEntries) Len() int { return len(n) }
+func (n ninjaLogEntries) Less(i, j int) bool { return n[i].Begin < n[j].Begin }
+func (n ninjaLogEntries) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
+
+// ImportNinjaLog reads a .ninja_log file from ninja and writes the events out
+// to the trace.
+//
+// startOffset is when the ninja process started, and is used to position the
+// relative times from the ninja log into the trace. It's also used to skip
+// reading the ninja log if nothing was run.
+func (t *tracerImpl) ImportNinjaLog(thread Thread, filename string, startOffset time.Time) {
+ t.Begin("ninja log import", thread)
+ defer t.End(thread)
+
+ if stat, err := os.Stat(filename); err != nil {
+ t.log.Println("Missing ninja log:", err)
+ return
+ } else if stat.ModTime().Before(startOffset) {
+ t.log.Verboseln("Ninja log not modified, not importing any entries.")
+ return
+ }
+
+ f, err := os.Open(filename)
+ if err != nil {
+ t.log.Println("Error opening ninja log:", err)
+ return
+ }
+ defer f.Close()
+
+ s := bufio.NewScanner(f)
+ header := true
+ entries := ninjaLogEntries{}
+ prevEnd := 0
+ for s.Scan() {
+ if header {
+ hdr := s.Text()
+ if hdr != "# ninja log v5" {
+ t.log.Printf("Unknown ninja log header: %q", hdr)
+ return
+ }
+ header = false
+ continue
+ }
+
+ fields := strings.Split(s.Text(), "\t")
+ begin, err := strconv.Atoi(fields[0])
+ if err != nil {
+ t.log.Printf("Unable to parse ninja entry %q: %v", s.Text(), err)
+ return
+ }
+ end, err := strconv.Atoi(fields[1])
+ if err != nil {
+ t.log.Printf("Unable to parse ninja entry %q: %v", s.Text(), err)
+ return
+ }
+ if end < prevEnd {
+ entries = nil
+ }
+ prevEnd = end
+ entries = append(entries, &ninjaLogEntry{
+ Name: fields[3],
+ Begin: begin,
+ End: end,
+ })
+ }
+ if err := s.Err(); err != nil {
+ t.log.Println("Unable to parse ninja log:", err)
+ return
+ }
+
+ sort.Sort(entries)
+
+ cpus := []int{}
+ offset := uint64(startOffset.UnixNano()) / 1000
+ for _, entry := range entries {
+ tid := -1
+ for cpu, endTime := range cpus {
+ if endTime <= entry.Begin {
+ tid = cpu
+ cpus[cpu] = entry.End
+ break
+ }
+ }
+ if tid == -1 {
+ tid = len(cpus)
+ cpus = append(cpus, entry.End)
+ }
+
+ t.writeEvent(&viewerEvent{
+ Name: entry.Name,
+ Phase: "X",
+ Time: offset + uint64(entry.Begin)*1000,
+ Dur: uint64(entry.End-entry.Begin) * 1000,
+ Pid: 1,
+ Tid: uint64(tid),
+ })
+ }
+}
diff --git a/ui/tracer/tracer.go b/ui/tracer/tracer.go
new file mode 100644
index 0000000..b372885
--- /dev/null
+++ b/ui/tracer/tracer.go
@@ -0,0 +1,244 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This package implements a trace file writer, whose files can be opened in
+// chrome://tracing.
+//
+// It implements the JSON Array Format defined here:
+// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit
+package tracer
+
+import (
+ "bytes"
+ "compress/gzip"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "android/soong/ui/logger"
+)
+
+type Thread uint64
+
+const (
+ MainThread = Thread(iota)
+ MaxInitThreads = Thread(iota)
+)
+
+type Tracer interface {
+ Begin(name string, thread Thread)
+ End(thread Thread)
+ Complete(name string, thread Thread, begin, end uint64)
+
+ ImportNinjaLog(thread Thread, filename string, startOffset time.Time)
+}
+
+type tracerImpl struct {
+ lock sync.Mutex
+ log logger.Logger
+
+ buf bytes.Buffer
+ file *os.File
+ w io.WriteCloser
+
+ firstEvent bool
+ nextTid uint64
+}
+
+var _ Tracer = &tracerImpl{}
+
+type viewerEvent struct {
+ Name string `json:"name,omitempty"`
+ Phase string `json:"ph"`
+ Scope string `json:"s,omitempty"`
+ Time uint64 `json:"ts"`
+ Dur uint64 `json:"dur,omitempty"`
+ Pid uint64 `json:"pid"`
+ Tid uint64 `json:"tid"`
+ ID uint64 `json:"id,omitempty"`
+ Arg interface{} `json:"args,omitempty"`
+}
+
+type nameArg struct {
+ Name string `json:"name"`
+}
+
+type nopCloser struct{ io.Writer }
+
+func (nopCloser) Close() error { return nil }
+
+// New creates a new Tracer, storing log in order to log errors later.
+// Events are buffered in memory until SetOutput is called.
+func New(log logger.Logger) *tracerImpl {
+ ret := &tracerImpl{
+ log: log,
+
+ firstEvent: true,
+ nextTid: uint64(MaxInitThreads),
+ }
+ ret.startBuffer()
+
+ return ret
+}
+
+func (t *tracerImpl) startBuffer() {
+ t.w = nopCloser{&t.buf}
+ fmt.Fprintln(t.w, "[")
+
+ t.defineThread(MainThread, "main")
+}
+
+func (t *tracerImpl) close() {
+ if t.file != nil {
+ fmt.Fprintln(t.w, "]")
+
+ if err := t.w.Close(); err != nil {
+ t.log.Println("Error closing trace writer:", err)
+ }
+
+ if err := t.file.Close(); err != nil {
+ t.log.Println("Error closing trace file:", err)
+ }
+ t.file = nil
+ t.startBuffer()
+ }
+}
+
+// SetOutput creates the output file (rotating old files).
+func (t *tracerImpl) SetOutput(filename string) {
+ t.lock.Lock()
+ defer t.lock.Unlock()
+
+ t.close()
+
+ // chrome://tracing requires that compressed trace files end in .gz
+ if !strings.HasSuffix(filename, ".gz") {
+ filename += ".gz"
+ }
+
+ f, err := logger.CreateFileWithRotation(filename, 5)
+ if err != nil {
+ t.log.Println("Failed to create trace file:", err)
+ return
+ }
+ // Save the file, since closing the gzip Writer doesn't close the
+ // underlying file.
+ t.file = f
+ t.w = gzip.NewWriter(f)
+
+ // Write out everything that happened since the start
+ if _, err := io.Copy(t.w, &t.buf); err != nil {
+ t.log.Println("Failed to write trace buffer to file:", err)
+ }
+ t.buf = bytes.Buffer{}
+}
+
+// Close closes the output file. Any future events will be buffered until the
+// next call to SetOutput.
+func (t *tracerImpl) Close() {
+ t.lock.Lock()
+ defer t.lock.Unlock()
+
+ t.close()
+}
+
+func (t *tracerImpl) writeEvent(event *viewerEvent) {
+ t.lock.Lock()
+ defer t.lock.Unlock()
+
+ t.writeEventLocked(event)
+}
+
+func (t *tracerImpl) writeEventLocked(event *viewerEvent) {
+ bytes, err := json.Marshal(event)
+ if err != nil {
+ t.log.Println("Failed to marshal event:", err)
+ t.log.Verbosef("Event: %#v", event)
+ return
+ }
+
+ if !t.firstEvent {
+ fmt.Fprintln(t.w, ",")
+ } else {
+ t.firstEvent = false
+ }
+
+ if _, err = t.w.Write(bytes); err != nil {
+ t.log.Println("Trace write error:", err)
+ }
+}
+
+func (t *tracerImpl) defineThread(thread Thread, name string) {
+ t.writeEventLocked(&viewerEvent{
+ Name: "thread_name",
+ Phase: "M",
+ Pid: 0,
+ Tid: uint64(thread),
+ Arg: &nameArg{
+ Name: name,
+ },
+ })
+}
+
+// NewThread returns a new Thread with an unused tid, writing the name out to
+// the trace file.
+func (t *tracerImpl) NewThread(name string) Thread {
+ t.lock.Lock()
+ defer t.lock.Unlock()
+
+ ret := Thread(t.nextTid)
+ t.nextTid += 1
+
+ t.defineThread(ret, name)
+ return ret
+}
+
+// Begin starts a new Duration Event. More than one Duration Event may be active
+// at a time on each Thread, but they're nested.
+func (t *tracerImpl) Begin(name string, thread Thread) {
+ t.writeEvent(&viewerEvent{
+ Name: name,
+ Phase: "B",
+ Time: uint64(time.Now().UnixNano()) / 1000,
+ Pid: 0,
+ Tid: uint64(thread),
+ })
+}
+
+// End finishes the most recent active Duration Event on the thread.
+func (t *tracerImpl) End(thread Thread) {
+ t.writeEvent(&viewerEvent{
+ Phase: "E",
+ Time: uint64(time.Now().UnixNano()) / 1000,
+ Pid: 0,
+ Tid: uint64(thread),
+ })
+}
+
+// Complete writes a Complete Event, which are like Duration Events, but include
+// a begin and end timestamp in the same event.
+func (t *tracerImpl) Complete(name string, thread Thread, begin, end uint64) {
+ t.writeEvent(&viewerEvent{
+ Name: name,
+ Phase: "X",
+ Time: begin / 1000,
+ Dur: (end - begin) / 1000,
+ Pid: 0,
+ Tid: uint64(thread),
+ })
+}