Start adding third party dependencies to new gradle setup

Bug: b/270105162
Change-Id: I5b1ed67cdab4905f462d76c37fa88bab61b28f8c
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
index a1796c3..4574e59 100644
--- a/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/DependenciesPlugin.kt
@@ -2,33 +2,65 @@
 // 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 java.io.File
+import java.net.URI
+import java.nio.file.Paths
+import org.gradle.api.JavaVersion
 import org.gradle.api.Plugin
 import org.gradle.api.Project
-import org.gradle.api.JavaVersion
 import org.gradle.api.Task
-import org.gradle.api.tasks.Exec
-import java.io.File
-import java.nio.file.Path
-import java.nio.file.Paths
-import java.net.URI
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.kotlin.dsl.register
+import org.gradle.nativeplatform.platform.OperatingSystem
+import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
 
 class DependenciesPlugin: Plugin<Project> {
 
   override fun apply(target: Project) {
     val dependenciesPath = "file:" +
-      "${target.getRoot().resolve("third_party").resolve("dependencies").getAbsolutePath()}"
+      target.getRoot().resolve("third_party").resolve("dependencies").getAbsolutePath()
     val dependenciesNewPath = "file:" +
-      "${target.getRoot().resolve("third_party").resolve("dependencies_new").getAbsolutePath()}"
+      target.getRoot().resolve("third_party").resolve("dependencies_new").getAbsolutePath()
     val repositories = target.getRepositories()
     repositories.maven { name = "LOCAL_MAVEN_REPO";  url = URI(dependenciesPath) }
     repositories.maven { name = "LOCAL_MAVEN_REPO_NEW";  url = URI(dependenciesNewPath) }
   }
 }
 
