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),
+	})
+}