CrossCompile/src/dorkbox/crossCompile/PrepareJdk.kt

416 lines
15 KiB
Kotlin

/*
* Copyright 2012 dorkbox, llc
*
* 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 dorkbox.crossCompile
import de.undercouch.gradle.tasks.download.Download
import de.undercouch.gradle.tasks.download.Verify
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream
import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream
import org.apache.commons.compress.compressors.pack200.Pack200CompressorInputStream
import org.apache.commons.compress.compressors.pack200.Pack200CompressorOutputStream
import org.gradle.api.*
import org.gradle.api.invocation.Gradle
import org.gradle.api.plugins.JavaBasePlugin
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.compile.AbstractCompile
import org.gradle.api.tasks.compile.GroovyCompile
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.*
import java.net.HttpURLConnection
import java.net.URL
import java.util.*
/**
* Downloads JDKs and configures the bootstrap classpath when cross-compiling java projects
*/
class PrepareJdk : Plugin<Project> {
companion object {
data class JDK(val file: File, val checksum: String)
private val versionInfo = HashMap<JavaVersion, JDK>()
private const val compressSuffix = ".pack.lzma"
private const val jarSuffix = ".jar"
val logger: Logger = LoggerFactory.getLogger(PrepareJdk::class.java)
}
override fun apply(project: Project) {
project.rootProject.pluginManager.apply(JavaBasePlugin::class.java)
project.rootProject.pluginManager.apply(JavaBasePlugin::class.java)
project.afterEvaluate {
// don't waste time if this is not a java project
val convention: JavaPluginConvention? = project.convention.plugins["java"] as? JavaPluginConvention
var needsJdk = false
if (convention != null) {
// check if we need to extract anything..
if (convention.targetCompatibility != JavaVersion.current()) {
needsJdk = true
}
project.tasks.forEach { task ->
if (!needsJdk && task is JavaCompile) {
if (JavaVersion.toVersion(task.targetCompatibility) != JavaVersion.current()) {
needsJdk = true
}
}
}
if (needsJdk) {
// if there is a clean task (usually the first thing to run, if run), run after the clean task, otherwise run first.
val hasClean = project.gradle.startParameter.taskNames.filter { taskName ->
taskName.lowercase(Locale.getDefault()).contains("clean")
}
if (hasClean.isNotEmpty()) {
val task = project.tasks.last { task -> task.name == hasClean.last() }
task.doLast {
setupDownload(project)
}
}
else {
setupDownload(project)
}
}
}
}
}
private fun setupDownload(project: Project) {
downloadAndExtractJdk(project)
project.tasks.forEach {
if (it is AbstractCompile) {
if (JavaVersion.toVersion(it.targetCompatibility) != JavaVersion.current()) {
// only supports JAVA and GROOVY
configureTaskBootstrapClassPath(project, it, JavaVersion.toVersion(it.targetCompatibility))
}
}
}
}
private fun downloadAndExtractJdk(project: Project) {
val outputDir = File(project.buildDir, "jdkRuntimes")
if (!outputDir.exists()) outputDir.mkdirs()
logger.info("Preparing cross-compile environment")
// download the JDK runtimes from github
val prefix = "https://github.com/dorkbox/JavaBuilder/raw/master/jdkRuntimes"
versionInfo[JavaVersion.VERSION_1_6] = JDK(File(outputDir, "openJdk6_rt.jar.pack.lzma").absoluteFile, "313a8b3fe4736520f7a4b6de37f1c80698502ee7")
versionInfo[JavaVersion.VERSION_1_7] = JDK(File(outputDir, "openJdk7_rt.jar.pack.lzma").absoluteFile, "b42aa62d1772d1f2e8f93664c1e8cb866374e511")
versionInfo[JavaVersion.VERSION_1_8] = JDK(File(outputDir, "openJdk8_rt.jar.pack.lzma").absoluteFile, "98616c3fc020750dce84f02bc65e5f66b839b29d")
// if we are offline, DO NOT try to download anything
if (!project.gradle.startParameter.isOffline) {
// download JDKS we don't know about
val downloadJdkList = getDownloadJdkList(prefix)
val elements = versionInfo.map { it.value.file.name }
downloadJdkList.removeAll(elements)
for (jdk in downloadJdkList) {
try {
val version = JavaVersion.toVersion(jdk.substring(7, jdk.indexOf('_')))
versionInfo[version] = JDK(File(outputDir, jdk).absoluteFile, "")
} catch (e: Exception) {
logger.error("Unable to parse/download $jdk")
}
}
// download all JDKS ...
for (jdk in versionInfo.values) {
downloadJDK(project, prefix, jdk.file, jdk.checksum)
}
}
val jarFiles = getFiles(outputDir, jarSuffix)
val compressedFiles = getFiles(outputDir, compressSuffix)
var hasFiles = false
// discover which files need compressing
val iterator = jarFiles.iterator()
while (iterator.hasNext()) {
val jarFile = iterator.next()
logger.debug("JarFile $jarFile")
hasFiles = true
val file = getCompressedFile(jarFile)
// Don't always need to compress the jdk files. This checks if the compressed version exists
if (file.canRead() && file.length() > 0) {
iterator.remove()
}
}
// discover which files need un-compressing
val iterator2 = compressedFiles.iterator()
while (iterator2.hasNext()) {
val compressedFile = iterator2.next()
logger.debug("CompressedFile $compressedFile")
hasFiles = true
val file = getUncompressedFile(compressedFile)
// Don't always need to decompress the jdk files. This checks if the extracted version exists
if (file.canRead() && file.length() > 0) {
iterator2.remove()
}
}
if (!hasFiles) {
throw GradleException("Unable to find or extract jar files, none were found in $outputDir")
}
if (!compressedFiles.isEmpty() || !jarFiles.isEmpty()) {
logger.info("Preparing cross compile environment")
}
// NOTE: we also compress files so we can automatically create NEW files if we need to
for (inputFile in jarFiles) {
// pack200 + LZMA
logger.debug("Compressing $inputFile")
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
inputStream = FileInputStream(inputFile)
inputStream = BufferedInputStream(inputStream)
outputStream = FileOutputStream(getCompressedFile(inputFile))
outputStream = BufferedOutputStream(outputStream)
// now pack and compress
outputStream = LZMACompressorOutputStream(outputStream)
outputStream = Pack200CompressorOutputStream(outputStream)
inputStream.copyTo(outputStream)
} catch (e: Exception) {
logger.error("Error compressing files", e)
} finally {
close(outputStream)
close(inputStream)
}
}
for (inputFile in compressedFiles) {
// unLZMA + unpack200
logger.debug("Extracting $inputFile")
var inputStream: InputStream? = null
var outputStream: OutputStream? = null
try {
inputStream = FileInputStream(inputFile)
inputStream = BufferedInputStream(inputStream)
outputStream = FileOutputStream(getUncompressedFile(inputFile))
outputStream = BufferedOutputStream(outputStream)
// now uncompress and unpack
inputStream = LZMACompressorInputStream(inputStream)
inputStream = Pack200CompressorInputStream(inputStream)
inputStream.copyTo(outputStream)
} catch (e: Exception) {
logger.error("Error extracting files", e)
} finally {
close(outputStream)
close(inputStream)
}
}
logger.info("Done preparing cross-compile environment")
}
private fun getDownloadJdkList(prefix: String): ArrayList<String> {
val jdkRuntimes = ArrayList<String>()
with(URL(prefix).openConnection() as HttpURLConnection) {
// optional default is GET
requestMethod = "GET"
logger.debug("Sending 'GET' request to URL : $url")
logger.debug("Response Code : $responseCode")
BufferedReader(InputStreamReader(inputStream)).use {
val jdkDirName = "jdkRuntimes"
val lzmaExtension = ".jar.pack.lzma"
var inputLine = it.readLine()
while (inputLine != null) {
val indexJdk = inputLine.lastIndexOf(jdkDirName)
if (indexJdk > -1) {
val startIndex = indexJdk + jdkDirName.length + 1
val indexLzma = inputLine.indexOf(lzmaExtension, startIndex, false)
if (indexLzma > -1) {
val message = inputLine.substring(startIndex, indexLzma + lzmaExtension.length)
jdkRuntimes.add(message)
logger.debug(message)
}
}
inputLine = it.readLine()
}
}
}
return jdkRuntimes
}
private fun configureTaskBootstrapClassPath(project: Project, task: Task, targetVersion: JavaVersion) {
val location = versionInfo[targetVersion]?.file
if (location != null) {
val file = getUncompressedFile(location)
if (task is KotlinCompile) {
logger.debug("Configuring task ${task.name} with ${file.absolutePath}")
task.kotlinOptions.jdkHome = file.absolutePath
}
else {
// java/groovy
val bootstrapClasspath = project.files(file)
val bootClasspath = bootstrapClasspath.joinToString(File.pathSeparator)
if (task is JavaCompile) {
logger.debug("Configuring task ${task.name} with $bootClasspath")
task.options.bootstrapClasspath = bootstrapClasspath
}
else if (task is GroovyCompile) {
logger.debug("Configuring task ${task.name} with $bootClasspath")
task.options.bootstrapClasspath = bootstrapClasspath
}
}
}
else {
logger.error("Unable to determine bootstrap path $targetVersion for ${task.name}")
}
}
private fun getFiles(directory: File, suffix: String): MutableList<File> {
val outputFiles = ArrayList<File>()
if (directory.isDirectory) {
val files = directory.listFiles()
for (file in files) {
val name = file.name
if (name.endsWith(suffix)) {
outputFiles.add(file)
}
}
}
return outputFiles
}
private fun close(inputStream: InputStream?) {
try {
inputStream?.close()
} catch (ignored: Exception) {
}
}
private fun close(outputStream: OutputStream?) {
try {
outputStream?.close()
} catch (ignored: Exception) {
}
}
private fun getUncompressedFile(compressedFile: File): File {
val nameLength = compressedFile.name.length
val fixedName = compressedFile.name.substring(0, nameLength - compressSuffix.length)
return File(compressedFile.parentFile, fixedName)
}
private fun getCompressedFile(jarFile: File): File {
return File(jarFile.parentFile, jarFile.name + compressSuffix)
}
private fun Gradle.versionGreaterThan(version: String): Boolean = versionCompareTo(version) > 0
private fun Gradle.versionCompareTo(version: String): Int {
return GradleVersion.version(gradleVersion).compareTo(GradleVersion.version(version))
}
/**
* checksum can be empty string to do a basic "does this file exist" check to determine if the file needs downloading
* Download, if necessary, the specified JDK
*/
private fun downloadJDK(project: Project, url: String, file: File, sha1Checksum: String) {
var valid = false
val fileName = file.name
if (sha1Checksum.isNotEmpty()) {
val verify = project.tasks.create("verify${fileName.replaceFirstChar { it.titlecase() }.substringBefore(".")}", Verify::class.java)
verify.group = "crossCompile"
verify.src(file)
verify.algorithm("SHA1")
verify.checksum(sha1Checksum)
try {
verify.verify()
valid = true
} catch (ignored: Exception) {
// verify throws an exception if it cannot verify the file.
}
}
else {
// lame way to verify, but unable to find or extract jar if there is no checksum...
valid = file.canRead() && file.length() > 0
}
if (!valid) {
val download = project.tasks.create("download$fileName", Download::class.java)
download.src("$url/$fileName")
download.dest(file)
download.quiet(true)
download.overwrite(false)
try {
download.download()
} catch (e: Exception) {
logger.error("Unable to download $url", e)
}
}
}
}