PoC APIs usage validation
^KT-65540
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 182e387..3bc8a8d 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -3382,6 +3382,12 @@
<sha256 value="3b33fb52e494a78d803c3836fd4cec45fd22ca1cad62829db83912c6bfa503b9" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="org.codehaus.mojo" name="animal-sniffer" version="1.23">
+ <artifact name="animal-sniffer-1.23.jar">
+ <md5 value="4d1723b6c0ff4a1b148b231497c1ee2b" origin="Generated by Gradle"/>
+ <sha256 value="a175ba9f939bca4b7066961842507dd5d25ad4236249c13cfbd3407ae436dedd" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="org.codehaus.mojo" name="animal-sniffer-annotations" version="1.14">
<artifact name="animal-sniffer-annotations-1.14.jar">
<md5 value="9d42e46845c874f1710a9f6a741f6c14" origin="Generated by Gradle"/>
@@ -3394,6 +3400,12 @@
<sha256 value="e67ec27ceeaf13ab5d54cf5fdbcc544c41b4db8d02d9f006678cca2c7c13ee9d" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="org.codehaus.mojo" name="animal-sniffer-annotations" version="1.23">
+ <artifact name="animal-sniffer-annotations-1.23.jar">
+ <md5 value="13729ebd1fbdddc25d7feb7694d3028d" origin="Generated by Gradle"/>
+ <sha256 value="9ffe526bf43a6348e9d8b33b9cd6f580a7f5eed0cf055913007eda263de974d0" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="org.codehaus.mojo.signature" name="java16" version="1.1">
<artifact name="java16-1.1.signature">
<md5 value="f54abe9fb83f358412ad738cc46fc158" origin="Generated by Gradle"/>
diff --git a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts
index d3867e8..9b2484e 100644
--- a/libraries/tools/kotlin-gradle-plugin/build.gradle.kts
+++ b/libraries/tools/kotlin-gradle-plugin/build.gradle.kts
@@ -305,3 +305,43 @@
dependsOn("functionalTest")
}
}
+
+fun copyConfigurationOverridingAndroidLibraries(
+ configuration: Configuration,
+ androidVersion: String,
+): Configuration {
+ val copy = configuration.copyRecursive()
+ copy.resolutionStrategy.disableDependencyVerification()
+ copy.resolutionStrategy {
+ force(
+ "com.android.tools.build:gradle-api:$androidVersion",
+ "com.android.tools.build:gradle:$androidVersion",
+ "com.android.tools.build:builder:$androidVersion",
+ "com.android.tools.build:builder-model:$androidVersion",
+ )
+ }
+ return copy
+}
+
+val oldAndroidLibraries = configurations.commonCompileClasspath.map {
+ copyConfigurationOverridingAndroidLibraries(it, "7.1.3")
+}
+val newAndroidLibraries = configurations.commonCompileClasspath.map {
+ val newAndroidLibrariesConf = copyConfigurationOverridingAndroidLibraries(it, "8.2.2")
+ newAndroidLibrariesConf.attributes.attribute(
+ Attribute.of("org.gradle.jvm.version", Integer::class.java),
+ @Suppress("DEPRECATION")
+ Integer(11),
+ )
+ newAndroidLibrariesConf
+}
+val validationTask = validateRuntimeAPIsUsage(
+ oldApis = project.files(oldAndroidLibraries),
+ newApis = project.files(newAndroidLibraries),
+ filesToValidate = project.files(kotlin.target.compilations.getByName("common").compileTaskProvider),
+ sourcesPath = layout.projectDirectory.dir("src/common").asFile,
+ unsafeApisUsageAnnotationFqn = "org.jetbrains.kotlin.gradle.utils.UnsafeAtRuntime",
+)
+tasks.named("check").configure {
+ dependsOn(validationTask)
+}
\ No newline at end of file
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/UnsafeAtRuntime.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/UnsafeAtRuntime.kt
new file mode 100644
index 0000000..c463b94
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/UnsafeAtRuntime.kt
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.gradle.utils
+
+@Retention(AnnotationRetention.BINARY)
+annotation class UnsafeAtRuntime
\ No newline at end of file
diff --git a/repo/gradle-build-conventions/buildsrc-compat/build.gradle.kts b/repo/gradle-build-conventions/buildsrc-compat/build.gradle.kts
index 735e8c1..c74bd96 100644
--- a/repo/gradle-build-conventions/buildsrc-compat/build.gradle.kts
+++ b/repo/gradle-build-conventions/buildsrc-compat/build.gradle.kts
@@ -124,6 +124,8 @@
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.platform.launcher)
testRuntimeOnly(libs.junit.jupiter.engine)
+
+ implementation("org.codehaus.mojo:animal-sniffer:1.23")
}
tasks.withType<Test>().configureEach {
diff --git a/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/apisUsageValidation.kt b/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/apisUsageValidation.kt
new file mode 100644
index 0000000..f0117fe
--- /dev/null
+++ b/repo/gradle-build-conventions/buildsrc-compat/src/main/kotlin/apisUsageValidation.kt
@@ -0,0 +1,114 @@
+import org.codehaus.mojo.animal_sniffer.SignatureBuilder
+import org.codehaus.mojo.animal_sniffer.SignatureChecker
+import org.codehaus.mojo.animal_sniffer.logging.PrintWriterLogger
+import org.codehaus.mojo.animal_sniffer.logging.Logger
+import org.gradle.api.Project
+import org.gradle.api.file.FileCollection
+import org.gradle.api.file.ProjectLayout
+import org.gradle.api.tasks.TaskProvider
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+
+
+fun Project.validateRuntimeAPIsUsage(
+ oldApis: FileCollection,
+ newApis: FileCollection,
+ filesToValidate: FileCollection,
+ sourcesPath: File,
+ unsafeApisUsageAnnotationFqn: String,
+): TaskProvider<*> {
+ return tasks.register("apiUsageValidation") {
+ inputs.files(
+ oldApis,
+ newApis,
+ filesToValidate,
+ )
+ val layout = project.layout
+ doLast {
+ val unrecognizedSelectorsInOldApis = dumpUnrecognizedSignature(
+ createKnownSignatureFile(
+ layout,
+ oldApis,
+ "oldApis",
+ ),
+ filesToValidate,
+ sourcesPath,
+ unsafeApisUsageAnnotationFqn
+ )
+ val unrecognizedSelectorsInNewApis = dumpUnrecognizedSignature(
+ createKnownSignatureFile(
+ layout,
+ newApis,
+ "newApis",
+ ),
+ filesToValidate,
+ sourcesPath,
+ unsafeApisUsageAnnotationFqn
+ )
+
+ val apisFromNewVersionNotPresentInOld = unrecognizedSelectorsInOldApis.subtract(unrecognizedSelectorsInNewApis)
+ val apisFromOldVersionNotPresentInNew = unrecognizedSelectorsInNewApis.subtract(unrecognizedSelectorsInOldApis)
+
+ if (apisFromNewVersionNotPresentInOld.isNotEmpty() || apisFromOldVersionNotPresentInNew.isNotEmpty()) {
+ error(
+ buildString {
+ appendLine("Unsafe APIs unmarked by $unsafeApisUsageAnnotationFqn detected.")
+ if (apisFromNewVersionNotPresentInOld.isNotEmpty()) {
+ appendLine("These APIs only exist in newer libraries:")
+ apisFromNewVersionNotPresentInOld.forEach(::appendLine)
+ appendLine()
+ }
+ if (apisFromOldVersionNotPresentInNew.isNotEmpty()) {
+ appendLine("These APIs only exist in older libraries:")
+ apisFromOldVersionNotPresentInNew.forEach(::appendLine)
+ }
+ }
+ )
+ }
+ }
+ }
+}
+
+private fun createKnownSignatureFile(
+ layout: ProjectLayout,
+ apis: FileCollection,
+ signatureFileName: String,
+): File {
+ val file = layout.buildDirectory.file(signatureFileName).get().asFile
+ FileOutputStream(file).use {
+ val builder = SignatureBuilder(it, PrintWriterLogger(System.out))
+ apis.forEach {
+ builder.process(it)
+ }
+ builder.close()
+ }
+ return file
+}
+
+private fun dumpUnrecognizedSignature(
+ knownSignatures: File,
+ classFiles: FileCollection,
+ sourcesPath: File,
+ unsafeApisUsageAnnotationFqn: String,
+): Set<String> {
+ val messages = mutableSetOf<String>()
+ val checker = SignatureChecker(
+ FileInputStream(knownSignatures), emptySet(),
+ object : Logger by PrintWriterLogger(System.out) {
+ override fun error(message: String?) {
+ message?.let { messages.add(it) }
+ }
+
+ override fun error(message: String?, t: Throwable?) {
+ message?.let { messages.add(it) }
+ }
+ }
+ )
+ checker.setAnnotationTypes(listOf(unsafeApisUsageAnnotationFqn))
+ checker.setSourcePath(listOf(sourcesPath))
+ classFiles.forEach {
+ checker.process(it)
+ }
+ return messages
+}
\ No newline at end of file