aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ui/terminal/Android.bp3
-rw-r--r--ui/terminal/dumb_status.go65
-rw-r--r--ui/terminal/format.go123
-rw-r--r--ui/terminal/smart_status.go148
-rw-r--r--ui/terminal/status.go118
-rw-r--r--ui/terminal/util.go2
-rw-r--r--ui/terminal/writer.go123
7 files changed, 352 insertions, 230 deletions
diff --git a/ui/terminal/Android.bp b/ui/terminal/Android.bp
index cf6cf0a3..683e3e39 100644
--- a/ui/terminal/Android.bp
+++ b/ui/terminal/Android.bp
@@ -17,6 +17,9 @@ bootstrap_go_package {
pkgPath: "android/soong/ui/terminal",
deps: ["soong-ui-status"],
srcs: [
+ "dumb_status.go",
+ "format.go",
+ "smart_status.go",
"status.go",
"writer.go",
"util.go",
diff --git a/ui/terminal/dumb_status.go b/ui/terminal/dumb_status.go
new file mode 100644
index 00000000..f2fcba79
--- /dev/null
+++ b/ui/terminal/dumb_status.go
@@ -0,0 +1,65 @@
+// Copyright 2019 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"
+
+ "android/soong/ui/status"
+)
+
+type dumbStatusOutput struct {
+ writer Writer
+ formatter formatter
+}
+
+// NewDumbStatusOutput returns a StatusOutput that represents the
+// current build status similarly to Ninja's built-in terminal
+// output.
+func NewDumbStatusOutput(w Writer, formatter formatter) status.StatusOutput {
+ return &dumbStatusOutput{
+ writer: w,
+ formatter: formatter,
+ }
+}
+
+func (s *dumbStatusOutput) Message(level status.MsgLevel, message string) {
+ if level >= status.StatusLvl {
+ fmt.Fprintln(s.writer, s.formatter.message(level, message))
+ }
+}
+
+func (s *dumbStatusOutput) StartAction(action *status.Action, counts status.Counts) {
+}
+
+func (s *dumbStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
+ str := result.Description
+ if str == "" {
+ str = result.Command
+ }
+
+ progress := s.formatter.progress(counts) + str
+
+ output := s.formatter.result(result)
+ output = string(stripAnsiEscapes([]byte(output)))
+
+ if output != "" {
+ fmt.Fprint(s.writer, progress, "\n", output)
+ } else {
+ fmt.Fprintln(s.writer, progress)
+ }
+}
+
+func (s *dumbStatusOutput) Flush() {}
diff --git a/ui/terminal/format.go b/ui/terminal/format.go
new file mode 100644
index 00000000..4205bdc2
--- /dev/null
+++ b/ui/terminal/format.go
@@ -0,0 +1,123 @@
+// Copyright 2019 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 formatter struct {
+ format string
+ quiet bool
+ start time.Time
+}
+
+// newFormatter returns a formatter for formatting output to
+// the terminal in a format similar to Ninja.
+// format takes nearly all the same options as NINJA_STATUS.
+// %c is currently unsupported.
+func newFormatter(format string, quiet bool) formatter {
+ return formatter{
+ format: format,
+ quiet: quiet,
+ start: time.Now(),
+ }
+}
+
+func (s formatter) message(level status.MsgLevel, message string) string {
+ if level >= status.ErrorLvl {
+ return fmt.Sprintf("FAILED: %s", message)
+ } else if level > status.StatusLvl {
+ return fmt.Sprintf("%s%s", level.Prefix(), message)
+ } else if level == status.StatusLvl {
+ return message
+ }
+ return ""
+}
+
+func (s formatter) 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()
+}
+
+func (s formatter) result(result status.ActionResult) string {
+ var ret string
+ if result.Error != nil {
+ targets := strings.Join(result.Outputs, " ")
+ if s.quiet || result.Command == "" {
+ ret = fmt.Sprintf("FAILED: %s\n%s", targets, result.Output)
+ } else {
+ ret = fmt.Sprintf("FAILED: %s\n%s\n%s", targets, result.Command, result.Output)
+ }
+ } else if result.Output != "" {
+ ret = result.Output
+ }
+
+ if len(ret) > 0 && ret[len(ret)-1] != '\n' {
+ ret += "\n"
+ }
+
+ return ret
+}
diff --git a/ui/terminal/smart_status.go b/ui/terminal/smart_status.go
new file mode 100644
index 00000000..5edc21a1
--- /dev/null
+++ b/ui/terminal/smart_status.go
@@ -0,0 +1,148 @@
+// Copyright 2019 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"
+ "sync"
+
+ "android/soong/ui/status"
+)
+
+type smartStatusOutput struct {
+ writer Writer
+ formatter formatter
+
+ lock sync.Mutex
+
+ haveBlankLine bool
+}
+
+// NewSmartStatusOutput returns a StatusOutput that represents the
+// current build status similarly to Ninja's built-in terminal
+// output.
+func NewSmartStatusOutput(w Writer, formatter formatter) status.StatusOutput {
+ return &smartStatusOutput{
+ writer: w,
+ formatter: formatter,
+
+ haveBlankLine: true,
+ }
+}
+
+func (s *smartStatusOutput) Message(level status.MsgLevel, message string) {
+ if level < status.StatusLvl {
+ return
+ }
+
+ str := s.formatter.message(level, message)
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if level > status.StatusLvl {
+ s.print(str)
+ } else {
+ s.statusLine(str)
+ }
+}
+
+func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
+ str := action.Description
+ if str == "" {
+ str = action.Command
+ }
+
+ progress := s.formatter.progress(counts)
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.statusLine(progress + str)
+}
+
+func (s *smartStatusOutput) FinishAction(result status.ActionResult, counts status.Counts) {
+ str := result.Description
+ if str == "" {
+ str = result.Command
+ }
+
+ progress := s.formatter.progress(counts) + str
+
+ output := s.formatter.result(result)
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if output != "" {
+ s.statusLine(progress)
+ s.requestLine()
+ s.print(output)
+ } else {
+ s.statusLine(progress)
+ }
+}
+
+func (s *smartStatusOutput) Flush() {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ s.requestLine()
+}
+
+func (s *smartStatusOutput) requestLine() {
+ if !s.haveBlankLine {
+ fmt.Fprintln(s.writer)
+ s.haveBlankLine = true
+ }
+}
+
+func (s *smartStatusOutput) print(str string) {
+ if !s.haveBlankLine {
+ fmt.Fprint(s.writer, "\r", "\x1b[K")
+ s.haveBlankLine = true
+ }
+ fmt.Fprint(s.writer, str)
+ if len(str) == 0 || str[len(str)-1] != '\n' {
+ fmt.Fprint(s.writer, "\n")
+ }
+}
+
+func (s *smartStatusOutput) statusLine(str string) {
+ 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 := s.writer.termWidth(); 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(s.writer, "\r", str, "\x1b[K")
+ s.haveBlankLine = false
+}
diff --git a/ui/terminal/status.go b/ui/terminal/status.go
index 2445c5b2..481c511a 100644
--- a/ui/terminal/status.go
+++ b/ui/terminal/status.go
@@ -15,21 +15,9 @@
package terminal
import (
- "fmt"
- "strings"
- "time"
-
"android/soong/ui/status"
)
-type statusOutput struct {
- writer Writer
- format string
-
- start time.Time
- quiet bool
-}
-
// NewStatusOutput returns a StatusOutput that represents the
// current build status similarly to Ninja's built-in terminal
// output.
@@ -37,109 +25,11 @@ type statusOutput struct {
// statusFormat takes nearly all the same options as NINJA_STATUS.
// %c is currently unsupported.
func NewStatusOutput(w Writer, statusFormat string, quietBuild bool) status.StatusOutput {
- return &statusOutput{
- writer: w,
- format: statusFormat,
-
- start: time.Now(),
- quiet: quietBuild,
- }
-}
-
-func (s *statusOutput) Message(level status.MsgLevel, message string) {
- if level >= status.ErrorLvl {
- s.writer.Print(fmt.Sprintf("FAILED: %s", message))
- } else 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
- }
+ formatter := newFormatter(statusFormat, quietBuild)
- 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 {
- targets := strings.Join(result.Outputs, " ")
- if s.quiet || result.Command == "" {
- s.writer.StatusAndMessage(progress, fmt.Sprintf("FAILED: %s\n%s", targets, result.Output))
- } else {
- s.writer.StatusAndMessage(progress, fmt.Sprintf("FAILED: %s\n%s\n%s", targets, result.Command, result.Output))
- }
- } else if result.Output != "" {
- s.writer.StatusAndMessage(progress, result.Output)
+ if w.isSmartTerminal() {
+ return NewSmartStatusOutput(w, formatter)
} 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 NewDumbStatusOutput(w, formatter)
}
- return buf.String()
}
diff --git a/ui/terminal/util.go b/ui/terminal/util.go
index 4309809c..3a11b79b 100644
--- a/ui/terminal/util.go
+++ b/ui/terminal/util.go
@@ -22,7 +22,7 @@ import (
"unsafe"
)
-func isTerminal(w io.Writer) bool {
+func isSmartTerminal(w io.Writer) bool {
if f, ok := w.(*os.File); ok {
var termios syscall.Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
diff --git a/ui/terminal/writer.go b/ui/terminal/writer.go
index ebe4b2aa..26e0e342 100644
--- a/ui/terminal/writer.go
+++ b/ui/terminal/writer.go
@@ -21,8 +21,6 @@ import (
"fmt"
"io"
"os"
- "strings"
- "sync"
)
// Writer provides an interface to write temporary and permanent messages to
@@ -39,22 +37,6 @@ type Writer interface {
// 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).
//
@@ -69,6 +51,7 @@ type Writer interface {
Write(p []byte) (n int, err error)
isSmartTerminal() bool
+ termWidth() (int, bool)
}
// NewWriter creates a new Writer based on the stdio and the TERM
@@ -76,124 +59,34 @@ type Writer interface {
func NewWriter(stdio StdioInterface) Writer {
w := &writerImpl{
stdio: stdio,
-
- haveBlankLine: true,
}
- if term, ok := os.LookupEnv("TERM"); ok && term != "dumb" {
- w.smartTerminal = isTerminal(stdio.Stdout())
- }
- w.stripEscapes = !w.smartTerminal
-
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.Stdout(), str)
if len(str) == 0 || str[len(str)-1] != '\n' {
fmt.Fprint(w.stdio.Stdout(), "\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]
- }
- }
+func (w *writerImpl) Finish() {}
- // 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) Write(p []byte) (n int, err error) {
+ return w.stdio.Stdout().Write(p)
}
-func (w *writerImpl) Finish() {
- w.lock.Lock()
- defer w.lock.Unlock()
-
- w.requestLine()
+func (w *writerImpl) isSmartTerminal() bool {
+ return isSmartTerminal(w.stdio.Stdout())
}
-func (w *writerImpl) Write(p []byte) (n int, err error) {
- w.Print(string(p))
- return len(p), nil
+func (w *writerImpl) termWidth() (int, bool) {
+ return termWidth(w.stdio.Stdout())
}
// StdioInterface represents a set of stdin/stdout/stderr Reader/Writers