Skip to content

Commit 32dfa7c

Browse files
committed
fix: handle android jar files along with class files directories.
1 parent c9b15ef commit 32dfa7c

File tree

9 files changed

+287
-19
lines changed

9 files changed

+287
-19
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2024. Tony Robalik.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.autonomousapps.android
4+
5+
6+
import com.autonomousapps.android.projects.AndroidTransformProject
7+
import org.gradle.util.GradleVersion
8+
9+
import static com.autonomousapps.advice.truth.BuildHealthSubject.buildHealth
10+
import static com.autonomousapps.utils.Runner.build
11+
import static com.google.common.truth.Truth.assertAbout
12+
13+
/** See https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/1346. */
14+
final class AndroidTransformSpec extends AbstractAndroidSpec {
15+
16+
def "does not recommend replace api with implementation (#gradleVersion AGP #agpVersion)"() {
17+
given:
18+
def project = new AndroidTransformProject(agpVersion as String)
19+
gradleProject = project.gradleProject
20+
21+
when:
22+
build(gradleVersion as GradleVersion, gradleProject.rootDir, 'buildHealth')
23+
24+
then:
25+
assertAbout(buildHealth())
26+
.that(project.actualBuildHealth())
27+
.containsExactlyDependencyAdviceIn(project.expectedBuildHealth)
28+
29+
where:
30+
[gradleVersion, agpVersion] << gradleAgpMatrix()
31+
}
32+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright (c) 2024. Tony Robalik.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.autonomousapps.android.projects
4+
5+
6+
import com.autonomousapps.kit.GradleProject
7+
import com.autonomousapps.kit.Source
8+
import com.autonomousapps.kit.gradle.dependencies.Plugins
9+
import com.autonomousapps.model.ProjectAdvice
10+
11+
import static com.autonomousapps.AdviceHelper.actualProjectAdvice
12+
import static com.autonomousapps.AdviceHelper.emptyProjectAdviceFor
13+
import static com.autonomousapps.kit.gradle.Dependency.project
14+
15+
/**
16+
* https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/1346.
17+
*/
18+
final class AndroidTransformProject extends AbstractAndroidProject {
19+
20+
final GradleProject gradleProject
21+
private final String agpVersion
22+
23+
AndroidTransformProject(String agpVersion) {
24+
super(agpVersion)
25+
26+
this.agpVersion = agpVersion
27+
this.gradleProject = build()
28+
}
29+
30+
private GradleProject build() {
31+
return newAndroidGradleProjectBuilder(agpVersion)
32+
.withAndroidLibProject('consumer', 'com.example.consumer') { consumer ->
33+
consumer.manifest = libraryManifest('com.example.consumer')
34+
consumer.sources = consumerSources
35+
consumer.withBuildScript { bs ->
36+
bs.plugins = [Plugins.androidLib, Plugins.kotlinAndroidNoVersion, Plugins.dependencyAnalysisNoVersion]
37+
bs.android = defaultAndroidLibBlock(true, 'com.example.consumer')
38+
39+
bs.dependencies(
40+
project('api', ':producer'),
41+
)
42+
bs.withGroovy(TRANSFORM_TASK)
43+
}
44+
}
45+
.withSubproject('producer') { producer ->
46+
producer.sources = producerSources
47+
producer.withBuildScript { bs ->
48+
bs.plugins = [Plugins.kotlinJvmNoVersion, Plugins.dependencyAnalysisNoVersion]
49+
}
50+
}
51+
.write()
52+
}
53+
54+
private static TRANSFORM_TASK =
55+
"""\
56+
import com.android.build.api.artifact.ScopedArtifact
57+
import com.android.build.api.variant.ScopedArtifacts
58+
import java.io.FileInputStream
59+
import java.io.FileOutputStream
60+
import java.nio.file.Files
61+
import java.nio.file.Paths
62+
import java.util.zip.ZipEntry
63+
import java.util.zip.ZipFile
64+
import java.util.zip.ZipOutputStream
65+
66+
abstract class TransformTask extends DefaultTask {
67+
@PathSensitive(PathSensitivity.RELATIVE)
68+
@InputFiles
69+
abstract ListProperty<RegularFile> getAllJars();
70+
71+
@PathSensitive(PathSensitivity.RELATIVE)
72+
@InputFiles
73+
abstract ListProperty<Directory> getAllDirs();
74+
75+
@OutputFile
76+
abstract RegularFileProperty getOutput();
77+
78+
@TaskAction
79+
void transform() {
80+
def outputFile = output.get().asFile
81+
def outputStream = new ZipOutputStream(new FileOutputStream(outputFile))
82+
try {
83+
allJars.get().forEach { jar ->
84+
addJarToZip(jar.asFile, outputStream)
85+
}
86+
allDirs.get().forEach { dir ->
87+
addDirectoryToZip(dir.asFile, outputStream, dir.asFile.path)
88+
}
89+
} finally {
90+
outputStream.close()
91+
}
92+
93+
println("Copying \${allJars.get()} and \${allDirs.get()} into \${output.get()}")
94+
println("Resulting jar file contents:")
95+
def zipFile = new ZipFile(outputFile)
96+
try {
97+
zipFile.entries().each {
98+
println(it.name)
99+
}
100+
} finally {
101+
zipFile.close()
102+
}
103+
}
104+
105+
void addJarToZip(File file, ZipOutputStream zipOut) {
106+
def zipFile = new ZipFile(file)
107+
zipFile.entries().each {
108+
def zipEntry = new ZipEntry(it.name)
109+
zipOut.putNextEntry(zipEntry)
110+
def inputStream = zipFile.getInputStream(it)
111+
try {
112+
inputStream.transferTo(zipOut)
113+
} finally {
114+
inputStream.close()
115+
}
116+
}
117+
}
118+
119+
void addDirectoryToZip(File directory, ZipOutputStream zipOut, String basePath) {
120+
Files.walk(directory.toPath()).forEach {
121+
def file = it.toFile()
122+
if (file.isFile()) {
123+
def fileInputStream = new FileInputStream(file)
124+
try {
125+
def zipEntry = new ZipEntry(Paths.get(basePath).relativize(file.toPath()).toString())
126+
zipOut.putNextEntry(zipEntry)
127+
fileInputStream.transferTo(zipOut)
128+
zipOut.closeEntry()
129+
} finally {
130+
fileInputStream.close()
131+
}
132+
}
133+
}
134+
}
135+
}
136+
137+
androidComponents {
138+
onVariants(selector().all()) { variant ->
139+
variant.artifacts
140+
.forScope(ScopedArtifacts.Scope.PROJECT)
141+
.use(tasks.register("transformTask\${variant.name.capitalize()}", TransformTask.class))
142+
.toTransform(ScopedArtifact.CLASSES.INSTANCE, { it.getAllJars() }, { it.getAllDirs() }, { it.getOutput() })
143+
}
144+
}
145+
""".stripIndent()
146+
147+
private List<Source> consumerSources = [
148+
Source.kotlin(
149+
"""
150+
package com.example.consumer
151+
152+
import com.example.producer.Producer
153+
154+
class Consumer : Producer() {
155+
override fun produce(): String {
156+
return "Hello, world!"
157+
}
158+
}
159+
""".stripIndent()
160+
)
161+
.withPath('com.example.consumer', 'Consumer')
162+
.build()
163+
]
164+
165+
private List<Source> producerSources = [
166+
Source.kotlin(
167+
"""
168+
package com.example.producer
169+
170+
abstract class Producer {
171+
abstract fun produce(): String
172+
}
173+
""".stripIndent()
174+
)
175+
.withPath('com.example.producer', 'Producer')
176+
.build()
177+
]
178+
179+
Set<ProjectAdvice> actualBuildHealth() {
180+
return actualProjectAdvice(gradleProject)
181+
}
182+
183+
final Set<ProjectAdvice> expectedBuildHealth =
184+
emptyProjectAdviceFor(':consumer', ':producer')
185+
}

src/main/kotlin/com/autonomousapps/internal/BytecodeParsers.kt

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import com.autonomousapps.model.internal.intermediates.consumer.ExplodingBytecod
1111
import com.autonomousapps.model.internal.intermediates.consumer.MemberAccess
1212
import org.gradle.api.logging.Logger
1313
import java.io.File
14+
import java.nio.file.Paths
15+
import java.util.jar.JarFile
1416

1517
internal sealed class ClassReferenceParser(private val buildDir: File) {
1618

@@ -30,30 +32,53 @@ internal sealed class ClassReferenceParser(private val buildDir: File) {
3032
/** Given a set of .class files, produce a set of FQCN references present in that set. */
3133
internal class ClassFilesParser(
3234
private val classes: Set<File>,
35+
private val jarFiles: Set<File>,
3336
buildDir: File
3437
) : ClassReferenceParser(buildDir) {
3538

3639
private val logger = getLogger<ClassFilesParser>()
3740

3841
override fun parseBytecode(): Set<ExplodingBytecode> {
39-
return classes.asSequenceOfClassFiles()
40-
.map { classFile ->
41-
val classFilePath = classFile.path
42-
val explodedClass = classFile.inputStream().use {
42+
return (classesExplodingBytecode() + jarsExplodingBytecode()).toSet()
43+
}
44+
45+
private fun classesExplodingBytecode(): List<ExplodingBytecode> {
46+
return classes.asSequenceOfClassFiles().map { classFile ->
47+
val classFilePath = classFile.path
48+
val explodedClass = classFile.inputStream().use {
49+
BytecodeReader(it.readBytes(), logger, classFilePath).parse()
50+
}
51+
52+
explodedClass.toExplodingBytecode(relativize(classFile))
53+
}.toList()
54+
}
55+
56+
private fun jarsExplodingBytecode(): List<ExplodingBytecode> {
57+
return jarFiles.asSequence().map { file ->
58+
val jarFile = JarFile(file)
59+
60+
jarFile.asSequenceOfClassFiles().map { classFileEntry ->
61+
val classFilePath = Paths.get(file.path, classFileEntry.name).toString()
62+
val classFileRelativePath = Paths.get(relativize(file), classFileEntry.name).toString()
63+
val explodedClass = jarFile.getInputStream(classFileEntry).use {
4364
BytecodeReader(it.readBytes(), logger, classFilePath).parse()
4465
}
4566

46-
ExplodingBytecode(
47-
relativePath = relativize(classFile),
48-
className = explodedClass.className,
49-
sourceFile = explodedClass.source,
50-
nonAnnotationClasses = explodedClass.nonAnnotationClasses,
51-
annotationClasses = explodedClass.annotationClasses,
52-
invisibleAnnotationClasses = explodedClass.invisibleAnnotationClasses,
53-
binaryClassAccesses = explodedClass.binaryClasses,
54-
)
67+
explodedClass.toExplodingBytecode(classFileRelativePath)
5568
}
56-
.toSet()
69+
}.flatten().toList()
70+
}
71+
72+
private fun ExplodedClass.toExplodingBytecode(relativePath: String): ExplodingBytecode {
73+
return ExplodingBytecode(
74+
relativePath = relativePath,
75+
className = className,
76+
sourceFile = source,
77+
nonAnnotationClasses = nonAnnotationClasses,
78+
annotationClasses = annotationClasses,
79+
invisibleAnnotationClasses = invisibleAnnotationClasses,
80+
binaryClassAccesses = binaryClasses,
81+
)
5782
}
5883
}
5984

src/main/kotlin/com/autonomousapps/internal/kotlin/PublicApiDump.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,15 @@ internal fun getBinaryAPI(jar: JarFile, visibilityFilter: (String) -> Boolean =
3838

3939
internal fun getBinaryAPI(
4040
classes: Set<File>,
41+
jarFiles: Set<JarFile>,
4142
visibilityFilter: (String) -> Boolean = { true }
42-
): List<ClassBinarySignature> =
43-
getBinaryAPI(classes.asSequence().map { it.inputStream() }, visibilityFilter)
43+
): List<ClassBinarySignature> {
44+
val classesBinaryAPI = getBinaryAPI(classes.asSequence().map { it.inputStream() }, visibilityFilter)
45+
val jarsBinaryAPI = jarFiles.flatMap { getBinaryAPI(it, visibilityFilter) }
46+
47+
return classesBinaryAPI + jarsBinaryAPI
48+
}
49+
4450

4551
internal fun getBinaryAPI(
4652
classStreams: Sequence<InputStream>,

src/main/kotlin/com/autonomousapps/internal/kotlin/abiDependencies.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import com.autonomousapps.internal.utils.allItems
99
import com.autonomousapps.internal.utils.flatMapToSet
1010
import com.autonomousapps.model.internal.intermediates.consumer.ExplodingAbi
1111
import java.io.File
12+
import java.util.jar.JarFile
1213

1314
internal fun computeAbi(
1415
classFiles: Set<File>,
16+
jarFiles: Set<JarFile>,
1517
exclusions: AbiExclusions,
1618
abiDumpFile: File? = null
17-
): Set<ExplodingAbi> = getBinaryAPI(classFiles).explodedAbi(exclusions, abiDumpFile)
19+
): Set<ExplodingAbi> = getBinaryAPI(classFiles, jarFiles).explodedAbi(exclusions, abiDumpFile)
1820

1921
private fun List<ClassBinarySignature>.explodedAbi(
2022
exclusions: AbiExclusions,

src/main/kotlin/com/autonomousapps/internal/utils/collections.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ internal fun Iterable<File>.filterToClassFiles(): List<File> {
6464
return filter { it.extension == "class" && !it.name.endsWith("module-info.class") }
6565
}
6666

67+
internal fun Iterable<File>.filterToJarFiles(): List<File> {
68+
return filter { it.extension == "jar" && !it.name.endsWith("R.jar") }
69+
}
70+
6771
/** Filters a [FileCollection] to contain only class files. */
6872
internal fun FileCollection.filterToClassFiles(): FileCollection {
6973
return filter {

src/main/kotlin/com/autonomousapps/tasks/AbiAnalysisTask.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.autonomousapps.internal.kotlin.computeAbi
1010
import com.autonomousapps.internal.utils.bufferWriteJsonSet
1111
import com.autonomousapps.internal.utils.filterToClassFiles
1212
import com.autonomousapps.internal.utils.fromJson
13+
import com.autonomousapps.internal.utils.mapToSet
1314
import com.autonomousapps.internal.utils.getAndDelete
1415
import org.gradle.api.file.ConfigurableFileCollection
1516
import org.gradle.api.file.RegularFileProperty
@@ -18,6 +19,7 @@ import org.gradle.api.tasks.*
1819
import org.gradle.workers.WorkAction
1920
import org.gradle.workers.WorkParameters
2021
import org.gradle.workers.WorkerExecutor
22+
import java.util.jar.JarFile
2123
import javax.inject.Inject
2224

2325
@CacheableTask
@@ -52,6 +54,7 @@ abstract class AbiAnalysisTask @Inject constructor(
5254
classFiles.setFrom(classes.asFileTree.filterToClassFiles().files)
5355
// Android projects
5456
classFiles.from(androidClassFiles())
57+
jarFiles.from(androidJarFiles())
5558

5659
exclusions.set(this@AbiAnalysisTask.exclusions)
5760
output.set(this@AbiAnalysisTask.output)
@@ -61,6 +64,7 @@ abstract class AbiAnalysisTask @Inject constructor(
6164

6265
interface AbiAnalysisParameters : WorkParameters {
6366
val classFiles: ConfigurableFileCollection
67+
val jarFiles: ConfigurableFileCollection
6468
val exclusions: Property<String>
6569
val output: RegularFileProperty
6670
val abiDump: RegularFileProperty
@@ -73,9 +77,10 @@ abstract class AbiAnalysisTask @Inject constructor(
7377
val outputAbiDump = parameters.abiDump.getAndDelete()
7478

7579
val classFiles = parameters.classFiles.files
80+
val jarFiles = parameters.jarFiles.mapToSet { JarFile(it) }
7681
val exclusions = parameters.exclusions.orNull?.fromJson<AbiExclusions>() ?: AbiExclusions.NONE
7782

78-
val explodingAbi = computeAbi(classFiles, exclusions, outputAbiDump)
83+
val explodingAbi = computeAbi(classFiles, jarFiles, exclusions, outputAbiDump)
7984

8085
output.bufferWriteJsonSet(explodingAbi)
8186
}

0 commit comments

Comments
 (0)