Introduce jdk-api-validator to ensure kotlin-reflect uses jdk6 API
Merge-request: KT-MR-6930 Merged-by: Abduqodiri Qurbonzoda <abduqodiri.qurbonzoda@jetbrains.com>
This commit is contained in:
committed by
Space Team
parent
5424c54fae
commit
7346cf4777
@@ -0,0 +1,46 @@
|
||||
# Verifies that a library compiled with a newer JDK is compatible with older JDK API
|
||||
|
||||
Currently, the project only checks `kotlin-reflect` for JDK 1.6 compatibility.
|
||||
The check is necessary to make sure applications using `kotlin-reflect` can run on older Android devices.
|
||||
|
||||
## How to run
|
||||
|
||||
Run from the root directory of the kotlin project:
|
||||
|
||||
`./gradlew :tools:jdk-api-validator:test`
|
||||
|
||||
## How to interpret the result
|
||||
|
||||
Successful completion of the `test` task means the checked libraries are compatible with JDK 1.6 API.
|
||||
In case of failure, the exact location and name of the violating API references are logged as error.
|
||||
|
||||
An example of validation error:
|
||||
|
||||
`[ERROR] /kotlin/libraries/reflect/build/libs/kotlin-reflect-1.9.255-SNAPSHOT.jar:kotlin/reflect/jvm/internal/ComputableClassValue.class:47: Undefined reference: void ClassValue.<init>()`
|
||||
|
||||
## How to fix a failure
|
||||
|
||||
If the violating reference can be desugared by R8 or its execution is prevented on Android platform,
|
||||
the error can be suppressed. See `suppressAnnotations` and `undefinedReferencesToIgnore` in `JdkApiUsageTest.kt`.
|
||||
Otherwise, the API should be avoided.
|
||||
|
||||
To check if an API can be desugared by R8:
|
||||
1. Identify the earliest R8 version that supports current Kotlin version [here](https://developer.android.com/build/kotlin-support).
|
||||
2. Download the R8 version jar artifact from Google's maven repository [here](https://maven.google.com/web/index.html#com.android.tools:r8).
|
||||
3. Run it using the `BackportedMethodList` entry point, e.g., `java -cp r8-8.0.40.jar com.android.tools.r8.BackportedMethodList`.
|
||||
4. Check if the violating API reference is in the printed list of methods.
|
||||
|
||||
Also, you can get the list of backported methods the downloaded version supports for a given Android API level:
|
||||
```shell
|
||||
$ java -cp r8-8.0.40.jar com.android.tools.r8.BackportedMethodList --help
|
||||
Usage: BackportedMethodList [options]
|
||||
Options are:
|
||||
--output <file> # Output result in <file>.
|
||||
--min-api <number> # Minimum Android API level for the application
|
||||
--desugared-lib <file> # Desugared library configuration (JSON from the
|
||||
# configuration)
|
||||
--lib <file> # The compilation SDK library (android.jar)
|
||||
--version # Print the version of BackportedMethodList.
|
||||
--help # Print this message.
|
||||
```
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
plugins {
|
||||
id("kotlin")
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
val testArtifacts by configurations.creating
|
||||
val signature by configurations.creating
|
||||
|
||||
sourceSets {
|
||||
"main" { none() }
|
||||
"test" { kotlin.srcDir("src/test") }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.codehaus.mojo:animal-sniffer:1.21")
|
||||
implementation(kotlinStdlib())
|
||||
|
||||
testImplementation(project(":kotlin-test:kotlin-test-junit"))
|
||||
|
||||
testArtifacts(project(":kotlin-reflect"))
|
||||
|
||||
signature("org.codehaus.mojo.signature:java16:1.1@signature")
|
||||
}
|
||||
|
||||
val signaturesDirectory = buildDir.resolve("signatures")
|
||||
|
||||
val collectSignatures by tasks.registering(Sync::class) {
|
||||
from(signature)
|
||||
into(signaturesDirectory)
|
||||
}
|
||||
|
||||
tasks.getByName<Test>("test") {
|
||||
dependsOn(collectSignatures)
|
||||
dependsOn(testArtifacts)
|
||||
|
||||
systemProperty("kotlinVersion", project.version)
|
||||
systemProperty("signaturesDirectory", signaturesDirectory)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2010-2023 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.tools.tests
|
||||
|
||||
import org.codehaus.mojo.animal_sniffer.*
|
||||
import org.codehaus.mojo.animal_sniffer.logging.Logger
|
||||
import org.codehaus.mojo.animal_sniffer.logging.PrintWriterLogger
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.*
|
||||
import kotlin.test.*
|
||||
|
||||
class JdkApiUsageTest {
|
||||
|
||||
@Test
|
||||
fun kotlinReflect() {
|
||||
testApiUsage(
|
||||
// Do not use the final jar artifact. It's shrunk by proguard.
|
||||
jarArtifact("../../reflect/build/libs", "kotlin-reflect", "shadow"),
|
||||
dependencies = listOf(jarArtifact("../../stdlib/jvm/build/libs", "kotlin-stdlib"))
|
||||
)
|
||||
}
|
||||
|
||||
private fun testApiUsage(artifact: Path, dependencies: List<Path>) {
|
||||
val logger = TestLogger()
|
||||
val additionalArtifacts = buildList {
|
||||
add(artifact)
|
||||
addAll(dependencies)
|
||||
}
|
||||
|
||||
val signatures = buildSignatures(additionalArtifacts, logger)
|
||||
if (logger.hasError) {
|
||||
fail("Building signatures has failed. See console logs for details.")
|
||||
}
|
||||
|
||||
checkSignatures(artifact, signatures, logger)
|
||||
if (logger.hasError) {
|
||||
fail("Checking signatures has failed. See console logs for details. "
|
||||
+ "See libraries/tools/jdk-api-validator/ReadMe.md to find out how to fix the failures.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkSignatures(artifact: Path, signatures: Path, logger: Logger) {
|
||||
val checker = SignatureChecker(signatures.inputStream(), emptySet(), logger)
|
||||
checker.setSourcePath(emptyList())
|
||||
checker.setAnnotationTypes(suppressAnnotations)
|
||||
checker.process(artifact.toFile()) // the overload that takes Path can't handle jar files
|
||||
}
|
||||
|
||||
private fun buildSignatures(additionalArtifacts: List<Path>, logger: Logger): Path {
|
||||
val signaturesDirectory = System.getProperty("signaturesDirectory")
|
||||
.let { requireNotNull(it) { "Specify signaturesDirectory with a system property" } }
|
||||
.let { Path(it) }
|
||||
|
||||
val mergedSignaturesDirectory = signaturesDirectory.resolveSibling("signatures-merged").createDirectories()
|
||||
val mergedSignaturesFile = createTempFile(mergedSignaturesDirectory, "merged", null)
|
||||
|
||||
val signatureInputStreams = signaturesDirectory.listDirectoryEntries()
|
||||
.map { it.inputStream() }
|
||||
val mergedSignaturesOutputStream = mergedSignaturesFile.outputStream()
|
||||
|
||||
val signatureBuilder = SignatureBuilder(signatureInputStreams.toTypedArray(), mergedSignaturesOutputStream, logger)
|
||||
try {
|
||||
additionalArtifacts.forEach {
|
||||
signatureBuilder.process(it.toFile()) // the overload that takes Path can't handle jar files
|
||||
}
|
||||
} finally {
|
||||
signatureBuilder.close()
|
||||
}
|
||||
|
||||
return mergedSignaturesFile
|
||||
}
|
||||
|
||||
private fun jarArtifact(basePath: String, jarBaseName: String, jarClassifier: String? = null): Path {
|
||||
val kotlinVersion = System.getProperty("kotlinVersion")
|
||||
.let { requireNotNull(it) { "Specify kotlinVersion with a system property" } }
|
||||
|
||||
val jarFullName = "$jarBaseName-$kotlinVersion${jarClassifier?.let { "-$it" } ?: ""}.jar"
|
||||
val base = Path(basePath).absolute().normalize()
|
||||
val file = base.listDirectoryEntries()
|
||||
.firstOrNull { it.name == jarFullName }
|
||||
|
||||
return file ?: throw Exception("No file with name $jarFullName in $base")
|
||||
}
|
||||
}
|
||||
|
||||
private val suppressAnnotations = listOf(
|
||||
"kotlin.reflect.jvm.internal.SuppressJdk6SignatureCheck",
|
||||
|
||||
// The following two fqn refer to the same annotation. The first is before its relocation,
|
||||
// the second is after. See kotlin-reflect build script.
|
||||
"org.jetbrains.kotlin.SuppressJdk6SignatureCheck",
|
||||
"kotlin.reflect.jvm.internal.impl.SuppressJdk6SignatureCheck",
|
||||
)
|
||||
|
||||
private val undefinedReferencesToIgnore = listOf(
|
||||
"int Integer.compareUnsigned(int, int)",
|
||||
"int Integer.remainderUnsigned(int, int)",
|
||||
"int Integer.divideUnsigned(int, int)",
|
||||
|
||||
"int Long.compareUnsigned(long, long)",
|
||||
"long Long.remainderUnsigned(long, long)",
|
||||
"long Long.divideUnsigned(long, long)",
|
||||
|
||||
"int Boolean.hashCode(boolean)",
|
||||
"int Byte.hashCode(byte)",
|
||||
"int Short.hashCode(short)",
|
||||
"int Integer.hashCode(int)",
|
||||
"int Long.hashCode(long)",
|
||||
"int Float.hashCode(float)",
|
||||
"int Double.hashCode(double)",
|
||||
)
|
||||
|
||||
private class TestLogger : Logger {
|
||||
private val logger = PrintWriterLogger(System.out)
|
||||
|
||||
var hasError: Boolean = false
|
||||
private set
|
||||
|
||||
override fun info(message: String) = logger.info(message)
|
||||
override fun info(message: String, t: Throwable?) = logger.info(message, t)
|
||||
|
||||
override fun debug(message: String) = logger.debug(message)
|
||||
override fun debug(message: String, t: Throwable?) = logger.debug(message, t)
|
||||
|
||||
override fun warn(message: String) = logger.warn(message)
|
||||
override fun warn(message: String, t: Throwable?) = logger.warn(message, t)
|
||||
|
||||
override fun error(message: String) = error(message, null)
|
||||
override fun error(message: String, t: Throwable?) {
|
||||
val shouldIgnore = undefinedReferencesToIgnore.any {
|
||||
message.endsWith("Undefined reference: $it")
|
||||
}
|
||||
if (shouldIgnore) {
|
||||
return
|
||||
}
|
||||
|
||||
hasError = true
|
||||
logger.error(message, t)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user