@@ -20,21 +20,18 @@ import com.android.build.api.artifact.ScopedArtifact
2020import com.android.build.api.variant.LibraryAndroidComponentsExtension
2121import com.android.build.api.variant.ScopedArtifacts
2222import com.android.build.gradle.LibraryPlugin
23- import java.io.BufferedInputStream
24- import java.io.BufferedOutputStream
23+ import com.google.firebase.gradle.plugins.license.LicenseResolverPlugin
2524import java.io.File
26- import java.io.FileInputStream
27- import java.io.FileOutputStream
28- import java.util.zip.ZipEntry
29- import java.util.zip.ZipFile
30- import java.util.zip.ZipOutputStream
3125import javax.inject.Inject
3226import org.gradle.api.DefaultTask
3327import org.gradle.api.GradleException
3428import org.gradle.api.Plugin
3529import org.gradle.api.Project
3630import org.gradle.api.artifacts.Configuration
31+ import org.gradle.api.file.ArchiveOperations
3732import org.gradle.api.file.Directory
33+ import org.gradle.api.file.FileSystemOperations
34+ import org.gradle.api.file.ProjectLayout
3835import org.gradle.api.file.RegularFile
3936import org.gradle.api.file.RegularFileProperty
4037import org.gradle.api.provider.ListProperty
@@ -46,43 +43,66 @@ import org.gradle.api.tasks.OutputFile
4643import org.gradle.api.tasks.TaskAction
4744import org.gradle.kotlin.dsl.apply
4845import org.gradle.kotlin.dsl.getByType
46+ import org.gradle.kotlin.dsl.register
47+ import org.gradle.kotlin.dsl.withType
4948import org.gradle.process.ExecOperations
49+ import org.jetbrains.kotlin.gradle.utils.extendsFrom
5050
51+ /* *
52+ * Gradle plugin for vendoring dependencies in an android library.
53+ *
54+ * We vendor dependencies by moving the dependency into the published package, and renaming all
55+ * imports to reference the vendored package.
56+ *
57+ * Registers the `vendor` configuration to be used for specifying vendored dependencies.
58+ *
59+ * Note that you should exclude any `java` or `javax` transitive dependencies, as `jarjar` (what we
60+ * use to do the actual vendoring) unconditionally skips them.
61+ *
62+ * ```
63+ * vendor("com.google.dagger:dagger:2.27") {
64+ * exclude(group = "javax.inject", module = "javax.inject")
65+ * }
66+ * ```
67+ *
68+ * @see VendorTask
69+ */
5170class VendorPlugin : Plugin <Project > {
5271 override fun apply (project : Project ) {
53- project.plugins.all {
54- when (this ) {
55- is LibraryPlugin -> configureAndroid(project)
56- }
57- }
72+ project.apply<LicenseResolverPlugin >()
73+ project.plugins.withType<LibraryPlugin >().configureEach { configureAndroid(project) }
5874 }
5975
60- fun configureAndroid (project : Project ) {
61- project.apply (plugin = " LicenseResolverPlugin" )
62-
63- val vendor = project.configurations.create(" vendor" )
64- project.configurations.all {
65- when (name) {
76+ private fun configureAndroid (project : Project ) {
77+ val vendor = project.configurations.register(" vendor" )
78+ val configurations =
79+ listOf (
6680 " releaseCompileOnly" ,
6781 " debugImplementation" ,
6882 " testImplementation" ,
69- " androidTestImplementation" -> extendsFrom(vendor)
70- }
83+ " androidTestImplementation" ,
84+ )
85+
86+ for (configuration in configurations) {
87+ project.configurations.named(configuration).extendsFrom(vendor)
7188 }
7289
73- val jarJar = project.configurations.create(" firebaseJarJarArtifact" )
74- project.dependencies.add(" firebaseJarJarArtifact" , " org.pantsbuild:jarjar:1.7.2" )
90+ val jarJarArtifact =
91+ project.configurations.register(" firebaseJarJarArtifact" ) {
92+ dependencies.add(project.dependencies.create(" org.pantsbuild:jarjar:1.7.2" ))
93+ }
7594
7695 val androidComponents = project.extensions.getByType<LibraryAndroidComponentsExtension >()
7796
78- androidComponents.onVariants(androidComponents.selector().withBuildType( " release " )) { variant ->
97+ androidComponents.onReleaseVariants {
7998 val vendorTask =
80- project.tasks.register(" ${variant .name} VendorTransform" , VendorTask :: class .java ) {
99+ project.tasks.register< VendorTask > (" ${it .name} VendorTransform" ) {
81100 vendorDependencies.set(vendor)
82- packageName.set(variant .namespace)
83- this . jarJar.set(jarJar )
101+ packageName.set(it .namespace)
102+ jarJar.set(jarJarArtifact )
84103 }
85- variant.artifacts
104+
105+ it.artifacts
86106 .forScope(ScopedArtifacts .Scope .PROJECT )
87107 .use(vendorTask)
88108 .toTransform(
@@ -95,67 +115,99 @@ class VendorPlugin : Plugin<Project> {
95115 }
96116}
97117
98- abstract class VendorTask @Inject constructor(private val execOperations : ExecOperations ) :
99- DefaultTask () {
118+ /* *
119+ * Executes the actual vendoring of a library.
120+ *
121+ * @see VendorPlugin
122+ */
123+ abstract class VendorTask
124+ @Inject
125+ constructor (
126+ private val exec: ExecOperations ,
127+ private val archive: ArchiveOperations ,
128+ private val fs: FileSystemOperations ,
129+ private val layout: ProjectLayout ,
130+ ) : DefaultTask () {
131+ /* * Dependencies that should be vendored. */
100132 @get: [InputFiles Classpath ]
101133 abstract val vendorDependencies: Property <Configuration >
102134
135+ /* * Configuration pointing to the `.jar` file for JarJar. */
103136 @get: [InputFiles Classpath ]
104137 abstract val jarJar: Property <Configuration >
105138
139+ /* *
140+ * The name of the package (or namespace) that we're vendoring for.
141+ *
142+ * We use this to rename the [vendorDependencies].
143+ */
106144 @get:Input abstract val packageName: Property <String >
107145
146+ /* * The jars generated for this package during a release. */
108147 @get:InputFiles abstract val inputJars: ListProperty <RegularFile >
109148
149+ /* * The directories generated for this package during a release. */
110150 @get:InputFiles abstract val inputDirs: ListProperty <Directory >
111151
152+ /* * The jar file to save the vendored artifact. */
112153 @get:OutputFile abstract val outputJar: RegularFileProperty
113154
114155 @TaskAction
115156 fun taskAction () {
116- val workDir = File .createTempFile(" vendorTmp" , null )
117- workDir.mkdirs()
118- workDir.deleteRecursively()
119-
120- val unzippedDir = File (workDir, " unzipped" )
121- val externalCodeDir = unzippedDir
157+ val unzippedDir = temporaryDir.childFile(" unzipped" )
122158
123- for (directory in inputDirs.get()) {
124- directory.asFile.copyRecursively(unzippedDir)
159+ logger.info(" Unpacking input directories" )
160+ fs.sync {
161+ from(inputDirs)
162+ into(unzippedDir)
125163 }
126- for (jar in inputJars.get()) {
127- unzipJar(jar.asFile, unzippedDir)
164+
165+ logger.info(" Unpacking input jars" )
166+ fs.copy {
167+ for (jar in inputJars.get()) {
168+ from(archive.zipTree(jar))
169+ }
170+ into(unzippedDir)
171+ exclude { it.path.contains(" META-INF" ) }
128172 }
129173
130- val ownPackageNames = inferPackages (unzippedDir)
174+ val ownPackageNames = inferPackageNames (unzippedDir)
131175
132- for (jar in vendorDependencies.get()) {
133- unzipJar(jar, externalCodeDir)
176+ logger.info(" Unpacking vendored files" )
177+ fs.copy {
178+ for (jar in vendorDependencies.get()) {
179+ from(archive.zipTree(jar))
180+ }
181+ into(unzippedDir)
182+ exclude { it.path.contains(" META-INF" ) }
134183 }
135- val externalPackageNames = inferPackages(externalCodeDir) subtract ownPackageNames
136- val java = File (externalCodeDir, " java" )
137- val javax = File (externalCodeDir, " javax" )
184+
185+ val externalPackageNames = inferPackageNames(unzippedDir) subtract ownPackageNames
186+ val java = unzippedDir.childFile(" java" )
187+ val javax = unzippedDir.childFile(" javax" )
138188 if (java.exists() || javax.exists()) {
139189 // JarJar unconditionally skips any classes whose package name starts with "java" or "javax".
190+ val dependencies = vendorDependencies.get().resolvedConfiguration.resolvedArtifacts
140191 throw GradleException (
141- " Vendoring java or javax packages is not supported. " +
142- " Please exclude one of the direct or transitive dependencies: \n " +
143- vendorDependencies
144- .get()
145- .resolvedConfiguration
146- .resolvedArtifacts
147- .joinToString(separator = " \n " )
192+ """
193+ |Vendoring java or javax packages is not supported.
194+ |Please exclude one of the direct or transitive dependencies:
195+ |${dependencies.joinToString(" \n " )}
196+ """
197+ .trimMargin()
148198 )
149199 }
150200
151- val jar = File (workDir, " intermediate.jar" )
152- zipAll(unzippedDir, jar)
153- transform(jar, ownPackageNames, externalPackageNames)
201+ val inputJar = temporaryDir.childFile(" intermediate.jar" )
202+ unzippedDir.zipFilesTo(inputJar)
203+
204+ transform(inputJar, ownPackageNames, externalPackageNames)
154205 }
155206
156- fun transform (inputJar : File , ownPackages : Set <String >, packagesToVendor : Set <String >) {
207+ private fun transform (inputJar : File , ownPackages : Set <String >, packagesToVendor : Set <String >) {
157208 val parentPackage = packageName.get()
158- val rulesFile = File .createTempFile(parentPackage, " .jarjar" )
209+ val rulesFile = temporaryDir.childFile(" $parentPackage .jarjar" )
210+
159211 rulesFile.printWriter().use {
160212 for (packageName in ownPackages) {
161213 it.println (" keep $packageName .**" )
@@ -164,12 +216,13 @@ abstract class VendorTask @Inject constructor(private val execOperations: ExecOp
164216 it.println (" rule $externalPackageName .** $parentPackage .@0" )
165217 }
166218 }
219+
167220 logger.info(" The following JarJar configuration will be used:\n ${rulesFile.readText()} " )
168221
169- execOperations
222+ exec
170223 .javaexec {
171224 mainClass.set(" org.pantsbuild.jarjar.Main" )
172- classpath = project .files(jarJar.get() )
225+ classpath = layout .files(jarJar)
173226 args =
174227 listOf (
175228 " process" ,
@@ -181,69 +234,13 @@ abstract class VendorTask @Inject constructor(private val execOperations: ExecOp
181234 }
182235 .assertNormalExitValue()
183236 }
184- }
185-
186- fun inferPackages (dir : File ): Set <String > {
187- return dir
188- .walk()
189- .filter { it.name.endsWith(" .class" ) }
190- .map { it.parentFile.toRelativeString(dir).replace(' /' , ' .' ) }
191- .toSet()
192- }
193-
194- fun unzipJar (jar : File , directory : File ) {
195- ZipFile (jar).use { zip ->
196- zip
197- .entries()
198- .asSequence()
199- .filter { ! it.isDirectory && ! it.name.startsWith(" META-INF" ) }
200- .forEach { entry ->
201- zip.getInputStream(entry).use { input ->
202- val entryFile = File (directory, entry.name)
203- entryFile.parentFile.mkdirs()
204- entryFile.outputStream().use { output -> input.copyTo(output) }
205- }
206- }
207- }
208- }
209-
210- fun zipAll (directory : File , zipFile : File ) {
211-
212- ZipOutputStream (BufferedOutputStream (FileOutputStream (zipFile))).use {
213- zipFiles(it, directory, " " )
214- }
215- }
216237
217- private fun zipFiles (zipOut : ZipOutputStream , sourceFile : File , parentDirPath : String ) {
218- val data = ByteArray (2048 )
219- sourceFile.listFiles()?.forEach { f ->
220- if (f.isDirectory) {
221- val path =
222- if (parentDirPath == " " ) {
223- f.name
224- } else {
225- parentDirPath + File .separator + f.name
226- }
227- // Call recursively to add files within this directory
228- zipFiles(zipOut, f, path)
229- } else {
230- FileInputStream (f).use { fi ->
231- BufferedInputStream (fi).use { origin ->
232- val path = parentDirPath + File .separator + f.name
233- val entry = ZipEntry (path)
234- entry.time = f.lastModified()
235- entry.isDirectory
236- entry.size = f.length()
237- zipOut.putNextEntry(entry)
238- while (true ) {
239- val readBytes = origin.read(data)
240- if (readBytes == - 1 ) {
241- break
242- }
243- zipOut.write(data, 0 , readBytes)
244- }
245- }
246- }
247- }
238+ /* * Given a directory of class files, constructs a list of all the class files. */
239+ private fun inferPackageNames (dir : File ): Set <String > {
240+ return dir
241+ .walk()
242+ .filter { it.name.endsWith(" .class" ) }
243+ .map { it.parentFile.toRelativeString(dir).replace(File .separator, " ." ) }
244+ .toSet()
248245 }
249246}
0 commit comments