Add a pinned JDK 9 to third_party and update Gradle to 5.2

This CL makes various changes to update our build to JDK 9 and Gradle 5.2. The
build will by default use third_party/{gradle/gradle, openjdk/openjdk-9.0.4} to
build. This is done by directly using third_party/gradle/gradle and reading the
location of the JDK from tools/jdk.py. That value is then set as JAVA_HOME for
the subprocess in tools/gradle.py and picked up by the running tests via
Syste.getProperty("java.home"). To override the JDK to use, both tools/gradle.py
and tools/test.py take a --java-home argument which is used to define
-Dorg.gradle.java.home which will take precedence over JAVA_HOME in gradle.

FYI, the build.gradle is configured to issue a warning if the current Java Home
differes from the expected pinned version, but will not change it.

The summary of changes needed to update are:

- Add tools/jdk.py which is the canonical place to update the current default JDK.

- Add --java-home flag for running with a non-pinned JDK (eg, JDK 8) to gradle.py.

- Add various max-heap-size settings to control heap size (for example, running
JDK 8 requires manually increasing size of the gradle instance for compilation).

- Update the inner-classes tests which are no longer considered malformed on JDK 9.

- Update path to classes build dir (changed in gradle 4+).

Change-Id: I28789c0eaacb865b31b46e8daec0da8e62bb24a3
diff --git a/.gitignore b/.gitignore
index c04d9dd..b8248ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -85,6 +85,12 @@
 third_party/ddmlib.tar.gz
 third_party/core-lambda-stubs
 third_party/core-lambda-stubs.tar.gz
+third_party/openjdk/openjdk-9.0.4/linux
+third_party/openjdk/openjdk-9.0.4/linux.tar.gz
+third_party/openjdk/openjdk-9.0.4/osx
+third_party/openjdk/openjdk-9.0.4/osx.tar.gz
+third_party/openjdk/openjdk-9.0.4/windows
+third_party/openjdk/openjdk-9.0.4/windows.tar.gz
 third_party/openjdk/openjdk-rt-1.8
 third_party/openjdk/openjdk-rt-1.8.tar.gz
 third_party/r8
@@ -98,6 +104,7 @@
 *.iml
 r8.ipr
 r8.iws
+local.properties
 #*#
 *~
 .#*
diff --git a/build.gradle b/build.gradle
index 4ce2e18..053bbfb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,12 +1,33 @@
 // Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
+
 import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
-import net.ltgt.gradle.errorprone.ErrorProneToolChain
+import net.ltgt.gradle.errorprone.CheckSeverity
 import org.gradle.internal.os.OperatingSystem
 import tasks.GetJarsFromConfiguration
 import utils.Utils
 
+buildscript {
+    repositories {
+        mavenCentral()
+        jcenter()
+        maven {
+            url "https://plugins.gradle.org/m2/"
+        }
+    }
+    dependencies {
+        classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
+    }
+}
+
+plugins {
+  id "net.ltgt.errorprone" version "0.7"
+}
+
+apply plugin: 'java'
+apply plugin: 'idea'
+
 ext {
     androidSupportVersion = '25.4.0'
     asmVersion = '6.2.1'
@@ -20,32 +41,9 @@
     kotlinVersion = '1.3.11'
     kotlinExtMetadataJVMVersion = '0.0.4'
     smaliVersion = '2.2b4'
+    errorproneVersion = '2.3.2'
 }
 
-def errorProneConfiguration = [
-    '-XepDisableAllChecks',
-    // D8 want to use reference equality, thus disable the checker explicitly
-    '-Xep:ReferenceEquality:OFF',
-    '-Xep:ClassCanBeStatic:ERROR',
-    '-Xep:OperatorPrecedence:ERROR',
-    '-Xep:RemoveUnusedImports:ERROR',
-    '-Xep:MissingOverride:ERROR',
-    '-Xep:IntLongMath:ERROR',
-    '-Xep:EqualsHashCode:ERROR',
-    '-Xep:InconsistentOverloads:ERROR',
-    '-Xep:ArrayHashCode:ERROR',
-    '-Xep:EqualsIncompatibleType:ERROR',
-    '-Xep:NonOverridingEquals:ERROR',
-    '-Xep:FallThrough:ERROR',
-    '-Xep:MissingCasesInEnumSwitch:ERROR',
-    '-Xep:MissingDefault:ERROR',
-    '-Xep:MultipleTopLevelClasses:ERROR',
-    '-Xep:NarrowingCompoundAssignment:ERROR',
-    '-Xep:BoxedPrimitiveConstructor:ERROR',
-    '-Xep:LogicalAssignment:ERROR',
-    '-Xep:FloatCast:ERROR',
-    '-Xep:ReturnValueIgnored:ERROR']
-
 apply from: 'copyAdditionalJctfCommonFiles.gradle'
 
 repositories {
@@ -54,32 +52,6 @@
     mavenCentral()
 }
 
-buildscript {
-    repositories {
-        mavenCentral()
-        jcenter()
-        maven {
-            url "https://plugins.gradle.org/m2/"
-        }
-    }
-    dependencies {
-        classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.4'
-        classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
-        classpath "com.gradle:build-scan-plugin:1.14"
-    }
-}
-
-apply plugin: "com.gradle.build-scan"
-
-buildScan {
-    licenseAgreementUrl = 'https://gradle.com/terms-of-service'
-    licenseAgree = 'yes'
-}
-
-apply plugin: 'java'
-apply plugin: 'idea'
-apply plugin: 'net.ltgt.errorprone-base'
-
 if (project.hasProperty('with_code_coverage')) {
     apply plugin: 'jacoco'
 }
@@ -206,10 +178,10 @@
         module {
             if (sources.name == "main") {
                 sourceDirs += sources.java.srcDirs
-                outputDir sources.output.classesDir
+                outputDir sources.output.classesDirs[0]
             } else {
                 testSourceDirs += sources.java.srcDirs
-                testOutputDir sources.output.classesDir
+                testOutputDir sources.output.classesDirs[0]
             }
         }
     }
@@ -262,6 +234,7 @@
     debugTestResourcesKotlinCompileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
     examplesKotlinCompileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
     kotlinR8TestResourcesCompileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+    errorprone("com.google.errorprone:error_prone_core:$errorproneVersion")
 }
 
 def r8LibPath = "$buildDir/libs/r8lib.jar"
@@ -320,9 +293,41 @@
         ]
 ]
 
