Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port Compose Desktop ProGuard support to IntelliJPlugin #1170

Draft
wants to merge 4 commits into
base: 1.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/main/kotlin/org/jetbrains/intellij/IntelliJPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import org.jetbrains.intellij.IntelliJPluginConstants.VERIFY_PLUGIN_CONFIGURATIO
import org.jetbrains.intellij.IntelliJPluginConstants.VERIFY_PLUGIN_TASK_NAME
import org.jetbrains.intellij.IntelliJPluginConstants.VERSION_LATEST
import org.jetbrains.intellij.dependency.*
import org.jetbrains.intellij.dsl.ProguardSettings
import org.jetbrains.intellij.jbr.JbrResolver
import org.jetbrains.intellij.model.MavenMetadata
import org.jetbrains.intellij.model.XmlExtractor
Expand All @@ -104,6 +105,7 @@ import org.jetbrains.intellij.tasks.*
import org.jetbrains.intellij.utils.*
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
Expand All @@ -117,6 +119,7 @@ abstract class IntelliJPlugin : Plugin<Project> {
private lateinit var archiveUtils: ArchiveUtils
private lateinit var dependenciesDownloader: DependenciesDownloader
private lateinit var context: String
private lateinit var proguardSettings: ProguardSettings

override fun apply(project: Project) {
context = project.logCategory()
Expand All @@ -126,6 +129,7 @@ abstract class IntelliJPlugin : Plugin<Project> {

archiveUtils = project.objects.newInstance()
dependenciesDownloader = project.objects.newInstance()
proguardSettings = project.objects.newInstance()

project.plugins.apply(JavaPlugin::class)
project.plugins.apply(IdeaExtPlugin::class)
Expand Down Expand Up @@ -221,6 +225,7 @@ abstract class IntelliJPlugin : Plugin<Project> {
configureBuildPluginTask(project)
configureRunPluginVerifierTask(project, extension)
configureSignPluginTask(project)
configureProguardTask(project)
configurePublishPluginTask(project)
configureProcessResources(project)
configureInstrumentation(project, extension, ideaDependencyProvider)
Expand Down Expand Up @@ -1349,6 +1354,48 @@ abstract class IntelliJPlugin : Plugin<Project> {
}
}

private fun configureProguardTask(project: Project) {
// TODO: how to enable? if block exists?
val runProguard = if (true) {
project.tasks.register<AbstractProguardTask>("proguard") {
proguardVersion.set(proguardSettings.version)

// TODO: a more standard way of loading resources?
val defaultConfig = AbstractProguardTask::class.java.getResourceAsStream("/proguard/default-config.pro")

if (defaultConfig != null) {
val defaultRulesFile = File.createTempFile("default", "pro")
val outputStream = FileOutputStream(defaultRulesFile)
outputStream.write(defaultConfig.readAllBytes())
outputStream.close()
defaultComposeRulesFile.set(defaultRulesFile)
}

configurationFiles.from(proguardSettings.configurationFiles)
// ProGuard uses -dontobfuscate option to turn off obfuscation, which is enabled by default
// We want to disable obfuscation by default, because often
// it is not needed, but makes troubleshooting much harder.
// If obfuscation is turned off by default,
// enabling (`isObfuscationEnabled.set(true)`) seems much better,
// than disabling obfuscation disabling (`dontObfuscate.set(false)`).
// That's why a task property is follows ProGuard design,
// when our DSL does the opposite.
dontobfuscate.set(proguardSettings.obfuscate.map { !it })
maxHeapSize.set(proguardSettings.maxHeapSize)
javaHome.set(System.getProperty("java.home") ?: error("'java.home' system property is not set"))
// TODO: where should the destination dir be?
destinationDir.convention(project.layout.buildDirectory.dir("lib/proguard"))
mainJar.fileProvider(project.provider {
project.tasks.getByPath("jar").outputs.files.singleFile
})

// TODO: need to shrink before building & use the shrunk version
// in the build task
dependsOn(BUILD_PLUGIN_TASK_NAME)
}
} else null
}

private fun configureBuildPluginTask(project: Project) {
info(context, "Configuring building plugin task")

Expand Down
25 changes: 25 additions & 0 deletions src/main/kotlin/org/jetbrains/intellij/dsl/ProguardSettings.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/

package org.jetbrains.intellij.dsl

import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import javax.inject.Inject
import org.jetbrains.intellij.utils.nullableProperty
import org.jetbrains.intellij.utils.notNullProperty

private const val DEFAULT_PROGUARD_VERSION = "7.2.2"

abstract class ProguardSettings @Inject constructor(
objects: ObjectFactory,
) {
val version: Property<String> = objects.notNullProperty(DEFAULT_PROGUARD_VERSION)
val maxHeapSize: Property<String?> = objects.nullableProperty()
val configurationFiles: ConfigurableFileCollection = objects.fileCollection()
val isEnabled: Property<Boolean> = objects.notNullProperty(false)
val obfuscate: Property<Boolean> = objects.notNullProperty(false)
}
182 changes: 182 additions & 0 deletions src/main/kotlin/org/jetbrains/intellij/tasks/AbstractProguardTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/

package org.jetbrains.intellij.tasks

import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.LocalState
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.TaskAction
import org.jetbrains.intellij.utils.ExternalToolRunner
import org.jetbrains.intellij.utils.cliArg
import org.jetbrains.intellij.utils.ioFile
import org.jetbrains.intellij.utils.jvmToolFile
import org.jetbrains.intellij.utils.mangledName
import org.jetbrains.intellij.utils.normalizedPath
import org.jetbrains.intellij.utils.notNullProperty
import org.jetbrains.intellij.utils.nullableProperty
import java.io.File
import java.io.Writer

// TODO: generalize some options e.g. defaultComposeRulesFile should just be defaultRulesFile
abstract class AbstractProguardTask : AbstractTask() {

@get:Optional
@get:InputFiles
val inputFiles: ConfigurableFileCollection = objects.fileCollection()

@get:InputFiles
val libraryJars: ConfigurableFileCollection = objects.fileCollection()

@get:InputFile
val mainJar: RegularFileProperty = objects.fileProperty()

@get:Internal
internal val mainJarInDestinationDir: Provider<RegularFile> = mainJar.flatMap {
destinationDir.file(it.asFile.name)
}

@get:InputFiles
val configurationFiles: ConfigurableFileCollection = objects.fileCollection()

@get:Optional
@get:Input
val dontobfuscate: Property<Boolean?> = objects.nullableProperty()

// todo: DSL for excluding default rules
// also consider pulling coroutines rules from coroutines artifact
// https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro
@get:Optional
@get:InputFile
val defaultComposeRulesFile: RegularFileProperty = objects.fileProperty()

@get:Input
val proguardVersion: Property<String> = objects.notNullProperty()

@get:Input
val javaHome: Property<String> = objects.notNullProperty(System.getProperty("java.home"))

@get:Optional
@get:Input
val mainClass: Property<String?> = objects.nullableProperty()

@get:Internal
val maxHeapSize: Property<String?> = objects.nullableProperty()

@get:OutputDirectory
val destinationDir: DirectoryProperty = objects.directoryProperty()

@get:LocalState
protected val workingDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/$name")

private val rootConfigurationFile = workingDir.map { it.file("root-config.pro") }

private val jarsConfigurationFile = workingDir.map { it.file("jars-config.pro") }

@TaskAction
fun execute() {
val javaHome = File(javaHome.get())
val proguardFiles = project.configurations.detachedConfiguration(
project.dependencies.create("com.guardsquare:proguard-gradle:${proguardVersion.get()}")
).files

cleanDirs(destinationDir, workingDir)
val destinationDir = destinationDir.ioFile.absoluteFile

// todo: can be cached for a jdk
val jmods = javaHome.resolve("jmods").walk().filter {
it.isFile && it.path.endsWith("jmod", ignoreCase = true)
}.toList()

val inputToOutputJars = LinkedHashMap<File, File>()
// avoid mangling mainJar
inputToOutputJars[mainJar.ioFile] = mainJarInDestinationDir.ioFile
for (inputFile in inputFiles) {
if (inputFile.name.endsWith(".jar", ignoreCase = true)) {
inputToOutputJars.putIfAbsent(inputFile, destinationDir.resolve(inputFile.mangledName()))
} else {
inputFile.copyTo(destinationDir.resolve(inputFile.name))
}
}

jarsConfigurationFile.ioFile.bufferedWriter().use { writer ->
for ((input, output) in inputToOutputJars.entries) {
writer.writeLn("-injars '${input.normalizedPath()}'")
writer.writeLn("-outjars '${output.normalizedPath()}'")
}

for (jmod in jmods) {
writer.writeLn("-libraryjars '${jmod.normalizedPath()}'(!**.jar;!module-info.class)")
}
for (libraryJar in libraryJars) {
writer.writeLn("-libraryjars '${libraryJar.canonicalPath}'")
}
// TODO: do this here or in the IntelliJPlugin?
val sourceSets = project.extensions.findByName("sourceSets") as SourceSetContainer
sourceSets.getByName("main").compileClasspath.forEach {
writer.writeLn("-libraryjars '${it.canonicalPath}'")
}
}

rootConfigurationFile.ioFile.bufferedWriter().use { writer ->
if (dontobfuscate.orNull == true) {
writer.writeLn("-dontobfuscate")
}

if (mainClass.isPresent) {
writer.writeLn("""
-keep public class ${mainClass.get()} {
public static void main(java.lang.String[]);
}
""".trimIndent())
}

val includeFiles = sequenceOf(
jarsConfigurationFile.ioFile,
defaultComposeRulesFile.ioFile
) + configurationFiles.files.asSequence()
for (configFile in includeFiles.filterNotNull()) {
writer.writeLn("-include '${configFile.normalizedPath()}'")
}
}

val javaBinary = jvmToolFile(toolName = "java", javaHome = javaHome)
val args = arrayListOf<String>().apply {
val maxHeapSize = maxHeapSize.orNull
if (maxHeapSize != null) {
add("-Xmx:$maxHeapSize")
}
cliArg("-cp", proguardFiles.map { it.normalizedPath() }.joinToString(File.pathSeparator))
add("proguard.ProGuard")
// todo: consider separate flag
cliArg("-verbose", verbose)
cliArg("-include", rootConfigurationFile)
}

runExternalTool(
tool = javaBinary,
args = args,
environment = emptyMap(),
logToConsole = ExternalToolRunner.LogToConsole.Always
).assertNormalExitValue()
}

private fun Writer.writeLn(s: String) {
write(s)
write("\n")
}
}
50 changes: 50 additions & 0 deletions src/main/kotlin/org/jetbrains/intellij/tasks/AbstractTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.jetbrains.intellij.tasks

import org.gradle.api.DefaultTask
import org.gradle.api.file.Directory
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.internal.file.FileOperations
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.LocalState
import org.gradle.process.ExecOperations
import org.jetbrains.intellij.utils.ExternalToolRunner
import javax.inject.Inject

// TODO: AbstractTask from Compose plugin. Keep this or merge into AbstractProguardTask?
abstract class AbstractTask : DefaultTask() {
@get:Inject
protected abstract val objects: ObjectFactory

@get:Inject
protected abstract val providers: ProviderFactory
@get:Internal
val verbose: Property<Boolean> = objects.property(Boolean::class.java).apply {
set(providers.provider {
logger.isDebugEnabled
})
}

@get:Inject
protected abstract val execOperations: ExecOperations

@get:Inject
protected abstract val fileOperations: FileOperations

@get:LocalState
protected val logsDir: Provider<Directory> = project.layout.buildDirectory.dir("intellij/logs/$name")

@get:Internal
internal val runExternalTool: ExternalToolRunner
get() = ExternalToolRunner(verbose, logsDir, execOperations)

protected fun cleanDirs(vararg dirs: Provider<out FileSystemLocation>) {
for (dir in dirs) {
fileOperations.delete(dir)
fileOperations.mkdir(dir)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,9 @@ abstract class RunPluginVerifierTask @Inject constructor(
if (teamCityOutputFormat.get()) {
args.add("-team-city")
}
if (subsystemsToCheck.orNull != null) {
subsystemsToCheck.orNull?.let {
args.add("-subsystems-to-check")
args.add(subsystemsToCheck.get())
args.add(it)
}
if (offline.get()) {
args.add("-offline")
Expand Down
Loading