Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ buildscript {
apply from: rootProject.file("dependencies.gradle")

repositories {
mavenLocal()
google()
mavenCentral()
gradlePluginPortal()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
package com.airbnb.deeplinkdispatch.base

/**
* Constants related to manifest generation for DeepLinkDispatch.
* Constants related to manifest and asset generation for DeepLinkDispatch.
*
* These constants are shared between:
* - The KSP processor that generates the manifest
* - The Gradle plugin that reads and merges the generated manifest
* - The KSP processor that generates the manifest and match index assets
* - The Gradle plugin that reads and merges the generated manifest and assets
*/
object ManifestGeneration {
/**
* KSP argument key to enable asset-based match index generation.
*
* When this argument is set to "true", the KSP processor will:
* - Write the binary match index as an asset file instead of encoding it as a string
* - Generate a registry class that loads the index from assets via AssetManager
*
* This is automatically set by ManifestGenerationPlugin when applied to library modules.
*/
const val OPTION_USE_ASSET_BASED_MATCH_INDEX = "deepLink.useAssetBasedMatchIndex"

/**
* The resource path where the KSP processor writes the generated AndroidManifest.xml.
*
Expand All @@ -18,4 +29,63 @@ object ManifestGeneration {
* - ManifestGenerationPlugin to locate and merge the generated manifest
*/
const val MANIFEST_RESOURCE_PATH = "deeplinkdispatch/AndroidManifest.xml"

/**
* The asset directory prefix where the KSP processor writes the binary match index files.
*
* The full path will be: build/generated/ksp/<variant>/resources/assets/<MATCH_INDEX_ASSET_PATH_PREFIX>/<module>.bin
*
* This path is used by:
* - DeepLinkProcessor in the processor to write the match index via XFiler.writeResource()
* - ManifestGenerationPlugin to locate and merge the generated assets
* - Generated registry classes at runtime to load the match index via AssetManager
*/
const val MATCH_INDEX_ASSET_PATH_PREFIX = "deeplinkdispatch"

/**
* File extension for match index asset files.
*/
const val MATCH_INDEX_ASSET_EXTENSION = ".bin"

/**
* Generates the asset path for a given module name's match index.
*
* @param moduleName The module name (will be lowercased)
* @return The asset path relative to the assets directory
*/
fun getMatchIndexAssetPath(moduleName: String): String =
"$MATCH_INDEX_ASSET_PATH_PREFIX/${moduleName.lowercase()}$MATCH_INDEX_ASSET_EXTENSION"

/**
* Generates the full resource path (including assets/ prefix) for writing the match index via XFiler.
*
* @param moduleName The module name (will be lowercased)
* @return The resource path for XFiler.writeResource()
*/
fun getMatchIndexResourcePath(moduleName: String): String = "assets/${getMatchIndexAssetPath(moduleName)}"

/**
* Subdirectory inside the KSP output where `XFiler.writeResource()` places files.
* Full path: `build/generated/ksp/<variant>/resources/...`
*/
const val KSP_RESOURCES_SUBDIR = "resources"

/**
* Returns the build-relative path to KSP's generated resources directory for a variant.
* Example: `generated/ksp/debug/resources`
*/
fun kspResourcesDir(variantName: String): String = "generated/ksp/$variantName/$KSP_RESOURCES_SUBDIR"

/**
* Returns the build-relative path to the generated manifest under KSP resources for a variant.
*/
fun kspGeneratedManifestPath(variantName: String): String =
"${kspResourcesDir(variantName)}/$MANIFEST_RESOURCE_PATH"

/**
* Returns the build-relative path to the directory containing the generated asset files under
* KSP resources for a variant.
*/
fun kspGeneratedAssetsDir(variantName: String): String =
"${kspResourcesDir(variantName)}/assets/$MATCH_INDEX_ASSET_PATH_PREFIX"
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.airbnb.deeplinkdispatch.gradleplugin

import com.android.build.api.variant.Variant
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import java.io.File

/**
* Gradle task that merges KSP-generated assets with AGP's merged assets.
*
* This task is wired into AGP's Artifacts API as an ASSETS transformer. It takes
* the existing assets from AGP and adds the DeepLinkDispatch binary match index
* files that were generated by KSP and relocated by RelocateDeepLinkAssetsTask.
*
* The match index files are placed under assets/deeplinkdispatch/<modulename>.bin
* and are used by the generated registry classes to load the deep link matching
* tree at runtime without the overhead of string parsing and encoding.
*/
@CacheableTask
abstract class MergeDeepLinkAssetsTask : DefaultTask() {

/**
* The input assets directory from AGP's merged assets.
*/
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val inputAssets: DirectoryProperty

/**
* The directory containing DeepLinkDispatch assets relocated from KSP output.
* Path: build/intermediates/deeplinkdispatch/<variant>/assets/deeplinkdispatch/
*/
@get:InputFiles
@get:Optional
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val additionalAssetsDir: DirectoryProperty

/**
* The output assets directory that will replace the merged assets.
*/
@get:OutputDirectory
abstract val outputAssets: DirectoryProperty

@TaskAction
fun taskAction() {
val inputDir = inputAssets.get().asFile
val outputDir = outputAssets.get().asFile
val additionalDir = additionalAssetsDir.orNull?.asFile

// Clean output directory
outputDir.deleteRecursively()
outputDir.mkdirs()

// Copy all existing assets from input
if (inputDir.exists()) {
inputDir.copyRecursively(outputDir, overwrite = true)
}

// Merge DeepLinkDispatch assets if they exist
if (additionalDir != null && additionalDir.exists() && additionalDir.isDirectory) {
val files = additionalDir.listFiles() ?: emptyArray()
if (files.isNotEmpty()) {
// Create deeplinkdispatch subdirectory in output
val deeplinkDispatchDir = File(outputDir, "deeplinkdispatch")
deeplinkDispatchDir.mkdirs()

// Copy each match index file
files.forEach { file ->
if (file.isFile) {
file.copyTo(File(deeplinkDispatchDir, file.name), overwrite = true)
}
}
}
}
}

companion object {
internal fun taskName(variant: Variant) =
"mergeDeepLinkAssets${variant.name.replaceFirstChar { it.uppercase() }}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.airbnb.deeplinkdispatch.gradleplugin

import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction

/**
* Task that relocates the KSP-generated assets from the resources directory to a safe location.
*
* This task must run after KSP and before any asset merge tasks. It moves the asset
* files out of the KSP resources directory to prevent them from being included as
* Java resources when this library is used as a project dependency.
*
* The assets are the binary match index files generated by the DeepLinkDispatch processor
* for KSP-based builds. They will be merged into the final assets via the asset transform task.
*
* This is a separate task (rather than a doLast on KSP) because doLast doesn't run when
* KSP is restored FROM-CACHE. This task will always run after KSP, even if KSP was cached.
*/
abstract class RelocateDeepLinkAssetsTask : DefaultTask() {

/**
* The directory where KSP generates the asset files.
* Path: build/generated/ksp/<variant>/resources/assets/deeplinkdispatch/
*/
// Using @InputFiles (instead of @InputDirectory) so the task can still execute when the
// directory hasn't been created yet — modules with no deep links never produce this dir.
@get:InputFiles
@get:Optional
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val kspAssetsDir: DirectoryProperty

/**
* The safe location where assets will be moved to.
* Path: build/intermediates/deeplinkdispatch/<variant>/assets/deeplinkdispatch/
*/
@get:OutputDirectory
abstract val safeAssetsDir: DirectoryProperty

init {
// Force the task to run when the source directory exists with files.
// Gradle's default up-to-date check compares content, but this task also *removes*
// the source files (to keep them out of Java-resource processing), so if KSP is
// restored FROM-CACHE we need to run again to re-delete them.
outputs.upToDateWhen {
val sourceDir = kspAssetsDir.orNull?.asFile
sourceDir == null || !sourceDir.exists() || sourceDir.listFiles()?.isEmpty() != false
}
}

@TaskAction
fun taskAction() {
val sourceDir = kspAssetsDir.orNull?.asFile
val destDir = safeAssetsDir.get().asFile

if (sourceDir != null && sourceDir.exists() && sourceDir.isDirectory) {
val files = sourceDir.listFiles()?.filter { it.isFile }.orEmpty()
if (files.isNotEmpty()) {
destDir.mkdirs()
files.forEach { file ->
file.copyTo(destDir.resolve(file.name), overwrite = true)
file.delete()
}
// Clean up empty parent dirs up to (but not including) the `resources` root
// so the dir tree doesn't leak into Java-resource processing.
var parentDir: java.io.File? = sourceDir
while (parentDir != null &&
parentDir.name != "resources" &&
parentDir.isDirectory &&
parentDir.list()?.isEmpty() == true
) {
val nextParent = parentDir.parentFile
parentDir.delete()
parentDir = nextParent
}
}
}
// If neither source nor dest has files, no assets were generated — fine for modules
// without deep links. If source is gone but dest has the previously-moved files, they
// remain valid across incremental builds.
}
}
Loading
Loading