Merge "Add a unified status reporting UI"
This commit is contained in:
commit
ecc71f8a49
|
@ -17,6 +17,7 @@ blueprint_go_binary {
|
||||||
deps: [
|
deps: [
|
||||||
"soong-ui-build",
|
"soong-ui-build",
|
||||||
"soong-ui-logger",
|
"soong-ui-logger",
|
||||||
|
"soong-ui-terminal",
|
||||||
"soong-ui-tracer",
|
"soong-ui-tracer",
|
||||||
"soong-zip",
|
"soong-zip",
|
||||||
],
|
],
|
||||||
|
|
|
@ -29,6 +29,8 @@ import (
|
||||||
|
|
||||||
"android/soong/ui/build"
|
"android/soong/ui/build"
|
||||||
"android/soong/ui/logger"
|
"android/soong/ui/logger"
|
||||||
|
"android/soong/ui/status"
|
||||||
|
"android/soong/ui/terminal"
|
||||||
"android/soong/ui/tracer"
|
"android/soong/ui/tracer"
|
||||||
"android/soong/zip"
|
"android/soong/zip"
|
||||||
)
|
)
|
||||||
|
@ -66,98 +68,34 @@ type Product struct {
|
||||||
ctx build.Context
|
ctx build.Context
|
||||||
config build.Config
|
config build.Config
|
||||||
logFile string
|
logFile string
|
||||||
|
action *status.Action
|
||||||
}
|
}
|
||||||
|
|
||||||
type Status struct {
|
func errMsgFromLog(filename string) string {
|
||||||
cur int
|
if filename == "" {
|
||||||
total int
|
return ""
|
||||||
failed int
|
|
||||||
|
|
||||||
ctx build.Context
|
|
||||||
haveBlankLine bool
|
|
||||||
smartTerminal bool
|
|
||||||
|
|
||||||
lock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStatus(ctx build.Context) *Status {
|
|
||||||
return &Status{
|
|
||||||
ctx: ctx,
|
|
||||||
haveBlankLine: true,
|
|
||||||
smartTerminal: ctx.IsTerminal(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Status) SetTotal(total int) {
|
|
||||||
s.total = total
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Status) Fail(product string, err error, logFile string) {
|
|
||||||
s.Finish(product)
|
|
||||||
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
if s.smartTerminal && !s.haveBlankLine {
|
|
||||||
fmt.Fprintln(s.ctx.Stdout())
|
|
||||||
s.haveBlankLine = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.failed++
|
data, err := ioutil.ReadFile(filename)
|
||||||
fmt.Fprintln(s.ctx.Stderr(), "FAILED:", product)
|
if err != nil {
|
||||||
s.ctx.Verboseln("FAILED:", product)
|
return ""
|
||||||
|
|
||||||
if logFile != "" {
|
|
||||||
data, err := ioutil.ReadFile(logFile)
|
|
||||||
if err == nil {
|
|
||||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
|
||||||
if len(lines) > errorLeadingLines+errorTrailingLines+1 {
|
|
||||||
lines[errorLeadingLines] = fmt.Sprintf("... skipping %d lines ...",
|
|
||||||
len(lines)-errorLeadingLines-errorTrailingLines)
|
|
||||||
|
|
||||||
lines = append(lines[:errorLeadingLines+1],
|
|
||||||
lines[len(lines)-errorTrailingLines:]...)
|
|
||||||
}
|
|
||||||
for _, line := range lines {
|
|
||||||
fmt.Fprintln(s.ctx.Stderr(), "> ", line)
|
|
||||||
s.ctx.Verboseln(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.ctx.Print(err)
|
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||||
}
|
if len(lines) > errorLeadingLines+errorTrailingLines+1 {
|
||||||
|
lines[errorLeadingLines] = fmt.Sprintf("... skipping %d lines ...",
|
||||||
|
len(lines)-errorLeadingLines-errorTrailingLines)
|
||||||
|
|
||||||
func (s *Status) Finish(product string) {
|
lines = append(lines[:errorLeadingLines+1],
|
||||||
s.lock.Lock()
|
lines[len(lines)-errorTrailingLines:]...)
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
s.cur++
|
|
||||||
line := fmt.Sprintf("[%d/%d] %s", s.cur, s.total, product)
|
|
||||||
|
|
||||||
if s.smartTerminal {
|
|
||||||
if max, ok := s.ctx.TermWidth(); ok {
|
|
||||||
if len(line) > max {
|
|
||||||
line = line[:max]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprint(s.ctx.Stdout(), "\r", line, "\x1b[K")
|
|
||||||
s.haveBlankLine = false
|
|
||||||
} else {
|
|
||||||
s.ctx.Println(line)
|
|
||||||
}
|
}
|
||||||
}
|
var buf strings.Builder
|
||||||
|
for _, line := range lines {
|
||||||
func (s *Status) Finished() int {
|
buf.WriteString("> ")
|
||||||
s.lock.Lock()
|
buf.WriteString(line)
|
||||||
defer s.lock.Unlock()
|
buf.WriteString("\n")
|
||||||
|
|
||||||
if !s.haveBlankLine {
|
|
||||||
fmt.Fprintln(s.ctx.Stdout())
|
|
||||||
s.haveBlankLine = true
|
|
||||||
}
|
}
|
||||||
return s.failed
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(b/70370883): This tool uses a lot of open files -- over the default
|
// TODO(b/70370883): This tool uses a lot of open files -- over the default
|
||||||
|
@ -194,6 +132,9 @@ func inList(str string, list []string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
writer := terminal.NewWriter(terminal.StdioImpl{})
|
||||||
|
defer writer.Finish()
|
||||||
|
|
||||||
log := logger.New(os.Stderr)
|
log := logger.New(os.Stderr)
|
||||||
defer log.Cleanup()
|
defer log.Cleanup()
|
||||||
|
|
||||||
|
@ -205,20 +146,24 @@ func main() {
|
||||||
trace := tracer.New(log)
|
trace := tracer.New(log)
|
||||||
defer trace.Close()
|
defer trace.Close()
|
||||||
|
|
||||||
|
stat := &status.Status{}
|
||||||
|
defer stat.Finish()
|
||||||
|
stat.AddOutput(terminal.NewStatusOutput(writer, ""))
|
||||||
|
|
||||||
build.SetupSignals(log, cancel, func() {
|
build.SetupSignals(log, cancel, func() {
|
||||||
trace.Close()
|
trace.Close()
|
||||||
log.Cleanup()
|
log.Cleanup()
|
||||||
|
stat.Finish()
|
||||||
})
|
})
|
||||||
|
|
||||||
buildCtx := build.Context{&build.ContextImpl{
|
buildCtx := build.Context{&build.ContextImpl{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: log,
|
Logger: log,
|
||||||
Tracer: trace,
|
Tracer: trace,
|
||||||
StdioInterface: build.StdioImpl{},
|
Writer: writer,
|
||||||
|
Status: stat,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
status := NewStatus(buildCtx)
|
|
||||||
|
|
||||||
config := build.NewConfig(buildCtx)
|
config := build.NewConfig(buildCtx)
|
||||||
if *outDir == "" {
|
if *outDir == "" {
|
||||||
name := "multiproduct-" + time.Now().Format("20060102150405")
|
name := "multiproduct-" + time.Now().Format("20060102150405")
|
||||||
|
@ -303,7 +248,8 @@ func main() {
|
||||||
|
|
||||||
log.Verbose("Got product list: ", products)
|
log.Verbose("Got product list: ", products)
|
||||||
|
|
||||||
status.SetTotal(len(products))
|
s := buildCtx.Status.StartTool()
|
||||||
|
s.SetTotalActions(len(products))
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
productConfigs := make(chan Product, len(products))
|
productConfigs := make(chan Product, len(products))
|
||||||
|
@ -315,8 +261,18 @@ func main() {
|
||||||
var stdLog string
|
var stdLog string
|
||||||
|
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
|
action := &status.Action{
|
||||||
|
Description: product,
|
||||||
|
Outputs: []string{product},
|
||||||
|
}
|
||||||
|
s.StartAction(action)
|
||||||
defer logger.Recover(func(err error) {
|
defer logger.Recover(func(err error) {
|
||||||
status.Fail(product, err, stdLog)
|
s.FinishAction(status.ActionResult{
|
||||||
|
Action: action,
|
||||||
|
Error: err,
|
||||||
|
Output: errMsgFromLog(stdLog),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
productOutDir := filepath.Join(config.OutDir(), product)
|
productOutDir := filepath.Join(config.OutDir(), product)
|
||||||
|
@ -339,12 +295,14 @@ func main() {
|
||||||
productLog.SetOutput(filepath.Join(productLogDir, "soong.log"))
|
productLog.SetOutput(filepath.Join(productLogDir, "soong.log"))
|
||||||
|
|
||||||
productCtx := build.Context{&build.ContextImpl{
|
productCtx := build.Context{&build.ContextImpl{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: productLog,
|
Logger: productLog,
|
||||||
Tracer: trace,
|
Tracer: trace,
|
||||||
StdioInterface: build.NewCustomStdio(nil, f, f),
|
Writer: terminal.NewWriter(terminal.NewCustomStdio(nil, f, f)),
|
||||||
Thread: trace.NewThread(product),
|
Thread: trace.NewThread(product),
|
||||||
|
Status: &status.Status{},
|
||||||
}}
|
}}
|
||||||
|
productCtx.Status.AddOutput(terminal.NewStatusOutput(productCtx.Writer, ""))
|
||||||
|
|
||||||
productConfig := build.NewConfig(productCtx)
|
productConfig := build.NewConfig(productCtx)
|
||||||
productConfig.Environment().Set("OUT_DIR", productOutDir)
|
productConfig.Environment().Set("OUT_DIR", productOutDir)
|
||||||
|
@ -352,7 +310,7 @@ func main() {
|
||||||
productConfig.Lunch(productCtx, product, *buildVariant)
|
productConfig.Lunch(productCtx, product, *buildVariant)
|
||||||
|
|
||||||
build.Build(productCtx, productConfig, build.BuildProductConfig)
|
build.Build(productCtx, productConfig, build.BuildProductConfig)
|
||||||
productConfigs <- Product{productCtx, productConfig, stdLog}
|
productConfigs <- Product{productCtx, productConfig, stdLog, action}
|
||||||
}(product)
|
}(product)
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -369,7 +327,11 @@ func main() {
|
||||||
for product := range productConfigs {
|
for product := range productConfigs {
|
||||||
func() {
|
func() {
|
||||||
defer logger.Recover(func(err error) {
|
defer logger.Recover(func(err error) {
|
||||||
status.Fail(product.config.TargetProduct(), err, product.logFile)
|
s.FinishAction(status.ActionResult{
|
||||||
|
Action: product.action,
|
||||||
|
Error: err,
|
||||||
|
Output: errMsgFromLog(product.logFile),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -400,7 +362,9 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
build.Build(product.ctx, product.config, buildWhat)
|
build.Build(product.ctx, product.config, buildWhat)
|
||||||
status.Finish(product.config.TargetProduct())
|
s.FinishAction(status.ActionResult{
|
||||||
|
Action: product.action,
|
||||||
|
})
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -421,7 +385,5 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if count := status.Finished(); count > 0 {
|
s.Finish()
|
||||||
log.Fatalln(count, "products failed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ blueprint_go_binary {
|
||||||
deps: [
|
deps: [
|
||||||
"soong-ui-build",
|
"soong-ui-build",
|
||||||
"soong-ui-logger",
|
"soong-ui-logger",
|
||||||
|
"soong-ui-terminal",
|
||||||
"soong-ui-tracer",
|
"soong-ui-tracer",
|
||||||
],
|
],
|
||||||
srcs: [
|
srcs: [
|
||||||
|
|
|
@ -26,6 +26,8 @@ import (
|
||||||
|
|
||||||
"android/soong/ui/build"
|
"android/soong/ui/build"
|
||||||
"android/soong/ui/logger"
|
"android/soong/ui/logger"
|
||||||
|
"android/soong/ui/status"
|
||||||
|
"android/soong/ui/terminal"
|
||||||
"android/soong/ui/tracer"
|
"android/soong/ui/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,7 +46,10 @@ func inList(s string, list []string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log := logger.New(os.Stderr)
|
writer := terminal.NewWriter(terminal.StdioImpl{})
|
||||||
|
defer writer.Finish()
|
||||||
|
|
||||||
|
log := logger.New(writer)
|
||||||
defer log.Cleanup()
|
defer log.Cleanup()
|
||||||
|
|
||||||
if len(os.Args) < 2 || !(inList("--make-mode", os.Args) ||
|
if len(os.Args) < 2 || !(inList("--make-mode", os.Args) ||
|
||||||
|
@ -60,16 +65,23 @@ func main() {
|
||||||
trace := tracer.New(log)
|
trace := tracer.New(log)
|
||||||
defer trace.Close()
|
defer trace.Close()
|
||||||
|
|
||||||
|
stat := &status.Status{}
|
||||||
|
defer stat.Finish()
|
||||||
|
stat.AddOutput(terminal.NewStatusOutput(writer, os.Getenv("NINJA_STATUS")))
|
||||||
|
stat.AddOutput(trace.StatusTracer())
|
||||||
|
|
||||||
build.SetupSignals(log, cancel, func() {
|
build.SetupSignals(log, cancel, func() {
|
||||||
trace.Close()
|
trace.Close()
|
||||||
log.Cleanup()
|
log.Cleanup()
|
||||||
|
stat.Finish()
|
||||||
})
|
})
|
||||||
|
|
||||||
buildCtx := build.Context{&build.ContextImpl{
|
buildCtx := build.Context{&build.ContextImpl{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: log,
|
Logger: log,
|
||||||
Tracer: trace,
|
Tracer: trace,
|
||||||
StdioInterface: build.StdioImpl{},
|
Writer: writer,
|
||||||
|
Status: stat,
|
||||||
}}
|
}}
|
||||||
var config build.Config
|
var config build.Config
|
||||||
if os.Args[1] == "--dumpvars-mode" || os.Args[1] == "--dumpvar-mode" {
|
if os.Args[1] == "--dumpvars-mode" || os.Args[1] == "--dumpvar-mode" {
|
||||||
|
@ -78,19 +90,19 @@ func main() {
|
||||||
config = build.NewConfig(buildCtx, os.Args[1:]...)
|
config = build.NewConfig(buildCtx, os.Args[1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.SetVerbose(config.IsVerbose())
|
|
||||||
build.SetupOutDir(buildCtx, config)
|
build.SetupOutDir(buildCtx, config)
|
||||||
|
|
||||||
|
logsDir := config.OutDir()
|
||||||
if config.Dist() {
|
if config.Dist() {
|
||||||
logsDir := filepath.Join(config.DistDir(), "logs")
|
logsDir = filepath.Join(config.DistDir(), "logs")
|
||||||
os.MkdirAll(logsDir, 0777)
|
|
||||||
log.SetOutput(filepath.Join(logsDir, "soong.log"))
|
|
||||||
trace.SetOutput(filepath.Join(logsDir, "build.trace"))
|
|
||||||
} else {
|
|
||||||
log.SetOutput(filepath.Join(config.OutDir(), "soong.log"))
|
|
||||||
trace.SetOutput(filepath.Join(config.OutDir(), "build.trace"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
os.MkdirAll(logsDir, 0777)
|
||||||
|
log.SetOutput(filepath.Join(logsDir, "soong.log"))
|
||||||
|
trace.SetOutput(filepath.Join(logsDir, "build.trace"))
|
||||||
|
stat.AddOutput(status.NewVerboseLog(log, filepath.Join(logsDir, "verbose.log")))
|
||||||
|
stat.AddOutput(status.NewErrorLog(log, filepath.Join(logsDir, "error.log")))
|
||||||
|
|
||||||
if start, ok := os.LookupEnv("TRACE_BEGIN_SOONG"); ok {
|
if start, ok := os.LookupEnv("TRACE_BEGIN_SOONG"); ok {
|
||||||
if !strings.HasSuffix(start, "N") {
|
if !strings.HasSuffix(start, "N") {
|
||||||
if start_time, err := strconv.ParseUint(start, 10, 64); err == nil {
|
if start_time, err := strconv.ParseUint(start, 10, 64); err == nil {
|
||||||
|
@ -114,6 +126,17 @@ func main() {
|
||||||
} else if os.Args[1] == "--dumpvars-mode" {
|
} else if os.Args[1] == "--dumpvars-mode" {
|
||||||
dumpVars(buildCtx, config, os.Args[2:])
|
dumpVars(buildCtx, config, os.Args[2:])
|
||||||
} else {
|
} else {
|
||||||
|
if config.IsVerbose() {
|
||||||
|
writer.Print("! The argument `showcommands` is no longer supported.")
|
||||||
|
writer.Print("! Instead, the verbose log is always written to a compressed file in the output dir:")
|
||||||
|
writer.Print("!")
|
||||||
|
writer.Print(fmt.Sprintf("! gzip -cd %s/verbose.log.gz | less -R", logsDir))
|
||||||
|
writer.Print("!")
|
||||||
|
writer.Print("! Older versions are saved in verbose.log.#.gz files")
|
||||||
|
writer.Print("")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
toBuild := build.BuildAll
|
toBuild := build.BuildAll
|
||||||
if config.Checkbuild() {
|
if config.Checkbuild() {
|
||||||
toBuild |= build.RunBuildTests
|
toBuild |= build.RunBuildTests
|
||||||
|
|
|
@ -59,7 +59,7 @@ function soong_build_go
|
||||||
BUILDDIR=$(getoutdir) \
|
BUILDDIR=$(getoutdir) \
|
||||||
SRCDIR=${TOP} \
|
SRCDIR=${TOP} \
|
||||||
BLUEPRINTDIR=${TOP}/build/blueprint \
|
BLUEPRINTDIR=${TOP}/build/blueprint \
|
||||||
EXTRA_ARGS="-pkg-path android/soong=${TOP}/build/soong" \
|
EXTRA_ARGS="-pkg-path android/soong=${TOP}/build/soong -pkg-path github.com/golang/protobuf=${TOP}/external/golang-protobuf" \
|
||||||
build_go $@
|
build_go $@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,8 @@ bootstrap_go_package {
|
||||||
deps: [
|
deps: [
|
||||||
"soong-ui-build-paths",
|
"soong-ui-build-paths",
|
||||||
"soong-ui-logger",
|
"soong-ui-logger",
|
||||||
|
"soong-ui-status",
|
||||||
|
"soong-ui-terminal",
|
||||||
"soong-ui-tracer",
|
"soong-ui-tracer",
|
||||||
"soong-shared",
|
"soong-shared",
|
||||||
"soong-finder",
|
"soong-finder",
|
||||||
|
@ -62,13 +64,11 @@ bootstrap_go_package {
|
||||||
darwin: {
|
darwin: {
|
||||||
srcs: [
|
srcs: [
|
||||||
"sandbox_darwin.go",
|
"sandbox_darwin.go",
|
||||||
"util_darwin.go"
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
srcs: [
|
srcs: [
|
||||||
"sandbox_linux.go",
|
"sandbox_linux.go",
|
||||||
"util_linux.go"
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,9 +105,7 @@ func checkCaseSensitivity(ctx Context, config Config) {
|
||||||
func help(ctx Context, config Config, what int) {
|
func help(ctx Context, config Config, what int) {
|
||||||
cmd := Command(ctx, config, "help.sh", "build/make/help.sh")
|
cmd := Command(ctx, config, "help.sh", "build/make/help.sh")
|
||||||
cmd.Sandbox = dumpvarsSandbox
|
cmd.Sandbox = dumpvarsSandbox
|
||||||
cmd.Stdout = ctx.Stdout()
|
cmd.RunAndPrintOrFatal()
|
||||||
cmd.Stderr = ctx.Stderr()
|
|
||||||
cmd.RunOrFatal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the tree. The 'what' argument can be used to chose which components of
|
// Build the tree. The 'what' argument can be used to chose which components of
|
||||||
|
|
|
@ -22,13 +22,14 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"android/soong/ui/logger"
|
"android/soong/ui/logger"
|
||||||
|
"android/soong/ui/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testContext() Context {
|
func testContext() Context {
|
||||||
return Context{&ContextImpl{
|
return Context{&ContextImpl{
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
Logger: logger.New(&bytes.Buffer{}),
|
Logger: logger.New(&bytes.Buffer{}),
|
||||||
StdioInterface: NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{}),
|
Writer: terminal.NewWriter(terminal.NewCustomStdio(&bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{})),
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,45 +16,14 @@ package build
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"android/soong/ui/logger"
|
"android/soong/ui/logger"
|
||||||
|
"android/soong/ui/status"
|
||||||
|
"android/soong/ui/terminal"
|
||||||
"android/soong/ui/tracer"
|
"android/soong/ui/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StdioInterface interface {
|
// Context combines a context.Context, logger.Logger, and terminal.Writer.
|
||||||
Stdin() io.Reader
|
|
||||||
Stdout() io.Writer
|
|
||||||
Stderr() io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
type StdioImpl struct{}
|
|
||||||
|
|
||||||
func (StdioImpl) Stdin() io.Reader { return os.Stdin }
|
|
||||||
func (StdioImpl) Stdout() io.Writer { return os.Stdout }
|
|
||||||
func (StdioImpl) Stderr() io.Writer { return os.Stderr }
|
|
||||||
|
|
||||||
var _ StdioInterface = StdioImpl{}
|
|
||||||
|
|
||||||
type customStdio struct {
|
|
||||||
stdin io.Reader
|
|
||||||
stdout io.Writer
|
|
||||||
stderr io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCustomStdio(stdin io.Reader, stdout, stderr io.Writer) StdioInterface {
|
|
||||||
return customStdio{stdin, stdout, stderr}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c customStdio) Stdin() io.Reader { return c.stdin }
|
|
||||||
func (c customStdio) Stdout() io.Writer { return c.stdout }
|
|
||||||
func (c customStdio) Stderr() io.Writer { return c.stderr }
|
|
||||||
|
|
||||||
var _ StdioInterface = customStdio{}
|
|
||||||
|
|
||||||
// Context combines a context.Context, logger.Logger, and StdIO redirection.
|
|
||||||
// These all are agnostic of the current build, and may be used for multiple
|
// These all are agnostic of the current build, and may be used for multiple
|
||||||
// builds, while the Config objects contain per-build information.
|
// builds, while the Config objects contain per-build information.
|
||||||
type Context struct{ *ContextImpl }
|
type Context struct{ *ContextImpl }
|
||||||
|
@ -62,7 +31,8 @@ type ContextImpl struct {
|
||||||
context.Context
|
context.Context
|
||||||
logger.Logger
|
logger.Logger
|
||||||
|
|
||||||
StdioInterface
|
Writer terminal.Writer
|
||||||
|
Status *status.Status
|
||||||
|
|
||||||
Thread tracer.Thread
|
Thread tracer.Thread
|
||||||
Tracer tracer.Tracer
|
Tracer tracer.Tracer
|
||||||
|
@ -88,28 +58,3 @@ func (c ContextImpl) CompleteTrace(name string, begin, end uint64) {
|
||||||
c.Tracer.Complete(name, c.Thread, begin, end)
|
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())
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"android/soong/ui/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DumpMakeVars can be used to extract the values of Make variables after the
|
// 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()
|
cmd.StartOrFatal()
|
||||||
// TODO: error out when Stderr contains any content
|
// TODO: error out when Stderr contains any content
|
||||||
katiRewriteOutput(ctx, pipe)
|
status.KatiReader(ctx.Status.StartTool(), pipe)
|
||||||
cmd.WaitOrFatal()
|
cmd.WaitOrFatal()
|
||||||
|
|
||||||
ret := make(map[string]string, len(vars))
|
ret := make(map[string]string, len(vars))
|
||||||
|
@ -175,7 +177,7 @@ func runMakeProductConfig(ctx Context, config Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print the banner like make does
|
// Print the banner like make does
|
||||||
fmt.Fprintln(ctx.Stdout(), Banner(make_vars))
|
ctx.Writer.Print(Banner(make_vars))
|
||||||
|
|
||||||
// Populate the environment
|
// Populate the environment
|
||||||
env := config.Environment()
|
env := config.Environment()
|
||||||
|
|
|
@ -122,3 +122,20 @@ func (c *Cmd) CombinedOutputOrFatal() []byte {
|
||||||
c.reportError(err)
|
c.reportError(err)
|
||||||
return ret
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -15,15 +15,14 @@
|
||||||
package build
|
package build
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"android/soong/ui/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
var spaceSlashReplacer = strings.NewReplacer("/", "_", " ", "_")
|
var spaceSlashReplacer = strings.NewReplacer("/", "_", " ", "_")
|
||||||
|
@ -117,77 +116,10 @@ func runKati(ctx Context, config Config) {
|
||||||
cmd.Stderr = cmd.Stdout
|
cmd.Stderr = cmd.Stdout
|
||||||
|
|
||||||
cmd.StartOrFatal()
|
cmd.StartOrFatal()
|
||||||
katiRewriteOutput(ctx, pipe)
|
status.KatiReader(ctx.Status.StartTool(), pipe)
|
||||||
cmd.WaitOrFatal()
|
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) {
|
func runKatiCleanSpec(ctx Context, config Config) {
|
||||||
ctx.BeginTrace("kati cleanspec")
|
ctx.BeginTrace("kati cleanspec")
|
||||||
defer ctx.EndTrace()
|
defer ctx.EndTrace()
|
||||||
|
@ -220,6 +152,6 @@ func runKatiCleanSpec(ctx Context, config Config) {
|
||||||
cmd.Stderr = cmd.Stdout
|
cmd.Stderr = cmd.Stdout
|
||||||
|
|
||||||
cmd.StartOrFatal()
|
cmd.StartOrFatal()
|
||||||
katiRewriteOutput(ctx, pipe)
|
status.KatiReader(ctx.Status.StartTool(), pipe)
|
||||||
cmd.WaitOrFatal()
|
cmd.WaitOrFatal()
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,15 +21,21 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"android/soong/ui/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
func runNinja(ctx Context, config Config) {
|
func runNinja(ctx Context, config Config) {
|
||||||
ctx.BeginTrace("ninja")
|
ctx.BeginTrace("ninja")
|
||||||
defer ctx.EndTrace()
|
defer ctx.EndTrace()
|
||||||
|
|
||||||
|
fifo := filepath.Join(config.OutDir(), ".ninja_fifo")
|
||||||
|
status.NinjaReader(ctx, ctx.Status.StartTool(), fifo)
|
||||||
|
|
||||||
executable := config.PrebuiltBuildTool("ninja")
|
executable := config.PrebuiltBuildTool("ninja")
|
||||||
args := []string{
|
args := []string{
|
||||||
"-d", "keepdepfile",
|
"-d", "keepdepfile",
|
||||||
|
fmt.Sprintf("--frontend=cat <&3 >%s", fifo),
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, config.NinjaArgs()...)
|
args = append(args, config.NinjaArgs()...)
|
||||||
|
@ -47,9 +53,6 @@ func runNinja(ctx Context, config Config) {
|
||||||
|
|
||||||
args = append(args, "-f", config.CombinedNinjaFile())
|
args = append(args, "-f", config.CombinedNinjaFile())
|
||||||
|
|
||||||
if config.IsVerbose() {
|
|
||||||
args = append(args, "-v")
|
|
||||||
}
|
|
||||||
args = append(args, "-w", "dupbuild=err")
|
args = append(args, "-w", "dupbuild=err")
|
||||||
|
|
||||||
cmd := Command(ctx, config, "ninja", executable, args...)
|
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)...)
|
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")
|
logPath := filepath.Join(config.OutDir(), ".ninja_log")
|
||||||
ninjaHeartbeatDuration := time.Minute * 5
|
ninjaHeartbeatDuration := time.Minute * 5
|
||||||
if overrideText, ok := cmd.Environment.Get("NINJA_HEARTBEAT_INTERVAL"); ok {
|
if overrideText, ok := cmd.Environment.Get("NINJA_HEARTBEAT_INTERVAL"); ok {
|
||||||
|
@ -99,10 +95,7 @@ func runNinja(ctx Context, config Config) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
startTime := time.Now()
|
cmd.RunAndPrintOrFatal()
|
||||||
defer ctx.ImportNinjaLog(logPath, startTime)
|
|
||||||
|
|
||||||
cmd.RunOrFatal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type statusChecker struct {
|
type statusChecker struct {
|
||||||
|
|
|
@ -15,12 +15,15 @@
|
||||||
package build
|
package build
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/blueprint/microfactory"
|
"github.com/google/blueprint/microfactory"
|
||||||
|
|
||||||
|
"android/soong/ui/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
func runSoong(ctx Context, config Config) {
|
func runSoong(ctx Context, config Config) {
|
||||||
|
@ -41,9 +44,8 @@ func runSoong(ctx Context, config Config) {
|
||||||
cmd.Environment.Set("SRCDIR", ".")
|
cmd.Environment.Set("SRCDIR", ".")
|
||||||
cmd.Environment.Set("TOPNAME", "Android.bp")
|
cmd.Environment.Set("TOPNAME", "Android.bp")
|
||||||
cmd.Sandbox = soongSandbox
|
cmd.Sandbox = soongSandbox
|
||||||
cmd.Stdout = ctx.Stdout()
|
|
||||||
cmd.Stderr = ctx.Stderr()
|
cmd.RunAndPrintOrFatal()
|
||||||
cmd.RunOrFatal()
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
|
@ -56,12 +58,18 @@ func runSoong(ctx Context, config Config) {
|
||||||
if _, err := os.Stat(envTool); err == nil {
|
if _, err := os.Stat(envTool); err == nil {
|
||||||
cmd := Command(ctx, config, "soong_env", envTool, envFile)
|
cmd := Command(ctx, config, "soong_env", envTool, envFile)
|
||||||
cmd.Sandbox = soongSandbox
|
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 {
|
if err := cmd.Run(); err != nil {
|
||||||
ctx.Verboseln("soong_env failed, forcing manifest regeneration")
|
ctx.Verboseln("soong_env failed, forcing manifest regeneration")
|
||||||
os.Remove(envFile)
|
os.Remove(envFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
ctx.Verboseln(buf.String())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.Verboseln("Missing soong_env tool, forcing manifest regeneration")
|
ctx.Verboseln("Missing soong_env tool, forcing manifest regeneration")
|
||||||
os.Remove(envFile)
|
os.Remove(envFile)
|
||||||
|
@ -100,22 +108,18 @@ func runSoong(ctx Context, config Config) {
|
||||||
ctx.BeginTrace(name)
|
ctx.BeginTrace(name)
|
||||||
defer ctx.EndTrace()
|
defer ctx.EndTrace()
|
||||||
|
|
||||||
|
fifo := filepath.Join(config.OutDir(), ".ninja_fifo")
|
||||||
|
status.NinjaReader(ctx, ctx.Status.StartTool(), fifo)
|
||||||
|
|
||||||
cmd := Command(ctx, config, "soong "+name,
|
cmd := Command(ctx, config, "soong "+name,
|
||||||
config.PrebuiltBuildTool("ninja"),
|
config.PrebuiltBuildTool("ninja"),
|
||||||
"-d", "keepdepfile",
|
"-d", "keepdepfile",
|
||||||
"-w", "dupbuild=err",
|
"-w", "dupbuild=err",
|
||||||
"-j", strconv.Itoa(config.Parallel()),
|
"-j", strconv.Itoa(config.Parallel()),
|
||||||
|
fmt.Sprintf("--frontend=cat <&3 >%s", fifo),
|
||||||
"-f", filepath.Join(config.SoongOutDir(), file))
|
"-f", filepath.Join(config.SoongOutDir(), file))
|
||||||
if config.IsVerbose() {
|
|
||||||
cmd.Args = append(cmd.Args, "-v")
|
|
||||||
}
|
|
||||||
cmd.Sandbox = soongSandbox
|
cmd.Sandbox = soongSandbox
|
||||||
cmd.Stdin = ctx.Stdin()
|
cmd.RunAndPrintOrFatal()
|
||||||
cmd.Stdout = ctx.Stdout()
|
|
||||||
cmd.Stderr = ctx.Stderr()
|
|
||||||
|
|
||||||
defer ctx.ImportNinjaLog(filepath.Join(config.OutDir(), ".ninja_log"), time.Now())
|
|
||||||
cmd.RunOrFatal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ninja("minibootstrap", ".minibootstrap/build.ninja")
|
ninja("minibootstrap", ".minibootstrap/build.ninja")
|
||||||
|
|
|
@ -15,13 +15,9 @@
|
||||||
package build
|
package build
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func absPath(ctx Context, p string) string {
|
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
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -49,48 +49,3 @@ func TestEnsureEmptyDirs(t *testing.T) {
|
||||||
|
|
||||||
ensureEmptyDirectoriesExist(ctx, filepath.Join(tmpDir, "a"))
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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",
|
||||||
|
],
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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.
|
|
@ -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,
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
aprotoc --go_out=paths=source_relative:. frontend.proto
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package build
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"syscall"
|
"syscall"
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package build
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"syscall"
|
"syscall"
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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{}
|
|
@ -15,10 +15,13 @@
|
||||||
bootstrap_go_package {
|
bootstrap_go_package {
|
||||||
name: "soong-ui-tracer",
|
name: "soong-ui-tracer",
|
||||||
pkgPath: "android/soong/ui/tracer",
|
pkgPath: "android/soong/ui/tracer",
|
||||||
deps: ["soong-ui-logger"],
|
deps: [
|
||||||
|
"soong-ui-logger",
|
||||||
|
"soong-ui-status",
|
||||||
|
],
|
||||||
srcs: [
|
srcs: [
|
||||||
"microfactory.go",
|
"microfactory.go",
|
||||||
"ninja.go",
|
"status.go",
|
||||||
"tracer.go",
|
"tracer.go",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,48 @@ package tracer
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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) {
|
func (t *tracerImpl) ImportMicrofactoryLog(filename string) {
|
||||||
if _, err := os.Stat(filename); err != nil {
|
if _, err := os.Stat(filename); err != nil {
|
||||||
return
|
return
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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) {}
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"android/soong/ui/logger"
|
"android/soong/ui/logger"
|
||||||
|
"android/soong/ui/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Thread uint64
|
type Thread uint64
|
||||||
|
@ -46,7 +47,8 @@ type Tracer interface {
|
||||||
Complete(name string, thread Thread, begin, end uint64)
|
Complete(name string, thread Thread, begin, end uint64)
|
||||||
|
|
||||||
ImportMicrofactoryLog(filename string)
|
ImportMicrofactoryLog(filename string)
|
||||||
ImportNinjaLog(thread Thread, filename string, startOffset time.Time)
|
|
||||||
|
StatusTracer() status.StatusOutput
|
||||||
|
|
||||||
NewThread(name string) Thread
|
NewThread(name string) Thread
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue