1 /*
<lambda>null2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.build
18 
19 import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
20 import groovy.lang.Closure
21 import org.gradle.api.Plugin
22 import org.gradle.api.Project
23 import org.gradle.api.plugins.JavaLibraryPlugin
24 import org.gradle.api.provider.Property
25 import org.gradle.api.tasks.SourceSetContainer
26 import org.gradle.api.tasks.TaskProvider
27 import org.gradle.jvm.tasks.Jar
28 import org.gradle.kotlin.dsl.create
29 import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin
30 
31 /**
32  * Plugin responsible for repackaging libraries. The plugin repackages what is set in the
33  * [RelocationExtension] by the user and reconfigures the JAR task to output the repackaged classes
34  * JAR.
35  */
36 @Suppress("unused")
37 class AndroidXRepackageImplPlugin : Plugin<Project> {
38 
39     override fun apply(project: Project) {
40         val relocationExtension =
41             project.extensions.create<RelocationExtension>(EXTENSION_NAME, project)
42         project.plugins.configureEach { plugin ->
43             when (plugin) {
44                 is JavaLibraryPlugin,
45                 is KotlinBasePlugin -> project.configureJavaOrKotlinLibrary(relocationExtension)
46             }
47         }
48     }
49 
50     private fun Project.configureJavaOrKotlinLibrary(relocationExtension: RelocationExtension) {
51         createConfigurations()
52 
53         val sourceSets = extensions.getByType(SourceSetContainer::class.java)
54         val libraryShadowJar =
55             tasks.register("shadowLibraryJar", ShadowJar::class.java) { task ->
56                 task.transformers.add(
57                     BundleInsideHelper.DontIncludeResourceTransformer().apply {
58                         dropResourcesWithSuffix = ".proto"
59                     }
60                 )
61                 task.transformers.add(
62                     BundleInsideHelper.DontIncludeResourceTransformer().apply {
63                         dropResourcesWithSuffix = ".proto.bin"
64                     }
65                 )
66                 task.from(sourceSets.named("main").map { it.output })
67                 relocationExtension.getRelocations().forEach {
68                     task.relocate(it.sourcePackage, it.targetPackage)
69                 }
70                 relocationExtension.artifactId.orNull?.let {
71                     task.configurations = listOf(configurations.getByName("repackageClasspath"))
72                 }
73             }
74         addArchiveToVariants(libraryShadowJar)
75     }
76 
77     private fun Project.createConfigurations() {
78         val repackage =
79             configurations.create("repackage") { config ->
80                 config.isCanBeConsumed = false
81                 config.isCanBeResolved = false
82             }
83 
84         configurations.create("repackageClasspath") { config ->
85             config.isCanBeConsumed = false
86             config.isCanBeResolved = true
87             config.extendsFrom(repackage)
88         }
89 
90         tasks.named("jar", Jar::class.java) {
91             // We cannot have two tasks with the same output as the ListTaskOutputsTask will fail.
92             // As we want the repackaged jar as the published artifact, we change the
93             // name of classifier of the JAR task
94             it.archiveClassifier.set("before-shadow")
95         }
96 
97         forceJarUsageForAndroid()
98     }
99 
100     /**
101      * This forces the use of repackaged JARs as opposed to the java-classes-directory for Android.
102      * Without this, AGP uses the artifacts in java-classes-directory, which do not have the classes
103      * repackaged to the target package.
104      *
105      * We attempted to extract the contents of the repackaged library JAR into classes/java/main,
106      * but the AGP transform depends on JavaCompile. We cannot make JavaCompile depend on the task
107      * that creates the shadowed library as that would result in a circular dependency.
108      */
109     private fun Project.forceJarUsageForAndroid() =
110         configurations.configureEach { configuration ->
111             if (configuration.name == "runtimeElements") {
112                 configuration.outgoing.variants.removeIf { it.name == "classes" }
113             }
114         }
115 
116     private fun Project.addArchiveToVariants(task: TaskProvider<ShadowJar>) =
117         configurations.configureEach { configuration ->
118             if (configuration.name == "apiElements" || configuration.name == "runtimeElements") {
119                 configuration.outgoing.artifacts.clear()
120                 configuration.outgoing.artifact(task)
121             }
122         }
123 
124     companion object {
125         const val EXTENSION_NAME = "repackage"
126     }
127 }
128 
129 class Relocation {
130     /* The package name and any import statements for a class that are to be relocated. */
131     var sourcePackage: String? = null
132 
133     /* The package name and any import statements for a class to which they should be relocated. */
134     var targetPackage: String? = null
135 }
136 
137 abstract class RelocationExtension(val project: Project) {
138 
139     private var relocations: MutableCollection<Relocation> = ArrayList()
140 
addRelocationnull141     fun addRelocation(closure: Closure<Any>): Relocation {
142         val relocation = project.configure(Relocation(), closure) as Relocation
143         relocations.add(relocation)
144         return relocation
145     }
146 
getRelocationsnull147     fun getRelocations(): Collection<Relocation> {
148         return relocations
149     }
150 
151     /* Optional artifact id if the user wants to publish the dependency in the shadowed config. */
152     abstract val artifactId: Property<String?>
153 }
154