Move version checking from Make into soong_ui

When kati keeps state around, it has to regenerate the ninja file every
time the state is changed. So move the java version checking into
soong_ui, where we can parallelize it with other operations instead of
only checking it occasionally.

Bug: 35970961
Test: Put java7 in PATH, m -j
Test: Put java8-google in PATH, m -j
Test: Put a space in TOP, m -j
Test: OUT_DIR=<case-preserving fs> m -j
Test: OUT_DIR=<path with space> m -j
Test: DIST_DIR=<path with sapce> m -j
Change-Id: I3245c8dd6d856240d17d54cb05d593dc9df71a27
diff --git a/ui/build/Android.bp b/ui/build/Android.bp
index d44c112..7a83684 100644
--- a/ui/build/Android.bp
+++ b/ui/build/Android.bp
@@ -25,6 +25,7 @@
         "context.go",
         "environment.go",
         "exec.go",
+        "java.go",
         "kati.go",
         "make.go",
         "ninja.go",
diff --git a/ui/build/build.go b/ui/build/build.go
index 6785082..b84dd7d 100644
--- a/ui/build/build.go
+++ b/ui/build/build.go
@@ -15,6 +15,7 @@
 package build
 
 import (
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"text/template"
@@ -59,6 +60,37 @@
 	BuildAll           = BuildProductConfig | BuildSoong | BuildKati | BuildNinja
 )
 
+func checkCaseSensitivity(ctx Context, config Config) {
+	outDir := config.OutDir()
+	lowerCase := filepath.Join(outDir, "casecheck.txt")
+	upperCase := filepath.Join(outDir, "CaseCheck.txt")
+	lowerData := "a"
+	upperData := "B"
+
+	err := ioutil.WriteFile(lowerCase, []byte(lowerData), 0777)
+	if err != nil {
+		ctx.Fatalln("Failed to check case sensitivity:", err)
+	}
+
+	err = ioutil.WriteFile(upperCase, []byte(upperData), 0777)
+	if err != nil {
+		ctx.Fatalln("Failed to check case sensitivity:", err)
+	}
+
+	res, err := ioutil.ReadFile(lowerCase)
+	if err != nil {
+		ctx.Fatalln("Failed to check case sensitivity:", err)
+	}
+
+	if string(res) != lowerData {
+		ctx.Println("************************************************************")
+		ctx.Println("You are building on a case-insensitive filesystem.")
+		ctx.Println("Please move your source tree to a case-sensitive filesystem.")
+		ctx.Println("************************************************************")
+		ctx.Fatalln("Case-insensitive filesystems not supported")
+	}
+}
+
 // Build the tree. The 'what' argument can be used to chose which components of
 // the build to run.
 func Build(ctx Context, config Config, what int) {
@@ -86,8 +118,13 @@
 		return
 	}
 
+	// Start getting java version as early as possible
+	getJavaVersions(ctx, config)
+
 	SetupOutDir(ctx, config)
 
+	checkCaseSensitivity(ctx, config)
+
 	if what&BuildProductConfig != 0 {
 		// Run make for product config
 		runMakeProductConfig(ctx, config)
@@ -99,6 +136,9 @@
 		runSoong(ctx, config)
 	}
 
+	// Check the java versions we read earlier
+	checkJavaVersion(ctx, config)
+
 	if what&BuildKati != 0 {
 		// Run ckati
 		runKati(ctx, config)
diff --git a/ui/build/config.go b/ui/build/config.go
index e677d93..0d29924 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -106,6 +106,32 @@
 		log.Fatalln("Error verifying tree state:", err)
 	}
 
+	if srcDir, err := filepath.Abs("."); err == nil {
+		if strings.ContainsRune(srcDir, ' ') {
+			log.Println("You are building in a directory whose absolute path contains a space character:")
+			log.Println()
+			log.Printf("%q\n", srcDir)
+			log.Println()
+			log.Fatalln("Directory names containing spaces are not supported")
+		}
+	}
+
+	if outDir := ret.OutDir(); strings.ContainsRune(outDir, ' ') {
+		log.Println("The absolute path of your output directory ($OUT_DIR) contains a space character:")
+		log.Println()
+		log.Printf("%q\n", outDir)
+		log.Println()
+		log.Fatalln("Directory names containing spaces are not supported")
+	}
+
+	if distDir := ret.DistDir(); strings.ContainsRune(distDir, ' ') {
+		log.Println("The absolute path of your dist directory ($DIST_DIR) contains a space character:")
+		log.Println()
+		log.Printf("%q\n", distDir)
+		log.Println()
+		log.Fatalln("Directory names containing spaces are not supported")
+	}
+
 	for _, arg := range args {
 		arg = strings.TrimSpace(arg)
 		if arg == "--make-mode" {
diff --git a/ui/build/exec.go b/ui/build/exec.go
index 4c45c50..c8c5c9a 100644
--- a/ui/build/exec.go
+++ b/ui/build/exec.go
@@ -84,24 +84,38 @@
 	}
 }
 
