aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/build/Android.bp36
-rw-r--r--ui/build/build.go105
-rw-r--r--ui/build/config.go274
-rw-r--r--ui/build/context.go64
-rw-r--r--ui/build/environment.go152
-rw-r--r--ui/build/environment_test.go80
-rw-r--r--ui/build/kati.go104
-rw-r--r--ui/build/make.go160
-rw-r--r--ui/build/ninja.go78
-rw-r--r--ui/build/signal.go60
-rw-r--r--ui/build/soong.go57
-rw-r--r--ui/build/util.go79
-rw-r--r--ui/logger/Android.bp24
-rw-r--r--ui/logger/logger.go302
-rw-r--r--ui/logger/logger_test.go198
15 files changed, 1773 insertions, 0 deletions
diff --git a/ui/build/Android.bp b/ui/build/Android.bp
new file mode 100644
index 00000000..d6da9507
--- /dev/null
+++ b/ui/build/Android.bp
@@ -0,0 +1,36 @@
+// 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",
+ ],
+ 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 00000000..506ff519
--- /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 00000000..b0a7d7ad
--- /dev/null
+++ b/ui/build/config.go
@@ -0,0 +1,274 @@
+// 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 (
+ "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
+}
+
+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
+
+ 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 00000000..59474f53
--- /dev/null
+++ b/ui/build/context.go
@@ -0,0 +1,64 @@
+// 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"
+
+ "android/soong/ui/logger"
+)
+
+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 *ContextImpl
+type ContextImpl struct {
+ context.Context
+ logger.Logger
+
+ StdioInterface
+}
diff --git a/ui/build/environment.go b/ui/build/environment.go
new file mode 100644
index 00000000..baab101b
--- /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 00000000..0294dac4
--- /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 00000000..6997fbec
--- /dev/null
+++ b/ui/build/kati.go
@@ -0,0 +1,104 @@
+// 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) {
+ 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 00000000..58805095
--- /dev/null
+++ b/ui/build/make.go
@@ -0,0 +1,160 @@
+// 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) {
+ 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 00000000..13e1834c
--- /dev/null
+++ b/ui/build/ninja.go
@@ -0,0 +1,78 @@
+// 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"
+ "strconv"
+ "strings"
+)
+
+func runNinja(ctx Context, config Config) {
+ 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)
+ 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 00000000..3c8c8e11
--- /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 00000000..88e4161c
--- /dev/null
+++ b/ui/build/soong.go
@@ -0,0 +1,57 @@
+// 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) {
+ 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) {
+ 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 00000000..ad084da6
--- /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 00000000..8091ef92
--- /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 00000000..db7e82ad
--- /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 00000000..0f88ab37
--- /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")
+}