Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support reading classes from resulting jar #99

Merged
merged 5 commits into from Oct 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Expand Up @@ -125,6 +125,26 @@ apiValidation {
}
```

### Producing dump of a jar

By default, binary compatibility validator analyzes project output class files from `build/classes` directory when building an API dump.
If you pack these classes into an output jar not in a regular way, for example, by excluding certain classes, applying `shadow` plugin, and so on,
the API dump built from the original class files may no longer reflect the resulting jar contents accurately.
In that case, it makes sense to use the resulting jar as an input of the `apuBuild` task:

Kotlin
```kotlin
tasks {
apiBuild {
// "jar" here is the name of the default Jar task producing the resulting jar file
// in a multiplatform project it can be named "jvmJar"
// if you applied the shadow plugin, it creates the "shadowJar" task that produces the transformed jar
inputJar.value(jar.flatMap { it.archiveFile })
}
}
```


### Workflow

When starting to validate your library public API, we recommend the following workflow:
Expand Down
39 changes: 39 additions & 0 deletions src/functionalTest/kotlin/kotlinx/validation/test/InputJarTest.kt
@@ -0,0 +1,39 @@
/*
* Copyright 2016-2022 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.validation.test

import kotlinx.validation.api.*
import org.junit.*

class InputJarTest : BaseKotlinGradleTest() {

@Test
fun testOverrideInputJar() {
val runner = test {
buildGradleKts {
resolve("examples/gradle/base/withPlugin.gradle.kts")
resolve("examples/gradle/configuration/jarAsInput/inputJar.gradle.kts")
}

kotlin("Properties.kt") {
resolve("examples/classes/Properties.kt")
}

apiFile(projectName = rootProjectDir.name) {
resolve("examples/classes/PropertiesJarTransformed.dump")
}

runner {
arguments.add(":apiCheck")
}
}

runner.build().apply {
assertTaskSuccess(":jar")
assertTaskSuccess(":apiCheck")
}
}
}
@@ -0,0 +1,8 @@
public final class foo/ClassWithProperties {
public fun <init> ()V
public final fun getBar1 ()I
public final fun getBar2 ()I
public final fun setBar1 (I)V
public final fun setBar2 (I)V
}

@@ -0,0 +1,9 @@
tasks {
jar {
exclude("foo/HiddenField.class")
exclude("foo/HiddenProperty.class")
}
apiBuild {
inputJar.value(jar.flatMap { it.archiveFile })
}
}
21 changes: 9 additions & 12 deletions src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt
Expand Up @@ -179,7 +179,7 @@ private fun Project.configureKotlinCompilation(
val apiDirProvider = targetConfig.apiDir
val apiBuildDir = apiDirProvider.map { buildDir.resolve(it) }

val apiBuild = task<KotlinApiBuildTask>(targetConfig.apiTaskName("Build"), extension) {
val apiBuild = task<KotlinApiBuildTask>(targetConfig.apiTaskName("Build")) {
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
// Do not enable task for empty umbrella modules
isEnabled =
apiCheckEnabled(projectName, extension) && compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } }
Expand All @@ -206,6 +206,12 @@ private fun Project.configureKotlinCompilation(
val Project.sourceSets: SourceSetContainer
get() = convention.getPlugin(JavaPluginConvention::class.java).sourceSets

internal val Project.apiValidationExtensionOrNull: ApiValidationExtension?
get() =
generateSequence(this) { it.parent }
.map { it.extensions.findByType(ApiValidationExtension::class.java) }
.firstOrNull { it != null }

fun apiCheckEnabled(projectName: String, extension: ApiValidationExtension): Boolean =
projectName !in extension.ignoredProjects && !extension.validationDisabled

Expand All @@ -216,7 +222,7 @@ private fun Project.configureApiTasks(
) {
val projectName = project.name
val apiBuildDir = targetConfig.apiDir.map { buildDir.resolve(it) }
val apiBuild = task<KotlinApiBuildTask>(targetConfig.apiTaskName("Build"), extension) {
val apiBuild = task<KotlinApiBuildTask>(targetConfig.apiTaskName("Build")) {
isEnabled = apiCheckEnabled(projectName, extension)
// 'group' is not specified deliberately so it will be hidden from ./gradlew tasks
description =
Expand Down Expand Up @@ -247,16 +253,7 @@ private fun Project.configureCheckTasks(
isEnabled = apiCheckEnabled(projectName, extension) && apiBuild.map { it.enabled }.getOrElse(true)
group = "verification"
description = "Checks signatures of public API against the golden value in API folder for $projectName"
run {
val d = apiCheckDir.get()
projectApiDir = if (d.exists()) {
d
} else {
nonExistingProjectApiDir = d.toString()
null
}
this.apiBuildDir = apiBuildDir.get()
}
compareApiDumps(apiReferenceDir = apiCheckDir.get(), apiBuildDir = apiBuildDir.get())
dependsOn(apiBuild)
}

Expand Down
53 changes: 41 additions & 12 deletions src/main/kotlin/KotlinApiBuildTask.kt
Expand Up @@ -10,15 +10,23 @@ import org.gradle.api.*
import org.gradle.api.file.*
import org.gradle.api.tasks.*
import java.io.*
import java.util.jar.JarFile
import javax.inject.Inject

open class KotlinApiBuildTask @Inject constructor(
private val extension: ApiValidationExtension
) : DefaultTask() {

private val extension = project.apiValidationExtensionOrNull

@InputFiles
@Optional
@PathSensitive(PathSensitivity.RELATIVE)
var inputClassesDirs: FileCollection? = null

@InputFile
@Optional
@PathSensitive(PathSensitivity.RELATIVE)
lateinit var inputClassesDirs: FileCollection
val inputJar: RegularFileProperty = this.project.objects.fileProperty()

@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
Expand All @@ -27,14 +35,23 @@ open class KotlinApiBuildTask @Inject constructor(
@OutputDirectory
lateinit var outputApiDir: File

private var _ignoredPackages: Set<String>? = null
@get:Input
val ignoredPackages : Set<String> get() = extension.ignoredPackages
var ignoredPackages : Set<String>
get() = _ignoredPackages ?: extension?.ignoredPackages ?: emptySet()
set(value) { _ignoredPackages = value }

private var _nonPublicMarkes: Set<String>? = null
@get:Input
val nonPublicMarkers : Set<String> get() = extension.nonPublicMarkers
var nonPublicMarkers : Set<String>
get() = _nonPublicMarkes ?: extension?.nonPublicMarkers ?: emptySet()
set(value) { _nonPublicMarkes = value }

private var _ignoredClasses: Set<String>? = null
@get:Input
val ignoredClasses : Set<String> get() = extension.ignoredClasses
var ignoredClasses : Set<String>
get() = _ignoredClasses ?: extension?.ignoredClasses ?: emptySet()
set(value) { _ignoredClasses = value }

@get:Internal
internal val projectName = project.name
Expand All @@ -44,17 +61,29 @@ open class KotlinApiBuildTask @Inject constructor(
cleanup(outputApiDir)
outputApiDir.mkdirs()

val signatures = inputClassesDirs.asFileTree.asSequence()
.filter {
!it.isDirectory && it.name.endsWith(".class") && !it.name.startsWith("META-INF/")
}
.map { it.inputStream() }
.loadApiFromJvmClasses()
val inputClassesDirs = inputClassesDirs
val signatures = when {
// inputJar takes precedence if specified
inputJar.isPresent ->
JarFile(inputJar.get().asFile).use { it.loadApiFromJvmClasses() }
inputClassesDirs != null ->
inputClassesDirs.asFileTree.asSequence()
.filter {
!it.isDirectory && it.name.endsWith(".class") && !it.name.startsWith("META-INF/")
}
.map { it.inputStream() }
.loadApiFromJvmClasses()
else ->
throw GradleException("KotlinApiBuildTask should have either inputClassesDirs, or inputJar property set")
}


val filteredSignatures = signatures
.filterOutNonPublic(ignoredPackages, ignoredClasses)
.filterOutAnnotated(nonPublicMarkers.map { it.replace(".", "/") }.toSet())

outputApiDir.resolve("$projectName.api").bufferedWriter().use { writer ->
signatures
filteredSignatures
.sortedBy { it.name }
.forEach { api ->
writer.append(api.signature).appendLine(" {")
Expand Down
10 changes: 10 additions & 0 deletions src/main/kotlin/KotlinApiCompareTask.kt
Expand Up @@ -32,6 +32,16 @@ open class KotlinApiCompareTask @Inject constructor(private val objects: ObjectF
@Optional
var nonExistingProjectApiDir: String? = null

fun compareApiDumps(apiReferenceDir: File, apiBuildDir: File) {
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
if (apiReferenceDir.exists()) {
projectApiDir = apiReferenceDir
} else {
projectApiDir = null
nonExistingProjectApiDir = apiReferenceDir.toString()
}
this.apiBuildDir = apiBuildDir
}

@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
lateinit var apiBuildDir: File
Expand Down