aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/build/Android.bp4
-rw-r--r--ui/build/build.go4
-rw-r--r--ui/build/config_test.go7
-rw-r--r--ui/build/context.go65
-rw-r--r--ui/build/dumpvars.go6
-rw-r--r--ui/build/exec.go17
-rw-r--r--ui/build/kati.go76
-rw-r--r--ui/build/ninja.go21
-rw-r--r--ui/build/soong.go34
-rw-r--r--ui/build/util.go82
-rw-r--r--ui/build/util_test.go45
-rw-r--r--ui/status/Android.bp42
-rw-r--r--ui/status/kati.go138
-rw-r--r--ui/status/kati_test.go175
-rw-r--r--ui/status/log.go136
-rw-r--r--ui/status/ninja.go153
-rw-r--r--ui/status/ninja_frontend/README3
-rw-r--r--ui/status/ninja_frontend/frontend.pb.go510
-rw-r--r--ui/status/ninja_frontend/frontend.proto84
-rwxr-xr-xui/status/ninja_frontend/regen.sh3
-rw-r--r--ui/status/status.go340
-rw-r--r--ui/status/status_test.go166
-rw-r--r--ui/terminal/Android.bp37
-rw-r--r--ui/terminal/status.go142
-rw-r--r--ui/terminal/util.go101
-rw-r--r--ui/terminal/util_darwin.go (renamed from ui/build/util_darwin.go)2
-rw-r--r--ui/terminal/util_linux.go (renamed from ui/build/util_linux.go)2
-rw-r--r--ui/terminal/util_test.go64
-rw-r--r--ui/terminal/writer.go229
-rw-r--r--ui/tracer/Android.bp7
-rw-r--r--ui/tracer/microfactory.go38
-rw-r--r--ui/tracer/ninja.go131
-rw-r--r--ui/tracer/status.go87
-rw-r--r--ui/tracer/tracer.go4
34 files changed, 2521 insertions, 434 deletions
diff --git a/ui/build/Android.bp b/ui/build/Android.bp
index 1fe5b6f2..a48a314b 100644
--- a/ui/build/Android.bp
+++ b/ui/build/Android.bp
@@ -30,6 +30,8 @@ bootstrap_go_package {
deps: [
"soong-ui-build-paths",
"soong-ui-logger",
+ "soong-ui-status",
+ "soong-ui-terminal",
"soong-ui-tracer",
"soong-shared",
"soong-finder",
@@ -62,13 +64,11 @@ bootstrap_go_package {
darwin: {
srcs: [
"sandbox_darwin.go",
- "util_darwin.go"
],
},
linux: {
srcs: [
"sandbox_linux.go",
- "util_linux.go"
],
},
}
diff --git a/ui/build/build.go b/ui/build/build.go
index acba2759..96cfdbb3 100644
--- a/ui/build/build.go
+++ b/ui/build/build.go
@@ -105,9 +105,7 @@ func checkCaseSensitivity(ctx Context, config Config) {
func help(ctx Context, config Config, what int) {
cmd := Command(ctx, config, "help.sh", "build/make/help.sh")
cmd.Sandbox = dumpvarsSandbox
- cmd.Stdout = ctx.Stdout()
- cmd.Stderr = ctx.Stderr()
- cmd.RunOrFatal()
+ cmd.RunAndPrintOrFatal()
}
// Build the tree. The 'what' argument can be used to chose which components of
diff --git a/ui/build/config_test.go b/ui/build/config_test.go
index e4eab944..242e3afb 100644
--- a/ui/build/config_test.go
+++ b/ui/build/config_test.go
@@ -22,13 +22,14 @@ import (
"testing"
"android/soong/ui/logger"
+ "android/soong/ui/terminal"
)
func testContext() Context {
return Context{&ContextImpl{
- Context: context.Background(),
- Logger: logger.New(&bytes.Buffer{}),
- StdioInterface: NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{}),
+ Context: context.Background(),
+ Logger: logger.New(&bytes.Buffer{}),
+ Writer: terminal.NewWriter(terminal.NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{})),
}}
}
diff --git a/ui/build/context.go b/ui/build/context.go
index 0636631d..c8b00c38 100644
--- a/ui/build/context.go
+++ b/ui/build/context.go
@@ -16,45 +16,14 @@ package build
import (
"context"
- "io"
- "os"
- "time"
"android/soong/ui/logger"
+ "android/soong/ui/status"
+ "android/soong/ui/terminal"
"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.
+// Context combines a context.Context, logger.Logger, and terminal.Writer.
// 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 }
@@ -62,7 +31,8 @@ type ContextImpl struct {
context.Context
logger.Logger
- StdioInterface
+ Writer terminal.Writer
+ Status *status.Status
Thread tracer.Thread
Tracer tracer.Tracer
@@ -88,28 +58,3 @@ func (c ContextImpl) CompleteTrace(name string, begin, end uint64) {
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)
- }
-}
-
-func (c ContextImpl) IsTerminal() bool {
- if term, ok := os.LookupEnv("TERM"); ok {
- return term != "dumb" && isTerminal(c.Stdout()) && isTerminal(c.Stderr())
- }
- return false
-}
-
-func (c ContextImpl) IsErrTerminal() bool {
- if term, ok := os.LookupEnv("TERM"); ok {
- return term != "dumb" && isTerminal(c.Stderr())
- }
- return false
-}
-
-func (c ContextImpl) TermWidth() (int, bool) {
- return termWidth(c.Stdout())
-}
diff --git a/ui/build/dumpvars.go b/ui/build/dumpvars.go
index 8429a8a2..06bd74f2 100644
--- a/ui/build/dumpvars.go
+++ b/ui/build/dumpvars.go
@@ -18,6 +18,8 @@ import (
"bytes"
"fmt"
"strings"
+
+ "android/soong/ui/status"
)
// DumpMakeVars can be used to extract the values of Make variables after the
@@ -60,7 +62,7 @@ func dumpMakeVars(ctx Context, config Config, goals, vars []string, write_soong_
}
cmd.StartOrFatal()
// TODO: error out when Stderr contains any content
- katiRewriteOutput(ctx, pipe)
+ status.KatiReader(ctx.Status.StartTool(), pipe)
cmd.WaitOrFatal()
ret := make(map[string]string, len(vars))
@@ -175,7 +177,7 @@ func runMakeProductConfig(ctx Context, config Config) {
}
// Print the banner like make does
- fmt.Fprintln(ctx.Stdout(), Banner(make_vars))
+ ctx.Writer.Print(Banner(make_vars))
// Populate the environment
env := config.Environment()
diff --git a/ui/build/exec.go b/ui/build/exec.go
index 90fb19de..5c312bcd 100644
--- a/ui/build/exec.go
+++ b/ui/build/exec.go
@@ -122,3 +122,20 @@ func (c *Cmd) CombinedOutputOrFatal() []byte {
c.reportError(err)
return ret
}
+
+// RunAndPrintOrFatal will run the command, then after finishing
+// print any output, then handling any errors with a call to
+// ctx.Fatal
+func (c *Cmd) RunAndPrintOrFatal() {
+ ret, err := c.CombinedOutput()
+ st := c.ctx.Status.StartTool()
+ if len(ret) > 0 {
+ if err != nil {
+ st.Error(string(ret))
+ } else {
+ st.Print(string(ret))
+ }
+ }
+ st.Finish()
+ c.reportError(err)
+}
diff --git a/ui/build/kati.go b/ui/build/kati.go
index 7635c10c..7cfa1cf7 100644
--- a/ui/build/kati.go
+++ b/ui/build/kati.go
@@ -15,15 +15,14 @@
package build
import (
- "bufio"
"crypto/md5"
"fmt"
- "io"
"io/ioutil"
"path/filepath"
- "regexp"
"strconv"
"strings"
+
+ "android/soong/ui/status"
)
var spaceSlashReplacer = strings.NewReplacer("/", "_", " ", "_")
@@ -117,77 +116,10 @@ func runKati(ctx Context, config Config) {
cmd.Stderr = cmd.Stdout
cmd.StartOrFatal()
- katiRewriteOutput(ctx, pipe)
+ status.KatiReader(ctx.Status.StartTool(), pipe)
cmd.WaitOrFatal()
}
-var katiIncludeRe = regexp.MustCompile(`^(\[\d+/\d+] )?including [^ ]+ ...$`)
-var katiLogRe = regexp.MustCompile(`^\*kati\*: `)
-
-func katiRewriteOutput(ctx Context, pipe io.ReadCloser) {
- haveBlankLine := true
- smartTerminal := ctx.IsTerminal()
- errSmartTerminal := ctx.IsErrTerminal()
-
- scanner := bufio.NewScanner(pipe)
- for scanner.Scan() {
- line := scanner.Text()
- verbose := katiIncludeRe.MatchString(line)
-
- // Only put kati debug/stat lines in our verbose log
- if katiLogRe.MatchString(line) {
- ctx.Verbose(line)
- continue
- }
-
- // For verbose lines, write them on the current line without a newline,
- // then overwrite them if the next thing we're printing is another
- // verbose line.
- if smartTerminal && verbose {
- // Limit line width to the terminal width, otherwise we'll wrap onto
- // another line and we won't delete the previous line.
- //
- // Run this on every line in case the window has been resized while
- // we're printing. This could be optimized to only re-run when we
- // get SIGWINCH if it ever becomes too time consuming.
- if max, ok := termWidth(ctx.Stdout()); ok {
- if len(line) > max {
- // Just do a max. Ninja elides the middle, but that's
- // more complicated and these lines aren't that important.
- line = line[:max]
- }
- }
-
- // Move to the beginning on the line, print the output, then clear
- // the rest of the line.
- fmt.Fprint(ctx.Stdout(), "\r", line, "\x1b[K")
- haveBlankLine = false
- continue
- } else if smartTerminal && !haveBlankLine {
- // If we've previously written a verbose message, send a newline to save
- // that message instead of overwriting it.
- fmt.Fprintln(ctx.Stdout())
- haveBlankLine = true
- } else if !errSmartTerminal {
- // Most editors display these as garbage, so strip them out.
- line = string(stripAnsiEscapes([]byte(line)))
- }
-
- // Assume that non-verbose lines are important enough for stderr
- fmt.Fprintln(ctx.Stderr(), line)
- }
-
- // Save our last verbose line.
- if !haveBlankLine {
- fmt.Fprintln(ctx.Stdout())
- }
-
- if err := scanner.Err(); err != nil {
- ctx.Println("Error from kati parser:", err)
- io.Copy(ctx.Stderr(), pipe)
- }
-}
-
func runKatiCleanSpec(ctx Context, config Config) {
ctx.BeginTrace("kati cleanspec")
defer ctx.EndTrace()
@@ -220,6 +152,6 @@ func runKatiCleanSpec(ctx Context, config Config) {
cmd.Stderr = cmd.Stdout
cmd.StartOrFatal()
- katiRewriteOutput(ctx, pipe)
+ status.KatiReader(ctx.Status.StartTool(), pipe)
cmd.WaitOrFatal()
}
diff --git a/ui/build/ninja.go b/ui/build/ninja.go
index 96b5e9d6..c48fe0f6 100644
--- a/ui/build/ninja.go
+++ b/ui/build/ninja.go
@@ -21,15 +21,21 @@ import (
"strconv"
"strings"
"time"
+
+ "android/soong/ui/status"
)
func runNinja(ctx Context, config Config) {
ctx.BeginTrace("ninja")
defer ctx.EndTrace()
+ fifo := filepath.Join(config.OutDir(), ".ninja_fifo")
+ status.NinjaReader(ctx, ctx.Status.StartTool(), fifo)
+
executable := config.PrebuiltBuildTool("ninja")
args := []string{
"-d", "keepdepfile",
+ fmt.Sprintf("--frontend=cat <&3 >%s", fifo),
}
args = append(args, config.NinjaArgs()...)
@@ -47,9 +53,6 @@ func runNinja(ctx Context, config Config) {
args = append(args, "-f", config.CombinedNinjaFile())
- if config.IsVerbose() {
- args = append(args, "-v")
- }
args = append(args, "-w", "dupbuild=err")
cmd := Command(ctx, config, "ninja", executable, args...)
@@ -66,13 +69,6 @@ func runNinja(ctx Context, config Config) {
cmd.Args = append(cmd.Args, strings.Fields(extra)...)
}
- if _, ok := cmd.Environment.Get("NINJA_STATUS"); !ok {
- cmd.Environment.Set("NINJA_STATUS", "[%p %f/%t] ")
- }
-
- cmd.Stdin = ctx.Stdin()
- cmd.Stdout = ctx.Stdout()
- cmd.Stderr = ctx.Stderr()
logPath := filepath.Join(config.OutDir(), ".ninja_log")
ninjaHeartbeatDuration := time.Minute * 5
if overrideText, ok := cmd.Environment.Get("NINJA_HEARTBEAT_INTERVAL"); ok {
@@ -99,10 +95,7 @@ func runNinja(ctx Context, config Config) {
}
}()
- startTime := time.Now()
- defer ctx.ImportNinjaLog(logPath, startTime)
-
- cmd.RunOrFatal()
+ cmd.RunAndPrintOrFatal()
}
type statusChecker struct {
diff --git a/ui/build/soong.go b/ui/build/soong.go
index a73082a4..6c94079e 100644
--- a/ui/build/soong.go
+++ b/ui/build/soong.go
@@ -15,12 +15,15 @@
package build
import (
+ "fmt"
"os"
"path/filepath"
"strconv"
- "time"
+ "strings"
"github.com/google/blueprint/microfactory"
+
+ "android/soong/ui/status"
)
func runSoong(ctx Context, config Config) {
@@ -41,9 +44,8 @@ func runSoong(ctx Context, config Config) {
cmd.Environment.Set("SRCDIR", ".")
cmd.Environment.Set("TOPNAME", "Android.bp")
cmd.Sandbox = soongSandbox
- cmd.Stdout = ctx.Stdout()
- cmd.Stderr = ctx.Stderr()
- cmd.RunOrFatal()
+
+ cmd.RunAndPrintOrFatal()
}()
func() {
@@ -56,12 +58,18 @@ func runSoong(ctx Context, config Config) {
if _, err := os.Stat(envTool); err == nil {
cmd := Command(ctx, config, "soong_env", envTool, envFile)
cmd.Sandbox = soongSandbox
- cmd.Stdout = ctx.Stdout()
- cmd.Stderr = ctx.Stderr()
+
+ var buf strings.Builder
+ cmd.Stdout = &buf
+ cmd.Stderr = &buf
if err := cmd.Run(); err != nil {
ctx.Verboseln("soong_env failed, forcing manifest regeneration")
os.Remove(envFile)
}
+
+ if buf.Len() > 0 {
+ ctx.Verboseln(buf.String())
+ }
} else {
ctx.Verboseln("Missing soong_env tool, forcing manifest regeneration")
os.Remove(envFile)
@@ -100,22 +108,18 @@ func runSoong(ctx Context, config Config) {
ctx.BeginTrace(name)
defer ctx.EndTrace()
+ fifo := filepath.Join(config.OutDir(), ".ninja_fifo")
+ status.NinjaReader(ctx, ctx.Status.StartTool(), fifo)
+
cmd := Command(ctx, config, "soong "+name,
config.PrebuiltBuildTool("ninja"),
"-d", "keepdepfile",
"-w", "dupbuild=err",
"-j", strconv.Itoa(config.Parallel()),
+ fmt.Sprintf("--frontend=cat <&3 >%s", fifo),
"-f", filepath.Join(config.SoongOutDir(), file))
- if config.IsVerbose() {
- cmd.Args = append(cmd.Args, "-v")
- }
cmd.Sandbox = soongSandbox
- cmd.Stdin = ctx.Stdin()
- cmd.Stdout = ctx.Stdout()
- cmd.Stderr = ctx.Stderr()
-
- defer ctx.ImportNinjaLog(filepath.Join(config.OutDir(), ".ninja_log"), time.Now())
- cmd.RunOrFatal()
+ cmd.RunAndPrintOrFatal()
}
ninja("minibootstrap", ".minibootstrap/build.ninja")
diff --git a/ui/build/util.go b/ui/build/util.go
index 96088fe7..0676a860 100644
--- a/ui/build/util.go
+++ b/ui/build/util.go
@@ -15,13 +15,9 @@
package build
import (
- "bytes"
- "io"
"os"
"path/filepath"
"strings"
- "syscall"
- "unsafe"
)
func absPath(ctx Context, p string) string {
@@ -117,81 +113,3 @@ func decodeKeyValue(str string) (string, string, bool) {
}
return str[:idx], str[idx+1:], true
}
-
-func isTerminal(w io.Writer) bool {
- if f, ok := w.(*os.File); ok {
- var termios syscall.Termios
- _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
- ioctlGetTermios, uintptr(unsafe.Pointer(&termios)),
- 0, 0, 0)
- return err == 0
- }
- return false
-}
-
-func termWidth(w io.Writer) (int, bool) {
- if f, ok := w.(*os.File); ok {
- var winsize struct {
- ws_row, ws_column uint16
- ws_xpixel, ws_ypixel uint16
- }
- _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
- syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&winsize)),
- 0, 0, 0)
- return int(winsize.ws_column), err == 0
- }
- return 0, false
-}
-
-// stripAnsiEscapes strips ANSI control codes from a byte array in place.
-func stripAnsiEscapes(input []byte) []byte {
- // read represents the remaining part of input that needs to be processed.
- read := input
- // write represents where we should be writing in input.
- // It will share the same backing store as input so that we make our modifications
- // in place.
- write := input
-
- // advance will copy count bytes from read to write and advance those slices
- advance := func(write, read []byte, count int) ([]byte, []byte) {
- copy(write, read[:count])
- return write[count:], read[count:]
- }
-
- for {
- // Find the next escape sequence
- i := bytes.IndexByte(read, 0x1b)
- // If it isn't found, or if there isn't room for <ESC>[, finish
- if i == -1 || i+1 >= len(read) {
- copy(write, read)
- break
- }
-
- // Not a CSI code, continue searching
- if read[i+1] != '[' {
- write, read = advance(write, read, i+1)
- continue
- }
-
- // Found a CSI code, advance up to the <ESC>
- write, read = advance(write, read, i)
-
- // Find the end of the CSI code
- i = bytes.IndexFunc(read, func(r rune) bool {
- return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
- })
- if i == -1 {
- // We didn't find the end of the code, just remove the rest
- i = len(read) - 1
- }
-
- // Strip off the end marker too
- i = i + 1
-
- // Skip the reader forward and reduce final length by that amount
- read = read[i:]
- input = input[:len(input)-i]
- }
-
- return input
-}
diff --git a/ui/build/util_test.go b/ui/build/util_test.go
index 0e0dbdfb..89bfc778 100644
--- a/ui/build/util_test.go
+++ b/ui/build/util_test.go
@@ -49,48 +49,3 @@ func TestEnsureEmptyDirs(t *testing.T) {
ensureEmptyDirectoriesExist(ctx, filepath.Join(tmpDir, "a"))
}
-
-func TestStripAnsiEscapes(t *testing.T) {
- testcases := []struct {
- input string
- output string
- }{
- {
- "",
- "",
- },
- {
- "This is a test",
- "This is a test",
- },
- {
- "interrupted: \x1b[12",
- "interrupted: ",
- },
- {
- "other \x1bescape \x1b",
- "other \x1bescape \x1b",
- },
- { // from pretty-error macro
- "\x1b[1mart/Android.mk: \x1b[31merror:\x1b[0m\x1b[1m art: test error \x1b[0m",
- "art/Android.mk: error: art: test error ",
- },
- { // from envsetup.sh make wrapper
- "\x1b[0;31m#### make failed to build some targets (2 seconds) ####\x1b[00m",
- "#### make failed to build some targets (2 seconds) ####",
- },
- { // from clang (via ninja testcase)
- "\x1b[1maffixmgr.cxx:286:15: \x1b[0m\x1b[0;1;35mwarning: \x1b[0m\x1b[1musing the result... [-Wparentheses]\x1b[0m",
- "affixmgr.cxx:286:15: warning: using the result... [-Wparentheses]",
- },
- }
- for _, tc := range testcases {
- got := string(stripAnsiEscapes([]byte(tc.input)))
- if got != tc.output {
- t.Errorf("output strings didn't match\n"+
- "input: %#v\n"+
- " want: %#v\n"+
- " got: %#v", tc.input, tc.output, got)
- }
- }
-}
diff --git a/ui/status/Android.bp b/ui/status/Android.bp
new file mode 100644
index 00000000..76caaef1
--- /dev/null
+++ b/ui/status/Android.bp
@@ -0,0 +1,42 @@
+// Copyright 2018 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-status",
+ pkgPath: "android/soong/ui/status",
+ deps: [
+ "golang-protobuf-proto",
+ "soong-ui-logger",
+ "soong-ui-status-ninja_frontend",
+ ],
+ srcs: [
+ "kati.go",
+ "log.go",
+ "ninja.go",
+ "status.go",
+ ],
+ testSrcs: [
+ "kati_test.go",
+ "status_test.go",
+ ],
+}
+
+bootstrap_go_package {
+ name: "soong-ui-status-ninja_frontend",
+ pkgPath: "android/soong/ui/status/ninja_frontend",
+ deps: ["golang-protobuf-proto"],
+ srcs: [
+ "ninja_frontend/frontend.pb.go",
+ ],
+}
diff --git a/ui/status/kati.go b/ui/status/kati.go
new file mode 100644
index 00000000..552a9e97
--- /dev/null
+++ b/ui/status/kati.go
@@ -0,0 +1,138 @@
+// Copyright 2018 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 status
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+var katiError = regexp.MustCompile(`^(\033\[1m)?[^ ]+:[0-9]+: (\033\[31m)?error:`)
+var katiIncludeRe = regexp.MustCompile(`^(\[(\d+)/(\d+)] )?((including [^ ]+|initializing build system|finishing build rules|writing build rules) ...)$`)
+var katiLogRe = regexp.MustCompile(`^\*kati\*: `)
+var katiNinjaMissing = regexp.MustCompile("^[^ ]+ is missing, regenerating...$")
+
+type katiOutputParser struct {
+ st ToolStatus
+
+ count int
+ total int
+ extra int
+
+ action *Action
+ buf strings.Builder
+ hasError bool
+}
+
+func (k *katiOutputParser) flushAction() {
+ if k.action == nil {
+ return
+ }
+
+ var err error
+ if k.hasError {
+ err = fmt.Errorf("makefile error")
+ }
+
+ k.st.FinishAction(ActionResult{
+ Action: k.action,
+ Output: k.buf.String(),
+ Error: err,
+ })
+
+ k.buf.Reset()
+ k.hasError = false
+}
+
+func (k *katiOutputParser) parseLine(line string) {
+ // Only put kati debug/stat lines in our verbose log
+ if katiLogRe.MatchString(line) {
+ k.st.Verbose(line)
+ return
+ }
+
+ if matches := katiIncludeRe.FindStringSubmatch(line); len(matches) > 0 {
+ k.flushAction()
+ k.count += 1
+
+ matches := katiIncludeRe.FindStringSubmatch(line)
+ if matches[2] != "" {
+ idx, err := strconv.Atoi(matches[2])
+
+ if err == nil && idx+k.extra != k.count {
+ k.extra = k.count - idx
+ k.st.SetTotalActions(k.total + k.extra)
+ }
+ } else {
+ k.extra += 1
+ k.st.SetTotalActions(k.total + k.extra)
+ }
+
+ if matches[3] != "" {
+ tot, err := strconv.Atoi(matches[3])
+
+ if err == nil && tot != k.total {
+ k.total = tot
+ k.st.SetTotalActions(k.total + k.extra)
+ }
+ }
+
+ k.action = &Action{
+ Description: matches[4],
+ }
+ k.st.StartAction(k.action)
+ } else if k.action != nil {
+ if katiError.MatchString(line) {
+ k.hasError = true
+ }
+ k.buf.WriteString(line)
+ k.buf.WriteString("\n")
+ } else {
+ // Before we've started executing actions from Kati
+ if line == "No need to regenerate ninja file" || katiNinjaMissing.MatchString(line) {
+ k.st.Status(line)
+ } else {
+ k.st.Print(line)
+ }
+ }
+}
+
+// KatiReader reads the output from Kati, and turns it into Actions and
+// messages that are passed into the ToolStatus API.
+func KatiReader(st ToolStatus, pipe io.ReadCloser) {
+ parser := &katiOutputParser{
+ st: st,
+ }
+
+ scanner := bufio.NewScanner(pipe)
+ for scanner.Scan() {
+ parser.parseLine(scanner.Text())
+ }
+
+ parser.flushAction()
+
+ if err := scanner.Err(); err != nil {
+ var buf strings.Builder
+ io.Copy(&buf, pipe)
+ st.Print(fmt.Sprintf("Error from kati parser: %s", err))
+ st.Print(buf.String())
+ }
+
+ st.Finish()
+}
diff --git a/ui/status/kati_test.go b/ui/status/kati_test.go
new file mode 100644
index 00000000..f2cb8132
--- /dev/null
+++ b/ui/status/kati_test.go
@@ -0,0 +1,175 @@
+// Copyright 2018 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 status
+
+import (
+ "testing"
+)
+
+type lastOutput struct {
+ counterOutput
+
+ action *Action
+ result ActionResult
+
+ msgLevel MsgLevel
+ msg string
+}
+
+func (l *lastOutput) StartAction(a *Action, c Counts) {
+ l.action = a
+ l.counterOutput.StartAction(a, c)
+}
+func (l *lastOutput) FinishAction(r ActionResult, c Counts) {
+ l.result = r
+ l.counterOutput.FinishAction(r, c)
+}
+func (l *lastOutput) Message(level MsgLevel, msg string) {
+ l.msgLevel = level
+ l.msg = msg
+}
+func (l *lastOutput) Flush() {}
+
+func TestKatiNormalCase(t *testing.T) {
+ status := &Status{}
+ output := &lastOutput{}
+ status.AddOutput(output)
+
+ parser := &katiOutputParser{
+ st: status.StartTool(),
+ }
+
+ msg := "*kati*: verbose msg"
+ parser.parseLine(msg)
+ output.Expect(t, Counts{})
+
+ if output.msgLevel != VerboseLvl {
+ t.Errorf("Expected verbose message, but got %d", output.msgLevel)
+ }
+ if output.msg != msg {
+ t.Errorf("unexpected message contents:\nwant: %q\n got: %q\n", msg, output.msg)
+ }
+
+ parser.parseLine("out/build-aosp_arm.ninja is missing, regenerating...")
+ output.Expect(t, Counts{})
+
+ parser.parseLine("[1/1] initializing build system ...")
+ output.Expect(t, Counts{
+ TotalActions: 1,
+ RunningActions: 1,
+ StartedActions: 1,
+ FinishedActions: 0,
+ })
+
+ parser.parseLine("[2/5] including out/soong/Android-aosp_arm.mk ...")
+ output.Expect(t, Counts{
+ TotalActions: 5,
+ RunningActions: 1,
+ StartedActions: 2,
+ FinishedActions: 1,
+ })
+
+ parser.parseLine("[3/5] including a ...")
+ msg = "a random message"
+ parser.parseLine(msg)
+
+ // Start the next line to flush the previous result
+ parser.parseLine("[4/5] finishing build rules ...")
+
+ msg += "\n"
+ if output.result.Output != msg {
+ t.Errorf("output for action did not match:\nwant: %q\n got: %q\n", msg, output.result.Output)
+ }
+
+ parser.parseLine("[5/5] writing build rules ...")
+ parser.parseLine("*kati*: verbose msg")
+ parser.flushAction()
+
+ if output.result.Output != "" {
+ t.Errorf("expected no output for last action, but got %q", output.result.Output)
+ }
+
+ output.Expect(t, Counts{
+ TotalActions: 5,
+ RunningActions: 0,
+ StartedActions: 5,
+ FinishedActions: 5,
+ })
+}
+
+func TestKatiExtraIncludes(t *testing.T) {
+ status := &Status{}
+ output := &lastOutput{}
+ status.AddOutput(output)
+
+ parser := &katiOutputParser{
+ st: status.StartTool(),
+ }
+
+ parser.parseLine("[1/1] initializing build system ...")
+ parser.parseLine("[2/5] including out/soong/Android-aosp_arm.mk ...")
+ output.Expect(t, Counts{
+ TotalActions: 5,
+ RunningActions: 1,
+ StartedActions: 2,
+ FinishedActions: 1,
+ })
+
+ parser.parseLine("including a ...")
+
+ output.Expect(t, Counts{
+ TotalActions: 6,
+ RunningActions: 1,
+ StartedActions: 3,
+ FinishedActions: 2,
+ })
+
+ parser.parseLine("including b ...")
+
+ output.Expect(t, Counts{
+ TotalActions: 7,
+ RunningActions: 1,
+ StartedActions: 4,
+ FinishedActions: 3,
+ })
+
+ parser.parseLine("[3/5] finishing build rules ...")
+
+ output.Expect(t, Counts{
+ TotalActions: 7,
+ RunningActions: 1,
+ StartedActions: 5,
+ FinishedActions: 4,
+ })
+}
+
+func TestKatiFailOnError(t *testing.T) {
+ status := &Status{}
+ output := &lastOutput{}
+ status.AddOutput(output)
+
+ parser := &katiOutputParser{
+ st: status.StartTool(),
+ }
+
+ parser.parseLine("[1/1] initializing build system ...")
+ parser.parseLine("[2/5] inclduing out/soong/Android-aosp_arm.mk ...")
+ parser.parseLine("build/make/tools/Android.mk:19: error: testing")
+ parser.flushAction()
+
+ if output.result.Error == nil {
+ t.Errorf("Expected the last action to be marked as an error")
+ }
+}
diff --git a/ui/status/log.go b/ui/status/log.go
new file mode 100644
index 00000000..921aa440
--- /dev/null
+++ b/ui/status/log.go
@@ -0,0 +1,136 @@
+// Copyright 2018 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 status
+
+import (
+ "android/soong/ui/logger"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "strings"
+)
+
+type verboseLog struct {
+ w io.WriteCloser
+}
+
+func NewVerboseLog(log logger.Logger, filename string) StatusOutput {
+ if !strings.HasSuffix(filename, ".gz") {
+ filename += ".gz"
+ }
+
+ f, err := logger.CreateFileWithRotation(filename, 5)
+ if err != nil {
+ log.Println("Failed to create verbose log file:", err)
+ return nil
+ }
+
+ w := gzip.NewWriter(f)
+
+ return &verboseLog{
+ w: w,
+ }
+}
+
+func (v *verboseLog) StartAction(action *Action, counts Counts) {}
+
+func (v *verboseLog) FinishAction(result ActionResult, counts Counts) {
+ cmd := result.Command
+ if cmd == "" {
+ cmd = result.Description
+ }
+
+ fmt.Fprintf(v.w, "[%d/%d] %s\n", counts.FinishedActions, counts.TotalActions, cmd)
+
+ if result.Error != nil {
+ fmt.Fprintf(v.w, "FAILED: %s\n", strings.Join(result.Outputs, " "))
+ }
+
+ if result.Output != "" {
+ fmt.Fprintln(v.w, result.Output)
+ }
+}
+
+func (v *verboseLog) Flush() {
+ v.w.Close()
+}
+
+func (v *verboseLog) Message(level MsgLevel, message string) {
+ fmt.Fprintf(v.w, "%s%s\n", level.Prefix(), message)
+}
+
+type errorLog struct {
+ w io.WriteCloser
+
+ empty bool
+}
+
+func NewErrorLog(log logger.Logger, filename string) StatusOutput {
+ f, err := logger.CreateFileWithRotation(filename, 5)
+ if err != nil {
+ log.Println("Failed to create error log file:", err)
+ return nil
+ }
+
+ return &errorLog{
+ w: f,
+ empty: true,
+ }
+}
+
+func (e *errorLog) StartAction(action *Action, counts Counts) {}
+
+func (e *errorLog) FinishAction(result ActionResult, counts Counts) {
+ if result.Error == nil {
+ return
+ }
+
+ cmd := result.Command
+ if cmd == "" {
+ cmd = result.Description
+ }
+
+ if !e.empty {
+ fmt.Fprintf(e.w, "\n\n")
+ }
+ e.empty = false
+
+ fmt.Fprintf(e.w, "FAILED: %s\n", result.Description)
+ if len(result.Outputs) > 0 {
+ fmt.Fprintf(e.w, "Outputs: %s\n", strings.Join(result.Outputs, " "))
+ }
+ fmt.Fprintf(e.w, "Error: %s\n", result.Error)
+ if result.Command != "" {
+ fmt.Fprintf(e.w, "Command: %s\n", result.Command)
+ }
+ fmt.Fprintf(e.w, "Output:\n%s\n", result.Output)
+}
+
+func (e *errorLog) Flush() {
+ e.w.Close()
+}
+
+func (e *errorLog) Message(level MsgLevel, message string) {
+ if level < ErrorLvl {
+ return
+ }
+
+ if !e.empty {
+ fmt.Fprintf(e.w, "\n\n")
+ }
+ e.empty = false
+
+ fmt.Fprintf(e.w, "error: %s\n", message)
+}
diff --git a/ui/status/ninja.go b/ui/status/ninja.go
new file mode 100644
index 00000000..4cce6815
--- /dev/null
+++ b/ui/status/ninja.go
@@ -0,0 +1,153 @@
+// Copyright 2018 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 status
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "syscall"
+
+ "github.com/golang/protobuf/proto"
+
+ "android/soong/ui/logger"
+ "android/soong/ui/status/ninja_frontend"
+)
+
+// NinjaReader reads the protobuf frontend format from ninja and translates it
+// into calls on the ToolStatus API.
+func NinjaReader(ctx logger.Logger, status ToolStatus, fifo string) {
+ os.Remove(fifo)
+
+ err := syscall.Mkfifo(fifo, 0666)
+ if err != nil {
+ ctx.Fatalf("Failed to mkfifo(%q): %v", fifo, err)
+ }
+
+ go ninjaReader(ctx, status, fifo)
+}
+
+func ninjaReader(ctx logger.Logger, status ToolStatus, fifo string) {
+ defer os.Remove(fifo)
+
+ f, err := os.Open(fifo)
+ if err != nil {
+ ctx.Fatal("Failed to open fifo:", err)
+ }
+ defer f.Close()
+
+ r := bufio.NewReader(f)
+
+ running := map[uint32]*Action{}
+
+ for {
+ size, err := readVarInt(r)
+ if err != nil {
+ if err != io.EOF {
+ ctx.Println("Got error reading from ninja:", err)
+ }
+ return
+ }
+
+ buf := make([]byte, size)
+ _, err = io.ReadFull(r, buf)
+ if err != nil {
+ if err == io.EOF {
+ ctx.Printf("Missing message of size %d from ninja\n", size)
+ } else {
+ ctx.Fatal("Got error reading from ninja:", err)
+ }
+ return
+ }
+
+ msg := &ninja_frontend.Status{}
+ err = proto.Unmarshal(buf, msg)
+ if err != nil {
+ ctx.Printf("Error reading message from ninja: %v\n", err)
+ continue
+ }
+
+ // Ignore msg.BuildStarted
+ if msg.TotalEdges != nil {
+ status.SetTotalActions(int(msg.TotalEdges.GetTotalEdges()))
+ }
+ if msg.EdgeStarted != nil {
+ action := &Action{
+ Description: msg.EdgeStarted.GetDesc(),
+ Outputs: msg.EdgeStarted.Outputs,
+ Command: msg.EdgeStarted.GetCommand(),
+ }
+ status.StartAction(action)
+ running[msg.EdgeStarted.GetId()] = action
+ }
+ if msg.EdgeFinished != nil {
+ if started, ok := running[msg.EdgeFinished.GetId()]; ok {
+ delete(running, msg.EdgeFinished.GetId())
+
+ var err error
+ exitCode := int(msg.EdgeFinished.GetStatus())
+ if exitCode != 0 {
+ err = fmt.Errorf("exited with code: %d", exitCode)
+ }
+
+ status.FinishAction(ActionResult{
+ Action: started,
+ Output: msg.EdgeFinished.GetOutput(),
+ Error: err,
+ })
+ }
+ }
+ if msg.Message != nil {
+ message := "ninja: " + msg.Message.GetMessage()
+ switch msg.Message.GetLevel() {
+ case ninja_frontend.Status_Message_INFO:
+ status.Status(message)
+ case ninja_frontend.Status_Message_WARNING:
+ status.Print("warning: " + message)
+ case ninja_frontend.Status_Message_ERROR:
+ status.Error(message)
+ default:
+ status.Print(message)
+ }
+ }
+ if msg.BuildFinished != nil {
+ status.Finish()
+ }
+ }
+}
+
+func readVarInt(r *bufio.Reader) (int, error) {
+ ret := 0
+ shift := uint(0)
+
+ for {
+ b, err := r.ReadByte()
+ if err != nil {
+ return 0, err
+ }
+
+ ret += int(b&0x7f) << (shift * 7)
+ if b&0x80 == 0 {
+ break
+ }
+ shift += 1
+ if shift > 4 {
+ return 0, fmt.Errorf("Expected varint32 length-delimited message")
+ }
+ }
+
+ return ret, nil
+}
diff --git a/ui/status/ninja_frontend/README b/ui/status/ninja_frontend/README
new file mode 100644
index 00000000..8c4b4510
--- /dev/null
+++ b/ui/status/ninja_frontend/README
@@ -0,0 +1,3 @@
+This comes from https://android.googlesource.com/platform/external/ninja/+/master/src/frontend.proto
+
+The only difference is the specification of a go_package. To regenerate frontend.pb.go, run regen.sh.
diff --git a/ui/status/ninja_frontend/frontend.pb.go b/ui/status/ninja_frontend/frontend.pb.go
new file mode 100644
index 00000000..7c05eed4
--- /dev/null
+++ b/ui/status/ninja_frontend/frontend.pb.go
@@ -0,0 +1,510 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: frontend.proto
+
+package ninja_frontend
+
+import proto "github.com/golang/protobuf/proto"
+import fmt "fmt"
+import math "math"
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
+
+type Status_Message_Level int32
+
+const (
+ Status_Message_INFO Status_Message_Level = 0
+ Status_Message_WARNING Status_Message_Level = 1
+ Status_Message_ERROR Status_Message_Level = 2
+)
+
+var Status_Message_Level_name = map[int32]string{
+ 0: "INFO",
+ 1: "WARNING",
+ 2: "ERROR",
+}
+var Status_Message_Level_value = map[string]int32{
+ "INFO": 0,
+ "WARNING": 1,
+ "ERROR": 2,
+}
+
+func (x Status_Message_Level) Enum() *Status_Message_Level {
+ p := new(Status_Message_Level)
+ *p = x
+ return p
+}
+func (x Status_Message_Level) String() string {
+ return proto.EnumName(Status_Message_Level_name, int32(x))
+}
+func (x *Status_Message_Level) UnmarshalJSON(data []byte) error {
+ value, err := proto.UnmarshalJSONEnum(Status_Message_Level_value, data, "Status_Message_Level")
+ if err != nil {
+ return err
+ }
+ *x = Status_Message_Level(value)
+ return nil
+}
+func (Status_Message_Level) EnumDescriptor() ([]byte, []int) {
+ return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 5, 0}
+}
+
+type Status struct {
+ TotalEdges *Status_TotalEdges `protobuf:"bytes,1,opt,name=total_edges,json=totalEdges" json:"total_edges,omitempty"`
+ BuildStarted *Status_BuildStarted `protobuf:"bytes,2,opt,name=build_started,json=buildStarted" json:"build_started,omitempty"`
+ BuildFinished *Status_BuildFinished `protobuf:"bytes,3,opt,name=build_finished,json=buildFinished" json:"build_finished,omitempty"`
+ EdgeStarted *Status_EdgeStarted `protobuf:"bytes,4,opt,name=edge_started,json=edgeStarted" json:"edge_started,omitempty"`
+ EdgeFinished *Status_EdgeFinished `protobuf:"bytes,5,opt,name=edge_finished,json=edgeFinished" json:"edge_finished,omitempty"`
+ Message *Status_Message `protobuf:"bytes,6,opt,name=message" json:"message,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *Status) Reset() { *m = Status{} }
+func (m *Status) String() string { return proto.CompactTextString(m) }
+func (*Status) ProtoMessage() {}
+func (*Status) Descriptor() ([]byte, []int) {
+ return fileDescriptor_frontend_5a49d9b15a642005, []int{0}
+}
+func (m *Status) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_Status.Unmarshal(m, b)
+}
+func (m *Status) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_Status.Marshal(b, m, deterministic)
+}
+func (dst *Status) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_Status.Merge(dst, src)
+}
+func (m *Status) XXX_Size() int {
+ return xxx_messageInfo_Status.Size(m)
+}
+func (m *Status) XXX_DiscardUnknown() {
+ xxx_messageInfo_Status.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Status proto.InternalMessageInfo
+
+func (m *Status) GetTotalEdges() *Status_TotalEdges {
+ if m != nil {
+ return m.TotalEdges
+ }
+ return nil
+}
+
+func (m *Status) GetBuildStarted() *Status_BuildStarted {
+ if m != nil {
+ return m.BuildStarted
+ }
+ return nil
+}
+
+func (m *Status) GetBuildFinished() *Status_BuildFinished {
+ if m != nil {
+ return m.BuildFinished
+ }
+ return nil
+}
+
+func (m *Status) GetEdgeStarted() *Status_EdgeStarted {
+ if m != nil {
+ return m.EdgeStarted
+ }
+ return nil
+}
+
+func (m *Status) GetEdgeFinished() *Status_EdgeFinished {
+ if m != nil {
+ return m.EdgeFinished
+ }
+ return nil
+}
+
+func (m *Status) GetMessage() *Status_Message {
+ if m != nil {
+ return m.Message
+ }
+ return nil
+}
+
+type Status_TotalEdges struct {
+ // New value for total edges in the build.
+ TotalEdges *uint32 `protobuf:"varint,1,opt,name=total_edges,json=totalEdges" json:"total_edges,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *Status_TotalEdges) Reset() { *m = Status_TotalEdges{} }
+func (m *Status_TotalEdges) String() string { return proto.CompactTextString(m) }
+func (*Status_TotalEdges) ProtoMessage() {}
+func (*Status_TotalEdges) Descriptor() ([]byte, []int) {
+ return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 0}
+}
+func (m *Status_TotalEdges) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_Status_TotalEdges.Unmarshal(m, b)
+}
+func (m *Status_TotalEdges) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_Status_TotalEdges.Marshal(b, m, deterministic)
+}
+func (dst *Status_TotalEdges) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_Status_TotalEdges.Merge(dst, src)
+}
+func (m *Status_TotalEdges) XXX_Size() int {
+ return xxx_messageInfo_Status_TotalEdges.Size(m)
+}
+func (m *Status_TotalEdges) XXX_DiscardUnknown() {
+ xxx_messageInfo_Status_TotalEdges.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Status_TotalEdges proto.InternalMessageInfo
+
+func (m *Status_TotalEdges) GetTotalEdges() uint32 {
+ if m != nil && m.TotalEdges != nil {
+ return *m.TotalEdges
+ }
+ return 0
+}
+
+type Status_BuildStarted struct {
+ // Number of jobs Ninja will run in parallel.
+ Parallelism *uint32 `protobuf:"varint,1,opt,name=parallelism" json:"parallelism,omitempty"`
+ // Verbose value passed to ninja.
+ Verbose *bool `protobuf:"varint,2,opt,name=verbose" json:"verbose,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *Status_BuildStarted) Reset() { *m = Status_BuildStarted{} }
+func (m *Status_BuildStarted) String() string { return proto.CompactTextString(m) }
+func (*Status_BuildStarted) ProtoMessage() {}
+func (*Status_BuildStarted) Descriptor() ([]byte, []int) {
+ return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 1}
+}
+func (m *Status_BuildStarted) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_Status_BuildStarted.Unmarshal(m, b)
+}
+func (m *Status_BuildStarted) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_Status_BuildStarted.Marshal(b, m, deterministic)
+}
+func (dst *Status_BuildStarted) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_Status_BuildStarted.Merge(dst, src)
+}
+func (m *Status_BuildStarted) XXX_Size() int {
+ return xxx_messageInfo_Status_BuildStarted.Size(m)
+}
+func (m *Status_BuildStarted) XXX_DiscardUnknown() {
+ xxx_messageInfo_Status_BuildStarted.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Status_BuildStarted proto.InternalMessageInfo
+
+func (m *Status_BuildStarted) GetParallelism() uint32 {
+ if m != nil && m.Parallelism != nil {
+ return *m.Parallelism
+ }
+ return 0
+}
+
+func (m *Status_BuildStarted) GetVerbose() bool {
+ if m != nil && m.Verbose != nil {
+ return *m.Verbose
+ }
+ return false
+}
+
+type Status_BuildFinished struct {
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *Status_BuildFinished) Reset() { *m = Status_BuildFinished{} }
+func (m *Status_BuildFinished) String() string { return proto.CompactTextString(m) }
+func (*Status_BuildFinished) ProtoMessage() {}
+func (*Status_BuildFinished) Descriptor() ([]byte, []int) {
+ return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 2}
+}
+func (m *Status_BuildFinished) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_Status_BuildFinished.Unmarshal(m, b)
+}
+func (m *Status_BuildFinished) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_Status_BuildFinished.Marshal(b, m, deterministic)
+}
+func (dst *Status_BuildFinished) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_Status_BuildFinished.Merge(dst, src)
+}
+func (m *Status_BuildFinished) XXX_Size() int {
+ return xxx_messageInfo_Status_BuildFinished.Size(m)
+}
+func (m *Status_BuildFinished) XXX_DiscardUnknown() {
+ xxx_messageInfo_Status_BuildFinished.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Status_BuildFinished proto.InternalMessageInfo
+
+type Status_EdgeStarted struct {
+ // Edge identification number, unique to a Ninja run.
+ Id *uint32 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`
+ // Edge start time in milliseconds since Ninja started.
+ StartTime *uint32 `protobuf:"varint,2,opt,name=start_time,json=startTime" json:"start_time,omitempty"`
+ // List of edge inputs.
+ Inputs []string `protobuf:"bytes,3,rep,name=inputs" json:"inputs,omitempty"`
+ // List of edge outputs.
+ Outputs []string `protobuf:"bytes,4,rep,name=outputs" json:"outputs,omitempty"`
+ // Description field from the edge.
+ Desc *string `protobuf:"bytes,5,opt,name=desc" json:"desc,omitempty"`
+ // Command field from the edge.
+ Command *string `protobuf:"bytes,6,opt,name=command" json:"command,omitempty"`
+ // Edge uses console.
+ Console *bool `protobuf:"varint,7,opt,name=console" json:"console,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *Status_EdgeStarted) Reset() { *m = Status_EdgeStarted{} }
+func (m *Status_EdgeStarted) String() string { return proto.CompactTextString(m) }
+func (*Status_EdgeStarted) ProtoMessage() {}
+func (*Status_EdgeStarted) Descriptor() ([]byte, []int) {
+ return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 3}
+}
+func (m *Status_EdgeStarted) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_Status_EdgeStarted.Unmarshal(m, b)
+}
+func (m *Status_EdgeStarted) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_Status_EdgeStarted.Marshal(b, m, deterministic)
+}
+func (dst *Status_EdgeStarted) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_Status_EdgeStarted.Merge(dst, src)
+}
+func (m *Status_EdgeStarted) XXX_Size() int {
+ return xxx_messageInfo_Status_EdgeStarted.Size(m)
+}
+func (m *Status_EdgeStarted) XXX_DiscardUnknown() {
+ xxx_messageInfo_Status_EdgeStarted.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Status_EdgeStarted proto.InternalMessageInfo
+
+func (m *Status_EdgeStarted) GetId() uint32 {
+ if m != nil && m.Id != nil {
+ return *m.Id
+ }
+ return 0
+}
+
+func (m *Status_EdgeStarted) GetStartTime() uint32 {
+ if m != nil && m.StartTime != nil {
+ return *m.StartTime
+ }
+ return 0
+}
+
+func (m *Status_EdgeStarted) GetInputs() []string {
+ if m != nil {
+ return m.Inputs
+ }
+ return nil
+}
+
+func (m *Status_EdgeStarted) GetOutputs() []string {
+ if m != nil {
+ return m.Outputs
+ }
+ return nil
+}
+
+func (m *Status_EdgeStarted) GetDesc() string {
+ if m != nil && m.Desc != nil {
+ return *m.Desc
+ }
+ return ""
+}
+
+func (m *Status_EdgeStarted) GetCommand() string {
+ if m != nil && m.Command != nil {
+ return *m.Command
+ }
+ return ""
+}
+
+func (m *Status_EdgeStarted) GetConsole() bool {
+ if m != nil && m.Console != nil {
+ return *m.Console
+ }
+ return false
+}
+
+type Status_EdgeFinished struct {
+ // Edge identification number, unique to a Ninja run.
+ Id *uint32 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`
+ // Edge end time in milliseconds since Ninja started.
+ EndTime *uint32 `protobuf:"varint,2,opt,name=end_time,json=endTime" json:"end_time,omitempty"`
+ // Exit status (0 for success).
+ Status *int32 `protobuf:"zigzag32,3,opt,name=status" json:"status,omitempty"`
+ // Edge output, may contain ANSI codes.
+ Output *string `protobuf:"bytes,4,opt,name=output" json:"output,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *Status_EdgeFinished) Reset() { *m = Status_EdgeFinished{} }
+func (m *Status_EdgeFinished) String() string { return proto.CompactTextString(m) }
+func (*Status_EdgeFinished) ProtoMessage() {}
+func (*Status_EdgeFinished) Descriptor() ([]byte, []int) {
+ return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 4}
+}
+func (m *Status_EdgeFinished) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_Status_EdgeFinished.Unmarshal(m, b)
+}
+func (m *Status_EdgeFinished) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_Status_EdgeFinished.Marshal(b, m, deterministic)
+}
+func (dst *Status_EdgeFinished) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_Status_EdgeFinished.Merge(dst, src)
+}
+func (m *Status_EdgeFinished) XXX_Size() int {
+ return xxx_messageInfo_Status_EdgeFinished.Size(m)
+}
+func (m *Status_EdgeFinished) XXX_DiscardUnknown() {
+ xxx_messageInfo_Status_EdgeFinished.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Status_EdgeFinished proto.InternalMessageInfo
+
+func (m *Status_EdgeFinished) GetId() uint32 {
+ if m != nil && m.Id != nil {
+ return *m.Id
+ }
+ return 0
+}
+
+func (m *Status_EdgeFinished) GetEndTime() uint32 {
+ if m != nil && m.EndTime != nil {
+ return *m.EndTime
+ }
+ return 0
+}
+
+func (m *Status_EdgeFinished) GetStatus() int32 {
+ if m != nil && m.Status != nil {
+ return *m.Status
+ }
+ return 0
+}
+
+func (m *Status_EdgeFinished) GetOutput() string {
+ if m != nil && m.Output != nil {
+ return *m.Output
+ }
+ return ""
+}
+
+type Status_Message struct {
+ // Message priority level (INFO, WARNING, or ERROR).
+ Level *Status_Message_Level `protobuf:"varint,1,opt,name=level,enum=ninja.Status_Message_Level,def=0" json:"level,omitempty"`
+ // Info/warning/error message from Ninja.
+ Message *string `protobuf:"bytes,2,opt,name=message" json:"message,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *Status_Message) Reset() { *m = Status_Message{} }
+func (m *Status_Message) String() string { return proto.CompactTextString(m) }
+func (*Status_Message) ProtoMessage() {}
+func (*Status_Message) Descriptor() ([]byte, []int) {
+ return fileDescriptor_frontend_5a49d9b15a642005, []int{0, 5}
+}
+func (m *Status_Message) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_Status_Message.Unmarshal(m, b)
+}
+func (m *Status_Message) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_Status_Message.Marshal(b, m, deterministic)
+}
+func (dst *Status_Message) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_Status_Message.Merge(dst, src)
+}
+func (m *Status_Message) XXX_Size() int {
+ return xxx_messageInfo_Status_Message.Size(m)
+}
+func (m *Status_Message) XXX_DiscardUnknown() {
+ xxx_messageInfo_Status_Message.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Status_Message proto.InternalMessageInfo
+
+const Default_Status_Message_Level Status_Message_Level = Status_Message_INFO
+
+func (m *Status_Message) GetLevel() Status_Message_Level {
+ if m != nil && m.Level != nil {
+ return *m.Level
+ }
+ return Default_Status_Message_Level
+}
+
+func (m *Status_Message) GetMessage() string {
+ if m != nil && m.Message != nil {
+ return *m.Message
+ }
+ return ""
+}
+
+func init() {
+ proto.RegisterType((*Status)(nil), "ninja.Status")
+ proto.RegisterType((*Status_TotalEdges)(nil), "ninja.Status.TotalEdges")
+ proto.RegisterType((*Status_BuildStarted)(nil), "ninja.Status.BuildStarted")
+ proto.RegisterType((*Status_BuildFinished)(nil), "ninja.Status.BuildFinished")
+ proto.RegisterType((*Status_EdgeStarted)(nil), "ninja.Status.EdgeStarted")
+ proto.RegisterType((*Status_EdgeFinished)(nil), "ninja.Status.EdgeFinished")
+ proto.RegisterType((*Status_Message)(nil), "ninja.Status.Message")
+ proto.RegisterEnum("ninja.Status_Message_Level", Status_Message_Level_name, Status_Message_Level_value)
+}
+
+func init() { proto.RegisterFile("frontend.proto", fileDescriptor_frontend_5a49d9b15a642005) }
+
+var fileDescriptor_frontend_5a49d9b15a642005 = []byte{
+ // 496 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x53, 0xd1, 0x6e, 0xd3, 0x30,
+ 0x14, 0xa5, 0x69, 0xd3, 0x34, 0x37, 0x6d, 0x28, 0x96, 0x40, 0x59, 0x10, 0xa2, 0xda, 0xd3, 0x78,
+ 0x20, 0x48, 0xbc, 0x20, 0x10, 0x12, 0xa2, 0xd2, 0x06, 0x43, 0xd0, 0x49, 0xde, 0x24, 0x24, 0x5e,
+ 0xaa, 0x74, 0xf6, 0x86, 0x51, 0xe2, 0x54, 0xb1, 0xbb, 0x5f, 0xe0, 0x7f, 0x78, 0xe0, 0xfb, 0x90,
+ 0xaf, 0xed, 0x2c, 0x65, 0x7b, 0xcb, 0xf1, 0x3d, 0xe7, 0xde, 0x73, 0x8f, 0x1d, 0x48, 0xaf, 0xda,
+ 0x46, 0x6a, 0x2e, 0x59, 0xb1, 0x6d, 0x1b, 0xdd, 0x90, 0x50, 0x0a, 0xf9, 0xab, 0x3c, 0xfc, 0x13,
+ 0xc1, 0xf8, 0x5c, 0x97, 0x7a, 0xa7, 0xc8, 0x5b, 0x48, 0x74, 0xa3, 0xcb, 0x6a, 0xcd, 0xd9, 0x35,
+ 0x57, 0xd9, 0x60, 0x31, 0x38, 0x4a, 0x5e, 0x67, 0x05, 0xf2, 0x0a, 0xcb, 0x29, 0x2e, 0x0c, 0xe1,
+ 0xd8, 0xd4, 0x29, 0xe8, 0xee, 0x9b, 0x7c, 0x80, 0xd9, 0x66, 0x27, 0x2a, 0xb6, 0x56, 0xba, 0x6c,
+ 0x35, 0x67, 0x59, 0x80, 0xe2, 0x7c, 0x5f, 0xbc, 0x34, 0x94, 0x73, 0xcb, 0xa0, 0xd3, 0x4d, 0x0f,
+ 0x91, 0x25, 0xa4, 0xb6, 0xc1, 0x95, 0x90, 0x42, 0xfd, 0xe4, 0x2c, 0x1b, 0x62, 0x87, 0xa7, 0xf7,
+ 0x74, 0x38, 0x71, 0x14, 0x6a, 0x67, 0x7a, 0x48, 0xde, 0xc3, 0xd4, 0x38, 0xef, 0x3c, 0x8c, 0xb0,
+ 0xc3, 0xc1, 0x7e, 0x07, 0xe3, 0xd7, 0x5b, 0x48, 0xf8, 0x2d, 0x30, 0x2b, 0xa0, 0xba, 0x33, 0x10,
+ 0xde, 0xb7, 0x82, 0x91, 0x77, 0xf3, 0x71, 0x5c, 0x37, 0xfe, 0x15, 0x44, 0x35, 0x57, 0xaa, 0xbc,
+ 0xe6, 0xd9, 0x18, 0xa5, 0x8f, 0xf7, 0xa5, 0xdf, 0x6c, 0x91, 0x7a, 0x56, 0xfe, 0x12, 0xe0, 0x36,
+ 0x4e, 0xf2, 0xfc, 0x6e, 0xfa, 0xb3, 0x7e, 0xc6, 0xf9, 0x17, 0x98, 0xf6, 0x03, 0x24, 0x0b, 0x48,
+ 0xb6, 0x65, 0x5b, 0x56, 0x15, 0xaf, 0x84, 0xaa, 0x9d, 0xa0, 0x7f, 0x44, 0x32, 0x88, 0x6e, 0x78,
+ 0xbb, 0x69, 0x14, 0xc7, 0xfb, 0x98, 0x50, 0x0f, 0xf3, 0x87, 0x30, 0xdb, 0x8b, 0x32, 0xff, 0x3b,
+ 0x80, 0xa4, 0x17, 0x0d, 0x49, 0x21, 0x10, 0xcc, 0xf5, 0x0c, 0x04, 0x23, 0xcf, 0x00, 0x30, 0xd6,
+ 0xb5, 0x16, 0xb5, 0xed, 0x36, 0xa3, 0x31, 0x9e, 0x5c, 0x88, 0x9a, 0x93, 0x27, 0x30, 0x16, 0x72,
+ 0xbb, 0xd3, 0x2a, 0x1b, 0x2e, 0x86, 0x47, 0x31, 0x75, 0xc8, 0x38, 0x68, 0x76, 0x1a, 0x0b, 0x23,
+ 0x2c, 0x78, 0x48, 0x08, 0x8c, 0x18, 0x57, 0x97, 0x98, 0x72, 0x4c, 0xf1, 0xdb, 0xb0, 0x2f, 0x9b,
+ 0xba, 0x2e, 0x25, 0xc3, 0x04, 0x63, 0xea, 0xa1, 0xad, 0x48, 0xd5, 0x54, 0x3c, 0x8b, 0xec, 0x26,
+ 0x0e, 0xe6, 0x02, 0xa6, 0xfd, 0x3b, 0xb9, 0x63, 0xfc, 0x00, 0x26, 0x5c, 0xb2, 0xbe, 0xed, 0x88,
+ 0x4b, 0xe6, 0x4d, 0x2b, 0xbc, 0x1a, 0x7c, 0x6b, 0x8f, 0xa8, 0x43, 0xe6, 0xdc, 0xba, 0xc4, 0x17,
+ 0x14, 0x53, 0x87, 0xf2, 0xdf, 0x03, 0x88, 0xdc, 0x25, 0x92, 0x37, 0x10, 0x56, 0xfc, 0x86, 0x57,
+ 0x38, 0x29, 0xfd, 0xff, 0x99, 0x3a, 0x56, 0xf1, 0xd5, 0x50, 0xde, 0x8d, 0x4e, 0x57, 0x27, 0x67,
+ 0xd4, 0xf2, 0xcd, 0x26, 0xfe, 0x95, 0x04, 0x76, 0x47, 0x07, 0x0f, 0x5f, 0x40, 0x88, 0x7c, 0x32,
+ 0x01, 0x54, 0xcc, 0x1f, 0x90, 0x04, 0xa2, 0xef, 0x1f, 0xe9, 0xea, 0x74, 0xf5, 0x69, 0x3e, 0x20,
+ 0x31, 0x84, 0xc7, 0x94, 0x9e, 0xd1, 0x79, 0xb0, 0x24, 0x9f, 0x87, 0x3f, 0x52, 0x9c, 0xb8, 0xf6,
+ 0x7f, 0xf5, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x2e, 0x8c, 0xef, 0xcb, 0xe0, 0x03, 0x00, 0x00,
+}
diff --git a/ui/status/ninja_frontend/frontend.proto b/ui/status/ninja_frontend/frontend.proto
new file mode 100644
index 00000000..13fd535d
--- /dev/null
+++ b/ui/status/ninja_frontend/frontend.proto
@@ -0,0 +1,84 @@
+// 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.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package ninja;
+option go_package = "ninja_frontend";
+
+message Status {
+ message TotalEdges {
+ // New value for total edges in the build.
+ optional uint32 total_edges = 1;
+ }
+
+ message BuildStarted {
+ // Number of jobs Ninja will run in parallel.
+ optional uint32 parallelism = 1;
+ // Verbose value passed to ninja.
+ optional bool verbose = 2;
+ }
+
+ message BuildFinished {
+ }
+
+ message EdgeStarted {
+ // Edge identification number, unique to a Ninja run.
+ optional uint32 id = 1;
+ // Edge start time in milliseconds since Ninja started.
+ optional uint32 start_time = 2;
+ // List of edge inputs.
+ repeated string inputs = 3;
+ // List of edge outputs.
+ repeated string outputs = 4;
+ // Description field from the edge.
+ optional string desc = 5;
+ // Command field from the edge.
+ optional string command = 6;
+ // Edge uses console.
+ optional bool console = 7;
+ }
+
+ message EdgeFinished {
+ // Edge identification number, unique to a Ninja run.
+ optional uint32 id = 1;
+ // Edge end time in milliseconds since Ninja started.
+ optional uint32 end_time = 2;
+ // Exit status (0 for success).
+ optional sint32 status = 3;
+ // Edge output, may contain ANSI codes.
+ optional string output = 4;
+ }
+
+ message Message {
+ enum Level {
+ INFO = 0;
+ WARNING = 1;
+ ERROR = 2;
+ }
+ // Message priority level (INFO, WARNING, or ERROR).
+ optional Level level = 1 [default = INFO];
+ // Info/warning/error message from Ninja.
+ optional string message = 2;
+ }
+
+ optional TotalEdges total_edges = 1;
+ optional BuildStarted build_started = 2;
+ optional BuildFinished build_finished = 3;
+ optional EdgeStarted edge_started = 4;
+ optional EdgeFinished edge_finished = 5;
+ optional Message message = 6;
+}
diff --git a/ui/status/ninja_frontend/regen.sh b/ui/status/ninja_frontend/regen.sh
new file mode 100755
index 00000000..d270731a
--- /dev/null
+++ b/ui/status/ninja_frontend/regen.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+aprotoc --go_out=paths=source_relative:. frontend.proto
diff --git a/ui/status/status.go b/ui/status/status.go
new file mode 100644
index 00000000..c851d7f5
--- /dev/null
+++ b/ui/status/status.go
@@ -0,0 +1,340 @@
+// Copyright 2018 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 status tracks actions run by various tools, combining the counts
+// (total actions, currently running, started, finished), and giving that to
+// multiple outputs.
+package status
+
+import (
+ "sync"
+)
+
+// Action describes an action taken (or as Ninja calls them, Edges).
+type Action struct {
+ // Description is a shorter, more readable form of the command, meant
+ // for users. It's optional, but one of either Description or Command
+ // should be set.
+ Description string
+
+ // Outputs is the (optional) list of outputs. Usually these are files,
+ // but they can be any string.
+ Outputs []string
+
+ // Command is the actual command line executed to perform the action.
+ // It's optional, but one of either Description or Command should be
+ // set.
+ Command string
+}
+
+// ActionResult describes the result of running an Action.
+type ActionResult struct {
+ // Action is a pointer to the original Action struct.
+ *Action
+
+ // Output is the output produced by the command (usually stdout&stderr
+ // for Actions that run commands)
+ Output string
+
+ // Error is nil if the Action succeeded, or set to an error if it
+ // failed.
+ Error error
+}
+
+// Counts describes the number of actions in each state
+type Counts struct {
+ // TotalActions is the total number of expected changes. This can
+ // generally change up or down during a build, but it should never go
+ // below the number of StartedActions
+ TotalActions int
+
+ // RunningActions are the number of actions that are currently running
+ // -- the number that have called StartAction, but not FinishAction.
+ RunningActions int
+
+ // StartedActions are the number of actions that have been started with
+ // StartAction.
+ StartedActions int
+
+ // FinishedActions are the number of actions that have been finished
+ // with FinishAction.
+ FinishedActions int
+}
+
+// ToolStatus is the interface used by tools to report on their Actions, and to
+// present other information through a set of messaging functions.
+type ToolStatus interface {
+ // SetTotalActions sets the expected total number of actions that will
+ // be started by this tool.
+ //
+ // This call be will ignored if it sets a number that is less than the
+ // current number of started actions.
+ SetTotalActions(total int)
+
+ // StartAction specifies that the associated action has been started by
+ // the tool.
+ //
+ // A specific *Action should not be specified to StartAction more than
+ // once, even if the previous action has already been finished, and the
+ // contents rewritten.
+ //
+ // Do not re-use *Actions between different ToolStatus interfaces
+ // either.
+ StartAction(action *Action)
+
+ // FinishAction specifies the result of a particular Action.
+ //
+ // The *Action embedded in the ActionResult structure must have already
+ // been passed to StartAction (on this interface).
+ //
+ // Do not call FinishAction twice for the same *Action.
+ FinishAction(result ActionResult)
+
+ // Verbose takes a non-important message that is never printed to the
+ // screen, but is in the verbose build log, etc
+ Verbose(msg string)
+ // Status takes a less important message that may be printed to the
+ // screen, but overwritten by another status message. The full message
+ // will still appear in the verbose build log.
+ Status(msg string)
+ // Print takes an message and displays it to the screen and other
+ // output logs, etc.
+ Print(msg string)
+ // Error is similar to Print, but treats it similarly to a failed
+ // action, showing it in the error logs, etc.
+ Error(msg string)
+
+ // Finish marks the end of all Actions being run by this tool.
+ //
+ // SetTotalEdges, StartAction, and FinishAction should not be called
+ // after Finish.
+ Finish()
+}
+
+// MsgLevel specifies the importance of a particular log message. See the
+// descriptions in ToolStatus: Verbose, Status, Print, Error.
+type MsgLevel int
+
+const (
+ VerboseLvl MsgLevel = iota
+ StatusLvl
+ PrintLvl
+ ErrorLvl
+)
+
+func (l MsgLevel) Prefix() string {
+ switch l {
+ case VerboseLvl:
+ return "verbose: "
+ case StatusLvl:
+ return "status: "
+ case PrintLvl:
+ return ""
+ case ErrorLvl:
+ return "error: "
+ default:
+ panic("Unknown message level")
+ }
+}
+
+// StatusOutput is the interface used to get status information as a Status
+// output.
+//
+// All of the functions here are guaranteed to be called by Status while
+// holding it's internal lock, so it's safe to assume a single caller at any
+// time, and that the ordering of calls will be correct. It is not safe to call
+// back into the Status, or one of its ToolStatus interfaces.
+type StatusOutput interface {
+ // StartAction will be called once every time ToolStatus.StartAction is
+ // called. counts will include the current counters across all
+ // ToolStatus instances, including ones that have been finished.
+ StartAction(action *Action, counts Counts)
+
+ // FinishAction will be called once every time ToolStatus.FinishAction
+ // is called. counts will include the current counters across all
+ // ToolStatus instances, including ones that have been finished.
+ FinishAction(result ActionResult, counts Counts)
+
+ // Message is the equivalent of ToolStatus.Verbose/Status/Print/Error,
+ // but the level is specified as an argument.
+ Message(level MsgLevel, msg string)
+
+ // Flush is called when your outputs should be flushed / closed. No
+ // output is expected after this call.
+ Flush()
+}
+
+// Status is the multiplexer / accumulator between ToolStatus instances (via
+// StartTool) and StatusOutputs (via AddOutput). There's generally one of these
+// per build process (though tools like multiproduct_kati may have multiple
+// independent versions).
+type Status struct {
+ counts Counts
+ outputs []StatusOutput
+
+ // Protects counts and outputs, and allows each output to
+ // expect only a single caller at a time.
+ lock sync.Mutex
+}
+
+// AddOutput attaches an output to this object. It's generally expected that an
+// output is attached to a single Status instance.
+func (s *Status) AddOutput(output StatusOutput) {
+ if output == nil {
+ return
+ }
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.outputs = append(s.outputs, output)
+}
+
+// StartTool returns a new ToolStatus instance to report the status of a tool.
+func (s *Status) StartTool() ToolStatus {
+ return &toolStatus{
+ status: s,
+ }
+}
+
+// Finish will call Flush on all the outputs, generally flushing or closing all
+// of their outputs. Do not call any other functions on this instance or any
+// associated ToolStatus instances after this has been called.
+func (s *Status) Finish() {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ for _, o := range s.outputs {
+ o.Flush()
+ }
+}
+
+func (s *Status) updateTotalActions(diff int) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.counts.TotalActions += diff
+}
+
+func (s *Status) startAction(action *Action) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.counts.RunningActions += 1
+ s.counts.StartedActions += 1
+
+ for _, o := range s.outputs {
+ o.StartAction(action, s.counts)
+ }
+}
+
+func (s *Status) finishAction(result ActionResult) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.counts.RunningActions -= 1
+ s.counts.FinishedActions += 1
+
+ for _, o := range s.outputs {
+ o.FinishAction(result, s.counts)
+ }
+}
+
+func (s *Status) message(level MsgLevel, msg string) {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ for _, o := range s.outputs {
+ o.Message(level, msg)
+ }
+}
+
+type toolStatus struct {
+ status *Status
+
+ counts Counts
+ // Protects counts
+ lock sync.Mutex
+}
+
+var _ ToolStatus = (*toolStatus)(nil)
+
+func (d *toolStatus) SetTotalActions(total int) {
+ diff := 0
+
+ d.lock.Lock()
+ if total >= d.counts.StartedActions && total != d.counts.TotalActions {
+ diff = total - d.counts.TotalActions
+ d.counts.TotalActions = total
+ }
+ d.lock.Unlock()
+
+ if diff != 0 {
+ d.status.updateTotalActions(diff)
+ }
+}
+
+func (d *toolStatus) StartAction(action *Action) {
+ totalDiff := 0
+
+ d.lock.Lock()
+ d.counts.RunningActions += 1
+ d.counts.StartedActions += 1
+
+ if d.counts.StartedActions > d.counts.TotalActions {
+ totalDiff = d.counts.StartedActions - d.counts.TotalActions
+ d.counts.TotalActions = d.counts.StartedActions
+ }
+ d.lock.Unlock()
+
+ if totalDiff != 0 {
+ d.status.updateTotalActions(totalDiff)
+ }
+ d.status.startAction(action)
+}
+
+func (d *toolStatus) FinishAction(result ActionResult) {
+ d.lock.Lock()
+ d.counts.RunningActions -= 1
+ d.counts.FinishedActions += 1
+ d.lock.Unlock()
+
+ d.status.finishAction(result)
+}
+
+func (d *toolStatus) Verbose(msg string) {
+ d.status.message(VerboseLvl, msg)
+}
+func (d *toolStatus) Status(msg string) {
+ d.status.message(StatusLvl, msg)
+}
+func (d *toolStatus) Print(msg string) {
+ d.status.message(PrintLvl, msg)
+}
+func (d *toolStatus) Error(msg string) {
+ d.status.message(ErrorLvl, msg)
+}
+
+func (d *toolStatus) Finish() {
+ d.lock.Lock()
+ defer d.lock.Unlock()
+
+ if d.counts.TotalActions != d.counts.StartedActions {
+ d.status.updateTotalActions(d.counts.StartedActions - d.counts.TotalActions)
+ }
+
+ // TODO: update status to correct running/finished edges?
+ d.counts.RunningActions = 0
+ d.counts.TotalActions = d.counts.StartedActions
+}
diff --git a/ui/status/status_test.go b/ui/status/status_test.go
new file mode 100644
index 00000000..e62785f4
--- /dev/null
+++ b/ui/status/status_test.go
@@ -0,0 +1,166 @@
+// Copyright 2018 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 status
+
+import "testing"
+
+type counterOutput Counts
+
+func (c *counterOutput) StartAction(action *Action, counts Counts) {
+ *c = counterOutput(counts)
+}
+func (c *counterOutput) FinishAction(result ActionResult, counts Counts) {
+ *c = counterOutput(counts)
+}
+func (c counterOutput) Message(level MsgLevel, msg string) {}
+func (c counterOutput) Flush() {}
+
+func (c counterOutput) Expect(t *testing.T, counts Counts) {
+ if Counts(c) == counts {
+ return
+ }
+ t.Helper()
+
+ if c.TotalActions != counts.TotalActions {
+ t.Errorf("Expected %d total edges, but got %d", counts.TotalActions, c.TotalActions)
+ }
+ if c.RunningActions != counts.RunningActions {
+ t.Errorf("Expected %d running edges, but got %d", counts.RunningActions, c.RunningActions)
+ }
+ if c.StartedActions != counts.StartedActions {
+ t.Errorf("Expected %d started edges, but got %d", counts.StartedActions, c.StartedActions)
+ }
+ if c.FinishedActions != counts.FinishedActions {
+ t.Errorf("Expected %d finished edges, but got %d", counts.FinishedActions, c.FinishedActions)
+ }
+}
+
+func TestBasicUse(t *testing.T) {
+ status := &Status{}
+ counts := &counterOutput{}
+ status.AddOutput(counts)
+ s := status.StartTool()
+
+ s.SetTotalActions(2)
+
+ a := &Action{}
+ s.StartAction(a)
+
+ counts.Expect(t, Counts{
+ TotalActions: 2,
+ RunningActions: 1,
+ StartedActions: 1,
+ FinishedActions: 0,
+ })
+
+ s.FinishAction(ActionResult{Action: a})
+
+ counts.Expect(t, Counts{
+ TotalActions: 2,
+ RunningActions: 0,
+ StartedActions: 1,
+ FinishedActions: 1,
+ })
+
+ a = &Action{}
+ s.StartAction(a)
+
+ counts.Expect(t, Counts{
+ TotalActions: 2,
+ RunningActions: 1,
+ StartedActions: 2,
+ FinishedActions: 1,
+ })
+
+ s.FinishAction(ActionResult{Action: a})
+
+ counts.Expect(t, Counts{
+ TotalActions: 2,
+ RunningActions: 0,
+ StartedActions: 2,
+ FinishedActions: 2,
+ })
+}
+
+// For when a tool claims to have 2 actions, but finishes after one.
+func TestFinishEarly(t *testing.T) {
+ status := &Status{}
+ counts := &counterOutput{}
+ status.AddOutput(counts)
+ s := status.StartTool()
+
+ s.SetTotalActions(2)
+
+ a := &Action{}
+ s.StartAction(a)
+ s.FinishAction(ActionResult{Action: a})
+ s.Finish()
+
+ s = status.StartTool()
+ s.SetTotalActions(2)
+
+ a = &Action{}
+ s.StartAction(a)
+
+ counts.Expect(t, Counts{
+ TotalActions: 3,
+ RunningActions: 1,
+ StartedActions: 2,
+ FinishedActions: 1,
+ })
+}
+
+// For when a tool claims to have 1 action, but starts two.
+func TestExtraActions(t *testing.T) {
+ status := &Status{}
+ counts := &counterOutput{}
+ status.AddOutput(counts)
+ s := status.StartTool()
+
+ s.SetTotalActions(1)
+
+ s.StartAction(&Action{})
+ s.StartAction(&Action{})
+
+ counts.Expect(t, Counts{
+ TotalActions: 2,
+ RunningActions: 2,
+ StartedActions: 2,
+ FinishedActions: 0,
+ })
+}
+
+// When a tool calls Finish() with a running Action
+func TestRunningWhenFinished(t *testing.T) {
+ status := &Status{}
+ counts := &counterOutput{}
+ status.AddOutput(counts)
+
+ s := status.StartTool()
+ s.SetTotalActions(1)
+ s.StartAction(&Action{})
+ s.Finish()
+
+ s = status.StartTool()
+ s.SetTotalActions(1)
+ s.StartAction(&Action{})
+
+ counts.Expect(t, Counts{
+ TotalActions: 2,
+ RunningActions: 2,
+ StartedActions: 2,
+ FinishedActions: 0,
+ })
+}
diff --git a/ui/terminal/Android.bp b/ui/terminal/Android.bp
new file mode 100644
index 00000000..7104a504
--- /dev/null
+++ b/ui/terminal/Android.bp
@@ -0,0 +1,37 @@
+// Copyright 2018 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-terminal",
+ pkgPath: "android/soong/ui/terminal",
+ deps: ["soong-ui-status"],
+ srcs: [
+ "status.go",
+ "writer.go",
+ "util.go",
+ ],
+ testSrcs: [
+ "util_test.go",
+ ],
+ darwin: {
+ srcs: [
+ "util_darwin.go",
+ ],
+ },
+ linux: {
+ srcs: [
+ "util_linux.go",
+ ],
+ },
+}
diff --git a/ui/terminal/status.go b/ui/terminal/status.go
new file mode 100644
index 00000000..5719456f
--- /dev/null
+++ b/ui/terminal/status.go
@@ -0,0 +1,142 @@
+// Copyright 2018 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 terminal
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "android/soong/ui/status"
+)
+
+type statusOutput struct {
+ writer Writer
+ format string
+
+ start time.Time
+}
+
+// NewStatusOutput returns a StatusOutput that represents the
+// current build status similarly to Ninja's built-in terminal
+// output.
+//
+// statusFormat takes nearly all the same options as NINJA_STATUS.
+// %c is currently unsupported.
+func NewStatusOutput(w Writer, statusFormat string) status.StatusOutput {
+ return &statusOutput{
+ writer: w,
+ format: statusFormat,
+
+ start: time.Now(),
+ }
+}
+
+func (s *statusOutput) Message(level status.MsgLevel, message string) {
+ if level > status.StatusLvl {
+ s.writer.Print(fmt.Sprintf("%s%s", level.Prefix(), message))
+ } else if level == status.StatusLvl {
+ s.writer.StatusLine(message)
+ }
+}
+
+func (s *statusOutput) StartAction(action *status.Action, counts status.Counts) {
+ if !s.writer.isSmartTerminal() {
+ return
+ }
+
+ str := action.Description
+ if str == "" {
+ str = action.Command
+ }
+
+ s.writer.StatusLine(s.progress(counts) + str)
+}
+
+func (s *statusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
+ str := result.Description
+ if str == "" {
+ str = result.Command
+ }
+
+ progress := s.progress(counts) + str
+
+ if result.Error != nil {
+ hasCommand := ""
+ if result.Command != "" {
+ hasCommand = "\n"
+ }
+
+ s.writer.StatusAndMessage(progress, fmt.Sprintf("FAILED: %s\n%s%s%s",
+ strings.Join(result.Outputs, " "), result.Command, hasCommand, result.Output))
+ } else if result.Output != "" {
+ s.writer.StatusAndMessage(progress, result.Output)
+ } else {
+ s.writer.StatusLine(progress)
+ }
+}
+
+func (s *statusOutput) Flush() {}
+
+func (s *statusOutput) progress(counts status.Counts) string {
+ if s.format == "" {
+ return fmt.Sprintf("[%3d%% %d/%d] ", 100*counts.FinishedActions/counts.TotalActions, counts.FinishedActions, counts.TotalActions)
+ }
+
+ buf := &strings.Builder{}
+ for i := 0; i < len(s.format); i++ {
+ c := s.format[i]
+ if c != '%' {
+ buf.WriteByte(c)
+ continue
+ }
+
+ i = i + 1
+ if i == len(s.format) {
+ buf.WriteByte(c)
+ break
+ }
+
+ c = s.format[i]
+ switch c {
+ case '%':
+ buf.WriteByte(c)
+ case 's':
+ fmt.Fprintf(buf, "%d", counts.StartedActions)
+ case 't':
+ fmt.Fprintf(buf, "%d", counts.TotalActions)
+ case 'r':
+ fmt.Fprintf(buf, "%d", counts.RunningActions)
+ case 'u':
+ fmt.Fprintf(buf, "%d", counts.TotalActions-counts.StartedActions)
+ case 'f':
+ fmt.Fprintf(buf, "%d", counts.FinishedActions)
+ case 'o':
+ fmt.Fprintf(buf, "%.1f", float64(counts.FinishedActions)/time.Since(s.start).Seconds())
+ case 'c':
+ // TODO: implement?
+ buf.WriteRune('?')
+ case 'p':
+ fmt.Fprintf(buf, "%3d%%", 100*counts.FinishedActions/counts.TotalActions)
+ case 'e':
+ fmt.Fprintf(buf, "%.3f", time.Since(s.start).Seconds())
+ default:
+ buf.WriteString("unknown placeholder '")
+ buf.WriteByte(c)
+ buf.WriteString("'")
+ }
+ }
+ return buf.String()
+}
diff --git a/ui/terminal/util.go b/ui/terminal/util.go
new file mode 100644
index 00000000..a85a517b
--- /dev/null
+++ b/ui/terminal/util.go
@@ -0,0 +1,101 @@
+// 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 terminal
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "syscall"
+ "unsafe"
+)
+
+func isTerminal(w io.Writer) bool {
+ if f, ok := w.(*os.File); ok {
+ var termios syscall.Termios
+ _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
+ ioctlGetTermios, uintptr(unsafe.Pointer(&termios)),
+ 0, 0, 0)
+ return err == 0
+ }
+ return false
+}
+
+func termWidth(w io.Writer) (int, bool) {
+ if f, ok := w.(*os.File); ok {
+ var winsize struct {
+ ws_row, ws_column uint16
+ ws_xpixel, ws_ypixel uint16
+ }
+ _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
+ syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&winsize)),
+ 0, 0, 0)
+ return int(winsize.ws_column), err == 0
+ }
+ return 0, false
+}
+
+// stripAnsiEscapes strips ANSI control codes from a byte array in place.
+func stripAnsiEscapes(input []byte) []byte {
+ // read represents the remaining part of input that needs to be processed.
+ read := input
+ // write represents where we should be writing in input.
+ // It will share the same backing store as input so that we make our modifications
+ // in place.
+ write := input
+
+ // advance will copy count bytes from read to write and advance those slices
+ advance := func(write, read []byte, count int) ([]byte, []byte) {
+ copy(write, read[:count])
+ return write[count:], read[count:]
+ }
+
+ for {
+ // Find the next escape sequence
+ i := bytes.IndexByte(read, 0x1b)
+ // If it isn't found, or if there isn't room for <ESC>[, finish
+ if i == -1 || i+1 >= len(read) {
+ copy(write, read)
+ break
+ }
+
+ // Not a CSI code, continue searching
+ if read[i+1] != '[' {
+ write, read = advance(write, read, i+1)
+ continue
+ }
+
+ // Found a CSI code, advance up to the <ESC>
+ write, read = advance(write, read, i)
+
+ // Find the end of the CSI code
+ i = bytes.IndexFunc(read, func(r rune) bool {
+ return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
+ })
+ if i == -1 {
+ // We didn't find the end of the code, just remove the rest
+ i = len(read) - 1
+ }
+
+ // Strip off the end marker too
+ i = i + 1
+
+ // Skip the reader forward and reduce final length by that amount
+ read = read[i:]
+ input = input[:len(input)-i]
+ }
+
+ return input
+}
diff --git a/ui/build/util_darwin.go b/ui/terminal/util_darwin.go
index 254a9b87..109a37f0 100644
--- a/ui/build/util_darwin.go
+++ b/ui/terminal/util_darwin.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package build
+package terminal
import (
"syscall"
diff --git a/ui/build/util_linux.go b/ui/terminal/util_linux.go
index 0a4e1d29..0a3d9dd1 100644
--- a/ui/build/util_linux.go
+++ b/ui/terminal/util_linux.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package build
+package terminal
import (
"syscall"
diff --git a/ui/terminal/util_test.go b/ui/terminal/util_test.go
new file mode 100644
index 00000000..82bde7c5
--- /dev/null
+++ b/ui/terminal/util_test.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 terminal
+
+import (
+ "testing"
+)
+
+func TestStripAnsiEscapes(t *testing.T) {
+ testcases := []struct {
+ input string
+ output string
+ }{
+ {
+ "",
+ "",
+ },
+ {
+ "This is a test",
+ "This is a test",
+ },
+ {
+ "interrupted: \x1b[12",
+ "interrupted: ",
+ },
+ {
+ "other \x1bescape \x1b",
+ "other \x1bescape \x1b",
+ },
+ { // from pretty-error macro
+ "\x1b[1mart/Android.mk: \x1b[31merror:\x1b[0m\x1b[1m art: test error \x1b[0m",
+ "art/Android.mk: error: art: test error ",
+ },
+ { // from envsetup.sh make wrapper
+ "\x1b[0;31m#### make failed to build some targets (2 seconds) ####\x1b[00m",
+ "#### make failed to build some targets (2 seconds) ####",
+ },
+ { // from clang (via ninja testcase)
+ "\x1b[1maffixmgr.cxx:286:15: \x1b[0m\x1b[0;1;35mwarning: \x1b[0m\x1b[1musing the result... [-Wparentheses]\x1b[0m",
+ "affixmgr.cxx:286:15: warning: using the result... [-Wparentheses]",
+ },
+ }
+ for _, tc := range testcases {
+ got := string(stripAnsiEscapes([]byte(tc.input)))
+ if got != tc.output {
+ t.Errorf("output strings didn't match\n"+
+ "input: %#v\n"+
+ " want: %#v\n"+
+ " got: %#v", tc.input, tc.output, got)
+ }
+ }
+}
diff --git a/ui/terminal/writer.go b/ui/terminal/writer.go
new file mode 100644
index 00000000..dd322268
--- /dev/null
+++ b/ui/terminal/writer.go
@@ -0,0 +1,229 @@
+// Copyright 2018 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 terminal provides a set of interfaces that can be used to interact
+// with the terminal (including falling back when the terminal is detected to
+// be a redirect or other dumb terminal)
+package terminal
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "sync"
+)
+
+// Writer provides an interface to write temporary and permanent messages to
+// the terminal.
+//
+// The terminal is considered to be a dumb terminal if TERM==dumb, or if a
+// terminal isn't detected on stdout/stderr (generally because it's a pipe or
+// file). Dumb terminals will strip out all ANSI escape sequences, including
+// colors.
+type Writer interface {
+ // Print prints the string to the terminal, overwriting any current
+ // status being displayed.
+ //
+ // On a dumb terminal, the status messages will be kept.
+ Print(str string)
+
+ // Status prints the first line of the string to the terminal,
+ // overwriting any previous status line. Strings longer than the width
+ // of the terminal will be cut off.
+ //
+ // On a dumb terminal, previous status messages will remain, and the
+ // entire first line of the string will be printed.
+ StatusLine(str string)
+
+ // StatusAndMessage prints the first line of status to the terminal,
+ // similarly to StatusLine(), then prints the full msg below that. The
+ // status line is retained.
+ //
+ // There is guaranteed to be no other output in between the status and
+ // message.
+ StatusAndMessage(status, msg string)
+
+ // Finish ensures that the output ends with a newline (preserving any
+ // current status line that is current displayed).
+ //
+ // This does nothing on dumb terminals.
+ Finish()
+
+ // Write implements the io.Writer interface. This is primarily so that
+ // the logger can use this interface to print to stderr without
+ // breaking the other semantics of this interface.
+ //
+ // Try to use any of the other functions if possible.
+ Write(p []byte) (n int, err error)
+
+ isSmartTerminal() bool
+}
+
+// NewWriter creates a new Writer based on the stdio and the TERM
+// environment variable.
+func NewWriter(stdio StdioInterface) Writer {
+ w := &writerImpl{
+ stdio: stdio,
+
+ haveBlankLine: true,
+ }
+
+ if term, ok := os.LookupEnv("TERM"); ok && term != "dumb" {
+ w.stripEscapes = !isTerminal(stdio.Stderr())
+ w.smartTerminal = isTerminal(stdio.Stdout()) && !w.stripEscapes
+ }
+
+ return w
+}
+
+type writerImpl struct {
+ stdio StdioInterface
+
+ haveBlankLine bool
+
+ // Protecting the above, we assume that smartTerminal and stripEscapes
+ // does not change after initial setup.
+ lock sync.Mutex
+
+ smartTerminal bool
+ stripEscapes bool
+}
+
+func (w *writerImpl) isSmartTerminal() bool {
+ return w.smartTerminal
+}
+
+func (w *writerImpl) requestLine() {
+ if !w.haveBlankLine {
+ fmt.Fprintln(w.stdio.Stdout())
+ w.haveBlankLine = true
+ }
+}
+
+func (w *writerImpl) Print(str string) {
+ if w.stripEscapes {
+ str = string(stripAnsiEscapes([]byte(str)))
+ }
+
+ w.lock.Lock()
+ defer w.lock.Unlock()
+ w.print(str)
+}
+
+func (w *writerImpl) print(str string) {
+ if !w.haveBlankLine {
+ fmt.Fprint(w.stdio.Stdout(), "\r", "\x1b[K")
+ w.haveBlankLine = true
+ }
+ fmt.Fprint(w.stdio.Stderr(), str)
+ if len(str) == 0 || str[len(str)-1] != '\n' {
+ fmt.Fprint(w.stdio.Stderr(), "\n")
+ }
+}
+
+func (w *writerImpl) StatusLine(str string) {
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ w.statusLine(str)
+}
+
+func (w *writerImpl) statusLine(str string) {
+ if !w.smartTerminal {
+ fmt.Fprintln(w.stdio.Stdout(), str)
+ return
+ }
+
+ idx := strings.IndexRune(str, '\n')
+ if idx != -1 {
+ str = str[0:idx]
+ }
+
+ // Limit line width to the terminal width, otherwise we'll wrap onto
+ // another line and we won't delete the previous line.
+ //
+ // Run this on every line in case the window has been resized while
+ // we're printing. This could be optimized to only re-run when we get
+ // SIGWINCH if it ever becomes too time consuming.
+ if max, ok := termWidth(w.stdio.Stdout()); ok {
+ if len(str) > max {
+ // TODO: Just do a max. Ninja elides the middle, but that's
+ // more complicated and these lines aren't that important.
+ str = str[:max]
+ }
+ }
+
+ // Move to the beginning on the line, print the output, then clear
+ // the rest of the line.
+ fmt.Fprint(w.stdio.Stdout(), "\r", str, "\x1b[K")
+ w.haveBlankLine = false
+}
+
+func (w *writerImpl) StatusAndMessage(status, msg string) {
+ if w.stripEscapes {
+ msg = string(stripAnsiEscapes([]byte(msg)))
+ }
+
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ w.statusLine(status)
+ w.requestLine()
+ w.print(msg)
+}
+
+func (w *writerImpl) Finish() {
+ w.lock.Lock()
+ defer w.lock.Unlock()
+
+ w.requestLine()
+}
+
+func (w *writerImpl) Write(p []byte) (n int, err error) {
+ w.Print(string(p))
+ return len(p), nil
+}
+
+// StdioInterface represents a set of stdin/stdout/stderr Reader/Writers
+type StdioInterface interface {
+ Stdin() io.Reader
+ Stdout() io.Writer
+ Stderr() io.Writer
+}
+
+// StdioImpl uses the OS stdin/stdout/stderr to implement StdioInterface
+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{}
diff --git a/ui/tracer/Android.bp b/ui/tracer/Android.bp
index 9729c7e5..af588f1d 100644
--- a/ui/tracer/Android.bp
+++ b/ui/tracer/Android.bp
@@ -15,10 +15,13 @@
bootstrap_go_package {
name: "soong-ui-tracer",
pkgPath: "android/soong/ui/tracer",
- deps: ["soong-ui-logger"],
+ deps: [
+ "soong-ui-logger",
+ "soong-ui-status",
+ ],
srcs: [
"microfactory.go",
- "ninja.go",
+ "status.go",
"tracer.go",
],
}
diff --git a/ui/tracer/microfactory.go b/ui/tracer/microfactory.go
index acb9be4c..c4c37c25 100644
--- a/ui/tracer/microfactory.go
+++ b/ui/tracer/microfactory.go
@@ -17,10 +17,48 @@ package tracer
import (
"bufio"
"os"
+ "sort"
"strconv"
"strings"
)
+type eventEntry struct {
+ Name string
+ Begin uint64
+ End uint64
+}
+
+func (t *tracerImpl) importEvents(entries []*eventEntry) {
+ sort.Slice(entries, func(i, j int) bool {
+ return entries[i].Begin < entries[j].Begin
+ })
+
+ cpus := []uint64{}
+ 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: entry.Begin,
+ Dur: entry.End - entry.Begin,
+ Pid: 1,
+ Tid: uint64(tid),
+ })
+ }
+}
+
func (t *tracerImpl) ImportMicrofactoryLog(filename string) {
if _, err := os.Stat(filename); err != nil {
return
diff --git a/ui/tracer/ninja.go b/ui/tracer/ninja.go
deleted file mode 100644
index 1980559c..00000000
--- a/ui/tracer/ninja.go
+++ /dev/null
@@ -1,131 +0,0 @@
-// 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 eventEntry struct {
- Name string
- Begin uint64
- End uint64
-}
-
-func (t *tracerImpl) importEvents(entries []*eventEntry) {
- sort.Slice(entries, func(i, j int) bool {
- return entries[i].Begin < entries[j].Begin
- })
-
- cpus := []uint64{}
- 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: entry.Begin,
- Dur: entry.End - entry.Begin,
- Pid: 1,
- Tid: uint64(tid),
- })
- }
-}
-
-// 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 := []*eventEntry{}
- prevEnd := 0
- offset := uint64(startOffset.UnixNano()) / 1000
- 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, &eventEntry{
- Name: fields[3],
- Begin: offset + uint64(begin)*1000,
- End: offset + uint64(end)*1000,
- })
- }
- if err := s.Err(); err != nil {
- t.log.Println("Unable to parse ninja log:", err)
- return
- }
-
- t.importEvents(entries)
-}
diff --git a/ui/tracer/status.go b/ui/tracer/status.go
new file mode 100644
index 00000000..af50e2d4
--- /dev/null
+++ b/ui/tracer/status.go
@@ -0,0 +1,87 @@
+// Copyright 2018 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 (
+ "android/soong/ui/status"
+ "time"
+)
+
+func (t *tracerImpl) StatusTracer() status.StatusOutput {
+ return &statusOutput{
+ tracer: t,
+
+ running: map[*status.Action]actionStatus{},
+ }
+}
+
+type actionStatus struct {
+ cpu int
+ start time.Time
+}
+
+type statusOutput struct {
+ tracer *tracerImpl
+
+ cpus []bool
+ running map[*status.Action]actionStatus
+}
+
+func (s *statusOutput) StartAction(action *status.Action, counts status.Counts) {
+ cpu := -1
+ for i, busy := range s.cpus {
+ if !busy {
+ cpu = i
+ s.cpus[i] = true
+ break
+ }
+ }
+
+ if cpu == -1 {
+ cpu = len(s.cpus)
+ s.cpus = append(s.cpus, true)
+ }
+
+ s.running[action] = actionStatus{
+ cpu: cpu,
+ start: time.Now(),
+ }
+}
+
+func (s *statusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
+ start, ok := s.running[result.Action]
+ if !ok {
+ return
+ }
+ delete(s.running, result.Action)
+ s.cpus[start.cpu] = false
+
+ str := result.Action.Description
+ if len(result.Action.Outputs) > 0 {
+ str = result.Action.Outputs[0]
+ }
+
+ s.tracer.writeEvent(&viewerEvent{
+ Name: str,
+ Phase: "X",
+ Time: uint64(start.start.UnixNano()) / 1000,
+ Dur: uint64(time.Since(start.start).Nanoseconds()) / 1000,
+ Pid: 1,
+ Tid: uint64(start.cpu),
+ })
+}
+
+func (s *statusOutput) Flush() {}
+func (s *statusOutput) Message(level status.MsgLevel, message string) {}
diff --git a/ui/tracer/tracer.go b/ui/tracer/tracer.go
index 87050403..b8fc87b2 100644
--- a/ui/tracer/tracer.go
+++ b/ui/tracer/tracer.go
@@ -31,6 +31,7 @@ import (
"time"
"android/soong/ui/logger"
+ "android/soong/ui/status"
)
type Thread uint64
@@ -46,7 +47,8 @@ type Tracer interface {
Complete(name string, thread Thread, begin, end uint64)
ImportMicrofactoryLog(filename string)
- ImportNinjaLog(thread Thread, filename string, startOffset time.Time)
+
+ StatusTracer() status.StatusOutput
NewThread(name string) Thread
}