Skip to content

Commit

Permalink
Add support for parsing LibraryGlideModules.
Browse files Browse the repository at this point in the history
Every round now writes a new Index containing all LibraryGlideModules in that round. When we encounter an AppGlideModule, we read all previously read Index classes, extract the LibraryGlideModules they point to, and write a merged AppGlideModule that calls the developer's AppGlideModule and all of the LibraryGlideModules.

Excluding LibraryGlideModules is not supported yet. The library is still in a pre-release state.

Progress towards #4492

PiperOrigin-RevId: 461946515
  • Loading branch information
sjudd authored and glide-copybara-robot committed Jul 19, 2022
1 parent c35ad13 commit 4016448
Show file tree
Hide file tree
Showing 4 changed files with 655 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.bumptech.glide.annotation.ksp

import com.bumptech.glide.annotation.Excludes
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getConstructors
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
Expand Down Expand Up @@ -38,6 +39,8 @@ object AppGlideModuleConstants {
internal data class AppGlideModuleData(
val name: ClassName,
val constructor: Constructor,
val allowedLibraryGlideModuleNames: List<String>,
val sources: List<KSDeclaration>,
) {
internal data class Constructor(val hasContext: Boolean)
}
Expand All @@ -57,7 +60,26 @@ internal class AppGlideModuleParser(
val constructor = parseAppGlideModuleConstructorOrThrow()
val name = ClassName.bestGuess(appGlideModuleClass.qualifiedName!!.asString())

return AppGlideModuleData(name = name, constructor = constructor)
val (indexFiles, allLibraryModuleNames) = getIndexesAndLibraryGlideModuleNames()
val excludedGlideModuleClassNames = getExcludedGlideModuleClassNames()
val filteredGlideModuleClassNames =
allLibraryModuleNames.filterNot { excludedGlideModuleClassNames.contains(it) }

return AppGlideModuleData(
name = name,
constructor = constructor,
allowedLibraryGlideModuleNames = filteredGlideModuleClassNames,
sources = indexFiles
)
}

private fun getExcludedGlideModuleClassNames(): Set<String> {
val excludesAnnotation = appGlideModuleClass.atMostOneExcludesAnnotation()
// TODO(judds): Implement support for the excludes annotation.
environment.logger.logging(
"Found excludes annotation arguments: ${excludesAnnotation?.arguments}"
)
return emptySet()
}

private fun parseAppGlideModuleConstructorOrThrow(): AppGlideModuleData.Constructor {
Expand All @@ -80,6 +102,22 @@ internal class AppGlideModuleParser(
val libraryModuleNames: List<String>,
)

@OptIn(KspExperimental::class)
private fun getIndexesAndLibraryGlideModuleNames(): IndexFilesAndLibraryModuleNames {
val allIndexFiles: MutableList<KSDeclaration> = mutableListOf()
val allLibraryGlideModuleNames: MutableList<String> = mutableListOf()
resolver.getDeclarationsFromPackage(GlideSymbolProcessorConstants.PACKAGE_NAME).forEach {
index: KSDeclaration ->
val libraryGlideModuleNames = extractGlideModulesFromIndexAnnotation(index)
if (libraryGlideModuleNames.isNotEmpty()) {
allIndexFiles.add(index)
allLibraryGlideModuleNames.addAll(libraryGlideModuleNames)
}
}

return IndexFilesAndLibraryModuleNames(allIndexFiles, allLibraryGlideModuleNames)
}

private fun extractGlideModulesFromIndexAnnotation(
index: KSDeclaration,
): List<String> {
Expand Down Expand Up @@ -155,7 +193,7 @@ internal class AppGlideModuleGenerator(private val appGlideModuleData: AppGlideM
.addModifiers(KModifier.INTERNAL)
.addProperty("appGlideModule", data.name, KModifier.PRIVATE)
.primaryConstructor(generateConstructor(data))
.addFunction(generateRegisterComponents())
.addFunction(generateRegisterComponents(data.allowedLibraryGlideModuleNames))
.addFunction(generateApplyOptions())
.addFunction(generateManifestParsingDisabled())
.build()
Expand Down Expand Up @@ -183,13 +221,17 @@ internal class AppGlideModuleGenerator(private val appGlideModuleData: AppGlideM
// TODO(judds): Log the discovered modules here.
}

// TODO(judds): call registerComponents on LibraryGlideModules here.
private fun generateRegisterComponents() =
private fun generateRegisterComponents(allowedGlideModuleNames: List<String>) =
FunSpec.builder("registerComponents")
.addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE)
.addParameter("context", AppGlideModuleConstants.CONTEXT_CLASS_NAME)
.addParameter("glide", ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "Glide"))
.addParameter("registry", ClassName(AppGlideModuleConstants.GLIDE_PACKAGE_NAME, "Registry"))
.apply {
allowedGlideModuleNames.forEach {
addStatement("%T().registerComponents(context, glide, registry)", ClassName.bestGuess(it))
}
}
.addStatement("appGlideModule.registerComponents(context, glide, registry)")
.build()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,29 +69,58 @@ class GlideSymbolProcessor(private val environment: SymbolProcessorEnvironment)
environment.logger.logging(
"Found AppGlideModules: $appGlideModules, LibraryGlideModules: $libraryGlideModules"
)
// TODO(judds): Add support for parsing LibraryGlideModules here.

if (libraryGlideModules.isNotEmpty()) {
if (isAppGlideModuleGenerated) {
throw InvalidGlideSourceException(
"""Found $libraryGlideModules LibraryGlideModules after processing the AppGlideModule.
If you generated these LibraryGlideModules via another annotation processing, either
don't or also generate the AppGlideModule and do so in the same round as the
LibraryGlideModules or in a subsequent round"""
)
}
parseLibraryModulesAndWriteIndex(libraryGlideModules)
return invalidSymbols + appGlideModules
}

if (appGlideModules.isNotEmpty()) {
parseAppGlideModuleAndWriteGeneratedAppGlideModule(resolver, appGlideModules.single())
parseAppGlideModuleAndIndexesAndWriteGeneratedAppGlideModule(
resolver,
appGlideModules.single()
)
}

return invalidSymbols
}

private fun parseAppGlideModuleAndWriteGeneratedAppGlideModule(
private fun parseAppGlideModuleAndIndexesAndWriteGeneratedAppGlideModule(
resolver: Resolver,
appGlideModule: KSClassDeclaration,
) {
val appGlideModuleData =
AppGlideModuleParser(environment, resolver, appGlideModule).parseAppGlideModule()
val appGlideModuleGenerator = AppGlideModuleGenerator(appGlideModuleData)
val appGlideModuleFileSpec: FileSpec = appGlideModuleGenerator.generateAppGlideModule()
val sources = appGlideModuleData.sources.mapNotNull { it.containingFile }.toMutableList()
if (appGlideModule.containingFile != null) {
sources.add(appGlideModule.containingFile!!)
}
writeFile(
appGlideModuleFileSpec,
listOfNotNull(appGlideModule.containingFile),
sources,
)
}

private fun parseLibraryModulesAndWriteIndex(
libraryGlideModuleClassDeclarations: List<KSClassDeclaration>,
) {
val libraryGlideModulesParser =
LibraryGlideModulesParser(environment, libraryGlideModuleClassDeclarations)
val uniqueLibraryGlideModules = libraryGlideModulesParser.parseUnique()
val index: FileSpec = IndexGenerator.generate(uniqueLibraryGlideModules.map { it.name })
writeFile(index, uniqueLibraryGlideModules.mapNotNull { it.containingFile })
}

private fun writeFile(file: FileSpec, sources: List<KSFile>) {
environment.codeGenerator
.createNewFile(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.bumptech.glide.annotation.ksp

import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.annotation.ksp.LibraryGlideModuleData.LibraryModuleName
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFile
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.DelicateKotlinPoetApi
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.TypeSpec
import java.util.UUID

internal data class LibraryGlideModuleData(
val name: LibraryModuleName,
val containingFile: KSFile?,
) {
data class LibraryModuleName(val qualifiedName: String)
}

internal class LibraryGlideModulesParser(
private val environment: SymbolProcessorEnvironment,
private val libraryGlideModules: List<KSClassDeclaration>,
) {
init {
require(libraryGlideModules.isNotEmpty())
}

fun parseUnique(): List<LibraryGlideModuleData> {
val allLibraryGlideModules =
libraryGlideModules
.map {
LibraryGlideModuleData(
LibraryModuleName(it.qualifiedName!!.asString()),
it.containingFile
)
}
.toList()
val uniqueLibraryGlideModules = allLibraryGlideModules.associateBy { it.name }.values.toList()
if (uniqueLibraryGlideModules != libraryGlideModules) {
// Find the set of modules that have been included more than once by mapping the qualified
// name of the module to a count of the number of times it's been seen. Duplicates are then
// any keys that have a value > 1.
val duplicateModules: List<String> =
allLibraryGlideModules
.groupingBy { it.name.qualifiedName }
.eachCount()
.filter { it.value > 1 }
.keys
.toList()
environment.logger.warn(
GlideSymbolProcessorConstants.DUPLICATE_LIBRARY_MODULE_ERROR.format(duplicateModules)
)
}

return uniqueLibraryGlideModules
}
}

/**
* Generates an empty class with an annotation containing the class names of one or more
* LibraryGlideModules and/or one or more GlideExtensions.
*
* We use a separate class so that LibraryGlideModules and GlideExtensions written in libraries can
* be bundled into an AAR and later retrieved by the annotation processor when it processes the
* AppGlideModule in an application.
*
* The output file generated by this class with a single LibraryGlideModule looks like this:
*
* ```
* @com.bumptech.glide.annotation.compiler.Index(
* ["com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule"]
* )
* class Indexer_GlideModule_com_bumptech_glide_integration_okhttp3_OkHttpLibraryGlideModule
* ```
*
* This class is not a public API and used only internally by the processor.
*/
internal object IndexGenerator {
private const val INDEXER_NAME_PREFIX = "GlideIndexer_"
private const val MAXIMUM_FILE_NAME_LENGTH = 255

@OptIn(DelicateKotlinPoetApi::class) // We're using AnnotationSpec.builder
fun generate(
libraryModuleNames: List<LibraryModuleName>,
): FileSpec {
val libraryModuleQualifiedNames: List<String> = libraryModuleNames.map { it.qualifiedName }

val indexAnnotation: AnnotationSpec =
AnnotationSpec.builder(Index::class.java)
.addRepeatedMember(libraryModuleQualifiedNames)
.build()
val indexName = generateUniqueName(libraryModuleQualifiedNames)

return FileSpec.builder(GlideSymbolProcessorConstants.PACKAGE_NAME, indexName)
.addType(TypeSpec.classBuilder(indexName).addAnnotation(indexAnnotation).build())
.build()
}

private fun generateUniqueName(libraryModuleQualifiedNames: List<String>): String {
val glideModuleBasedName = generateNameFromLibraryModules(libraryModuleQualifiedNames)

// If the indexer name has too many packages/modules, it can exceed the file name length
// allowed by the file system, which can break compilation. To avoid that, fall back to a
// deterministic UUID.
return if (glideModuleBasedName.exceedsFileSystemMaxNameLength()) {
generateShortUUIDBasedName(glideModuleBasedName)
} else {
glideModuleBasedName
}
}

private fun String.exceedsFileSystemMaxNameLength() =
length >= MAXIMUM_FILE_NAME_LENGTH - INDEXER_NAME_PREFIX.length

private fun generateShortUUIDBasedName(glideModuleBasedName: String) =
INDEXER_NAME_PREFIX +
UUID.nameUUIDFromBytes(glideModuleBasedName.toByteArray()).toString().replace("-", "_")

private fun generateNameFromLibraryModules(libraryModuleQualifiedNames: List<String>): String {
return libraryModuleQualifiedNames.joinToString(
prefix = INDEXER_NAME_PREFIX + GlideModule::class.java.simpleName + "_",
separator = "_"
) { it.replace(".", "_") }
}

private fun AnnotationSpec.Builder.addRepeatedMember(repeatedMember: List<String>) =
addMember("[\n" + "%S,\n".repeat(repeatedMember.size) + "]", *repeatedMember.toTypedArray())
}
Loading

0 comments on commit 4016448

Please sign in to comment.