Skip to content

Commit

Permalink
feat: support R8 or ProgGuard available in the classpath (bundled) an…
Browse files Browse the repository at this point in the history
…d support loading in the classpath as a more stable alternative to external run

Signed-off-by: Artyom Shendrik <[email protected]>
  • Loading branch information
amal committed Feb 1, 2024
1 parent 62607bd commit 563c97d
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 39 deletions.
3 changes: 0 additions & 3 deletions checks/gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,4 @@ setupGradlePlugin {
allWarningsAsErrors = true
shrinkArtifacts = true
shrinkingConfig.useBothShrinkers = true

// FIXME: Verify to work on CI under 3 main OSes
// shrinkingConfig.useBothShrinkers.set(true)
}
10 changes: 2 additions & 8 deletions fluxo-kmp-conf/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ArgumentListWrapping:LoadAndApplyPluginIfNotApplied.kt$("Found plugin '$pluginId' class on the classpath for '${project.path}': $className")</ID>
<ID>ComplexCondition:SetupTestsReport.kt$!enabled || mergedReportTask == null || mergedReportService == null || !isTestTaskAllowed()</ID>
<ID>CyclomaticComplexMethod:LoadAndApplyPluginIfNotApplied.kt$private fun FluxoKmpConfContext.loadPluginArtifactAndGetClass( pluginId: String, pluginVersion: String?, className: String?, catalogPluginAlias: String?, lookupClassName: Boolean, canLoadDynamically: Boolean, project: Project, ): Class&lt;*>?</ID>
<ID>CyclomaticComplexMethod:SetupAndroidLint.kt$internal fun Project.setupAndroidLint( conf: FluxoConfigurationExtensionImpl, ignoredBuildTypes: List&lt;String>, ignoredFlavors: List&lt;String>, )</ID>
<ID>ForbiddenComment:AbstractShrinkerTask.kt$// FIXME: Before Java 9, the runtime classes were packaged in a single jar file.</ID>
<ID>ForbiddenComment:AbstractShrinkerTask.kt$// TODO: Move into a separate file, loaded from resources</ID>
<ID>ForbiddenComment:AbstractShrinkerTask.kt$AbstractShrinkerTask$// FIXME: Move mapping to the output dir?</ID>
<ID>ForbiddenComment:AbstractShrinkerTask.kt$AbstractShrinkerTask$// FIXME: Print the version of the bundled shrinker.</ID>
<ID>ForbiddenComment:AbstractShrinkerTask.kt$AbstractShrinkerTask$// FIXME: When java toolchain used or JDK target is specified, read the specified JDK.</ID>
<ID>ForbiddenComment:AbstractShrinkerTask.kt$AbstractShrinkerTask$// TODO: Automatic main class detection from jar manifest and/or project configuration</ID>
<ID>ForbiddenComment:AbstractShrinkerTask.kt$AbstractShrinkerTask$// TODO: Can be cached for a JDK.</ID>
<ID>ForbiddenComment:AbstractShrinkerTask.kt$AbstractShrinkerTask$// TODO: Debugging</ID>
<ID>ForbiddenComment:AbstractShrinkerTask.kt$AbstractShrinkerTask$// TODO: Process output and print only main information if not verbose</ID>
Expand Down Expand Up @@ -109,20 +109,14 @@
<ID>Indentation:DependencyUpdatesPlugin.kt.kt$ </ID>
<ID>Indentation:FluxoConfigurationExtensionAndroidImpl.kt$FluxoConfigurationExtensionAndroidImpl$ </ID>
<ID>Indentation:PropsAndEnv.kt$ </ID>
<ID>LongMethod:LoadAndApplyPluginIfNotApplied.kt$private fun FluxoKmpConfContext.loadPluginArtifactAndGetClass( pluginId: String, pluginVersion: String?, className: String?, catalogPluginAlias: String?, lookupClassName: Boolean, canLoadDynamically: Boolean, project: Project, ): Class&lt;*>?</ID>
<ID>LongMethod:SetupAndroidLint.kt$internal fun Project.setupAndroidLint( conf: FluxoConfigurationExtensionImpl, ignoredBuildTypes: List&lt;String>, ignoredFlavors: List&lt;String>, )</ID>
<ID>LongMethod:SetupTestsReport.kt$internal fun FluxoKmpConfContext.setupTestsReport()</ID>
<ID>LongMethod:TestReportsMergeTask.kt$TestReportsMergeTask$@TaskAction fun merge()</ID>
<ID>MaxLineLength:DependencyGuardPlugin.kt$/** @see com.dropbox.gradle.plugins.dependencyguard.internal.ConfigurationValidators.isClasspathConfig */</ID>
<ID>MaxLineLength:LoadAndApplyPluginIfNotApplied.kt$"You may want to add it to the classpath in the root build.gradle.kts instead! $example"</ID>
<ID>MaxLineLength:LoadAndApplyPluginIfNotApplied.kt$logger.v("Found plugin '$pluginId' class on the classpath for '${project.path}': $className")</ID>
<ID>MaxLineLength:PropsAndEnv.kt$if</ID>
<ID>MaxLineLength:SetupMultiplatform.kt$*</ID>
<ID>MaximumLineLength:LoadAndApplyPluginIfNotApplied.kt$ </ID>
<ID>MaximumLineLength:PropsAndEnv.kt$ </ID>
<ID>NestedBlockDepth:KotlinSourceSetsReportTask.kt$KotlinSourceSetsReportTask.KotlinSourceSetsModel$fun buildTrees(): List&lt;GraphNode></ID>
<ID>NestedBlockDepth:LoadAndApplyPluginIfNotApplied.kt$private fun FluxoKmpConfContext.getPluginIdAndVersion( id: String, version: String?, catalogPluginIds: Array&lt;out String>? = null, catalogPluginId: String?, logger: Logger, catalogVersionIds: Array&lt;out String>?, catalogVersionId: String?, ): Triple&lt;String, String?, String?></ID>
<ID>NestedBlockDepth:LoadAndApplyPluginIfNotApplied.kt$private fun FluxoKmpConfContext.loadPluginArtifactAndGetClass( pluginId: String, pluginVersion: String?, className: String?, catalogPluginAlias: String?, lookupClassName: Boolean, canLoadDynamically: Boolean, project: Project, ): Class&lt;*>?</ID>
<ID>NestedBlockDepth:TestReportsMergeTask.kt$TestReportsMergeTask$@TaskAction fun merge()</ID>
<ID>ReturnCount:KotlinSourceSetsReportTask.kt$KotlinSourceSetsReportTask.KotlinSourceSetsModel$private fun GraphNode.isReachableThrough(node: GraphNode): Boolean</ID>
<ID>ReturnCount:KotlinSourceSetsReportTask.kt$KotlinSourceSetsReportTask.SourceSetNodeRenderer$context(StyledTextOutput) @Suppress("CyclomaticComplexMethod") override fun renderAttrs(node: RenderableNode, parent: RenderableNode?)</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ internal abstract class FluxoKmpConfContext
// https://r8.googlesource.com/r8/+refs
// https://issuetracker.google.com/issues/193543616#comment4
// https://mvnrepository.com/artifact/com.android.tools/r8
/** Cannot use [com.android.tools.r8.Version.LABEL] directly here
* as it will be inlined during compilation. */
val r8 = com.android.tools.r8.Version.getVersionString().substringBefore(" (")
m += ", Bundled R8 $r8"
} catch (_: Throwable) {
Expand Down Expand Up @@ -160,10 +162,13 @@ internal abstract class FluxoKmpConfContext
taskGraphBasedProjectSyncDetection()
}