+def cloudSystemDependencies = [
+        linux: [
+                "third_party": ["openjdk/openjdk-9.0.4/linux"],
+        ],
+        osx: [
+                "third_party": ["openjdk/openjdk-9.0.4/osx"],
+        ],
+        windows: [
+                "third_party": ["openjdk/openjdk-9.0.4/windows"],
+        ],
+]
+
+if (OperatingSystem.current().isWindows()) {
+    cloudSystemDependencies.windows.each { entry ->
+        cloudDependencies.get(entry.key).addAll(entry.value)
+    }
+} else if (OperatingSystem.current().isLinux()) {
+    cloudSystemDependencies.linux.each { entry ->
+        cloudDependencies.get(entry.key).addAll(entry.value)
+    }
+} else if (OperatingSystem.current().isMacOsX()) {
+    cloudSystemDependencies.osx.each { entry ->
+        cloudDependencies.get(entry.key).addAll(entry.value)
+    }
+} else {
+    println "WARNING: Unsupported system: " + OperatingSystem.current()
+}
+
+def getDownloadDepsTaskName(entryKey, entryFile) {
+    return "download_deps_${entryKey}_${entryFile.replace('/', '_').replace('\\', '_')}"
+}
+
 cloudDependencies.each { entry ->
     entry.value.each { entryFile ->
-        task "download_deps_${entry.key}/${entryFile}"(type: Exec) {
+        task "${getDownloadDepsTaskName(entry.key, entryFile)}"(type: Exec) {
             def outputDir = "${entry.key}/${entryFile}"
             def gzFile = "${outputDir}.tar.gz"
             def sha1File = "${gzFile}.sha1"
@@ -372,7 +377,7 @@
 
 x20Dependencies.each { entry ->
     entry.value.each { entryFile ->
-        task "download_deps_${entry.key}/${entryFile}"(type: Exec) {
+        task "${getDownloadDepsTaskName(entry.key, entryFile)}"(type: Exec) {
             def outputDir = "${entry.key}/${entryFile}"
             def gzFile = "${outputDir}.tar.gz"
             def sha1File = "${gzFile}.sha1"
@@ -388,7 +393,7 @@
     cloudDependencies.each { entry ->
         entry.value.each { entryFile ->
             if (entryFile.contains("proguard")) {
-                dependsOn "download_deps_${entry.key}/${entryFile}"
+                dependsOn "${getDownloadDepsTaskName(entry.key, entryFile)}"
             }
         }
     }
@@ -398,7 +403,7 @@
    cloudDependencies.each { entry ->
         entry.value.each { entryFile ->
             if (entryFile.contains("openjdk-rt")) {
-                dependsOn "download_deps_${entry.key}/${entryFile}"
+                dependsOn "${getDownloadDepsTaskName(entry.key, entryFile)}"
             }
         }
     }
@@ -408,7 +413,7 @@
     cloudDependencies.each { entry ->
         entry.value.each { entryFile ->
             if (entryFile.endsWith("/dx")) {
-                dependsOn "download_deps_${entry.key}/${entryFile}"
+                dependsOn "${getDownloadDepsTaskName(entry.key, entryFile)}"
             }
         }
     }
@@ -418,7 +423,7 @@
     cloudDependencies.each { entry ->
         entry.value.each { entryFile ->
             if (entryFile.contains("android_cts_baseline")) {
-                dependsOn "download_deps_${entry.key}/${entryFile}"
+                dependsOn "${getDownloadDepsTaskName(entry.key, entryFile)}"
             }
         }
     }
@@ -427,13 +432,13 @@
 task downloadDeps {
     cloudDependencies.each { entry ->
         entry.value.each { entryFile ->
-            dependsOn "download_deps_${entry.key}/${entryFile}"
+            dependsOn "${getDownloadDepsTaskName(entry.key, entryFile)}"
         }
     }
     if (!project.hasProperty('no_internal')) {
         x20Dependencies.each { entry ->
             entry.value.each { entryFile ->
-                dependsOn "download_deps_${entry.key}/${entryFile}"
+                dependsOn "${getDownloadDepsTaskName(entry.key, entryFile)}"
             }
         }
     }
@@ -444,22 +449,63 @@
     targetCompatibility = JavaVersion.VERSION_1_8
 }
 
-// Javac often runs out of stack space when compiling the tests.
-// Increase the stack size for the javac process.
-tasks.withType(JavaCompile) {
-    options.fork = true
-    options.forkOptions.jvmArgs = ["-Xss4m"]
+// Check if running with the JDK location from tools/jdk.py.
+if (OperatingSystem.current().isWindows()) {
+    println "NOTE: Running with JDK: " + org.gradle.internal.jvm.Jvm.current().javaHome
+} else {
+    def javaHomeOut = new StringBuilder()
+    def javaHomeErr = new StringBuilder()
+    def javaHomeProc = './tools/jdk.py'.execute()
+    javaHomeProc.waitForProcessOutput(javaHomeOut, javaHomeErr)
+    def jdkHome = new File(javaHomeOut.toString().trim())
+    if (!jdkHome.exists()) {
+        println "WARNING: Failed to find the ./tools/jdk.py specified JDK: " + jdkHome
+    } else if (jdkHome != org.gradle.internal.jvm.Jvm.current().javaHome) {
+        println("WARNING: Gradle is running in a non-pinned Java"
+                + ". Gradle Java Home: " + org.gradle.internal.jvm.Jvm.current().javaHome
+                + ". Expected: " + jdkHome)
+    }
 }
 
-tasks.withType(JavaCompile) {
-    options.compilerArgs << '-Xlint:unchecked'
+sourceSets.configureEach { sourceSet ->
+    tasks.named(sourceSet.compileJavaTaskName).configure {
+        // Default disable errorprone (enabled and setup below).
+        options.errorprone.enabled = false
+        options.compilerArgs << '-Xlint:unchecked'
+        // Javac often runs out of stack space when compiling the tests.
+        // Increase the stack size for the javac process.
+        options.forkOptions.jvmArgs << "-Xss4m"
+        // Set the bootclass path so compilation is consistent with 1.8 target compatibility.
+        options.forkOptions.jvmArgs << "-Xbootclasspath/a:third_party/openjdk/openjdk-rt-1.8/rt.jar"
+    }
 }
 
-if (!project.hasProperty('without_error_prone')) {
+if (!project.hasProperty('without_error_prone') &&
+        // Don't enable error prone on Java 8 as the plugin setup does not support it.
+        !org.gradle.internal.jvm.Jvm.current().javaVersion.java8) {
     compileJava {
         // Enable error prone for D8/R8 sources.
-        toolChain ErrorProneToolChain.create(project)
-        options.compilerArgs += errorProneConfiguration
+        options.errorprone.enabled = true
+        options.errorprone.disableAllChecks = true
+        options.errorprone.check('ClassCanBeStatic', CheckSeverity.ERROR)
+        options.errorprone.check('OperatorPrecedence', CheckSeverity.ERROR)
+        options.errorprone.check('RemoveUnusedImports', CheckSeverity.ERROR)
+        options.errorprone.check('MissingOverride', CheckSeverity.ERROR)
+        options.errorprone.check('IntLongMath', CheckSeverity.ERROR)
+        options.errorprone.check('EqualsHashCode', CheckSeverity.ERROR)
+        options.errorprone.check('InconsistentOverloads', CheckSeverity.ERROR)
+        options.errorprone.check('ArrayHashCode', CheckSeverity.ERROR)
+        options.errorprone.check('EqualsIncompatibleType', CheckSeverity.ERROR)
+        options.errorprone.check('NonOverridingEquals', CheckSeverity.ERROR)
+        options.errorprone.check('FallThrough', CheckSeverity.ERROR)
+        options.errorprone.check('MissingCasesInEnumSwitch', CheckSeverity.ERROR)
+        options.errorprone.check('MissingDefault', CheckSeverity.ERROR)
+        options.errorprone.check('MultipleTopLevelClasses', CheckSeverity.ERROR)
+        options.errorprone.check('NarrowingCompoundAssignment', CheckSeverity.ERROR)
+        options.errorprone.check('BoxedPrimitiveConstructor', CheckSeverity.ERROR)
+        options.errorprone.check('LogicalAssignment', CheckSeverity.ERROR)
+        options.errorprone.check('FloatCast', CheckSeverity.ERROR)
+        options.errorprone.check('ReturnValueIgnored', CheckSeverity.ERROR)
     }
 }
 
@@ -788,8 +834,7 @@
 task createArtTests(type: Exec) {
     def outputDir = "build/generated/test/java/com/android/tools/r8/art"
     def createArtTestsScript = "tools/create_art_tests.py"
-    inputs.file "tests/2017-10-04/art.tar.gz"
-    inputs.file createArtTestsScript
+    inputs.files files("tests/2017-10-04/art.tar.gz", createArtTestsScript)
     outputs.dir outputDir
     dependsOn downloadDeps
     commandLine "python", createArtTestsScript
@@ -864,7 +909,7 @@
         args "--dex"
         args "--output=build/test/${hostDexJar}"
         args "build/test/${hostJar}"
-        inputs.file file("build/test/${hostJar}")
+        inputs.files files("build/test/${hostJar}")
         outputs.file file("build/test/${hostDexJar}")
     }
     dependsOn dex_debuginfo_examples
@@ -1017,8 +1062,9 @@
             def proguardJarPath = "${exampleOutputDir}/${jarName}"
             def proguardMapPath = "${exampleOutputDir}/${name}/${name}.map"
             task "jar_example_${name}"(type: Exec, dependsOn: "pre_proguard_example_${name}") {
-                inputs.files tasks.getByPath("pre_proguard_example_${name}")
-                inputs.file  proguardConfigPath
+                inputs.files files(
+                        tasks.getByPath("pre_proguard_example_${name}"),
+                        proguardConfigPath)
                 // Enable these to get stdout and stderr redirected to files...
                 // standardOutput = new FileOutputStream('proguard.stdout')
                 // errorOutput = new FileOutputStream('proguard.stderr')
@@ -1338,7 +1384,7 @@
         def smaliOutputDir = file("build/test/smali/" + relativeDir);
         smaliOutputDir.mkdirs()
         outputs.dir smaliOutputDir
-        def taskName = "smali_build_${relativeDir.toString().replace('/', '_')}"
+        def taskName = "smali_build_${relativeDir.toString().replace('/', '_').replace('\\', '_')}"
         def smaliFiles = fileTree(dir: dir, include: '*.smali')
         def javaFiles = fileTree(dir: dir, include: '*.java')
         def destDir = smaliOutputDir;
@@ -1410,7 +1456,7 @@
 task buildPreNJdwpTestsDex(type: Exec, dependsOn: "buildPreNJdwpTestsJar") {
     def inFile = buildPreNJdwpTestsJar.archivePath
     def outFile = new File(buildPreNJdwpTestsJar.destinationDir, buildPreNJdwpTestsJar.baseName + '-dex.jar')
-    inputs.file inFile
+    inputs.files files(inFile)
     outputs.file outFile
     if (OperatingSystem.current().isWindows()) {
         executable file("tools/windows/dx/bin/dx.bat")
@@ -1431,7 +1477,7 @@
 task AospJarTest(type: Exec) {
     dependsOn CompatDx, downloadDeps
     def script = "tools/test_aosp_jar.py"
-    inputs.file script
+    inputs.files files(script)
     commandLine "python", script, "--no-build"
     workingDir = projectDir
 }
@@ -1447,7 +1493,7 @@
 
 task configureTestForR8Lib(type: Copy) {
     dependsOn testJar
-    inputs.file "$buildDir/libs/r8tests.jar"
+    inputs.files files("$buildDir/libs/r8tests.jar")
     if (getR8LibTask() != null) {
         dependsOn getR8LibTask()
         delete r8LibTestPath
@@ -1489,6 +1535,9 @@
 }
 
 test {
+    // TODO(b/124091860): Increase the max heap size to avoid OOM when running tests.
+    maxHeapSize = "3g"
+
     if (project.hasProperty('generate_golden_files_to')) {
         systemProperty 'generate_golden_files_to', project.property('generate_golden_files_to')
         assert project.hasProperty('HEAD_sha1')
@@ -1591,7 +1640,7 @@
                 sourceSets.test.runtimeClasspath -
                 sourceSets.main.output -
                 files(['build/classes/test'])
-        testClassesDir = new File(r8LibTestPath)
+        testClassesDirs = files(r8LibTestPath)
     }
     if (OperatingSystem.current().isLinux()
             || OperatingSystem.current().isMacOsX()
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 0f172d6..74c5cfd 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -12,3 +12,6 @@
     compile group: 'com.google.guava', name: 'guava', version: '19.0'
     compile group: 'org.smali', name: 'smali', version: '2.2b4'
 }
+
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
diff --git a/buildSrc/src/main/java/dx/DexMerger.java b/buildSrc/src/main/java/dx/DexMerger.java
index d3d8427..6303009 100644
--- a/buildSrc/src/main/java/dx/DexMerger.java
+++ b/buildSrc/src/main/java/dx/DexMerger.java
@@ -8,34 +8,38 @@
 import org.gradle.api.Action;
 import org.gradle.api.DefaultTask;
 import org.gradle.api.UncheckedIOException;
+import org.gradle.api.file.FileCollection;
 import org.gradle.api.file.FileTree;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.OutputFile;
 import org.gradle.api.tasks.TaskAction;
 import org.gradle.process.ExecSpec;
 import utils.Utils;
 
 public class DexMerger extends DefaultTask {
 
-  private FileTree source;
+  private FileCollection source;
   private File destination;
   private File dexMergerExecutable;
   private boolean debug;
 
-  public FileTree getSource() {
+  @InputFiles
+  public FileCollection getSource() {
     return source;
   }
 
-  public void setSource(FileTree source) {
+  public void setSource(FileCollection source) {
     this.source = source;
-    getInputs().file(source);
   }
 
+  @OutputFile
   public File getDestination() {
     return destination;
   }
 
   public void setDestination(File destination) {
     this.destination = destination;
-    getOutputs().file(destination);
   }
 
   public File getDexMergerExecutable() {
diff --git a/buildSrc/src/main/java/dx/Dx.java b/buildSrc/src/main/java/dx/Dx.java
index 86d1bce..b03ad0f 100644
--- a/buildSrc/src/main/java/dx/Dx.java
+++ b/buildSrc/src/main/java/dx/Dx.java
@@ -8,37 +8,38 @@
 import org.gradle.api.Action;
 import org.gradle.api.DefaultTask;
 import org.gradle.api.UncheckedIOException;
+import org.gradle.api.file.FileCollection;
 import org.gradle.api.file.FileTree;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.OutputFile;
 import org.gradle.api.tasks.TaskAction;
 import org.gradle.process.ExecSpec;
 import utils.Utils;
 
 public class Dx extends DefaultTask {
 
-  private FileTree source;
+  private FileCollection source;
   private File destination;
   private File dxExecutable;
   private boolean debug;
 
-  public FileTree getSource() {
+  @InputFiles
+  public FileCollection getSource() {
     return source;
   }
 
-  public void setSource(FileTree source) {
+  public void setSource(FileCollection source) {
     this.source = source;
-    getInputs().file(source);
   }
 
+  @OutputDirectory
   public File getDestination() {
     return destination;
   }
 
   public void setDestination(File destination) {
     this.destination = destination;
-    File classesFile = destination.toPath().resolve("classes.dex").toFile();
-    // The output from running DX is classes.dex in the destination directory.
-    // TODO(sgjesse): Handle multidex?
-    getOutputs().file(classesFile);
   }
 
   public File getDxExecutable() {
diff --git a/buildSrc/src/main/java/smali/Smali.java b/buildSrc/src/main/java/smali/Smali.java
index bd217d7..7a5241e 100644
--- a/buildSrc/src/main/java/smali/Smali.java
+++ b/buildSrc/src/main/java/smali/Smali.java
@@ -10,6 +10,9 @@
 import org.gradle.api.DefaultTask;
 import org.gradle.api.UncheckedIOException;
 import org.gradle.api.file.FileTree;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.OutputFile;
 import org.gradle.api.tasks.TaskAction;
 
 public class Smali extends DefaultTask {
@@ -18,22 +21,22 @@
   private File destination;
   private File smaliScript;
 
+  @InputFiles
   public FileTree getSource() {
     return source;
   }
 
   public void setSource(FileTree source) {
     this.source = source;
-    getInputs().file(source);
   }
 
+  @OutputFile
   public File getDestination() {
     return destination;
   }
 
   public void setDestination(File destination) {
     this.destination = destination;
-    getOutputs().file(destination);
   }
 
   public File getSmaliScript() {
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..9924d92
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,6 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+rootProject.name = 'r8'
+
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index e4a809c..a12ba39 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -778,6 +778,10 @@
     return getPlatform().startsWith("Windows");
   }
 
+  public static boolean isJava8Runtime() {
+    return System.getProperty("java.specification.version").equals("8");
+  }
+
   public static boolean isJava9Runtime() {
     return System.getProperty("java.specification.version").equals("9");
   }
@@ -804,7 +808,7 @@
   }
 
   public static Path getClassPathForTests() {
-    return Paths.get(BUILD_DIR, "classes", "test");
+    return Paths.get(BUILD_DIR, "classes", "java", "test");
   }
 
   private static List<String> getNamePartsForTestPackage(Package pkg) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
index e4ef08e..2a28a71 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
@@ -234,8 +234,8 @@
         .addOptionsModification(this::configure)
         .run(MAIN);
     if (enableMinification) {
-      if (backend == Backend.CF) {
-        // TODO(b/120639028): Incorrect inner-class structure fails on JVM.
+      if (backend == Backend.CF && ToolHelper.isJava8Runtime()) {
+        // TODO(b/120639028): Incorrect inner-class structure fails on JVM prior to JDK 9.
         result.assertFailureWithErrorThatMatches(containsString("Malformed class name"));
         return;
       } else {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/InnerClassNameTestRunner.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/InnerClassNameTestRunner.java
index 0a71fe6..167d953 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/InnerClassNameTestRunner.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/InnerClassNameTestRunner.java
@@ -171,7 +171,7 @@
         if (backend == Backend.CF && minify) {
           // TODO(b/120639028) R8 does not keep the structure of inner classes.
           r8RunResult.assertFailureWithErrorThatMatches(containsString("Malformed class name"));
-        } else if (backend == Backend.CF) {
+        } else if (backend == Backend.CF && ToolHelper.isJava8Runtime()) {
           // $$ as separator and InnerClass as name, results in $InnerClass from getSimpleName...
           String expectedWithDollarOnInnerName =
               getExpectedNonMinified("$" + config.getInnerClassName());
@@ -182,20 +182,21 @@
           r8RunResult.assertSuccessWithOutput(
               minify ? getExpectedMinified(inspector) : expectedWithDollarOnInnerName);
         } else {
-          // $$ in DEX will not change the InnerName/getSimpleName.
+          // $$ in DEX or JDK 9+ will not change the InnerName/getSimpleName.
           r8RunResult.assertSuccessWithOutput(getExpectedMinified(inspector));
         }
         break;
       case EMTPY_SEPARATOR:
       case UNDERBAR_SEPARATOR:
       case NON_NESTED_INNER:
-        if (backend == Backend.CF) {
+        if (backend == Backend.CF && ToolHelper.isJava8Runtime()) {
           // NOTE(b/120597515): These cases should fail, but if they succeed, we have recovered via
           // minification, likely by not using the same separator from output in input.
           // Any non-$ separator results in a runtime exception in getCanonicalName.
+          // NOTE: Behavior changed in JDK 9 so the class is no longer considered malformed.
           r8RunResult.assertFailureWithErrorThatMatches(containsString("Malformed class name"));
         } else {
-          assert backend == Backend.DEX;
+          assert backend == Backend.DEX || !ToolHelper.isJava8Runtime();
           r8RunResult.assertSuccessWithOutput(getExpectedMinified(inspector));
         }
         break;
diff --git a/third_party/gradle/gradle.tar.gz.sha1 b/third_party/gradle/gradle.tar.gz.sha1
index c9f900a..a219d61 100644
--- a/third_party/gradle/gradle.tar.gz.sha1
+++ b/third_party/gradle/gradle.tar.gz.sha1
@@ -1 +1 @@
-2d9bb7b50771ad8252cdcf83a1a9648af7eee837
\ No newline at end of file
+4a07bcbff312d890bc1b96c39aaffdcab882cd3a
\ No newline at end of file
diff --git a/third_party/openjdk/jdk9.tar.gz.sha1 b/third_party/openjdk/jdk9.tar.gz.sha1
new file mode 100644
index 0000000..fe944e4
--- /dev/null
+++ b/third_party/openjdk/jdk9.tar.gz.sha1
@@ -0,0 +1 @@
+b7fd72327317977e4221244115bdbbac4edd3444
\ No newline at end of file
diff --git a/third_party/openjdk/openjdk-9.0.4/linux.tar.gz.sha1 b/third_party/openjdk/openjdk-9.0.4/linux.tar.gz.sha1
new file mode 100644
index 0000000..4670484
--- /dev/null
+++ b/third_party/openjdk/openjdk-9.0.4/linux.tar.gz.sha1
@@ -0,0 +1 @@
+ed9e71246f8bba2d7ec0c4d56813c241d8315960
\ No newline at end of file
diff --git a/third_party/openjdk/openjdk-9.0.4/osx.tar.gz.sha1 b/third_party/openjdk/openjdk-9.0.4/osx.tar.gz.sha1
new file mode 100644
index 0000000..7db6817
--- /dev/null
+++ b/third_party/openjdk/openjdk-9.0.4/osx.tar.gz.sha1
@@ -0,0 +1 @@
+a68b718365f7ce214eec2f6bb89311885940b10e
\ No newline at end of file
diff --git a/third_party/openjdk/openjdk-9.0.4/windows.tar.gz.sha1 b/third_party/openjdk/openjdk-9.0.4/windows.tar.gz.sha1
new file mode 100644
index 0000000..dedab10
--- /dev/null
+++ b/third_party/openjdk/openjdk-9.0.4/windows.tar.gz.sha1
@@ -0,0 +1 @@
+d9d352dfa1484bc1b7eaff0e013f720a120ff963
\ No newline at end of file
diff --git a/tools/gradle.py b/tools/gradle.py
index ff90779..374f885 100755
--- a/tools/gradle.py
+++ b/tools/gradle.py
@@ -7,9 +7,12 @@
 # Will make sure we pulled down gradle before running, and will use the pulled
 # down version to have a consistent developer experience.
 
+import optparse
 import os
 import subprocess
 import sys
+
+import jdk
 import utils
 
 GRADLE_DIR = os.path.join(utils.REPO_ROOT, 'third_party', 'gradle')
@@ -21,6 +24,15 @@
 else:
   GRADLE = os.path.join(GRADLE_DIR, 'gradle', 'bin', 'gradle')
 
+def ParseOptions():
+  result = optparse.OptionParser()
+  result.add_option('--java-home', '--java_home',
+      help='Use a custom java version to run gradle.')
+  return result.parse_args()
+
+def GetJavaEnv(env):
+  return dict(env if env else os.environ, JAVA_HOME = jdk.GetJdkHome())
+
 def PrintCmd(s):
   if type(s) is list:
     s = ' '.join(s)
@@ -29,17 +41,12 @@
   sys.stdout.flush()
 
 def EnsureGradle():
-  if not os.path.exists(GRADLE) or os.path.getmtime(GRADLE_TGZ) < os.path.getmtime(GRADLE_SHA1):
-    # Bootstrap or update gradle, everything else is controlled using gradle.
-    utils.DownloadFromGoogleCloudStorage(GRADLE_SHA1)
-    # Update the mtime of the tar file to make sure we do not run again unless
-    # there is an update.
-    os.utime(GRADLE_TGZ, None)
-  else:
-    print 'gradle.py: Gradle binary present'
+  utils.EnsureDepFromGoogleCloudStorage(
+    GRADLE, GRADLE_TGZ, GRADLE_SHA1, 'Gradle binary')
 
 def EnsureDeps():
   EnsureGradle()
+  jdk.EnsureJdk()
 
 def RunGradleIn(gradleCmd, args, cwd, throw_on_failure=True, env=None):
   EnsureDeps()
@@ -47,7 +54,7 @@
   cmd.extend(args)
   utils.PrintCmd(cmd)
   with utils.ChangedWorkingDirectory(cwd):
-    return_value = subprocess.call(cmd, env=env)
+    return_value = subprocess.call(cmd, env=GetJavaEnv(env))
     if throw_on_failure and return_value != 0:
       raise Exception('Failed to execute gradle')
     return return_value
@@ -69,7 +76,7 @@
   cmd.extend(args)
   utils.PrintCmd(cmd)
   with utils.ChangedWorkingDirectory(cwd):
-    return subprocess.check_output(cmd, env=env)
+    return subprocess.check_output(cmd, env=GetJavaEnv(env))
 
 def RunGradleWrapperInGetOutput(args, cwd, env=None):
   return RunGradleInGetOutput('./gradlew', args, cwd, env=env)
@@ -78,7 +85,11 @@
   return RunGradleInGetOutput(GRADLE, args, utils.REPO_ROOT, env=env)
 
 def Main():
-  RunGradle(sys.argv[1:])
+  (options, args) = ParseOptions()
+  gradle_args = sys.argv[1:]
+  if options.java_home:
+    gradle_args.append('-Dorg.gradle.java.home=' + options.java_home)
+  return RunGradle(gradle_args)
 
 if __name__ == '__main__':
   sys.exit(Main())
diff --git a/tools/java.py b/tools/java.py
new file mode 100755
index 0000000..3abfcdd
--- /dev/null
+++ b/tools/java.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import jdk
+import utils
+import subprocess
+import sys
+
+def run(args):
+  cmd = [jdk.GetJavaExecutable()] + args
+  utils.PrintCmd(cmd)
+  result = subprocess.check_output(cmd)
+  print result
+  return result
+
+def main():
+  try:
+    run(sys.argv[1:])
+  except subprocess.CalledProcessError as e:
+    # In case anything relevant was printed to stdout, normally this is already
+    # on stderr.
+    print e.output
+    return e.returncode
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/tools/javac.py b/tools/javac.py
new file mode 100755
index 0000000..0d8ac05
--- /dev/null
+++ b/tools/javac.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import jdk
+import utils
+import subprocess
+import sys
+
+def run(args):
+  cmd = [jdk.GetJavacExecutable()] + args
+  utils.PrintCmd(cmd)
+  result = subprocess.check_output(cmd)
+  print result
+  return result
+
+def main():
+  try:
+    run(sys.argv[1:])
+  except subprocess.CalledProcessError as e:
+    # In case anything relevant was printed to stdout, normally this is already
+    # on stderr.
+    print e.output
+    return e.returncode
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/tools/jdk.py b/tools/jdk.py
new file mode 100755
index 0000000..5a20901
--- /dev/null
+++ b/tools/jdk.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+# for details. All rights reserved. Use of this source code is governed by a
+# BSD-style license that can be found in the LICENSE file.
+
+import os
+import sys
+import utils
+
+JDK_DIR = os.path.join(utils.REPO_ROOT, 'third_party', 'openjdk')
+
+def GetJdkHome():
+  root = os.path.join(JDK_DIR, 'openjdk-9.0.4')
+  if utils.IsLinux():
+    return os.path.join(root, 'linux')
+  elif utils.IsOsX():
+    return os.path.join(root, 'osx')
+  elif utils.IsWindows():
+    return os.path.join(root, 'windows')
+  else:
+    return os.environ['JAVA_HOME']
+  return jdkHome
+
+def GetJavaExecutable(jdkHome=None):
+  jdkHome = jdkHome if jdkHome else GetJdkHome()
+  executable = 'java.exe' if utils.IsWindows() else 'java'
+  return os.path.join(jdkHome, 'bin', executable) if jdkHome else executable
+
+def GetJavacExecutable(jdkHome=None):
+  jdkHome = jdkHome if jdkHome else GetJdkHome()
+  executable = 'javac.exe' if utils.IsWindows() else 'javac'
+  return os.path.join(jdkHome, 'bin', executable) if jdkHome else executable
+
+def EnsureJdk():
+  jdkHome = GetJdkHome()
+  jdkTgz = jdkHome + '.tar.gz'
+  jdkSha1 = jdkTgz + '.sha1'
+  utils.EnsureDepFromGoogleCloudStorage(jdkHome, jdkTgz, jdkSha1, 'JDK')
+
+def Main():
+  print GetJdkHome()
+
+if __name__ == '__main__':
+  sys.exit(Main())
diff --git a/tools/test.py b/tools/test.py
index bb6a98a..d296496 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -7,16 +7,17 @@
 # if an argument is given, run only tests with that pattern. This script will
 # force the tests to run, even if no input changed.
 
-import os
-import gradle
 import optparse
+import os
 import subprocess
 import sys
 import thread
 import time
-import utils
 import uuid
+
+import gradle
 import notify
+import utils
 
 ALL_ART_VMS = [
     "default",
@@ -96,6 +97,8 @@
           ' Note that the directory will not be cleared before the test.')
   result.add_option('--java-home', '--java_home',
       help='Use a custom java version to run tests.')
+  result.add_option('--java-max-memory-size', '--java_max_memory_size',
+      help='Use a custom max memory size for the gradle java instance, eg, 4g')
   result.add_option('--generate-golden-files-to', '--generate_golden_files_to',
       help='Store dex files produced by tests in the specified directory.'
            ' It is aimed to be read on platforms with no host runtime available'
@@ -166,6 +169,8 @@
       os.makedirs(options.test_dir)
   if options.java_home:
     gradle_args.append('-Dorg.gradle.java.home=' + options.java_home)
+  if options.java_max_memory_size:
+    gradle_args.append('-Dorg.gradle.jvmargs=-Xmx' + options.java_max_memory_size)
   if options.generate_golden_files_to:
     gradle_args.append('-Pgenerate_golden_files_to=' + options.generate_golden_files_to)
     if not os.path.exists(options.generate_golden_files_to):
diff --git a/tools/utils.py b/tools/utils.py
index 08faec6..20d7bc7 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -156,7 +156,22 @@
       return stdout
 
 def IsWindows():
-  return os.name == 'nt'
+  return sys.platform.startswith('win')
+
+def IsLinux():
+  return sys.platform.startswith('linux')
+
+def IsOsX():
+  return sys.platform.startswith('darwin')
+
+def EnsureDepFromGoogleCloudStorage(dep, tgz, sha1, msg):
+  if not os.path.exists(dep) or os.path.getmtime(tgz) < os.path.getmtime(sha1):
+    DownloadFromGoogleCloudStorage(sha1)
+    # Update the mtime of the tar file to make sure we do not run again unless
+    # there is an update.
+    os.utime(tgz, None)
+  else:
+    print 'Ensure cloud dependency:', msg, 'present'
 
 def DownloadFromX20(sha1_file):
   download_script = os.path.join(REPO_ROOT, 'tools', 'download_from_x20.py')