diff --git a/cmd/javac_filter/Android.bp b/cmd/javac_wrapper/Android.bp similarity index 87% rename from cmd/javac_filter/Android.bp rename to cmd/javac_wrapper/Android.bp index cbdabb98b..c00f4bd16 100644 --- a/cmd/javac_filter/Android.bp +++ b/cmd/javac_wrapper/Android.bp @@ -13,11 +13,11 @@ // limitations under the License. blueprint_go_binary { - name: "soong_javac_filter", + name: "soong_javac_wrapper", srcs: [ - "javac_filter.go", + "javac_wrapper.go", ], testSrcs: [ - "javac_filter_test.go", + "javac_wrapper_test.go", ], } diff --git a/cmd/javac_filter/javac_filter.go b/cmd/javac_wrapper/javac_wrapper.go similarity index 59% rename from cmd/javac_filter/javac_filter.go rename to cmd/javac_wrapper/javac_wrapper.go index a089acd4f..a32347317 100644 --- a/cmd/javac_filter/javac_filter.go +++ b/cmd/javac_wrapper/javac_wrapper.go @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -// soong_javac_filter expects the output of javac on stdin, and produces -// an ANSI colorized version of the output on stdout. +// soong_javac_wrapper expects a javac command line and argments, executes +// it, and produces an ANSI colorized version of the output on stdout. // // It also hides the unhelpful and unhideable "warning there is a warning" // messages. @@ -24,17 +24,19 @@ import ( "fmt" "io" "os" + "os/exec" "regexp" + "syscall" ) // Regular expressions are based on // https://chromium.googlesource.com/chromium/src/+/master/build/android/gyp/javac.py // Colors are based on clang's output var ( - filelinePrefix = `^[-.\w/\\]+.java:[0-9]+:` - warningRe = regexp.MustCompile(filelinePrefix + ` (warning:) .*$`) - errorRe = regexp.MustCompile(filelinePrefix + ` (.*?:) .*$`) - markerRe = regexp.MustCompile(`\s*(\^)\s*$`) + filelinePrefix = `^([-.\w/\\]+.java:[0-9]+: )` + warningRe = regexp.MustCompile(filelinePrefix + `?(warning:) .*$`) + errorRe = regexp.MustCompile(filelinePrefix + `(.*?:) .*$`) + markerRe = regexp.MustCompile(`()\s*(\^)\s*$`) escape = "\x1b" reset = escape + "[0m" @@ -45,11 +47,69 @@ var ( ) func main() { - err := process(bufio.NewReader(os.Stdin), os.Stdout) + exitCode, err := Main(os.Args[0], os.Args[1:]) if err != nil { - fmt.Fprintln(os.Stderr, "reading standard input:", err) - os.Exit(-1) + fmt.Fprintln(os.Stderr, err.Error()) } + os.Exit(exitCode) +} + +func Main(name string, args []string) (int, error) { + if len(args) < 1 { + return 1, fmt.Errorf("usage: %s javac ...", name) + } + + pr, pw, err := os.Pipe() + if err != nil { + return 1, fmt.Errorf("creating output pipe: %s", err) + } + + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = pw + cmd.Stderr = pw + err = cmd.Start() + if err != nil { + return 1, fmt.Errorf("starting subprocess: %s", err) + } + + pw.Close() + + // Process subprocess stdout asynchronously + errCh := make(chan error) + go func() { + errCh <- process(pr, os.Stdout) + }() + + // Wait for subprocess to finish + cmdErr := cmd.Wait() + + // Wait for asynchronous stdout processing to finish + err = <-errCh + + // Check for subprocess exit code + if cmdErr != nil { + if exitErr, ok := cmdErr.(*exec.ExitError); ok { + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + if status.Exited() { + return status.ExitStatus(), nil + } else if status.Signaled() { + exitCode := 128 + int(status.Signal()) + return exitCode, nil + } else { + return 1, exitErr + } + } else { + return 1, nil + } + } + } + + if err != nil { + return 1, err + } + + return 0, nil } func process(r io.Reader, w io.Writer) error { @@ -61,7 +121,11 @@ func process(r io.Reader, w io.Writer) error { for scanner.Scan() { processLine(w, scanner.Text()) } - return scanner.Err() + err := scanner.Err() + if err != nil { + return fmt.Errorf("scanning input: %s", err) + } + return nil } func processLine(w io.Writer, line string) { @@ -83,7 +147,7 @@ func processLine(w io.Writer, line string) { // Returns line, modified if it matched, and true if it matched. func applyColor(line, color string, re *regexp.Regexp) (string, bool) { if m := re.FindStringSubmatchIndex(line); m != nil { - tagStart, tagEnd := m[2], m[3] + tagStart, tagEnd := m[4], m[5] line = bold + line[:tagStart] + color + line[tagStart:tagEnd] + reset + bold + line[tagEnd:] + reset diff --git a/cmd/javac_filter/javac_filter_test.go b/cmd/javac_wrapper/javac_wrapper_test.go similarity index 60% rename from cmd/javac_filter/javac_filter_test.go rename to cmd/javac_wrapper/javac_wrapper_test.go index 43381ce89..9f41078d1 100644 --- a/cmd/javac_filter/javac_filter_test.go +++ b/cmd/javac_wrapper/javac_wrapper_test.go @@ -16,6 +16,7 @@ package main import ( "bytes" + "strconv" "testing" ) @@ -38,6 +39,10 @@ var testCases = []struct { in: "File.java:398: warning: [RectIntersectReturnValueIgnored] Return value of com.blah.function() must be checked\n", out: "\x1b[1mFile.java:398: \x1b[35mwarning:\x1b[0m\x1b[1m [RectIntersectReturnValueIgnored] Return value of com.blah.function() must be checked\x1b[0m\n", }, + { + in: "warning: [options] bootstrap class path not set in conjunction with -source 1.7\n", + out: "\x1b[1m\x1b[35mwarning:\x1b[0m\x1b[1m [options] bootstrap class path not set in conjunction with -source 1.7\x1b[0m\n", + }, { in: " (see http://go/errorprone/bugpattern/RectIntersectReturnValueIgnored.md)\n", out: " (see http://go/errorprone/bugpattern/RectIntersectReturnValueIgnored.md)\n", @@ -60,15 +65,50 @@ Note: dir/file.java uses unchecked or unsafe operations. } func TestJavacColorize(t *testing.T) { - for _, test := range testCases { - buf := new(bytes.Buffer) - err := process(bytes.NewReader([]byte(test.in)), buf) - if err != nil { - t.Errorf("error: %q", err) - } - got := string(buf.Bytes()) - if got != test.out { - t.Errorf("expected %q got %q", test.out, got) - } + for i, test := range testCases { + t.Run(strconv.Itoa(i), func(t *testing.T) { + buf := new(bytes.Buffer) + err := process(bytes.NewReader([]byte(test.in)), buf) + if err != nil { + t.Errorf("error: %q", err) + } + got := string(buf.Bytes()) + if got != test.out { + t.Errorf("expected %q got %q", test.out, got) + } + }) } } + +func TestSubprocess(t *testing.T) { + t.Run("failure", func(t *testing.T) { + exitCode, err := Main("test", []string{"sh", "-c", "exit 9"}) + if err != nil { + t.Fatal("unexpected error", err) + } + if exitCode != 9 { + t.Fatal("expected exit code 9, got", exitCode) + } + }) + + t.Run("signal", func(t *testing.T) { + exitCode, err := Main("test", []string{"sh", "-c", "kill -9 $$"}) + if err != nil { + t.Fatal("unexpected error", err) + } + if exitCode != 137 { + t.Fatal("expected exit code 137, got", exitCode) + } + }) + + t.Run("success", func(t *testing.T) { + exitCode, err := Main("test", []string{"echo"}) + if err != nil { + t.Fatal("unexpected error", err) + } + if exitCode != 0 { + t.Fatal("expected exit code 0, got", exitCode) + } + }) + +}