+func (c *Cmd) reportError(err error) {
+	if err == nil {
+		return
+	}
+	if e, ok := err.(*exec.ExitError); ok {
+		c.ctx.Fatalf("%s failed with: %v", c.name, e.ProcessState.String())
+	} else {
+		c.ctx.Fatalf("Failed to run %s: %v", c.name, err)
+	}
+}
+
 // RunOrFatal is equivalent to Run, but handles the error with a call to ctx.Fatal
 func (c *Cmd) RunOrFatal() {
-	if err := c.Run(); err != nil {
-		if e, ok := err.(*exec.ExitError); ok {
-			c.ctx.Fatalf("%s failed with: %v", c.name, e.ProcessState.String())
-		} else {
-			c.ctx.Fatalf("Failed to run %s: %v", c.name, err)
-		}
-	}
+	c.reportError(c.Run())
 }
 
 // WaitOrFatal is equivalent to Wait, but handles the error with a call to ctx.Fatal
 func (c *Cmd) WaitOrFatal() {
-	if err := c.Wait(); err != nil {
-		if e, ok := err.(*exec.ExitError); ok {
-			c.ctx.Fatalf("%s failed with: %v", c.name, e.ProcessState.String())
-		} else {
-			c.ctx.Fatalf("Failed to run %s: %v", c.name, err)
-		}
-	}
+	c.reportError(c.Wait())
+}
+
+// OutputOrFatal is equivalent to Output, but handles the error with a call to ctx.Fatal
+func (c *Cmd) OutputOrFatal() []byte {
+	ret, err := c.Output()
+	c.reportError(err)
+	return ret
+}
+
+// CombinedOutputOrFatal is equivalent to CombinedOutput, but handles the error with
+// a call to ctx.Fatal
+func (c *Cmd) CombinedOutputOrFatal() []byte {
+	ret, err := c.CombinedOutput()
+	c.reportError(err)
+	return ret
 }
