Skip to content

Commit

Permalink
fix: handle android jar files along with class files directories.
Browse files Browse the repository at this point in the history
Do not ignore R.jar file.

Filter parent class name for inner class and vice versa.
  • Loading branch information
dkostyrev committed Jan 12, 2025
1 parent 6df0bfa commit d52436f
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2024. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.android


import com.autonomousapps.android.projects.AndroidTransformProject
import org.gradle.util.GradleVersion

import static com.autonomousapps.advice.truth.BuildHealthSubject.buildHealth
import static com.autonomousapps.utils.Runner.build
import static com.google.common.truth.Truth.assertAbout

/** See https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/1346. */
final class AndroidTransformSpec extends AbstractAndroidSpec {

def "does not recommend replace api with implementation (#gradleVersion AGP #agpVersion)"() {
given:
def project = new AndroidTransformProject(agpVersion as String)
gradleProject = project.gradleProject
when:
build(gradleVersion as GradleVersion, gradleProject.rootDir, 'buildHealth')
then:
assertAbout(buildHealth())
.that(project.actualBuildHealth())
.containsExactlyDependencyAdviceIn(project.expectedBuildHealth)
where:
[gradleVersion, agpVersion] << gradleAgpMatrix()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright (c) 2024. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.android.projects


import com.autonomousapps.kit.GradleProject
import com.autonomousapps.kit.Source
import com.autonomousapps.kit.gradle.dependencies.Plugins
import com.autonomousapps.model.ProjectAdvice

import static com.autonomousapps.AdviceHelper.actualProjectAdvice
import static com.autonomousapps.AdviceHelper.emptyProjectAdviceFor
import static com.autonomousapps.kit.gradle.Dependency.project

/**
* https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/1346.
*/
final class AndroidTransformProject extends AbstractAndroidProject {

final GradleProject gradleProject
private final String agpVersion

AndroidTransformProject(String agpVersion) {
super(agpVersion)

this.agpVersion = agpVersion
this.gradleProject = build()
}

private GradleProject build() {
return newAndroidGradleProjectBuilder(agpVersion)
.withAndroidLibProject('consumer', 'com.example.consumer') { consumer ->
consumer.manifest = libraryManifest('com.example.consumer')
consumer.sources = consumerSources
consumer.withBuildScript { bs ->
bs.plugins = [Plugins.androidLib, Plugins.kotlinAndroidNoVersion, Plugins.dependencyAnalysisNoVersion]
bs.android = defaultAndroidLibBlock(true, 'com.example.consumer')

bs.dependencies(
project('api', ':producer'),
)
bs.withGroovy(TRANSFORM_TASK)
}
}
.withSubproject('producer') { producer ->
producer.sources = producerSources
producer.withBuildScript { bs ->
bs.plugins = [Plugins.kotlinJvmNoVersion, Plugins.dependencyAnalysisNoVersion]
}
}
.write()
}

private static TRANSFORM_TASK =
"""\
import com.android.build.api.artifact.ScopedArtifact
import com.android.build.api.variant.ScopedArtifacts
import java.io.FileInputStream
import java.io.FileOutputStream
import java.nio.file.Files
import java.nio.file.Paths
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
abstract class TransformTask extends DefaultTask {
@PathSensitive(PathSensitivity.RELATIVE)
@InputFiles
abstract ListProperty<RegularFile> getAllJars();
@PathSensitive(PathSensitivity.RELATIVE)
@InputFiles
abstract ListProperty<Directory> getAllDirs();
@OutputFile
abstract RegularFileProperty getOutput();
@TaskAction
void transform() {
def outputFile = output.get().asFile
def outputStream = new ZipOutputStream(new FileOutputStream(outputFile))
try {
allJars.get().forEach { jar ->
addJarToZip(jar.asFile, outputStream)
}
allDirs.get().forEach { dir ->
addDirectoryToZip(dir.asFile, outputStream, dir.asFile.path)
}
} finally {
outputStream.close()
}
println("Copying \${allJars.get()} and \${allDirs.get()} into \${output.get()}")
println("Resulting jar file contents:")
def zipFile = new ZipFile(outputFile)
try {
zipFile.entries().each {
println(it.name)
}
} finally {
zipFile.close()
}
}
void addJarToZip(File file, ZipOutputStream zipOut) {
def zipFile = new ZipFile(file)
zipFile.entries().each {
def zipEntry = new ZipEntry(it.name)
zipOut.putNextEntry(zipEntry)
def inputStream = zipFile.getInputStream(it)
try {
inputStream.transferTo(zipOut)
} finally {
inputStream.close()
}
}
}
void addDirectoryToZip(File directory, ZipOutputStream zipOut, String basePath) {
Files.walk(directory.toPath()).forEach {
def file = it.toFile()
if (file.isFile()) {
def fileInputStream = new FileInputStream(file)
try {
def zipEntry = new ZipEntry(Paths.get(basePath).relativize(file.toPath()).toString())
zipOut.putNextEntry(zipEntry)
fileInputStream.transferTo(zipOut)
zipOut.closeEntry()
} finally {
fileInputStream.close()
}
}
}
}
}
androidComponents {
onVariants(selector().all()) { variant ->
variant.artifacts
.forScope(ScopedArtifacts.Scope.PROJECT)
.use(tasks.register("transformTask\${variant.name.capitalize()}", TransformTask.class))
.toTransform(ScopedArtifact.CLASSES.INSTANCE, { it.getAllJars() }, { it.getAllDirs() }, { it.getOutput() })
}
}
""".stripIndent()

private List<Source> consumerSources = [
Source.kotlin(
"""
package com.example.consumer
import com.example.producer.Producer
class Consumer : Producer() {
override fun produce(): String {
return "Hello, world!"
}
}
""".stripIndent()
)
.withPath('com.example.consumer', 'Consumer')
.build()
]

private List<Source> producerSources = [
Source.kotlin(
"""
package com.example.producer
abstract class Producer {
abstract fun produce(): String
}
""".stripIndent()
)
.withPath('com.example.producer', 'Producer')
.build()
]

Set<ProjectAdvice> actualBuildHealth() {
return actualProjectAdvice(gradleProject)
}

final Set<ProjectAdvice> expectedBuildHealth =
emptyProjectAdviceFor(':consumer', ':producer')
}
61 changes: 45 additions & 16 deletions src/main/kotlin/com/autonomousapps/internal/BytecodeParsers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import com.autonomousapps.model.internal.intermediates.consumer.ExplodingBytecod
import com.autonomousapps.model.internal.intermediates.consumer.MemberAccess
import org.gradle.api.logging.Logger
import java.io.File
import java.nio.file.Paths
import java.util.jar.JarFile

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

Expand All @@ -31,32 +33,55 @@ internal sealed class ClassReferenceParser(private val buildDir: File) {
/** Given a set of .class files, produce a set of FQCN references present in that set. */
internal class ClassFilesParser(
private val classes: Set<File>,
private val jarFiles: Set<File>,
buildDir: File
) : ClassReferenceParser(buildDir) {

private val logger = getLogger<ClassFilesParser>()

override fun parseBytecode(): Set<ExplodingBytecode> {
return classes.asSequenceOfClassFiles()
.map { classFile ->
val classFilePath = classFile.path
val explodedClass = classFile.inputStream().use {
return (classesExplodingBytecode() + jarsExplodingBytecode()).toSet()
}

private fun classesExplodingBytecode(): List<ExplodingBytecode> {
return classes.asSequenceOfClassFiles().map { classFile ->
val classFilePath = classFile.path
val explodedClass = classFile.inputStream().use {
BytecodeReader(it.readBytes(), logger, classFilePath).parse()
}

explodedClass.toExplodingBytecode(relativize(classFile))
}.toList()
}

private fun jarsExplodingBytecode(): List<ExplodingBytecode> {
return jarFiles.asSequence().map { file ->
val jarFile = JarFile(file)

jarFile.asSequenceOfClassFiles().map { classFileEntry ->
val classFilePath = Paths.get(file.path, classFileEntry.name).toString()
val classFileRelativePath = Paths.get(relativize(file), classFileEntry.name).toString()
val explodedClass = jarFile.getInputStream(classFileEntry).use {
BytecodeReader(it.readBytes(), logger, classFilePath).parse()
}

ExplodingBytecode(
relativePath = relativize(classFile),
className = explodedClass.className,
superClass = explodedClass.superClass,
interfaces = explodedClass.interfaces,
sourceFile = explodedClass.source,
nonAnnotationClasses = explodedClass.nonAnnotationClasses,
annotationClasses = explodedClass.annotationClasses,
invisibleAnnotationClasses = explodedClass.invisibleAnnotationClasses,
binaryClassAccesses = explodedClass.binaryClasses,
)
explodedClass.toExplodingBytecode(classFileRelativePath)
}
.toSet()
}.flatten().toList()
}

private fun ExplodedClass.toExplodingBytecode(relativePath: String): ExplodingBytecode {
return ExplodingBytecode(
relativePath = relativePath,
className = className,
superClass = superClass,
interfaces = interfaces,
sourceFile = source,
nonAnnotationClasses = nonAnnotationClasses,
annotationClasses = annotationClasses,
invisibleAnnotationClasses = invisibleAnnotationClasses,
binaryClassAccesses = binaryClasses,
)
}
}

Expand Down Expand Up @@ -121,6 +146,8 @@ private class BytecodeReader(
.filterNot { it.startsWith("java/") }
// Filter out a "used class" that is exactly the class under analysis
.filterNot { it == classAnalyzer.className }
// Filter out parent class name for inner classes
.filterNot { it.substringBefore('$') == classAnalyzer.className.substringBefore('$') }
// More human-readable
.map { canonicalize(it) }
.toSortedSet()
Expand All @@ -135,6 +162,8 @@ private class BytecodeReader(
.filterKeys { !it.startsWith("java/") }
// Filter out a "used class" that is exactly the class under analysis
.filterKeys { it != classAnalyzer.className }
// Filter out parent class name for inner classes
.filterKeys { it.substringBefore('$') != classAnalyzer.className.substringBefore('$') }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,15 @@ internal fun getBinaryAPI(jar: JarFile, visibilityFilter: (String) -> Boolean =

internal fun getBinaryAPI(
classes: Set<File>,
jarFiles: Set<JarFile>,
visibilityFilter: (String) -> Boolean = { true }
): List<ClassBinarySignature> =
getBinaryAPI(classes.asSequence().map { it.inputStream() }, visibilityFilter)
): List<ClassBinarySignature> {
val classesBinaryAPI = getBinaryAPI(classes.asSequence().map { it.inputStream() }, visibilityFilter)
val jarsBinaryAPI = jarFiles.flatMap { getBinaryAPI(it, visibilityFilter) }

return classesBinaryAPI + jarsBinaryAPI
}


internal fun getBinaryAPI(
classStreams: Sequence<InputStream>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import com.autonomousapps.internal.utils.allItems
import com.autonomousapps.internal.utils.flatMapToSet
import com.autonomousapps.model.internal.intermediates.consumer.ExplodingAbi
import java.io.File
import java.util.jar.JarFile

internal fun computeAbi(
classFiles: Set<File>,
jarFiles: Set<JarFile>,
exclusions: AbiExclusions,
abiDumpFile: File? = null
): Set<ExplodingAbi> = getBinaryAPI(classFiles).explodedAbi(exclusions, abiDumpFile)
): Set<ExplodingAbi> = getBinaryAPI(classFiles, jarFiles).explodedAbi(exclusions, abiDumpFile)

private fun List<ClassBinarySignature>.explodedAbi(
exclusions: AbiExclusions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ internal fun Iterable<File>.filterToClassFiles(): List<File> {
return filter { it.extension == "class" && !it.name.endsWith("module-info.class") }
}

internal fun Iterable<File>.filterToJarFiles(): List<File> {
return filter { it.extension == "jar" }
}

/** Filters a [FileCollection] to contain only class files. */
internal fun FileCollection.filterToClassFiles(): FileCollection {
return filter {
Expand Down
Loading

0 comments on commit d52436f

Please sign in to comment.