Skip to content

Commit 2843bea

Browse files
authored
Add dependency vendoring support. (#1821)
* Add dependency vendoring support. The `VendorPlugin`(applied with `id 'firebase-vendor'`) adds a dedicated `vendor` gradle configuration to the project, which can be used to include the dependencies in the output library. Such dependencies are are shaded under the library's package name to avoid symbol collisions. Example use: ```kotlin plugins { id("com.android.library") id("firebase-vendor") } android { // ... } dependencies { implementation("com.example:somelib:1.0") // this will make this library available at compile time as well as // will vendor it inside the produced aar under `com.mylib.com.example`. vendor("com.example:libtovendor:1.0") { // IMPORTANT: it the library (transitively) depends on any library that contains `javax` or `java` packages, it must be excluded here and added as a pom dependency below. exclude("javax.inject", "javax.inject") } implementation("javax.inject:javax.inject:1") } ``` * Fix dep * Address review comments.
1 parent 8c187fd commit 2843bea

File tree

6 files changed

+479
-9
lines changed

6 files changed

+479
-9
lines changed

buildSrc/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ gradlePlugin {
7575
id = "firebase-java-library"
7676
implementationClass = 'com.google.firebase.gradle.plugins.FirebaseJavaLibraryPlugin'
7777
}
78+
79+
firebaseVendorPlugin {
80+
id = "firebase-vendor"
81+
implementationClass = 'com.google.firebase.gradle.plugins.VendorPlugin'
82+
}
7883
}
7984
}
8085

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.gradle.plugins
16+
17+
import com.android.build.api.transform.Format
18+
import com.android.build.api.transform.QualifiedContent
19+
import com.android.build.api.transform.Transform
20+
import com.android.build.api.transform.TransformInvocation
21+
import com.android.build.gradle.LibraryExtension
22+
import com.android.build.gradle.LibraryPlugin
23+
import java.io.BufferedInputStream
24+
import java.io.BufferedOutputStream
25+
import 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
31+
import org.gradle.api.GradleException
32+
import org.gradle.api.Plugin
33+
import org.gradle.api.Project
34+
import org.gradle.api.artifacts.Configuration
35+
import org.gradle.api.logging.Logger
36+
37+
class VendorPlugin : Plugin<Project> {
38+
override fun apply(project: Project) {
39+
project.plugins.all {
40+
when (this) {
41+
is LibraryPlugin -> configureAndroid(project)
42+
}
43+
}
44+
}
45+
46+
fun configureAndroid(project: Project) {
47+
48+
val vendor = project.configurations.create("vendor")
49+
project.configurations.all {
50+
when (name) {
51+
"compileOnly", "testImplementation", "androidTestImplementation" -> extendsFrom(vendor)
52+
}
53+
}
54+
55+
val jarJar = project.configurations.create("firebaseJarJarArtifact")
56+
project.dependencies.add("firebaseJarJarArtifact", "org.pantsbuild:jarjar:1.7.2")
57+
58+
val android = project.extensions.getByType(LibraryExtension::class.java)
59+
60+
android.registerTransform(VendorTransform(
61+
android,
62+
vendor,
63+
JarJarTransformer(
64+
parentPackageProvider = {
65+
android.libraryVariants.find { it.name == "release" }!!.applicationId
66+
},
67+
jarJarProvider = { jarJar.resolve() },
68+
project = project,
69+
logger = project.logger),
70+
logger = project.logger))
71+
}
72+
}
73+
74+
interface JarTransformer {
75+
fun transform(inputJar: File, outputJar: File, packagesToVendor: Set<String>)
76+
}
77+
78+
class JarJarTransformer(
79+
private val parentPackageProvider: () -> String,
80+
private val jarJarProvider: () -> Collection<File>,
81+
private val project: Project,
82+
private val logger: Logger
83+
) : JarTransformer {
84+
override fun transform(inputJar: File, outputJar: File, packagesToVendor: Set<String>) {
85+
val parentPackage = parentPackageProvider()
86+
val rulesFile = File.createTempFile(parentPackage, ".jarjar")
87+
rulesFile.printWriter().use {
88+
for (externalPackageName in packagesToVendor) {
89+
it.println("rule $externalPackageName.** $parentPackage.@0")
90+
}
91+
}
92+
logger.info("The following JarJar configuration will be used:\n ${rulesFile.readText()}")
93+
94+
project.javaexec {
95+
main = "org.pantsbuild.jarjar.Main"
96+
classpath = project.files(jarJarProvider())
97+
args = listOf("process", rulesFile.absolutePath, inputJar.absolutePath, outputJar.absolutePath)
98+
systemProperties = mapOf("verbose" to "true", "misplacedClassStrategy" to "FATAL")
99+
}.assertNormalExitValue()
100+
}
101+
}
102+
103+
class VendorTransform(
104+
private val android: LibraryExtension,
105+
private val configuration: Configuration,
106+
private val jarTransformer: JarTransformer,
107+
private val logger: Logger
108+
) :
109+
Transform() {
110+
override fun getName() = "firebaseVendorTransform"
111+
112+
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
113+
return mutableSetOf(QualifiedContent.DefaultContentType.CLASSES)
114+
}
115+
116+
override fun isIncremental() = false
117+
118+
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
119+
return mutableSetOf(QualifiedContent.Scope.PROJECT)
120+
}
121+
122+
override fun getReferencedScopes(): MutableSet<in QualifiedContent.Scope> {
123+
return mutableSetOf(QualifiedContent.Scope.PROJECT)
124+
}
125+
126+
override fun transform(transformInvocation: TransformInvocation) {
127+
if (configuration.resolve().isEmpty()) {
128+
logger.warn("Nothing to vendor. " +
129+
"If you don't need vendor functionality please disable 'firebase-vendor' plugin. " +
130+
"Otherwise use the 'vendor' configuration to add dependencies you want vendored in.")
131+
for (input in transformInvocation.inputs) {
132+
for (directoryInput in input.directoryInputs) {
133+
val directoryOutput = transformInvocation.outputProvider.getContentLocation(
134+
directoryInput.name,
135+
setOf(QualifiedContent.DefaultContentType.CLASSES),
136+
mutableSetOf(QualifiedContent.Scope.PROJECT),
137+
Format.DIRECTORY)
138+
directoryInput.file.copyRecursively(directoryOutput, overwrite = true)
139+
}
140+
}
141+
return
142+
}
143+
144+
val contentLocation = transformInvocation.outputProvider.getContentLocation(
145+
"sourceAndVendoredLibraries",
146+
setOf(QualifiedContent.DefaultContentType.CLASSES),
147+
mutableSetOf(QualifiedContent.Scope.PROJECT),
148+
Format.DIRECTORY)
149+
contentLocation.deleteRecursively()
150+
contentLocation.mkdirs()
151+
val tmpDir = File(contentLocation, "tmp")
152+
tmpDir.mkdirs()
153+
try {
154+
val fatJar = process(tmpDir, transformInvocation)
155+
unzipJar(fatJar, contentLocation)
156+
} finally {
157+
tmpDir.deleteRecursively()
158+
}
159+
}
160+
161+
private fun isTest(transformInvocation: TransformInvocation): Boolean {
162+
return android.testVariants.find { it.name == transformInvocation.context.variantName } != null
163+
}
164+
165+
private fun process(workDir: File, transformInvocation: TransformInvocation): File {
166+
transformInvocation.context.variantName
167+
val unzippedDir = File(workDir, "unzipped")
168+
val unzippedExcludedDir = File(workDir, "unzipped-excluded")
169+
unzippedDir.mkdirs()
170+
unzippedExcludedDir.mkdirs()
171+
172+
val externalCodeDir = if (isTest(transformInvocation)) unzippedExcludedDir else unzippedDir
173+
174+
for (input in transformInvocation.inputs) {
175+
for (directoryInput in input.directoryInputs) {
176+
directoryInput.file.copyRecursively(unzippedDir)
177+
}
178+
}
179+
180+
val ownPackageNames = inferPackages(unzippedDir)
181+
182+
for (jar in configuration.resolve()) {
183+
unzipJar(jar, externalCodeDir)
184+
}
185+
val externalPackageNames = inferPackages(externalCodeDir) subtract ownPackageNames
186+
val java = File(externalCodeDir, "java")
187+
val javax = File(externalCodeDir, "javax")
188+
if (java.exists() || javax.exists()) {
189+
// JarJar unconditionally skips any classes whose package name starts with "java" or "javax".
190+
throw GradleException("Vendoring java or javax packages is not supported. " +
191+
"Please exclude one of the direct or transitive dependencies: \n" +
192+
configuration.resolvedConfiguration.resolvedArtifacts.joinToString(separator = "\n"))
193+
}
194+
val jar = File(workDir, "intermediate.jar")
195+
zipAll(unzippedDir, jar)
196+
val outputJar = File(workDir, "output.jar")
197+
198+
jarTransformer.transform(jar, outputJar, externalPackageNames)
199+
return outputJar
200+
}
201+
202+
private fun inferPackages(dir: File): Set<String> {
203+
return dir.walk().filter { it.name.endsWith(".class") }.map { it.parentFile.toRelativeString(dir).replace('/', '.') }.toSet()
204+
}
205+
}
206+
207+
fun unzipJar(jar: File, directory: File) {
208+
ZipFile(jar).use { zip ->
209+
zip.entries().asSequence().filter { !it.isDirectory && !it.name.startsWith("META-INF") }.forEach { entry ->
210+
zip.getInputStream(entry).use { input ->
211+
val entryFile = File(directory, entry.name)
212+
entryFile.parentFile.mkdirs()
213+
entryFile.outputStream().use { output ->
214+
input.copyTo(output)
215+
}
216+
}
217+
}
218+
}
219+
}
220+
221+
fun zipAll(directory: File, zipFile: File) {
222+
223+
ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use {
224+
zipFiles(it, directory, "")
225+
}
226+
}
227+
228+
private fun zipFiles(zipOut: ZipOutputStream, sourceFile: File, parentDirPath: String) {
229+
val data = ByteArray(2048)
230+
sourceFile.listFiles()?.forEach { f ->
231+
if (f.isDirectory) {
232+
val path = if (parentDirPath == "") {
233+
f.name
234+
} else {
235+
parentDirPath + File.separator + f.name
236+
}
237+
// Call recursively to add files within this directory
238+
zipFiles(zipOut, f, path)
239+
} else {
240+
FileInputStream(f).use { fi ->
241+
BufferedInputStream(fi).use { origin ->
242+
val path = parentDirPath + File.separator + f.name
243+
val entry = ZipEntry(path)
244+
entry.time = f.lastModified()
245+
entry.isDirectory
246+
entry.size = f.length()
247+
zipOut.putNextEntry(entry)
248+
while (true) {
249+
val readBytes = origin.read(data)
250+
if (readBytes == -1) {
251+
break
252+
}
253+
zipOut.write(data, 0, readBytes)
254+
}
255+
}
256+
}
257+
}
258+
}
259+
}

0 commit comments

Comments
 (0)