Skip to content

Instantly share code, notes, and snippets.

@eaceto
Created August 30, 2024 11:05
Show Gist options
  • Select an option

  • Save eaceto/5a19ba57547316361bfb36d7db76f0cf to your computer and use it in GitHub Desktop.

Select an option

Save eaceto/5a19ba57547316361bfb36d7db76f0cf to your computer and use it in GitHub Desktop.
Shadow AAR (Gradle) dependencies plugin
package com.eaceto.android.plugins.shadow
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.DependencySet
import org.gradle.api.artifacts.ResolvedArtifact
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.Input
import org.gradle.kotlin.dsl.dependencies
import java.io.File
// Extension class to allow users to specify relocation rules for namespaces
abstract class AddShadowedAARDependenciesPluginExtension {
// List of namespace pairs to be relocated (from -> to)
@get:Input
abstract val relocations: ListProperty<List<String>>
}
// Main plugin class
class AddShadowedAARDependenciesPlugin : Plugin<Project> {
// Constants for configuration and task names
private val SHADOW_AAR_EXTENSION_NAME = "shadowAarDependencies"
private val SHADOW_AAR_CONFIGURATION_NAME = "shadowAARImplementation"
private val UNZIP_AAR_DIR = "unzip-aars"
private val SHADOW_JAR_TASK_NAME = "relocateImplementationJARs"
private val ANDROID_ARTIFACT_EXTENSION = "aar"
private val JAVA_ARTIFACT_EXTENSION = "jar"
private val JAR_IN_AAR_FILE_NAME = "classes.jar"
// Checks if the artifact is an Android AAR package
private fun isAndroidArtifact(artifact: ResolvedArtifact): Boolean =
artifact.extension == ANDROID_ARTIFACT_EXTENSION
// Checks if the artifact is a Java JAR file
private fun isJavaArtifact(artifact: ResolvedArtifact): Boolean =
artifact.extension == JAVA_ARTIFACT_EXTENSION
// Checks if the artifact belongs to a specific group
private fun isFromGroups(artifact: ResolvedArtifact, groups: List<String?>): Boolean =
groups.any { artifact.moduleVersion.id.group == it }
// Filters AAR artifacts that belong to the top-level dependency groups
private fun getAarsWithTopLevelDependencyGroups(
artifacts: Set<ResolvedArtifact>,
topLevelDeps: DependencySet
): List<ResolvedArtifact> {
val topLevelDepGroups = topLevelDeps.map { it.group }
return artifacts.filter {
isAndroidArtifact(it) && isFromGroups(it, topLevelDepGroups)
}
}
// Filters JAR artifacts that belong to the top-level dependency groups
private fun getJarsWithTopLevelDependencyGroups(
artifacts: Set<ResolvedArtifact>,
topLevelDeps: DependencySet
): List<ResolvedArtifact> {
val topLevelDepGroups = topLevelDeps.map { it.group }
return artifacts.filter {
isJavaArtifact(it) && isFromGroups(it, topLevelDepGroups)
}
}
// Extracts the classes.jar from an AAR file and renames it
private fun extractJarFromAar(project: Project, targetDir: File, aar: ResolvedArtifact): File {
val jarName = "${aar.moduleVersion.id.name}.$JAVA_ARTIFACT_EXTENSION"
project.copy {
from(project.zipTree(aar.file)) {
rename(JAR_IN_AAR_FILE_NAME, jarName)
}
into(targetDir)
}
return targetDir.resolve(jarName)
}
// Generates a fully qualified dependency coordinate string (group:name:version)
private fun getFullyQualifiedDependencyCoordinates(artifact: ResolvedArtifact): String {
val version = artifact.moduleVersion.id.version
val group = artifact.moduleVersion.id.group
val name = artifact.moduleVersion.id.name
return "$group:$name:$version"
}
// The entry point for the plugin, where configurations and tasks are created
override fun apply(project: Project) {
// Create an extension to allow users to configure relocation rules
val extension = project.extensions.create(
SHADOW_AAR_EXTENSION_NAME,
AddShadowedAARDependenciesPluginExtension::class.java
)
// Create a custom configuration to handle AAR dependencies that need to be relocated
val aarConfig = project.configurations.create(SHADOW_AAR_CONFIGURATION_NAME)
// Get the existing 'implementation' configuration
val implementationConfig = project.configurations.getByName("implementation")
// After the project is evaluated (configured), execute the following logic
project.afterEvaluate {
val unzipDir = project.mkdir(UNZIP_AAR_DIR)
val relocations = extension.relocations.get()
// Resolve all artifacts for the AAR configuration
val allArtifacts = aarConfig.resolvedConfiguration.resolvedArtifacts
val aarTopLevelDeps = aarConfig.dependencies
// Get the AAR and JAR dependencies that belong to the top-level groups
val aarDepsForRelocation = getAarsWithTopLevelDependencyGroups(allArtifacts, aarTopLevelDeps)
val jarDepsForRelocation = getJarsWithTopLevelDependencyGroups(allArtifacts, aarTopLevelDeps)
// Extract JAR files from AARs and combine them with JAR dependencies
val allJarsForRelocation = aarDepsForRelocation.map {
extractJarFromAar(project, unzipDir, it)
} + jarDepsForRelocation.map { it.file }
// Register a ShadowJar task to relocate namespaces in the extracted JAR files
val task = project.tasks.register(SHADOW_JAR_TASK_NAME, ShadowJar::class.java) {
from(allJarsForRelocation)
relocations.forEach {
val (namespaceFrom, namespaceTo) = it
relocate(namespaceFrom, namespaceTo)
}
}
// Add non-relocated dependencies to the 'implementation' configuration
project.dependencies {
val artifactsNotInNeedOfRelocation = allArtifacts - aarDepsForRelocation.toSet() - jarDepsForRelocation.toSet()
artifactsNotInNeedOfRelocation.forEach {
val depCoords = getFullyQualifiedDependencyCoordinates(it)
add(implementationConfig.name, depCoords)
}
// Add the relocated JARs to the 'implementation' configuration
val relocatedJars = task.get().outputs.files
add(implementationConfig.name, relocatedJars)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment