diff --git a/build.gradle.kts b/build.gradle.kts index 32a152fb..2c5c1cc2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { group = "com.github.holgerbrandl.kscript.launcher" dependencies { - compileOnly("org.jetbrains.kotlin:kotlin-stdlib") + compile("org.jetbrains.kotlin:kotlin-stdlib") compile("com.offbytwo:docopt:0.6.0.20150202") diff --git a/src/main/kotlin/kscript/app/AppHelpers.kt b/src/main/kotlin/kscript/app/AppHelpers.kt index 89078bbc..a5a51d9b 100644 --- a/src/main/kotlin/kscript/app/AppHelpers.kt +++ b/src/main/kotlin/kscript/app/AppHelpers.kt @@ -372,7 +372,7 @@ private fun createSymLink(link: File, target: File) { * Create and use a temporary gradle project to package the compiled script using capsule. * See https://github.com/puniverse/capsule */ -fun packageKscript(scriptJar: File, wrapperClassName: String, dependencies: List, customRepos: List, runtimeOptions: String, appName: String) { +fun packageKscript(scriptJar: File, wrapperClassName: String, dependencies: List, customRepos: List, runtimeOptions: String, appName: String, proguardConfig: List?) { requireInPath("gradle", "gradle is required to package kscripts") infoMsg("Packaging script '$appName' into standalone executable...") @@ -393,10 +393,46 @@ fun packageKscript(scriptJar: File, wrapperClassName: String, dependencies: List // https://shekhargulati.com/2015/09/10/gradle-tip-using-gradle-plugin-from-local-maven-repository/ - val gradleScript = """ + createGradleFile(proguardConfig, stringifiedRepos, stringifiedDeps, scriptJar, wrapperClassName, tmpProjectDir, appName, jvmOptions) + + + val pckgedJar = File(Paths.get("").toAbsolutePath().toFile(), appName).absoluteFile + + // create exec_header to allow for direction execution (see http://www.capsule.io/user-guide/#really-executable-capsules) + // from https://github.com/puniverse/capsule/blob/master/capsule-util/src/main/resources/capsule/execheader.sh + val execHeaderFile = File(tmpProjectDir, "exec_header.sh").also { + it.writeText("""#!/usr/bin/env bash +exec java -jar ${'$'}0 "${'$'}@" +""") + } + + createProguardFile(tmpProjectDir, proguardConfig) + + val pckgResult = evalBash("cd '${tmpProjectDir}' && gradle ${if (proguardConfig != null) "shadowJar proguard" else "simpleCapsule"}") + + with(pckgResult) { + kscript.app.errorIf(exitCode != 0) { "packaging of '$appName' failed:\n$pckgResult" } + } + + pckgedJar.delete() + if (proguardConfig != null) { + execHeaderFile.let { + it.appendBytes(File(tmpProjectDir, "build/libs/${tmpProjectDir.name}-proguarded.jar").readBytes()) + it.copyTo(pckgedJar, true).setExecutable(true) + } + } else { + File(tmpProjectDir, "build/libs/${appName}").copyTo(pckgedJar, true).setExecutable(true) + } + + infoMsg("Finished packaging into ${pckgedJar}") +} + +private fun createGradleFile(proguardConfig: List?, stringifiedRepos: String, stringifiedDeps: String, scriptJar: File, wrapperClassName: String, tmpProjectDir: File, appName: String, jvmOptions: String) { + File(tmpProjectDir, "build.gradle").writeText(""" +${proguardBuildScripts(proguardConfig)} plugins { id "org.jetbrains.kotlin.jvm" version "${KotlinVersion.CURRENT}" - id "it.gianluz.capsule" version "1.0.3" + ${if (proguardConfig != null) "id \"com.github.johnrengelman.shadow\" version \"6.0.0\"" else "id \"it.gianluz.capsule\" version \"1.0.3\""} } repositories { @@ -415,6 +451,35 @@ $stringifiedDeps compile files('${scriptJar.invariantSeparatorsPath}') } +${gradleTasks(proguardConfig, wrapperClassName, tmpProjectDir, appName, jvmOptions)}""".trimIndent()) } + +private fun gradleTasks(proguardConfig: List?, wrapperClassName: String, tmpProjectDir: File, appName: String, jvmOptions: String): String { + return if (proguardConfig != null) """ +jar { + manifest { + attributes 'Main-Class': '$wrapperClassName' + } +} + +task ('proguard', type: proguard.gradle.ProGuardTask) { + + configuration("proguard.pro") + + injars 'build/libs/${tmpProjectDir.name}-all.jar' + outjars 'build/libs/${tmpProjectDir.name}-proguarded.jar' + + // Automatically handle the Java version of this build. + if (System.getProperty('java.version').startsWith('1.')) { + // Before Java 9, the runtime classes were packaged in a single jar file. + libraryjars "${"$"}{System.getProperty('java.home')}/lib/rt.jar" + } else { + // As of Java 9, the runtime classes are packaged in modular jmod files. + libraryjars "${"$"}{System.getProperty('java.home')}/jmods/java.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class' + //libraryjars ${"$"}{System.getProperty('java.home')}/jmods/....." + } +} +""" else """ + task simpleCapsule(type: FatCapsule){ applicationClass '$wrapperClassName' @@ -429,27 +494,101 @@ task simpleCapsule(type: FatCapsule){ //systemProperties['java.awt.headless'] = true } } - """.trimIndent() +""" +} - val pckgedJar = File(Paths.get("").toAbsolutePath().toFile(), appName).absoluteFile +private fun proguardBuildScripts(proguardConfig: List?): String { + return if (proguardConfig != null) """ +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.guardsquare:proguard-gradle:7.0.0' + classpath "com.github.jengelman.gradle.plugins:shadow:6.0.0" + } +} +""" else "" +} +private fun createProguardFile(tmpProjectDir: File, proguardConfig: List?) { + proguardConfig?.let { + File(tmpProjectDir, "proguard.pro").writeText( + """ +### Custom project based configuration - // create exec_header to allow for direction execution (see http://www.capsule.io/user-guide/#really-executable-capsules) - // from https://github.com/puniverse/capsule/blob/master/capsule-util/src/main/resources/capsule/execheader.sh - File(tmpProjectDir, "exec_header.sh").writeText("""#!/usr/bin/env bash -exec java -jar ${'$'}0 "${'$'}@" -""") +${proguardConfig?.joinToString(separator = "\n")} - File(tmpProjectDir, "build.gradle").writeText(gradleScript) +### Default app configuration +# +# This ProGuard configuration file illustrates how to process applications. +# Usage: +# java -jar proguard.jar @applications.pro +# - val pckgResult = evalBash("cd '${tmpProjectDir}' && gradle simpleCapsule") +-verbose - with(pckgResult) { - kscript.app.errorIf(exitCode != 0) { "packaging of '$appName' failed:\n$pckgResult" } - } +-dontwarn - pckgedJar.delete() - File(tmpProjectDir, "build/libs/${appName}").copyTo(pckgedJar, true).setExecutable(true) +# Save the obfuscation mapping to a file, so you can de-obfuscate any stack +# traces later on. Keep a fixed source file attribute and all line number +# tables to get line numbers in the stack traces. +# You can comment this out if you're not interested in stack traces. - infoMsg("Finished packaging into ${pckgedJar}") +-printmapping out.map +-renamesourcefileattribute SourceFile +-keepattributes SourceFile,LineNumberTable + +# Preserve all annotations. + +-keepattributes *Annotation* + +# You can print out the seeds that are matching the keep options below. + +#-printseeds out.seeds + +# Preserve all public applications. + +-keepclasseswithmembers public class * { + public static void main(java.lang.String[]); +} + +# Preserve all native method names and the names of their classes. + +-keepclasseswithmembernames,includedescriptorclasses class * { + native ; +} + +# Preserve the special static methods that are required in all enumeration +# classes. + +-keepclassmembers,allowoptimization enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +# Explicitly preserve all serialization members. The Serializable interface +# is only a marker interface, so it wouldn't save them. +# You can comment this out if your application doesn't use serialization. +# If your code contains serializable classes that have to be backward +# compatible, please refer to the manual. + +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + static final java.io.ObjectStreamField[] serialPersistentFields; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} + +# Your application may contain more items that need to be preserved; +# typically classes that are dynamically created using Class.forName: + +# -keep public class com.example.MyClass +# -keep public interface com.example.MyInterface +# -keep public class * implements com.example.MyInterface + """.trimIndent()) + } } diff --git a/src/main/kotlin/kscript/app/Kscript.kt b/src/main/kotlin/kscript/app/Kscript.kt index 7ee17ecc..88fe095c 100644 --- a/src/main/kotlin/kscript/app/Kscript.kt +++ b/src/main/kotlin/kscript/app/Kscript.kt @@ -42,6 +42,7 @@ Options: --idea Open script in temporary Intellij session -s --silent Suppress status logging to stderr --package Package script and dependencies into self-dependent binary + --proguard Works together with --package and will Proguard process the output --add-bootstrap-header Prepend bash header that installs kscript if necessary @@ -147,6 +148,13 @@ fun main(args: Array) { val dependencies = (script.collectDependencies() + Script(rawScript).collectDependencies()).distinct() val customRepos = (script.collectRepos() + Script(rawScript).collectRepos()).distinct() + // Find all extra Proguard configuration + val progurdConfigurations: List? by lazy { + if (docopt.getBoolean("proguard")) { + (script.collectProguardConfig() + Script(rawScript).collectProguardConfig()).distinct() + } else null + } + // Extract kotlin arguments val kotlinOpts = script.collectRuntimeOptions() val compilerOpts = script.collectCompilerOptions() @@ -272,7 +280,15 @@ fun main(args: Array) { "k" + scriptFile.nameWithoutExtension } - packageKscript(jarFile, execClassName, dependencies, customRepos, kotlinOpts, binaryName) + packageKscript( + scriptJar = jarFile, + wrapperClassName = execClassName, + dependencies = dependencies, + customRepos = customRepos, + runtimeOptions = kotlinOpts, + appName = binaryName, + proguardConfig = progurdConfigurations + ) quit(0) } diff --git a/src/main/kotlin/kscript/app/Script.kt b/src/main/kotlin/kscript/app/Script.kt index 9cc284f6..bb7265d1 100644 --- a/src/main/kotlin/kscript/app/Script.kt +++ b/src/main/kotlin/kscript/app/Script.kt @@ -69,8 +69,7 @@ data class Script(val lines: List, val extension: String = "kts") : Iter } } - -private val KSCRIPT_DIRECTIVE_ANNO: List = listOf("DependsOn", "KotlinOpts", "Include", "EntryPoint", "MavenRepository", "DependsOnMaven", "CompilerOpts") +private val KSCRIPT_DIRECTIVE_ANNO: List = listOf("DependsOn", "KotlinOpts", "Include", "EntryPoint", "MavenRepository", "DependsOnMaven", "CompilerOpts" , "ProguardConfig") .map { "^@file:$it[(]".toRegex() } private fun isKscriptAnnotation(line: String) = @@ -129,7 +128,7 @@ fun Script.collectDependencies(): List { // if annotations are used add dependency on kscript-annotations if (lines.any { isKscriptAnnotation(it) }) { - dependencies += "com.github.holgerbrandl:kscript-annotations:1.4" + dependencies += "com.github.holgerbrandl:kscript-annotations:1.5" } return dependencies.distinct() @@ -210,6 +209,16 @@ fun Script.collectRepos(): List { } } +fun Script.collectProguardConfig(): List { + + // Supports parsing Proguard config configured like this: + // + // @file:ProguardConfig("-keepclassmembers class CliArgs { *;}") + + val proguardConfigRegex = "(?