commit b318c44c2ec1cc71d1f46904d9b292e58d5466e6 Author: nathan Date: Sat Jun 30 15:20:54 2018 +0200 Commit of cross-compile logic ported from java builder diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9c69c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,124 @@ +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries +.idea/**/codeStyles/ +.idea/**/codeStyleSettings.xml + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/shelf/ + + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +###################### +# End JetBrains IDEs # +###################### + + +# From https://github.com/github/gitignore/blob/master/Gradle.gitignore +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +gradle/wrapper/gradle-wrapper.properties + + + +# From https://github.com/github/gitignore/blob/master/Java.gitignore +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + + +########################################################## +# Specific to this module +.iml diff --git a/src/dorkbox/crossCompile/PrepareJdk.kt b/src/dorkbox/crossCompile/PrepareJdk.kt new file mode 100644 index 0000000..a6785ec --- /dev/null +++ b/src/dorkbox/crossCompile/PrepareJdk.kt @@ -0,0 +1,423 @@ +/* + * 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.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 { + + companion object { + data class JDK(val file: File, val checksum: String) + + private val versionInfo = HashMap() + + 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.plugins.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 { + if (!needsJdk && it is JavaCompile) { + if (JavaVersion.toVersion(it.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 { it.toLowerCase().contains("clean") } + if (hasClean.isNotEmpty()) { + val task = project.tasks.last { it.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 { + val jdkRuntimes = ArrayList() + + 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 + } + + @Suppress("DEPRECATION") + private fun configureTaskBootstrapClassPath(project: Project, task: Task, targetVersion: JavaVersion) { + val location = versionInfo[targetVersion]?.file + + if (location != null) { + val file = getUncompressedFile(location) + + val bootstrapClasspath = project.files(file) + val bootClasspath = bootstrapClasspath.joinToString(File.pathSeparator) + + + if (task is JavaCompile) { + logger.debug("Configuring task ${task.name} with $bootClasspath") + + if (project.gradle.versionGreaterThan("4.2.1")) { + task.options.bootstrapClasspath = bootstrapClasspath + } + else { + task.options.bootClasspath = bootClasspath + } + } + else if (task is GroovyCompile) { + logger.debug("Configuring task ${task.name} with $bootClasspath") + + if (project.gradle.versionGreaterThan("4.2.1")) { + task.options.bootstrapClasspath = bootstrapClasspath + } + else { + task.options.bootClasspath = bootClasspath + } + } + // project.plugins.withId("kotlin") { + // logger.debug("Configuring task ${task.name} with $bootClasspath ?????") + // + // withType(KotlinCompile::class.java) { + // it.kotlinOptions.jdkHome = file.jdkHome + // } + // } + } + else { + logger.error("Unable to determine bootstrap path $targetVersion for ${task.name}") + } + } + + + + private fun getFiles(directory: File, suffix: String): MutableList { + val outputFiles = ArrayList() + + 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.isEmpty()) { + val verify = project.tasks.create("verify$fileName", Verify::class.java) + 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 tnable to find or extract jar fihere 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) + } + } + } +}