val includedBuilds = gradle.includedBuilds.size
if (isInCompositeBuild) {
val includedBuilds = gradle.includedBuilds.size
logger.l("COMPOSITE build is enabled! ($includedBuilds includedBuilds)")
logger.l("COMPOSITE build is ENABLED! ($includedBuilds includedBuilds)")
} else if (isVerbose) {
logger.l("COMPOSITE build is disabled! ($includedBuilds includedBuilds)")
}

if (isCI) logger.l("CI mode is enabled!")
if (isRelease) logger.l("RELEASE mode is enabled!")
if (composeMetricsEnabled) logger.l("COMPOSE_METRICS are enabled!")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ internal fun FluxoKmpConfContext.loadAndApplyPluginIfNotApplied(
return ApplyPluginResult(applied = true, pluginClass = pluginClass, id = id)
}

@Suppress("CyclomaticComplexMethod", "LongMethod", "NestedBlockDepth")
private fun FluxoKmpConfContext.loadPluginArtifactAndGetClass(
pluginId: String,
pluginVersion: String?,
Expand Down Expand Up @@ -104,7 +105,9 @@ private fun FluxoKmpConfContext.loadPluginArtifactAndGetClass(
// Try the classpath first.
var pluginClass: Class<*>? = tryGetClassForName(className)
if (pluginClass != null) {
logger.v("Found plugin '$pluginId' class on the classpath for '${project.path}': $className")
logger.v(
"Found plugin '$pluginId' class on the classpath for '${project.path}': $className",
)
return pluginClass
}
if (lookupClassName && classNames.isEmpty()) {
Expand Down Expand Up @@ -144,9 +147,11 @@ private fun FluxoKmpConfContext.loadPluginArtifactAndGetClass(
for (name in classNames) {
pluginClass = Class.forName(className, true, classLoader)
if (pluginClass != null) {
val confFile = "build.gradle.kts"
val warn = "Dynamically loaded plugin '$pluginId'" +
" in '${project.path}' from [$coords]$detected.\n " +
"You may want to add it to the classpath in the root build.gradle.kts instead! $example"
"You may want to add it to the classpath in the root $confFile instead! " +
example
logger.w(warn)
return pluginClass
}
Expand Down Expand Up @@ -190,17 +195,21 @@ private fun ClassLoader.findPluginClassNames(

private const val IMPLEMENTATION_CLASS = "implementation-class="

private fun tryGetClassForName(className: String?): Class<*>? {
internal fun tryGetClassForName(className: String?, classLoader: ClassLoader? = null): Class<*>? {
if (className.isNullOrEmpty()) {
return null
}
return try {
Class.forName(className)
when (classLoader) {
null -> Class.forName(className)
else -> Class.forName(className, true, classLoader)
}
} catch (_: ClassNotFoundException) {
null
}
}

@Suppress("NestedBlockDepth")
private fun FluxoKmpConfContext.getPluginIdAndVersion(
id: String,
version: String?,
Expand Down
82 changes: 63 additions & 19 deletions fluxo-kmp-conf/src/main/kotlin/fluxo/shrink/AbstractShrinkerTask.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
@file:Suppress("KDocUnresolvedReference")
@file:Suppress(
"KDocUnresolvedReference",
"LeakingThis",
"LongParameterList",
"LongParameterList",
"NestedBlockDepth",
"TooManyFunctions",
)

package fluxo.shrink

Expand Down Expand Up @@ -54,7 +61,6 @@ import org.gradle.language.base.plugins.LifecycleBasePlugin
* @see proguard.gradle.ProGuardTask
*/
@CacheableTask
@Suppress("LeakingThis", "TooManyFunctions")
internal abstract class AbstractShrinkerTask : AbstractExternalFluxoTask() {

@get:Input
Expand Down Expand Up @@ -132,6 +138,14 @@ internal abstract class AbstractShrinkerTask : AbstractExternalFluxoTask() {
@get:Optional
val androidMinSdk: Property<Int?> = objects.nullableProperty()

@get:Input
@get:Optional
val forceUnbundledShrinker: Property<Boolean> = objects.notNullProperty(false)

@get:Input
@get:Optional
val forceExternalShrinkerRun: Property<Boolean> = objects.notNullProperty(false)

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

Expand Down Expand Up @@ -252,22 +266,49 @@ internal abstract class AbstractShrinkerTask : AbstractExternalFluxoTask() {
val rootConfigFile = workingDir.file("root-config.pro").asFile
writeRootConfiguration(rootConfigFile, reportsDir, jarsConfFile, shrinker)

// 1. Try to use the bundled ProGuard/R8 first.
// 2. Fallback to custom classloader.
// 3. Call as a separate process.
val forceExternalShrinkerRun = forceExternalShrinkerRun.get()
val start = currentTimeMillis()
val called = when (forceExternalShrinkerRun) {
true -> false
else -> ShrinkerReflectiveCaller(
shrinker = shrinker,
logger = logger,
toolJars = toolJars,
forceUnbundledShrinker = forceUnbundledShrinker.get(),
) { getShrinkerArgs(rootConfigFile, outJars, reportsDir, javaHome, external = false) }
.execute()
}
if (!called) {
val l = if (forceExternalShrinkerRun) " (forceExternal)" else ""
logger.w("Calling $shrinker externally! $l")
callShrikerExternally(javaHome, rootConfigFile, outJars, reportsDir)
}

@Suppress("MagicNumber")
val elapsedSec = (currentTimeMillis() - start) / 1_000f
reportSavings(destinationDir, initialSize, elapsedSec)
}

private fun callShrikerExternally(
javaHome: File,
rootConfigFile: File?,
outJars: MutableList<File>,
reportsDir: Directory,
) {
val workDir = workDir.ioFileOrNull
val javaBinary = jvmToolFile(toolName = "java", javaHome = javaHome)
val args = getShrinkerArgs(rootConfigFile, outJars, reportsDir, javaHome, workDir)

// TODO: Process output and print only main information if not verbose
val start = currentTimeMillis()
val javaBinary = jvmToolFile(toolName = "java", javaHome = javaHome)
runExternalTool(
tool = javaBinary,
args = args,
workingDir = workDir,
logToConsole = ExternalToolRunner.LogToConsole.OnlyWhenVerbose,
).assertNormalExitValue()

@Suppress("MagicNumber")
val elapsedSec = (currentTimeMillis() - start) / 1_000f
reportSavings(destinationDir, initialSize, elapsedSec)
}

private fun reportSavings(destinationDir: File, initialSize: Long, elapsedSec: Float) {
Expand Down Expand Up @@ -310,19 +351,22 @@ internal abstract class AbstractShrinkerTask : AbstractExternalFluxoTask() {
outJars: List<File>,
reportsDir: Directory,
javaHome: File,
workDir: File?,
workDir: File? = null,
external: Boolean = true,
): List<String> {
return arrayListOf<String>().apply {
maxHeapSize.orNull?.let {
add("-Xmx:$it")
}
if (vmOptions.get()) {
add("-XX:+TieredCompilation")
if (external) {
maxHeapSize.orNull?.let {
add("-Xmx:$it")
}
if (vmOptions.get()) {
add("-XX:+TieredCompilation")
}
cliArg(
"-cp",
toolJars.joinToString(File.pathSeparator) { it.normalizedPath() },
)
}
cliArg(
"-cp",
toolJars.joinToString(File.pathSeparator) { it.normalizedPath() },
)

when (checkNotNull(shrinker.get())) {
ProGuard -> {
Expand Down Expand Up @@ -421,6 +465,7 @@ internal abstract class AbstractShrinkerTask : AbstractExternalFluxoTask() {
// -dump
// -verbose

// TODO: Automatic main class detection from jar manifest and/or project configuration
mainClass.orNull?.let { mainClass ->
writer.ln(
"""
Expand Down Expand Up @@ -454,7 +499,6 @@ internal abstract class AbstractShrinkerTask : AbstractExternalFluxoTask() {
private fun getFinalConfigFile(reportsDir: Directory, base: File? = null): String =
reportsDir.file("final-config.pro").asFile.normalizedPath(base)

@Suppress("LongParameterList", "NestedBlockDepth")
private fun writeJarsConfigurationFile(
file: File,
inputToOutputJars: LinkedHashMap<File, File?>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,19 @@ public interface FluxoShrinkerConfig {
public val useBothShrinkers: Property<Boolean>

/**
* Whether to use extrenal R8 jar instead of the one bundled/avaliable in the classpath.
* Whether to always use external R8 jar instead of the one bundled/avaliable in the classpath.
*
* `false` by default.
*
* @TODO: Support R8 or ProgGuard available in the classpath (bundled).
*/
public val forceUnbundledShrinker: Property<Boolean>

/**
* Whether to always use external CLI run of R8 or ProGuard.
*
* `false` by default.
*/
public val forceExternalShrinkerRun: Property<Boolean>

/**
* `true` by default.
*
Expand Down Expand Up @@ -123,6 +128,7 @@ internal abstract class FluxoShrinkerConfigImpl @Inject constructor(
override val r8FullMode: Property<Boolean> = objects.notNullProperty(false)
override val useBothShrinkers: Property<Boolean> = objects.notNullProperty(false)
override val forceUnbundledShrinker: Property<Boolean> = objects.notNullProperty(false)
override val forceExternalShrinkerRun: Property<Boolean> = objects.notNullProperty(false)
override val autoGenerateKeepRulesFromApis: Property<Boolean> = objects.notNullProperty(true)
override val maxHeapSize: Property<String?> = objects.nullableProperty()
override val autoGenerateKeepModifiers: Property<String> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ internal fun Project.registerShrinkerTask(
dontoptimize.set(settings.optimize.map { !it })

maxHeapSize.set(settings.maxHeapSize)
forceUnbundledShrinker.set(settings.forceUnbundledShrinker)
forceExternalShrinkerRun.set(settings.forceExternalShrinkerRun)

(conf.androidMinSdk as? Int)?.let { androidMinSdk.set(it) }
conf.kotlinConfig.jvmTarget?.let { jvmTarget.set(it) }
Expand Down
Loading

0 comments on commit 563c97d

Please sign in to comment.