diff --git a/ui/build/java.go b/ui/build/java.go
new file mode 100644
index 0000000..5a09b1a
--- /dev/null
+++ b/ui/build/java.go
@@ -0,0 +1,156 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+	"regexp"
+	"runtime"
+	"strings"
+	"sync"
+)
+
+const incompatibleJavacStr = "google"
+
+var javaVersionInfo = struct {
+	once      sync.Once
+	startOnce sync.Once
+
+	java_version_output  string
+	javac_version_output string
+}{}
+
+func getJavaVersions(ctx Context, config Config) {
+	javaVersionInfo.startOnce.Do(func() {
+		go func() {
+			if ctx.Tracer != nil {
+				thread := ctx.Tracer.NewThread("java_version")
+				ctx.Tracer.Begin("get version", thread)
+				defer ctx.Tracer.End(thread)
+			}
+
+			getJavaVersionsImpl(ctx, config)
+		}()
+	})
+}
+
+func getJavaVersionsImpl(ctx Context, config Config) {
+	javaVersionInfo.once.Do(func() {
+		cmd := Command(ctx, config, "java", "java", "-version")
+		cmd.Environment.Unset("_JAVA_OPTIONS")
+		javaVersionInfo.java_version_output = string(cmd.CombinedOutputOrFatal())
+
+		cmd = Command(ctx, config, "javac", "javac", "-version")
+		cmd.Environment.Unset("_JAVA_OPTIONS")
+		javaVersionInfo.javac_version_output = string(cmd.CombinedOutputOrFatal())
+	})
+}
+
+func checkJavaVersion(ctx Context, config Config) {
+	ctx.BeginTrace("java_version_check")
+	defer ctx.EndTrace()
+
+	getJavaVersionsImpl(ctx, config)
+
+	var required_java_version string
+	var java_version_regexp *regexp.Regexp
+	var javac_version_regexp *regexp.Regexp
+	if legacy, _ := config.Environment().Get("LEGACY_USE_JAVA7"); legacy != "" {
+		required_java_version = "1.7"
+		java_version_regexp = regexp.MustCompile(`^java .*[ "]1\.7[\. "$]`)
+		javac_version_regexp = regexp.MustCompile(`[ "]1\.7[\. "$]`)
+	} else {
+		required_java_version = "1.8"
+		java_version_regexp = regexp.MustCompile(`[ "]1\.8[\. "$]`)
+		javac_version_regexp = java_version_regexp
+	}
+
+	java_version := javaVersionInfo.java_version_output
+	javac_version := javaVersionInfo.javac_version_output
+
+	found := false
+	for _, l := range strings.Split(java_version, "\n") {
+		if java_version_regexp.MatchString(l) {
+			java_version = l
+			found = true
+			break
+		}
+	}
+	if !found {
+		ctx.Println("***************************************************************")
+		ctx.Println("You are attempting to build with the incorrect version of java.")
+		ctx.Println()
+		ctx.Println("Your version is:", java_version)
+		ctx.Println("The required version is:", required_java_version+".x")
+		ctx.Println()
+		ctx.Println("Please follow the machine setup instructions at:")
+		ctx.Println("    https://source.android.com/source/initializing.html")
+		ctx.Println("***************************************************************")
+		ctx.Fatalln("stop")
+	}
+
+	if runtime.GOOS == "linux" {
+		if !strings.Contains(java_version, "openjdk") {
+			ctx.Println("*******************************************************")
+			ctx.Println("You are attempting to build with an unsupported JDK.")
+			ctx.Println()
+			ctx.Println("Only an OpenJDK based JDK is supported.")
+			ctx.Println()
+			ctx.Println("Please follow the machine setup instructions at:")
+			ctx.Println("    https://source.android.com/source/initializing.html")
+			ctx.Println("*******************************************************")
+			ctx.Fatalln("stop")
+		}
+	} else { // darwin
+		if strings.Contains(java_version, "openjdk") {
+			ctx.Println("*******************************************************")
+			ctx.Println("You are attempting to build with an unsupported JDK.")
+			ctx.Println()
+			ctx.Println("You use OpenJDK, but only Sun/Oracle JDK is supported.")
+			ctx.Println()
+			ctx.Println("Please follow the machine setup instructions at:")
+			ctx.Println("    https://source.android.com/source/initializing.html")
+			ctx.Println("*******************************************************")
+			ctx.Fatalln("stop")
+		}
+	}
+
+	incompatible_javac := strings.Contains(javac_version, incompatibleJavacStr)
+
+	found = false
+	for _, l := range strings.Split(javac_version, "\n") {
+		if javac_version_regexp.MatchString(l) {
+			javac_version = l
+			found = true
+			break
+		}
+	}
+	if !found || incompatible_javac {
+		ctx.Println("****************************************************************")
+		ctx.Println("You are attempting to build with the incorrect version of javac.")
+		ctx.Println()
+		ctx.Println("Your version is:", javac_version)
+		if incompatible_javac {
+			ctx.Println("The '" + incompatibleJavacStr + "' version is not supported for Android platform builds.")
+			ctx.Println("Use a publically available JDK and make sure you have run envsetup.sh / lunch.")
+		} else {
+			ctx.Println("The required version is:", required_java_version)
+		}
+		ctx.Println()
+		ctx.Println("Please follow the machine setup instructions at:")
+		ctx.Println("    https://source.android.com/source/initializing.html")
+		ctx.Println("****************************************************************")
+		ctx.Fatalln("stop")
+	}
+}
diff --git a/ui/tracer/tracer.go b/ui/tracer/tracer.go
index b372885..f19ac18 100644
--- a/ui/tracer/tracer.go
+++ b/ui/tracer/tracer.go
@@ -46,6 +46,8 @@
 	Complete(name string, thread Thread, begin, end uint64)
 
 	ImportNinjaLog(thread Thread, filename string, startOffset time.Time)
+
+	NewThread(name string) Thread
 }
 
 type tracerImpl struct {