Skip to content

Commit

Permalink
Merge pull request #3 from joomcode/android-components
Browse files Browse the repository at this point in the history
AndroidComponents API support
  • Loading branch information
dkostyrev committed Jun 3, 2022
2 parents 47ee172 + a65dff3 commit ee88733
Show file tree
Hide file tree
Showing 25 changed files with 638 additions and 112 deletions.
11 changes: 8 additions & 3 deletions README.md
Expand Up @@ -29,6 +29,9 @@ apply plugin: 'com.joom.paranoid'
Now you can just annotate classes with strings that need to be obfuscated with `@Obfuscate`.
After you project compiles every string in annotated classes will be obfuscated.

When using Android Gradle Plugin 7.1.0 and later the plugin must be applied to every
Gradle project that has classes with `@Obfuscate` annotation.

Configuration
-------------
Paranoid plugin can be configured using `paranoid` extension object:
Expand All @@ -40,11 +43,13 @@ paranoid {
```

The extension object contains the following properties:
- `cacheable``boolean`. Allows to enable caching for the transform. Default value is `false`.
- `includeSubprojects``boolean`. Allows to enable obfuscation for subprojects. Default value is `false`.
- `obfuscationSeed` - `Integer`. A seed that can be used to make obfuscation stable across builds. Default value is `null`, which means that the seed
is computed from input files on each build.
- `applyToBuildTypes` - Allows to apply paranoid transform for specific build types. Possible values are `'ALL'`, `'NONE'`, `'NOT_DEBUGGABLE'`. Default value is `'ALL'`
- `cacheable``boolean`. Allows to enable caching for the transform. Default value is `false`. *Deprecated*. When using AGP 7.1.0 and later the transformation
is cacheable by default.
- `includeSubprojects``boolean`. Allows to enable obfuscation for subprojects. Default value is `false`. *Deprecated*. When using AGP 7.1.0 and later the plugin
*must* be applied to every subproject with classes annotated by `@Obfuscate` annotation.
- `enabled``boolean`. Allows to disable obfuscation for the project. Default value is `true`. *Deprecated*. Use `applyToBuildTypes = 'NONE'`

How it works
Expand Down Expand Up @@ -97,7 +102,7 @@ public class MainActivity extends AppCompatActivity {

License
=======
Copyright 2021 SIA Joom
Copyright 2022 SIA Joom

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Expand Up @@ -9,17 +9,20 @@ allprojects {
buildscript {
repositories {
google()
mavenLocal()
mavenCentral()
}

dependencies {
classpath "com.android.tools.build:gradle:$androidToolsVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.joom.paranoid:paranoid-gradle-plugin:$version"
}
}

repositories {
google()
mavenLocal()
mavenCentral()
}
}
1 change: 1 addition & 0 deletions gradle.properties
@@ -1,3 +1,4 @@
android.useAndroidX=true
development=true
pablo.shadow.enabled=false
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -Dfile.encoding=UTF-8
@@ -1,12 +1,21 @@
package com.joom.paranoid.plugin

import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.publishing.AndroidArtifacts
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.ArtifactCollection
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.artifacts.type.ArtifactTypeDefinition
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.compile.JavaCompile

val Project.sourceSets: SourceSetContainer
Expand All @@ -17,8 +26,27 @@ val Project.sourceSets: SourceSetContainer

val Project.hasAndroid: Boolean
get() = extensions.findByName("android") is BaseExtension
val Project.hasJava: Boolean
get() = extensions.findByType(JavaPluginExtension::class.java) != null
val Project.android: BaseExtension
get() = extensions.getByName("android") as BaseExtension
val Project.java: JavaPluginExtension
get() = extensions.getByType(JavaPluginExtension::class.java)

val Project.androidComponents: AndroidComponentsExtension<*, *, *>?
get() = applicationAndroidComponents ?: libraryAndroidComponents
val Project.applicationAndroidComponents: ApplicationAndroidComponentsExtension?
get() = extensions.findByName("androidComponents") as? ApplicationAndroidComponentsExtension
val Project.libraryAndroidComponents: LibraryAndroidComponentsExtension?
get() = extensions.findByName("androidComponents") as? LibraryAndroidComponentsExtension

inline fun <reified T : Task> Project.registerTask(name: String): TaskProvider<T> {
return tasks.register(name, T::class.java)
}

inline fun <reified T : Task> Project.getTaskByName(name: String): TaskProvider<out T> {
return tasks.named(name, T::class.java)
}

val SourceSetContainer.main: SourceSet
get() = getByName("main")
Expand All @@ -33,3 +61,18 @@ val TaskContainer.compileTestJava: JavaCompile
operator fun TaskContainer.get(name: String): Task? {
return findByName(name)
}

fun Configuration.incomingJarArtifacts(componentFilter: ((ComponentIdentifier) -> Boolean)? = null): ArtifactCollection {
return incoming
.artifactView { configuration ->
configuration.attributes { attributes ->
@Suppress("UnstableApiUsage")
attributes.attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, AndroidArtifacts.ArtifactType.CLASSES_JAR.type)
}

componentFilter?.let {
configuration.componentFilter(it)
}
}
.artifacts
}
Expand Up @@ -14,16 +14,14 @@
* limitations under the License.
*/

@file:Suppress("DEPRECATION")
package com.joom.paranoid.plugin

import com.android.build.api.transform.QualifiedContent
import java.io.File

object ObfuscationSeedCalculator {

fun calculate(inputs: List<QualifiedContent>): Int {
return inputs.maxLastModified { getLastModified(it.file) }.hashCode()
fun <T : Any> calculate(inputs: List<T>, fileSelector: (T) -> File): Int {
return inputs.maxLastModified { getLastModified(fileSelector(it)) }.hashCode()
}

private fun getLastModified(file: File): Long {
Expand Down
Expand Up @@ -16,22 +16,34 @@

package com.joom.paranoid.plugin

import java.io.File

import com.joom.paranoid.processor.logging.getLogger
import java.io.File
import kotlin.properties.Delegates
import kotlin.properties.ReadWriteProperty

open class ParanoidExtension {
@Deprecated(IS_ENABLED_DEPRECATION_WARNING)
var isEnabled: Boolean = true
set(value) {
getLogger().warn("WARNING: $IS_ENABLED_DEPRECATION_WARNING")
field = value
}
var isCacheable: Boolean = false
var includeSubprojects: Boolean = false

@Deprecated(IS_ENABLED_DEPRECATION_WARNING, replaceWith = ReplaceWith("applyToBuildTypes"))
var isEnabled: Boolean by deprecatedProperty(true, IS_ENABLED_DEPRECATION_WARNING) { enabled ->
if (enabled) applyToBuildTypes = BuildType.ALL else BuildType.NONE
}

@Deprecated(IS_CACHEABLE_DEPRECATION_WARNING)
var isCacheable: Boolean by deprecatedProperty(false, IS_CACHEABLE_DEPRECATION_WARNING)

@Deprecated(INCLUDE_SUBPROJECT_DEPRECATION_WARNING)
var includeSubprojects by deprecatedProperty(false, INCLUDE_SUBPROJECT_DEPRECATION_WARNING)

var obfuscationSeed: Int? = null
var bootClasspath: List<File> = emptyList()
var applyToBuildTypes: BuildType = BuildType.ALL

private inline fun <reified T : Any> deprecatedProperty(initial: T, message: String, crossinline onChange: (T) -> Unit = {}): ReadWriteProperty<Any?, T> {
return Delegates.observable(initial) { _, _, new ->
getLogger().warn("WARNING: $message")
onChange(new)
}
}
}

enum class BuildType {
Expand All @@ -40,4 +52,6 @@ enum class BuildType {
NOT_DEBUGGABLE
}

private const val IS_ENABLED_DEPRECATION_WARNING = "paranoid.enabled is deprecated. Use paranoid.applyToBuildTypes"
private const val IS_ENABLED_DEPRECATION_WARNING = "paranoid.enabled is deprecated. Use paranoid.applyToBuildTypes"
private const val IS_CACHEABLE_DEPRECATION_WARNING = "paranoid.isCacheable is deprecated"
private const val INCLUDE_SUBPROJECT_DEPRECATION_WARNING = "paranoid.includeSubprojects is deprecated"
Expand Up @@ -16,21 +16,141 @@

package com.joom.paranoid.plugin

import com.android.build.api.AndroidPluginVersion
import com.android.build.api.artifact.MultipleArtifact
import com.android.build.api.variant.Variant
import com.android.build.gradle.internal.scope.getDirectories
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.tasks.Jar

class ParanoidPlugin : Plugin<Project> {
private lateinit var project: Project
override fun apply(project: Project) {
this.project = project

val extension = project.extensions.create("paranoid", ParanoidExtension::class.java)
project.addDependencies(getDefaultConfiguration())

if (!project.hasAndroid && project.hasJava) {
registerParanoidForJava(extension)
return
}

if (!project.hasAndroid) {
throw GradleException("Paranoid plugin must be applied *AFTER* Android plugin")
}

val extension = project.extensions.create("paranoid", ParanoidExtension::class.java)
val transform = ParanoidTransform(extension)
val androidComponentsExtension = project.androidComponents
if (androidComponentsExtension != null && androidComponentsExtension.pluginVersion >= VARIANT_API_REQUIRED_VERSION) {
project.logger.info("Registering paranoid with variant API")

project.addDependencies(getDefaultConfiguration())
registerParanoidWithVariantApi(extension)
} else {
project.logger.info("Registering paranoid with transform API")

registerParanoidWithTransform(extension)
}
}

private fun registerParanoidForJava(extension: ParanoidExtension) {
if (extension.applyToBuildTypes == BuildType.NONE) {
return
}

val mainSourceSet = project.sourceSets.main
val compileTask = project.getTaskByName<JavaCompile>(mainSourceSet.compileJavaTaskName)
val taskProvider = project.registerTask<ParanoidTransformTask>(formatParanoidTaskName(project.name))
val input = mainSourceSet.output.classesDirs.getDirectories(project.layout.projectDirectory)
val output = project.layout.buildDirectory.dir("intermediates/paranoid/classes")
val runtimeClasspath = project.configurations.named(mainSourceSet.runtimeClasspathConfigurationName)
taskProvider.configure { task ->
val javaCompileTask = compileTask.get()
task.obfuscationSeed = extension.obfuscationSeed
task.bootClasspath.setFrom(javaCompileTask.options.bootstrapClasspath?.files.orEmpty())
task.classpath.setFrom(javaCompileTask.classpath)
task.validationClasspath.setFrom(runtimeClasspath.map { it.incomingJarArtifacts { it is ProjectComponentIdentifier }.artifactFiles })
task.inputClasses.set(input)
task.output.set(output)
}

project.getTaskByName<Jar>(JavaPlugin.JAR_TASK_NAME).configure { jar ->
val inputDirectories = lazy { input.get().map { it.asFile } }
jar.dependsOn(taskProvider)
jar.from(output)
jar.exclude { element ->
inputDirectories.value.any { element.file.startsWith(it) }
}
}
}

private fun registerParanoidWithVariantApi(extension: ParanoidExtension) {
if (extension.applyToBuildTypes == BuildType.NONE) {
return
}

project.applicationAndroidComponents?.apply {
onVariants { variant ->
if (extension.applyToBuildTypes == BuildType.NOT_DEBUGGABLE && variant.isDebuggable()) {
return@onVariants
}

variant.registerParanoidTransformTask(extension, validateClasspath = true)
}
}

project.libraryAndroidComponents?.apply {
onVariants { variant ->
if (extension.applyToBuildTypes == BuildType.NOT_DEBUGGABLE && variant.isDebuggable()) {
return@onVariants
}

variant.registerParanoidTransformTask(extension, validateClasspath = false)
}
}
}

private fun Variant.isDebuggable(): Boolean {
return buildType?.let { project.android.buildTypes.getByName(it) }?.isDebuggable ?: false
}

private fun Variant.registerParanoidTransformTask(
extension: ParanoidExtension,
validateClasspath: Boolean,
) {
val taskProvider = project.registerTask<ParanoidTransformTask>(formatParanoidTaskName(name))
@Suppress("UnstableApiUsage")
artifacts.use(taskProvider)
.wiredWith(ParanoidTransformTask::inputClasses, ParanoidTransformTask::output)
.toTransform(MultipleArtifact.ALL_CLASSES_DIRS)

val runtimeClasspath = project.configurations.getByName("${name}RuntimeClasspath")

taskProvider.configure { task ->
task.obfuscationSeed = extension.obfuscationSeed
task.validateClasspath = validateClasspath
task.validationClasspath.setFrom(
runtimeClasspath.incomingJarArtifacts { it is ProjectComponentIdentifier }.artifactFiles
)
task.classpath.setFrom(
runtimeClasspath.incomingJarArtifacts().artifactFiles
)

task.bootClasspath.setFrom(project.android.bootClasspath)
}
}

private fun formatParanoidTaskName(variantName: String): String {
return "${ParanoidTransformTask.TASK_PREFIX}${variantName.replaceFirstChar { it.uppercase() }}"
}

@Suppress("DEPRECATION")
private fun registerParanoidWithTransform(extension: ParanoidExtension) {
val transform = ParanoidTransform(extension)
project.android.registerTransform(transform)

project.afterEvaluate {
Expand All @@ -45,4 +165,8 @@ class ParanoidPlugin : Plugin<Project> {
private fun Project.addDependencies(configurationName: String) {
dependencies.add(configurationName, "com.joom.paranoid:paranoid-core:${Build.VERSION}")
}

private companion object {
private val VARIANT_API_REQUIRED_VERSION = AndroidPluginVersion(major = 7, minor = 2, micro = 0)
}
}

0 comments on commit ee88733

Please sign in to comment.