-enum class Jdk(val folder : String) {
-  JDK_11("jdk-11"),
-  JDK_17("jdk-17"),
-  JDK_20("jdk-20");
+enum class Jdk(val name : String, val folder : String) {
+  JDK_8("jdk8", "jdk8"),
+  JDK_9("jdk9", "openjdk-9.0.4"),
+  JDK_11("jdk11", "jdk-11"),
+  JDK_17("jdk17", "jdk-17"),
+  JDK_20("jdk20", "jdk-20");
+
+  fun isJdk8() : Boolean {
+    return this == JDK_8
+  }
+
+  fun getThirdPartyDependency() : ThirdPartyDependency {
+    val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
+    val subFolder : String
+    val fileName : String
+    if (os.isLinux) {
+      subFolder = if(isJdk8()) "linux-x86" else "linux"
+      fileName = "java"
+    } else if (os.isMacOsX) {
+      subFolder = if(isJdk8()) "darwin-x86" else "osx"
+      fileName = "java"
+    } else {
+      assert(os.isWindows())
+      if (isJdk8()) {
+        throw RuntimeException("No Jdk8 on Windows")
+      }
+      subFolder = "windows"
+      fileName = "java.bat"
+    }
+    return ThirdPartyDependency(
+      name,
+      Paths.get("third_party", "openjdk", folder, subFolder, "bin", fileName).toFile(),
+      Paths.get("third_party", "openjdk", folder, "$subFolder.tar.gz.sha1").toFile())
+  }
 }
 
 fun Project.getRoot() : File {
@@ -43,6 +75,27 @@
   return "****** ${title} ******"
 }
 
+fun Project.ensureThirdPartyDependencies(name : String, deps : List<ThirdPartyDependency>) : Task {
+  val outputFiles : MutableList<File> = mutableListOf()
+  val depsTasks = deps.map({
+      tasks.register<DownloadDependencyTask>("download-third-party-${it.packageName}") {
+        setDependency(
+          it.packageName,
+          getRoot().resolve(it.sha1File),
+          getRoot().resolve(it.path).parentFile,
+          it.type)
+        outputFiles.add(it.path)
+      }})
+  return tasks.register("ensure-third-party-$name") {
+    dependsOn(depsTasks)
+    outputs.files(outputFiles)
+  }.get()
+}
+
+fun Project.resolve(thirdPartyDependency: ThirdPartyDependency) : ConfigurableFileCollection {
+  return files(project.getRoot().resolve(thirdPartyDependency.path))
+}
+
 /**
  * When using composite builds, referecing tasks in other projects do not give a Task but a
  * TaskReference. To get outputs from other tasks we need to have a proper task and gradle do not
@@ -83,11 +136,12 @@
 }
 
 fun Project.baseCompilerCommandLine(
-  jar : File, deps : File, compiler : String, args : List<String> = listOf()) : List<String> {
+  jar: File, deps: File, compiler: String, args: List<String> = listOf(),
+) : List<String> {
   // Execute r8 commands against a stable r8 with dependencies.
   // TODO(b/139725780): See if we can remove or lower the heap size (-Xmx8g).
   return listOf(
-    "${getJavaPath(Jdk.JDK_17)}",
+    getJavaPath(Jdk.JDK_17),
     "-Xmx8g",
     "-ea",
     "-cp",
@@ -97,11 +151,12 @@
 }
 
 fun Project.baseCompilerCommandLine(
-  jar : File, compiler : String, args : List<String> = listOf()) : List<String> {
+  jar: File, compiler: String, args: List<String> = listOf(),
+) : List<String> {
   // Execute r8 commands against a stable r8 with dependencies.
   // TODO(b/139725780): See if we can remove or lower the heap size (-Xmx8g).
   return listOf(
-    "${getJavaPath(Jdk.JDK_17)}",
+    getJavaPath(Jdk.JDK_17),
     "-Xmx8g",
     "-ea",
     "-cp",
@@ -111,14 +166,14 @@
 }
 
 fun Project.createR8LibCommandLine(
-  r8Compiler : File,
-  input : File,
+  r8Compiler: File,
+  input: File,
   output: File,
-  pgConf : List<File>,
-  excludingDepsVariant : Boolean,
-  lib : List<File> = listOf(),
-  classpath : List<File> = listOf(),
-  args : List<String> = listOf()) : List<String> {
+  pgConf: List<File>,
+  excludingDepsVariant: Boolean,
+  lib: List<File> = listOf(),
+  classpath: List<File> = listOf()
+) : List<String> {
   val pgList = pgConf.flatMap({ listOf("--pg-conf", "$it") })
   val libList = lib.flatMap({ listOf("--lib", "$it") })
   val cpList = classpath.flatMap({ listOf("--classpath", "$it") })
@@ -142,15 +197,15 @@
 
 object Versions {
   const val asmVersion = "9.5"
+  const val errorproneVersion = "2.18.0"
   const val fastUtilVersion = "7.2.1"
   const val gsonVersion = "2.7"
   const val guavaVersion = "31.1-jre"
+  const val javassist = "3.29.2-GA"
   const val junitVersion = "4.13-beta-2"
   const val kotlinVersion = "1.8.10"
   const val kotlinMetadataVersion = "0.6.2"
   const val smaliVersion = "3.0.3"
-  const val errorproneVersion = "2.18.0"
-  const val javassist = "3.29.2-GA"
 }
 
 object Deps {
@@ -169,3 +224,104 @@
   val smali by lazy { "com.android.tools.smali:smali:${Versions.smaliVersion}" }
   val errorprone by lazy { "com.google.errorprone:error_prone_core:${Versions.errorproneVersion}" }
 }
+
+object ThirdPartyDeps {
+  val apiDatabase = ThirdPartyDependency(
+    "apiDatabase",
+    Paths.get(
+      "third_party",
+      "api_database",
+      "api_database",
+      "resources",
+      "new_api_database.ser").toFile(),
+    Paths.get("third_party", "api_database", "api_database.tar.gz.sha1").toFile())
+  val ddmLib = ThirdPartyDependency(
+    "ddmlib",
+    Paths.get("third_party", "ddmlib", "ddmlib.jar").toFile(),
+    Paths.get("third_party", "ddmlib.tar.gz.sha1").toFile())
+  val jasmin = ThirdPartyDependency(
+    "jasmin",
+    Paths.get("third_party", "jasmin", "jasmin-2.4.jar").toFile(),
+    Paths.get("third_party", "jasmin.tar.gz.sha1").toFile())
+  val jdwpTests = ThirdPartyDependency(
+    "jdwp-tests",
+    Paths.get("third_party", "jdwp-tests", "apache-harmony-jdwp-tests-host.jar").toFile(),
+    Paths.get("third_party", "jdwp-tests.tar.gz.sha1").toFile())
+  val androidJars : List<ThirdPartyDependency> = getThirdPartyAndroidJars()
+  val java8Runtime = ThirdPartyDependency(
+    "openjdk-rt-1.8",
+    Paths.get("third_party", "openjdk", "openjdk-rt-1.8", "rt.jar").toFile(),
+    Paths.get("third_party", "openjdk", "openjdk-rt-1.8.tar.gz.sha1").toFile()
+  )
+  val androidVMs : List<ThirdPartyDependency> = getThirdPartyAndroidVms()
+  val jdks : List<ThirdPartyDependency> = getJdks()
+}
+
+fun getThirdPartyAndroidJars() : List<ThirdPartyDependency> {
+  return listOf(
+    "libcore_latest",
+    "lib-master",
+    "lib-v14",
+    "lib-v15",
+    "lib-v19",
+    "lib-v21",
+    "lib-v22",
+    "lib-v23",
+    "lib-v24",
+    "lib-v25",
+    "lib-v26",
+    "lib-v27",
+    "lib-v28",
+    "lib-v29",
+    "lib-v30",
+    "lib-v31",
+    "lib-v32",
+    "lib-v33",
+    "lib-v34"
+  ).map(::getThirdPartyAndroidJar)
+}
+
+fun getThirdPartyAndroidJar(version : String) : ThirdPartyDependency {
+  return ThirdPartyDependency(
+    version,
+    Paths.get("third_party", "android_jar", version, "android.jar").toFile(),
+    Paths.get("third_party", "android_jar", "$version.tar.gz.sha1").toFile())
+}
+
+fun getThirdPartyAndroidVms() : List<ThirdPartyDependency> {
+  return listOf(
+    listOf("host", "art-master"),
+    listOf("host", "art-14.0.0-dp1"),
+    listOf("host", "art-13.0.0"),
+    listOf("host", "art-12.0.0-beta4"),
+    listOf("art-10.0.0"),
+    listOf("art-5.1.1"),
+    listOf("art-6.0.1"),
+    listOf("art-7.0.0"),
+    listOf("art-8.1.0"),
+    listOf("art-9.0.0"),
+    listOf("art"),
+    listOf("dalvik-4.0.4"),
+    listOf("dalvik")).map(::getThirdPartyAndroidVm)
+}
+
+fun getThirdPartyAndroidVm(version : List<String>) : ThirdPartyDependency {
+  val output = Paths.get("tools", "linux", *version.toTypedArray(), "bin", "art").toFile()
+  return ThirdPartyDependency(
+    version.last(),
+    output,
+    Paths.get(
+      "tools",
+      "linux",
+      *version.slice(0..version.size - 2).toTypedArray(),
+      "${version.last()}.tar.gz.sha1").toFile())
+}
+
+fun getJdks() : List<ThirdPartyDependency> {
+  val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
+  if (os.isLinux || os.isMacOsX) {
+    return Jdk.values().map{ it.getThirdPartyDependency()}
+  } else {
+    return Jdk.values().filter{ !it.isJdk8() }.map{ it.getThirdPartyDependency()}
+  }
+}
\ No newline at end of file
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/DownloadDependencyTask.kt b/d8_r8/commonBuildSrc/src/main/kotlin/DownloadDependencyTask.kt
new file mode 100644
index 0000000..24185c6
--- /dev/null
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/DownloadDependencyTask.kt
@@ -0,0 +1,143 @@
+// Copyright (c) 2023, 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 java.io.BufferedReader
+import java.io.File
+import java.io.IOException
+import java.io.InputStreamReader
+import java.nio.charset.StandardCharsets
+import java.util.Arrays
+import java.util.stream.Collectors
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.options.Option
+import org.gradle.internal.os.OperatingSystem
+import org.gradle.workers.WorkAction
+import org.gradle.workers.WorkParameters
+import org.gradle.workers.WorkerExecutor
+
+abstract class DownloadDependencyTask : DefaultTask() {
+
+  private var dependencyType: DependencyType = DependencyType.GOOGLE_STORAGE
+  private var _outputDir: File? = null
+  private var _tarGzFile: File? = null
+  private var _sha1File: File? = null
+
+  @OutputDirectory
+  fun getOutputDir(): File? {
+    return _outputDir
+  }
+
+  @Inject
+  protected abstract fun getWorkerExecutor(): WorkerExecutor?
+
+  @Option(
+    option = "dependency",
+    description = "Sets the dependency information for a cloud stored file")
+  fun setDependency(
+    dependencyName : String, sha1File: File, outputDir : File, dependencyType: DependencyType) {
+    _outputDir = outputDir
+    _sha1File = sha1File
+    _tarGzFile = sha1File.resolveSibling(sha1File.name.replace(".sha1", ""))
+    this.dependencyType = dependencyType
+  }
+
+  @TaskAction
+  fun execute() {
+    val sha1File = _sha1File!!
+    val outputDir = _outputDir!!
+    val tarGzFile = _tarGzFile!!
+    if (!sha1File.exists()) {
+      throw RuntimeException("Missing sha1 file: $sha1File")
+    }
+    // First run will write the tar.gz file, causing the second run to still be out-of-date.
+    // Check if the modification time of the tar is newer than the sha in which case we are done.
+    // Also, check the contents of the out directory because gradle appears to create it for us...
+    if (outputDir.exists()
+      && outputDir.isDirectory
+      && outputDir.list().isNotEmpty() && tarGzFile.exists()
+      && sha1File.lastModified() <= tarGzFile.lastModified()) {
+      return
+    }
+    if (outputDir.exists() && outputDir.isDirectory) {
+      outputDir.delete()
+    }
+    getWorkerExecutor()!!
+      .noIsolation()
+      .submit(RunDownload::class.java) {
+        this.type.set(dependencyType)
+        this.sha1File.set(sha1File)
+      }
+  }
+
+
+  interface RunDownloadParameters : WorkParameters {
+    val type : Property<DependencyType>
+    val sha1File : RegularFileProperty
+  }
+
+  abstract class RunDownload : WorkAction<RunDownloadParameters> {
+    override fun execute() {
+      try {
+        val parameters: RunDownloadParameters = parameters
+        val type: DependencyType = parameters.type.get()
+        val sha1File: File = parameters.sha1File.asFile.get()
+        if (type == DependencyType.GOOGLE_STORAGE) {
+          downloadFromGoogleStorage(sha1File)
+        } else if (type == DependencyType.X20) {
+          downloadFromX20(sha1File)
+        } else {
+          throw RuntimeException("Unexpected or missing dependency type: $type")
+        }
+      } catch (e: Exception) {
+        throw RuntimeException(e)
+      }
+    }
+
+    @Throws(IOException::class, InterruptedException::class)
+    private fun downloadFromGoogleStorage(sha1File: File) {
+      val args = Arrays.asList("-n", "-b", "r8-deps", "-s", "-u", sha1File.toString())
+      if (OperatingSystem.current().isWindows) {
+        val command: MutableList<String> = ArrayList()
+        command.add("download_from_google_storage.bat")
+        command.addAll(args)
+        runProcess(ProcessBuilder().command(command))
+      } else {
+        runProcess(
+          ProcessBuilder()
+            .command("bash",
+                     "-c",
+                     "download_from_google_storage " + java.lang.String.join(" ", args)))
+      }
+    }
+
+    @Throws(IOException::class, InterruptedException::class)
+    private fun downloadFromX20(sha1File: File) {
+      if (OperatingSystem.current().isWindows) {
+        throw RuntimeException("Downloading from x20 unsupported on windows")
+      }
+      runProcess(
+        ProcessBuilder()
+          .command("bash", "-c", "tools/download_from_x20.py $sha1File"))
+    }
+
+    @Throws(IOException::class, InterruptedException::class)
+    private fun runProcess(builder: ProcessBuilder) {
+      val command = java.lang.String.join(" ", builder.command())
+      val p = builder.start()
+      val exit = p.waitFor()
+      if (exit != 0) {
+        throw IOException("Process failed for $command\n"
+            + BufferedReader(
+            InputStreamReader(p.errorStream, StandardCharsets.UTF_8))
+            .lines()
+            .collect(Collectors.joining("\n")))
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/d8_r8/commonBuildSrc/src/main/kotlin/ThirdPartyDependency.kt b/d8_r8/commonBuildSrc/src/main/kotlin/ThirdPartyDependency.kt
new file mode 100644
index 0000000..510892d
--- /dev/null
+++ b/d8_r8/commonBuildSrc/src/main/kotlin/ThirdPartyDependency.kt
@@ -0,0 +1,16 @@
+// Copyright (c) 2023, 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 java.io.File
+
+enum class DependencyType {
+  GOOGLE_STORAGE,
+  X20
+}
+
+data class ThirdPartyDependency(
+  val packageName : String,
+  val path : File,
+  val sha1File : File,
+  val type: DependencyType = DependencyType.GOOGLE_STORAGE)
diff --git a/d8_r8/main/build.gradle.kts b/d8_r8/main/build.gradle.kts
index 34a1c14..681c7c3 100644
--- a/d8_r8/main/build.gradle.kts
+++ b/d8_r8/main/build.gradle.kts
@@ -23,8 +23,8 @@
 dependencies {
   implementation(":keepanno")
   compileOnly(Deps.asm)
-  compileOnly(Deps.asmUtil)
   compileOnly(Deps.asmCommons)
+  compileOnly(Deps.asmUtil)
   compileOnly(Deps.fastUtil)
   compileOnly(Deps.gson)
   compileOnly(Deps.guava)
@@ -32,6 +32,10 @@
   errorprone(Deps.errorprone)
 }
 
+val thirdPartyResourceDependenciesTask = ensureThirdPartyDependencies(
+  "resourceDeps",
+  listOf(ThirdPartyDeps.apiDatabase))
+
 val keepAnnoJarTask = projectTask("keepanno", "jar")
 
 fun mainJarDependencies() : FileCollection {
@@ -52,6 +56,10 @@
     }
   }
 
+  withType<ProcessResources> {
+    dependsOn(thirdPartyResourceDependenciesTask)
+  }
+
   val swissArmyKnife by registering(Jar::class) {
     from(sourceSets.main.get().output)
     manifest {
diff --git a/d8_r8/settings.gradle.kts b/d8_r8/settings.gradle.kts
index 70bb38b..402137e 100644
--- a/d8_r8/settings.gradle.kts
+++ b/d8_r8/settings.gradle.kts
@@ -29,7 +29,7 @@
   )
   println("Executing command: ${cmd.joinToString(" ")}")
   var process = ProcessBuilder().command(cmd).start()
-  process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS)
+  process.waitFor()
   if (process.exitValue() != 0) {
     throw GradleException(
       "Bootstrapping dependencies_new download failed:\n"
diff --git a/d8_r8/test_modules/tests_java_8/build.gradle.kts b/d8_r8/test_modules/tests_java_8/build.gradle.kts
index 55ee7a8..28f5076 100644
--- a/d8_r8/test_modules/tests_java_8/build.gradle.kts
+++ b/d8_r8/test_modules/tests_java_8/build.gradle.kts
@@ -29,6 +29,8 @@
   implementation(projectTask("keepanno", "jar").outputs.files)
   implementation(projectTask("main", "jar").outputs.files)
   implementation(Deps.asm)
+  implementation(Deps.asmCommons)
+  implementation(Deps.asmUtil)
   implementation(Deps.gson)
   implementation(Deps.guava)
   implementation(Deps.javassist)
@@ -36,16 +38,28 @@
   implementation(Deps.kotlinStdLib)
   implementation(Deps.kotlinReflect)
   implementation(Deps.kotlinMetadata)
-  implementation(files(root.resolveAll("third_party", "ddmlib", "ddmlib.jar")))
-  implementation(
-    files(
-      root.resolveAll("third_party", "jdwp-tests", "apache-harmony-jdwp-tests-host.jar")))
-  implementation(files(root.resolveAll("third_party", "jasmin", "jasmin-2.4.jar")))
+  implementation(resolve(ThirdPartyDeps.ddmLib))
+  implementation(resolve(ThirdPartyDeps.jasmin))
+  implementation(resolve(ThirdPartyDeps.jdwpTests))
   implementation(Deps.fastUtil)
   implementation(Deps.smali)
-  implementation(Deps.asmUtil)
 }
 
+val thirdPartyCompileDependenciesTask = ensureThirdPartyDependencies(
+  "compileDeps",
+  listOf(
+    ThirdPartyDeps.apiDatabase,
+    ThirdPartyDeps.ddmLib,
+    ThirdPartyDeps.jasmin,
+    ThirdPartyDeps.jdwpTests))
+
+val thirdPartyRuntimeDependenciesTask = ensureThirdPartyDependencies(
+  "runtimeDeps",
+  listOf(ThirdPartyDeps.java8Runtime)
+    + ThirdPartyDeps.androidJars
+    + ThirdPartyDeps.androidVMs
+    + ThirdPartyDeps.jdks)
+
 fun testDependencies() : FileCollection {
   return sourceSets
     .test
@@ -62,6 +76,7 @@
   withType<JavaCompile> {
     dependsOn(gradle.includedBuild("keepanno").task(":jar"))
     dependsOn(gradle.includedBuild("main").task(":jar"))
+    dependsOn(thirdPartyCompileDependenciesTask)
     options.setFork(true)
     options.forkOptions.memoryMaximumSize = "3g"
     options.forkOptions.jvmArgs = listOf(
@@ -73,6 +88,7 @@
   withType<KotlinCompile> {
     dependsOn(gradle.includedBuild("keepanno").task(":jar"))
     dependsOn(gradle.includedBuild("main").task(":jar"))
+    dependsOn(thirdPartyCompileDependenciesTask)
     kotlinOptions {
       // We cannot use languageVersion.set(JavaLanguageVersion.of(8)) because gradle cannot figure
       // out that the jdk is 1_8 and will try to download it.
@@ -80,12 +96,32 @@
     }
   }
 
+  withType<Test> {
+    dependsOn(thirdPartyRuntimeDependenciesTask)
+    println("NOTE: Number of processors " + Runtime.getRuntime().availableProcessors())
+    val userDefinedCoresPerFork = System.getenv("R8_GRADLE_CORES_PER_FORK")
+    val processors = Runtime.getRuntime().availableProcessors()
+    // See https://docs.gradle.org/current/dsl/org.gradle.api.tasks.testing.Test.html.
+    if (!userDefinedCoresPerFork.isNullOrEmpty()) {
+      maxParallelForks = userDefinedCoresPerFork.toInt()
+    } else {
+      // On normal work machines this seems to give the best test execution time (without freezing)
+      maxParallelForks = processors / 3
+      // On low cpu count machines (bots) we under subscribe, so increase the count.
+      if (processors == 8) {
+        maxParallelForks = 3
+      }
+    }
+    println("NOTE: Max parallel forks " + maxParallelForks)
+  }
+
   val testJar by registering(Jar::class) {
     from(sourceSets.test.get().output)
   }
 
   val depsJar by registering(Jar::class) {
     dependsOn(gradle.includedBuild("keepanno").task(":jar"))
+    dependsOn(thirdPartyCompileDependenciesTask)
     println(header("Test Java 8 dependencies"))
     testDependencies().forEach({ println(it) })
     from(testDependencies().map(::zipTree))
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 069acbf..30ea8e0 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -101,11 +101,14 @@
   }
 
   public static String getProjectRoot() {
-    String userDirProperty = System.getProperty("user.dir");
-    if (userDirProperty.endsWith("d8_r8/test")) {
-      return Paths.get(userDirProperty).getParent().getParent().toString() + "/";
+    String current = System.getProperty("user.dir");
+    if (!current.contains("test_modules")) {
+      return "";
     }
-    return "";
+    while (current.contains("test_modules")) {
+      current = Paths.get(current).getParent().toString();
+    }
+    return Paths.get(current).getParent().toString() + "/";
   }
 
   public static final String SOURCE_DIR = "src/main/java/";
@@ -972,7 +975,7 @@
     }
     if (isLinux() || isMac()) {
       // The Linux version is used on Mac, where it is run in a Docker container.
-      return TOOLS_DIR + "/linux/" + dir;
+      return TOOLS_DIR + "linux/" + dir;
     }
     fail("Unsupported platform, we currently only support mac and linux: " + getPlatform());
     return ""; //never here