Skip to content

Commit 5a03695

Browse files
committed
Avoid using hasAnySourcesPredicate-predicate to filter klib-related tasks
1 parent 42abf7b commit 5a03695

File tree

7 files changed

+144
-48
lines changed

7 files changed

+144
-48
lines changed

src/functionalTest/kotlin/kotlinx/validation/test/KlibVerificationTests.kt

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -666,12 +666,29 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() {
666666
runApiDump()
667667
}
668668

669-
runner.build().apply {
669+
runner.withDebug(true).build().apply {
670670
assertTaskSkipped(":klibApiDump")
671671
}
672672
assertFalse(runner.projectDir.resolve("api").exists())
673673
}
674674

675+
@Test
676+
fun `apiDump should remove dump file if the project does not contain sources anymore`() {
677+
val runner = test {
678+
baseProjectSetting()
679+
addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest")
680+
abiFile(projectName = "testproject") {
681+
resolve("/examples/classes/AnotherBuildConfig.klib.dump")
682+
}
683+
runApiDump()
684+
}
685+
686+
runner.build().apply {
687+
assertTaskSuccess(":klibApiDump")
688+
}
689+
assertFalse(runner.projectDir.resolve("api").resolve("testproject.klib.api").exists())
690+
}
691+
675692
@Test
676693
fun `apiDump should not fail if there is only one target`() {
677694
val runner = test {
@@ -698,8 +715,7 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() {
698715
val runner = test {
699716
baseProjectSetting()
700717
additionalBuildConfig("/examples/gradle/configuration/generatedSources/generatedSources.gradle.kts")
701-
// TODO: enable configuration cache back when we start skipping tasks correctly
702-
runner(withConfigurationCache = false) {
718+
runner {
703719
arguments.add(":apiDump")
704720
}
705721
}
@@ -714,8 +730,7 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() {
714730
abiFile(projectName = "testproject") {
715731
resolve("/examples/classes/GeneratedSources.klib.dump")
716732
}
717-
// TODO: enable configuration cache back when we start skipping tasks correctly
718-
runner(withConfigurationCache = false) {
733+
runner {
719734
arguments.add(":apiCheck")
720735
}
721736
}

src/main/kotlin/-Utils.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import kotlinx.validation.api.klib.konanTargetNameMapping
1010
import org.gradle.api.file.RegularFileProperty
1111
import org.gradle.api.tasks.Input
1212
import org.gradle.api.tasks.InputFiles
13+
import org.gradle.api.tasks.SkipWhenEmpty
1314
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
1415
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
1516
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
@@ -51,5 +52,6 @@ public class KlibDumpMetadata(
5152
* Path to a resulting dump file.
5253
*/
5354
@get:InputFiles
55+
@get:SkipWhenEmpty
5456
public val dumpFile: RegularFileProperty
5557
) : Serializable

src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ private fun Project.configureCheckTasks(
304304
}
305305

306306
val dumpFileName = project.jvmDumpFileName
307-
val apiDump = task<CopyFile>(targetConfig.apiTaskName("Dump")) {
307+
val apiDump = task<SyncFile>(targetConfig.apiTaskName("Dump")) {
308308
isEnabled = apiCheckEnabled(projectName, extension) && apiBuild.map { it.enabled }.getOrElse(true)
309309
group = "other"
310310
description = "Syncs API from build dir to ${targetConfig.apiDir} dir for $projectName"
@@ -343,7 +343,7 @@ private const val KLIB_INFERRED_DUMPS_DIRECTORY = "klib-all"
343343
* Here's how different tasks depend on each other:
344344
* - `klibApiCheck` ([KotlinApiCompareTask]) depends on `klibApiMerge` and `klibApiExtractForValidation` tasks;
345345
* this task itself does not perform anything except comparing the result of a merge, with a preprocessed golden value;
346-
* - `klibApiDump` ([CopyFile]) depends on `klibApiMergeInferred` and simply moves the merged ABI dump into a configured
346+
* - `klibApiDump` ([SyncFile]) depends on `klibApiMergeInferred` and simply moves the merged ABI dump into a configured
347347
* api directory within a project;
348348
* - `klibApiMerge` and `klibApiMergeInferred` are both [KotlinKlibMergeAbiTask] instances merging multiple individual
349349
* KLib ABI dumps into a single merged dump file; these tasks differs only by their dependencies and input dump files
@@ -387,51 +387,36 @@ private class KlibValidationPipelineBuilder(
387387
val klibMergeInferred = project.mergeInferredKlibsUmbrellaTask(klibDumpConfig, klibMergeInferredDir)
388388
val klibDump = project.dumpKlibsTask(klibDumpConfig)
389389
val klibExtractAbiForSupportedTargets = project.extractAbi(klibDumpConfig, klibApiDir, klibExtractedFileDir)
390-
val klibCheck = project.checkKlibsTask(klibDumpConfig, project.provider { klibExtractedFileDir }, klibMergeDir)
390+
val klibCheck = project.checkKlibsTask(klibDumpConfig)
391391
klibDump.configure {
392392
it.from.set(klibMergeInferred.flatMap { it.mergedApiFile })
393393
val filename = project.klibDumpFileName
394394
it.to.fileProvider(klibApiDir.map { it.resolve(filename) })
395395
}
396-
commonApiDump.configure { it.dependsOn(klibDump) }
397-
398-
klibDump.configure { it.dependsOn(klibMergeInferred) }
399-
// Extraction task depends on supportedTargets() provider which returns a set of targets supported
400-
// by the host compiler and having some sources. A set of sources for a target may change until the actual
401-
// klib compilation will take place, so we may observe incorrect value if check source sets earlier.
402-
// Merge task already depends on compilations, so instead of adding each compilation task to the extraction's
403-
// dependency set, we can depend on the merge task itself.
404-
klibExtractAbiForSupportedTargets.configure {
405-
it.dependsOn(klibMerge)
406-
}
407396
klibCheck.configure {
408-
it.dependsOn(klibExtractAbiForSupportedTargets)
397+
it.projectApiFile.set(klibExtractAbiForSupportedTargets.flatMap { it.outputAbiFile })
398+
it.generatedApiFile.set(klibMerge.flatMap { it.mergedApiFile })
409399
}
400+
commonApiDump.configure { it.dependsOn(klibDump) }
410401
commonApiCheck.configure { it.dependsOn(klibCheck) }
411402
project.configureTargets(klibApiDir, klibMerge, klibMergeInferred)
412403
}
413404

414-
private fun Project.checkKlibsTask(
415-
klibDumpConfig: TargetConfig,
416-
klibApiDir: Provider<File>,
417-
klibMergeDir: File
418-
) = project.task<KotlinApiCompareTask>(klibDumpConfig.apiTaskName("Check")) {
405+
private fun Project.checkKlibsTask(klibDumpConfig: TargetConfig)
406+
= project.task<KotlinApiCompareLazyTask>(klibDumpConfig.apiTaskName("Check")) {
419407
isEnabled = klibAbiCheckEnabled(project.name, extension)
420408
group = "verification"
421-
description = "Checks signatures of a public KLib ABI against the golden value in ABI folder for " +
422-
project.name
423-
projectApiFile = klibApiDir.get().resolve(klibDumpFileName)
424-
generatedApiFile = klibMergeDir.resolve(klibDumpFileName)
425-
val hasCompilableTargets = project.hasCompilableTargetsPredicate()
426-
onlyIf("There are no klibs compiled for the project") { hasCompilableTargets.get() }
409+
description = "Checks signatures of a public KLib ABI against the golden value in ABI folder for ${project.name}"
427410
}
428411

429-
private fun Project.dumpKlibsTask(klibDumpConfig: TargetConfig) = project.task<CopyFile>(klibDumpConfig.apiTaskName("Dump")) {
412+
private fun Project.dumpKlibsTask(klibDumpConfig: TargetConfig) = project.task<SyncFile>(klibDumpConfig.apiTaskName("Dump")) {
430413
isEnabled = klibAbiCheckEnabled(project.name, extension)
431414
description = "Syncs a KLib ABI dump from a build dir to the ${klibDumpConfig.apiDir} dir for ${project.name}"
432415
group = "other"
433-
val hasCompilableTargets = project.hasCompilableTargetsPredicate()
434-
onlyIf("There are no klibs compiled for the project") { hasCompilableTargets.get() }
416+
onlyIf {
417+
it as SyncFile
418+
it.to.get().asFile.exists() || it.from.get().asFile.exists()
419+
}
435420
}
436421

437422
private fun Project.extractAbi(
@@ -450,8 +435,6 @@ private class KlibValidationPipelineBuilder(
450435
requiredTargets.addAll(supportedTargets())
451436
inputAbiFile.set(klibApiDir.get().resolve(klibDumpFileName))
452437
outputAbiFile.set(klibOutputDir.resolve(klibDumpFileName))
453-
val hasCompilableTargets = project.hasCompilableTargetsPredicate()
454-
onlyIf("There are no klibs compiled for the project") { hasCompilableTargets.get() }
455438
}
456439

457440
private fun Project.mergeInferredKlibsUmbrellaTask(
@@ -466,8 +449,6 @@ private class KlibValidationPipelineBuilder(
466449
"different targets (including inferred dumps for unsupported targets) " +
467450
"into a single merged KLib ABI dump"
468451
mergedApiFile.set(klibMergeDir.resolve(klibDumpFileName))
469-
val hasCompilableTargets = project.hasCompilableTargetsPredicate()
470-
onlyIf("There are no dumps to merge") { hasCompilableTargets.get() }
471452
}
472453

473454
private fun Project.mergeKlibsUmbrellaTask(
@@ -478,8 +459,6 @@ private class KlibValidationPipelineBuilder(
478459
description = "Merges multiple KLib ABI dump files generated for " +
479460
"different targets into a single merged KLib ABI dump"
480461
mergedApiFile.set(klibMergeDir.resolve(klibDumpFileName))
481-
val hasCompilableTargets = project.hasCompilableTargetsPredicate()
482-
onlyIf("There are no dumps to merge") { hasCompilableTargets.get() }
483462
}
484463

485464
fun Project.bannedTargets(): Set<String> {
@@ -597,8 +576,6 @@ private class KlibValidationPipelineBuilder(
597576
val buildTask = project.task<KotlinKlibAbiBuildTask>(targetConfig.apiTaskName("Build")) {
598577
// Do not enable task for empty umbrella modules
599578
isEnabled = klibAbiCheckEnabled(projectName, extension)
600-
val hasSourcesPredicate = compilation.hasAnySourcesPredicate()
601-
onlyIf { hasSourcesPredicate.get() }
602579
// 'group' is not specified deliberately, so it will be hidden from ./gradlew tasks
603580
description = "Builds Kotlin KLib ABI dump for 'main' compilations of $projectName. " +
604581
"Complementary task and shouldn't be called manually"

src/main/kotlin/KotlinApiCompareTask.kt

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,96 @@ public open class KotlinApiCompareTask @Inject constructor(private val objects:
104104
return diff.joinToString("\n")
105105
}
106106
}
107+
108+
// TODO: decide what to do with to old compare task
109+
internal abstract class KotlinApiCompareLazyTask @Inject constructor(private val objects: ObjectFactory): DefaultTask() {
110+
111+
@get:SkipWhenEmpty
112+
@get:InputFiles
113+
@get:PathSensitive(PathSensitivity.RELATIVE)
114+
abstract val projectApiFile: RegularFileProperty
115+
116+
@get:SkipWhenEmpty
117+
@get:InputFiles
118+
abstract val generatedApiFile: RegularFileProperty
119+
120+
private val projectName = project.name
121+
122+
private val rootDir = project.rootProject.rootDir
123+
124+
@TaskAction
125+
internal fun verify() {
126+
val projectApiDir = projectApiFile.get().asFile.parentFile
127+
if (!projectApiDir.exists()) {
128+
error("Expected folder with API declarations '$projectApiDir' does not exist.\n" +
129+
"Please ensure that ':apiDump' was executed in order to get API dump to compare the build against")
130+
}
131+
val buildApiDir = generatedApiFile.get().asFile.parentFile
132+
if (!buildApiDir.exists()) {
133+
error("Expected folder with generate API declarations '$buildApiDir' does not exist.")
134+
}
135+
val subject = projectName
136+
137+
/*
138+
* We use case-insensitive comparison to workaround issues with case-insensitive OSes
139+
* and Gradle behaving slightly different on different platforms.
140+
* We neither know original sensitivity of existing .api files, not
141+
* build ones, because projectName that is part of the path can have any sensitvity.
142+
* To workaround that, we replace paths we are looking for the same paths that
143+
* actually exist on FS.
144+
*/
145+
fun caseInsensitiveMap() = TreeMap<String, RelativePath> { rp, rp2 ->
146+
rp.compareTo(rp2, true)
147+
}
148+
149+
val apiBuildDirFiles = caseInsensitiveMap()
150+
val expectedApiFiles = caseInsensitiveMap()
151+
152+
objects.fileTree().from(buildApiDir).visit { file ->
153+
apiBuildDirFiles[file.name] = file.relativePath
154+
}
155+
objects.fileTree().from(projectApiDir).visit { file ->
156+
expectedApiFiles[file.name] = file.relativePath
157+
}
158+
159+
if (!expectedApiFiles.containsKey(projectApiFile.get().asFile.name)) {
160+
error("File ${projectApiFile.get().asFile.name} is missing from ${projectApiDir.relativeDirPath()}, please run " +
161+
":$subject:apiDump task to generate one")
162+
}
163+
if (!apiBuildDirFiles.containsKey(generatedApiFile.get().asFile.name)) {
164+
error("File ${generatedApiFile.get().asFile.name} is missing from dump results.")
165+
}
166+
167+
// Normalize case-sensitivity
168+
val expectedApiDeclaration = expectedApiFiles.getValue(projectApiFile.get().asFile.name)
169+
val actualApiDeclaration = apiBuildDirFiles.getValue(generatedApiFile.get().asFile.name)
170+
val diffSet = mutableSetOf<String>()
171+
val expectedFile = expectedApiDeclaration.getFile(projectApiDir)
172+
val actualFile = actualApiDeclaration.getFile(buildApiDir)
173+
val diff = compareFiles(expectedFile, actualFile)
174+
if (diff != null) diffSet.add(diff)
175+
if (diffSet.isNotEmpty()) {
176+
val diffText = diffSet.joinToString("\n\n")
177+
error("API check failed for project $subject.\n$diffText\n\n You can run :$subject:apiDump task to overwrite API declarations")
178+
}
179+
}
180+
181+
private fun File.relativeDirPath(): String {
182+
return toRelativeString(rootDir) + File.separator
183+
}
184+
185+
private fun compareFiles(checkFile: File, builtFile: File): String? {
186+
val checkText = checkFile.readText()
187+
val builtText = builtFile.readText()
188+
189+
// We don't compare full text because newlines on Windows & Linux/macOS are different
190+
val checkLines = checkText.lines()
191+
val builtLines = builtText.lines()
192+
if (checkLines == builtLines)
193+
return null
194+
195+
val patch = DiffUtils.diff(checkLines, builtLines)
196+
val diff = UnifiedDiffUtils.generateUnifiedDiff(checkFile.toString(), builtFile.toString(), checkLines, patch, 3)
197+
return diff.joinToString("\n")
198+
}
199+
}

src/main/kotlin/KotlinKlibAbiBuildTask.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import org.gradle.api.provider.Property
1212
import org.gradle.api.tasks.Input
1313
import org.gradle.api.tasks.InputFiles
1414
import org.gradle.api.tasks.OutputFile
15+
import org.gradle.api.tasks.SkipWhenEmpty
1516
import org.gradle.api.tasks.TaskAction
1617

1718
/**
@@ -23,6 +24,7 @@ public abstract class KotlinKlibAbiBuildTask : BuildTaskBase() {
2324
* Collection consisting of a single path to a compiled klib (either file, or directory).
2425
*/
2526
@get:InputFiles
27+
@get:SkipWhenEmpty
2628
public abstract val klibFile: ConfigurableFileCollection
2729

2830
/**

src/main/kotlin/KotlinKlibExtractAbiTask.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public abstract class KotlinKlibExtractAbiTask : DefaultTask() {
2424
/**
2525
* Merged KLib dump that should be filtered by this task.
2626
*/
27-
@get:InputFile
27+
@get:InputFiles
2828
public abstract val inputAbiFile: RegularFileProperty
2929

3030
/**
@@ -49,6 +49,7 @@ public abstract class KotlinKlibExtractAbiTask : DefaultTask() {
4949
@TaskAction
5050
internal fun generate() {
5151
val inputFile = inputAbiFile.asFile.get()
52+
if (!inputFile.exists()) return
5253
if (inputFile.length() == 0L) {
5354
error("Project ABI file $inputAbiFile is empty.")
5455
}

src/main/kotlin/CopyFile.kt renamed to src/main/kotlin/SyncFile.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,33 @@ package kotlinx.validation
77

88
import org.gradle.api.DefaultTask
99
import org.gradle.api.file.RegularFileProperty
10-
import org.gradle.api.tasks.InputFile
1110
import org.gradle.api.tasks.InputFiles
1211
import org.gradle.api.tasks.OutputFile
12+
import org.gradle.api.tasks.OutputFiles
1313
import org.gradle.api.tasks.TaskAction
14-
import java.io.File
1514
import java.nio.file.Files
1615
import java.nio.file.StandardCopyOption
1716

1817
// Built-in Gradle's Copy/Sync tasks accepts only a destination directory (not a single file)
1918
// and registers it as an output dependency. If there's another task reading from that particular
2019
// directory or writing into it, their input/output dependencies would clash and as long as
2120
// there will be no explicit ordering or dependencies between these tasks, Gradle would be unhappy.
22-
internal abstract class CopyFile : DefaultTask() {
23-
@get:InputFile
21+
internal abstract class SyncFile : DefaultTask() {
22+
@get:InputFiles
2423
abstract val from: RegularFileProperty
2524

2625
@get:OutputFile
2726
abstract val to: RegularFileProperty
2827

2928
@TaskAction
3029
fun copy() {
31-
Files.copy(from.asFile.get().toPath(), to.asFile.get().toPath(), StandardCopyOption.REPLACE_EXISTING)
30+
val fromFile = from.asFile.get()
31+
val toFile = to.asFile.get()
32+
if (fromFile.exists()) {
33+
toFile.parentFile.mkdirs()
34+
Files.copy(fromFile.toPath(), toFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
35+
} else {
36+
Files.deleteIfExists(toFile.toPath())
37+
}
3238
}
3339
}

0 commit comments

Comments
 (0)