Skip to content

Commit 1a3ed0b

Browse files
committed
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") } ```
1 parent cc79cc6 commit 1a3ed0b

File tree

8 files changed

+501
-9
lines changed

8 files changed

+501
-9
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ buildscript {
3939
classpath 'digital.wup:android-maven-publish:3.6.3'
4040
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
4141
classpath 'org.jlleitschuh.gradle:ktlint-gradle:9.2.1'
42+
classpath 'ru.tinkoff.gradle:jarjar:1.1.0'
4243
}
4344
}
4445

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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
interface Foo<T> {}
18+
19+
class Bar<T> implements Foo<T> {}
20+
21+
public class TypeMap<T, U extends Foo<T>> {
22+
U hello() {
23+
return null;
24+
}
25+
26+
void add(T key, U value) {}
27+
28+
public static void main(String[] args) {
29+
TypeMap<String, Bar<String>> map = new TypeMap<>();
30+
map.hello();
31+
map.add("", new Bar<>());
32+
}
33+
}
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
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.Action
32+
import org.gradle.api.GradleException
33+
import org.gradle.api.Plugin
34+
import org.gradle.api.Project
35+
import org.gradle.api.artifacts.Configuration
36+
import org.gradle.api.logging.Logger
37+
38+
class VendorPlugin : Plugin<Project> {
39+
override fun apply(project: Project) {
40+
project.plugins.all(object : Action<Plugin<Any>> {
41+
override fun execute(plugin: Plugin<Any>) {
42+
when (plugin) {
43+
is LibraryPlugin -> configureAndroid(project)
44+
}
45+
}
46+
})
47+
}
48+
49+
fun configureAndroid(project: Project) {
50+
51+
val vendor = project.configurations.create("vendor")
52+
project.configurations.named("compileOnly") {
53+
extendsFrom(vendor)
54+
}
55+
56+
val jarJar = project.configurations.create("firebaseJarJarArtifact")
57+
project.dependencies.add("firebaseJarJarArtifact", "org.pantsbuild:jarjar:1.7.2")
58+
59+
val android = project.extensions.getByType(LibraryExtension::class.java)
60+
61+
android.registerTransform(VendorTransform(
62+
vendor,
63+
JarJarTransformer(
64+
parentPackageProvider = { variantName ->
65+
android.libraryVariants.find { it.name == variantName }!!.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(variantName: String, input: File, output: File, ownPackageNames: Set<String>, externalPackageNames: Set<String>)
76+
}
77+
78+
class JarJarTransformer(
79+
private val parentPackageProvider: (String) -> String,
80+
private val jarJarProvider: () -> Collection<File>,
81+
private val project: Project,
82+
private val logger: Logger
83+
) : JarTransformer {
84+
override fun transform(variantName: String, input: File, output: File, ownPackageNames: Set<String>, externalPackageNames: Set<String>) {
85+
val parentPackage = parentPackageProvider(variantName)
86+
val rulesFile = File.createTempFile("$parentPackage-$variantName", ".jarjar")
87+
rulesFile.printWriter().use {
88+
for (packageName in ownPackageNames) {
89+
it.println("keep $packageName.**")
90+
}
91+
for (externalPackageName in externalPackageNames) {
92+
it.println("rule $externalPackageName.** $parentPackage.@0")
93+
}
94+
}
95+
logger.info("The following JarJar configuration will be used:\n ${rulesFile.readText()}")
96+
97+
project.javaexec {
98+
main = "org.pantsbuild.jarjar.Main"
99+
classpath = project.files(jarJarProvider())
100+
// jvmArgs = listOf("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005")
101+
args = listOf("process", rulesFile.absolutePath, input.absolutePath, output.absolutePath)
102+
systemProperties = mapOf("verbose" to "true", "misplacedClassStrategy" to "FATAL")
103+
}.assertNormalExitValue()
104+
}
105+
}
106+
107+
class VendorTransform(
108+
private val configuration: Configuration,
109+
private val jarTransformer: JarTransformer,
110+
private val logger: Logger
111+
) :
112+
Transform() {
113+
override fun getName() = "firebaseVendorTransform"
114+
115+
override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
116+
return mutableSetOf(QualifiedContent.DefaultContentType.CLASSES)
117+
}
118+
119+
override fun isIncremental() = false
120+
121+
override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
122+
return mutableSetOf(QualifiedContent.Scope.PROJECT)
123+
}
124+
125+
override fun transform(transformInvocation: TransformInvocation) {
126+
if (configuration.resolve().isEmpty()) {
127+
logger.info("Nothing to vendor. " +
128+
"If you don't need vendor functionality please disable 'firebase-vendor' plugin. " +
129+
"Otherwise use the 'vendor' configuration to add dependencies you want vendored in.")
130+
for (input in transformInvocation.inputs) {
131+
for (directoryInput in input.directoryInputs) {
132+
val directoryOutput = transformInvocation.outputProvider.getContentLocation(
133+
directoryInput.name,
134+
setOf(QualifiedContent.DefaultContentType.CLASSES),
135+
mutableSetOf(QualifiedContent.Scope.PROJECT),
136+
Format.DIRECTORY)
137+
directoryInput.file.copyRecursively(directoryOutput, overwrite = true)
138+
}
139+
}
140+
return
141+
}
142+
143+
val contentLocation = transformInvocation.outputProvider.getContentLocation(
144+
"sourceAndVendoredLibraries",
145+
setOf(QualifiedContent.DefaultContentType.CLASSES),
146+
mutableSetOf(QualifiedContent.Scope.PROJECT),
147+
Format.DIRECTORY)
148+
contentLocation.deleteRecursively()
149+
contentLocation.mkdirs()
150+
val tmpDir = File(contentLocation, "tmp")
151+
tmpDir.mkdirs()
152+
try {
153+
val fatJar = process(tmpDir, transformInvocation)
154+
unzipJar(fatJar, contentLocation)
155+
} finally {
156+
tmpDir.deleteRecursively()
157+
}
158+
}
159+
160+
private fun process(workDir: File, transformInvocation: TransformInvocation): File {
161+
transformInvocation.context.variantName
162+
val unzippedDir = File(workDir, "unzipped")
163+
unzippedDir.mkdirs()
164+
165+
for (input in transformInvocation.inputs) {
166+
for (directoryInput in input.directoryInputs) {
167+
directoryInput.file.copyRecursively(unzippedDir)
168+
}
169+
}
170+
val foo: List<String> = mutableListOf("Hello")
171+
val ownPackageNames = inferPackages(unzippedDir)
172+
173+
for (jar in configuration.resolve()) {
174+
unzipJar(jar, unzippedDir)
175+
}
176+
val externalPackageNames = inferPackages(unzippedDir) subtract ownPackageNames
177+
val java = File(unzippedDir, "java")
178+
val javax = File(unzippedDir, "javax")
179+
if (java.exists() || javax.exists()) {
180+
// JarJar unconditionally skips any classes whose package name starts with "java" or "javax".
181+
throw GradleException("Vendoring java or javax packages is not supported. " +
182+
"Please exclude one of the direct or transitive dependencies: \n" +
183+
configuration.resolvedConfiguration.resolvedArtifacts.joinToString(separator = "\n"))
184+
}
185+
val jar = File(workDir, "intermediate.jar")
186+
zipAll(unzippedDir, jar)
187+
val jar2 = File(workDir, "output.jar")
188+
jarTransformer.transform(transformInvocation.context.variantName, jar, jar2, ownPackageNames, externalPackageNames)
189+
return jar2
190+
}
191+
192+
private fun inferPackages(dir: File): Set<String> {
193+
return dir.walk().filter { it.name.endsWith(".class") }.map { it.parentFile.toRelativeString(dir).replace('/', '.') }.toSet()
194+
}
195+
}
196+
197+
fun unzipJar(jar: File, directory: File) {
198+
ZipFile(jar).use { zip ->
199+
zip.entries().asSequence().filter { !it.isDirectory && !it.name.startsWith("META-INF") }.forEach { entry ->
200+
zip.getInputStream(entry).use { input ->
201+
val entryFile = File(directory, entry.name)
202+
entryFile.parentFile.mkdirs()
203+
entryFile.outputStream().use { output ->
204+
input.copyTo(output)
205+
}
206+
}
207+
}
208+
}
209+
}
210+
211+
fun zipAll(directory: File, zipFile: File) {
212+
213+
ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use {
214+
zipFiles(it, directory, "")
215+
}
216+
}
217+
218+
private fun zipFiles(zipOut: ZipOutputStream, sourceFile: File, parentDirPath: String) {
219+
val data = ByteArray(2048)
220+
sourceFile.listFiles()?.forEach { f ->
221+
if (f.isDirectory) {
222+
val path = 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+
}
248+
}
249+
}

0 commit comments

Comments
 (0)