blob: 370bcade8b572945ed43c390367b824b4e3c5d01 [file] [log] [blame]
// 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 DependenciesPlugin.Companion.computeRoot
import java.io.BufferedReader
import java.io.File
import java.io.IOException
import java.io.InputStreamReader
import java.io.RandomAccessFile
import java.lang.Thread.sleep
import java.nio.channels.FileChannel
import java.nio.channels.FileLock
import java.nio.channels.OverlappingFileLockException
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.InputFile
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
private var _root: File? = null
@InputFile
fun getInputFile(): File? {
return _sha1File
}
@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(sha1File: File, outputDir: File, dependencyType: DependencyType, root: File) {
_outputDir = outputDir
_sha1File = sha1File
_tarGzFile = sha1File.resolveSibling(sha1File.name.replace(".sha1", ""))
_root = root
this.dependencyType = dependencyType
}
@TaskAction
fun execute() {
val sha1File = _sha1File!!
val outputDir = _outputDir!!
val tarGzFile = _tarGzFile!!
if (!sha1File.exists()) {
throw RuntimeException("Missing sha1 file: $sha1File")
}
if (!shouldExecute(outputDir, tarGzFile, sha1File)) {
return
}
// Create a lock to ensure sequential a single downloader per third party dependency.
val lockFile = sha1File.parentFile.resolve(sha1File.name + ".download_deps_lock")
if (!lockFile.exists()) {
lockFile.createNewFile()
}
getWorkerExecutor()!!
.noIsolation()
.submit(RunDownload::class.java) {
type.set(dependencyType)
this.sha1File.set(sha1File)
this.outputDir.set(outputDir)
this.tarGzFile.set(tarGzFile)
this.lockFile.set(lockFile)
this.root.set(_root!!)
}
}
interface RunDownloadParameters : WorkParameters {
val type : Property<DependencyType>
val sha1File : RegularFileProperty
val outputDir : RegularFileProperty
val tarGzFile : RegularFileProperty
val lockFile : RegularFileProperty
val root : RegularFileProperty
}
abstract class RunDownload : WorkAction<RunDownloadParameters> {
override fun execute() {
var lock : FileLock? = null
try {
val sha1File = parameters.sha1File.asFile.get()
val outputDir = parameters.outputDir.asFile.get()
val tarGzFile = parameters.tarGzFile.asFile.get()
if (!shouldExecute(outputDir, tarGzFile, sha1File)) {
return;
}
val lockFile = parameters.lockFile.asFile.get()
val channel: FileChannel = RandomAccessFile(lockFile, "rw").getChannel()
// Block until we have the lock.
var couldTakeLock = false
while (!couldTakeLock) {
try {
lock = channel.lock()
couldTakeLock = true;
} catch (ignored: OverlappingFileLockException) {
sleep(50);
}
}
if (!shouldExecute(outputDir, tarGzFile, sha1File)) {
return;
}
if (outputDir.exists() && outputDir.isDirectory) {
outputDir.delete()
}
when (parameters.type.get()) {
DependencyType.GOOGLE_STORAGE -> {
downloadFromGoogleStorage(parameters, sha1File)
}
DependencyType.X20 -> {
downloadFromX20(parameters, sha1File)
}
}
} catch (e: Exception) {
throw RuntimeException(e)
} finally {
lock?.release()
}
}
@Throws(IOException::class, InterruptedException::class)
private fun downloadFromGoogleStorage(parameters: RunDownloadParameters, 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(parameters, ProcessBuilder().command(command))
} else {
runProcess(
parameters,
ProcessBuilder()
.command("bash",
"-c",
"download_from_google_storage " + java.lang.String.join(" ", args)))
}
}
@Throws(IOException::class, InterruptedException::class)
private fun downloadFromX20(parameters: RunDownloadParameters, sha1File: File) {
if (OperatingSystem.current().isWindows) {
throw RuntimeException("Downloading from x20 unsupported on windows")
}
runProcess(parameters,
ProcessBuilder()
.command("bash", "-c", "tools/download_from_x20.py $sha1File"))
}
@Throws(IOException::class, InterruptedException::class)
private fun runProcess(parameters: RunDownloadParameters, builder: ProcessBuilder) {
builder.directory(parameters.root.asFile.get())
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")))
}
}
}
companion object {
fun shouldExecute(outputDir: File, tarGzFile: File, sha1File: File) : Boolean {
// 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.
if (outputDir.exists()
&& outputDir.isDirectory
&& outputDir.list().isNotEmpty()
&& tarGzFile.exists()
&& sha1File.lastModified() <= tarGzFile.lastModified()) {
return false
}
return true
}
}
}