Skip to content

Commit e478dd6

Browse files
authored
Correctly handle compilations without sources (#200)
* Correctly handle targets with a main compilation resulting in no compiled artifacts * Postpone source directories check by wrapping it into a provider Fixes #199
1 parent c695677 commit e478dd6

File tree

5 files changed

+116
-34
lines changed

5 files changed

+116
-34
lines changed

src/functionalTest/kotlin/kotlinx/validation/api/Assert.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ internal fun BuildResult.assertTaskFailure(task: String) {
2424
assertTaskOutcome(TaskOutcome.FAILED, task)
2525
}
2626

27+
/**
28+
* Helper `fun` for asserting a [TaskOutcome] to be equal to [TaskOutcome.SKIPPED]
29+
*/
30+
internal fun BuildResult.assertTaskSkipped(task: String) {
31+
assertTaskOutcome(TaskOutcome.SKIPPED, task)
32+
}
33+
2734
private fun BuildResult.assertTaskOutcome(taskOutcome: TaskOutcome, taskName: String) {
2835
assertEquals(taskOutcome, task(taskName)?.outcome)
2936
}

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import org.junit.Test
1818
import java.io.File
1919
import java.nio.file.Files
2020
import java.nio.file.Paths
21+
import kotlin.test.assertFalse
2122
import kotlin.test.assertTrue
2223

2324
internal const val BANNED_TARGETS_PROPERTY_NAME = "binary.compatibility.validator.klib.targets.disabled.for.testing"
@@ -633,4 +634,39 @@ internal class KlibVerificationTests : BaseKotlinGradleTest() {
633634
)
634635
}
635636
}
637+
638+
@Test
639+
fun `apiDump should not fail for empty project`() {
640+
val runner = test {
641+
baseProjectSetting()
642+
addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest")
643+
runApiDump()
644+
}
645+
646+
runner.build().apply {
647+
assertTaskSkipped(":klibApiDump")
648+
}
649+
assertFalse(runner.projectDir.resolve("api").exists())
650+
}
651+
652+
@Test
653+
fun `apiDump should not fail if there is only one target`() {
654+
val runner = test {
655+
baseProjectSetting()
656+
addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest")
657+
addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "linuxX64Main")
658+
runApiDump()
659+
}
660+
checkKlibDump(runner.build(), "/examples/classes/AnotherBuildConfig.klib.linuxX64Only.dump")
661+
}
662+
663+
@Test
664+
fun `apiCheck should not fail for empty project`() {
665+
val runner = test {
666+
baseProjectSetting()
667+
addToSrcSet("/examples/classes/AnotherBuildConfig.kt", sourceSet = "commonTest")
668+
runApiCheck()
669+
}
670+
runner.build()
671+
}
636672
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Klib ABI Dump
2+
// Targets: [linuxX64]
3+
// Rendering settings:
4+
// - Signature version: 2
5+
// - Show manifest properties: true
6+
// - Show declarations: true
7+
8+
// Library unique name: <testproject>
9+
final class org.different.pack/BuildConfig { // org.different.pack/BuildConfig|null[0]
10+
constructor <init>() // org.different.pack/BuildConfig.<init>|<init>(){}[0]
11+
final fun f1(): kotlin/Int // org.different.pack/BuildConfig.f1|f1(){}[0]
12+
final val p1 // org.different.pack/BuildConfig.p1|{}p1[0]
13+
final fun <get-p1>(): kotlin/Int // org.different.pack/BuildConfig.p1.<get-p1>|<get-p1>(){}[0]
14+
}

src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public class BinaryCompatibilityValidatorPlugin : Plugin<Project> {
105105
kotlin.targets.matching { it.jvmBased }.all { target ->
106106
val targetConfig = TargetConfig(project, extension, target.name, jvmDirConfig)
107107
if (target.platformType == KotlinPlatformType.jvm) {
108-
target.mainCompilations.all {
108+
target.mainCompilationOrNull?.also {
109109
project.configureKotlinCompilation(it, extension, targetConfig, commonApiDump, commonApiCheck)
110110
}
111111
} else if (target.platformType == KotlinPlatformType.androidJvm) {
@@ -219,11 +219,9 @@ private fun Project.configureKotlinCompilation(
219219

220220
val apiBuild = task<KotlinApiBuildTask>(targetConfig.apiTaskName("Build")) {
221221
// Do not enable task for empty umbrella modules
222-
isEnabled =
223-
apiCheckEnabled(
224-
projectName,
225-
extension
226-
) && compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } }
222+
isEnabled = apiCheckEnabled(projectName, extension)
223+
val hasSourcesPredicate = compilation.hasAnySourcesPredicate()
224+
onlyIf { hasSourcesPredicate.get() }
227225
// 'group' is not specified deliberately, so it will be hidden from ./gradlew tasks
228226
description =
229227
"Builds Kotlin API for 'main' compilations of $projectName. Complementary task and shouldn't be called manually"
@@ -419,6 +417,8 @@ private class KlibValidationPipelineBuilder(
419417
project.name
420418
projectApiFile = klibApiDir.get().resolve(klibDumpFileName)
421419
generatedApiFile = klibMergeDir.resolve(klibDumpFileName)
420+
val hasCompilableTargets = project.hasCompilableTargetsPredicate()
421+
onlyIf("There are no klibs compiled for the project") { hasCompilableTargets.get() }
422422
}
423423

424424
private fun Project.dumpKlibsTask(
@@ -431,6 +431,8 @@ private class KlibValidationPipelineBuilder(
431431
group = "other"
432432
from = klibMergeDir.resolve(klibDumpFileName)
433433
to = klibApiDir.get().resolve(klibDumpFileName)
434+
val hasCompilableTargets = project.hasCompilableTargetsPredicate()
435+
onlyIf("There are no klibs compiled for the project") { hasCompilableTargets.get() }
434436
}
435437

436438
private fun Project.extractAbi(
@@ -449,6 +451,8 @@ private class KlibValidationPipelineBuilder(
449451
supportedTargets = supportedTargets()
450452
inputAbiFile = klibApiDir.get().resolve(klibDumpFileName)
451453
outputAbiFile = klibOutputDir.resolve(klibDumpFileName)
454+
val hasCompilableTargets = project.hasCompilableTargetsPredicate()
455+
onlyIf("There are no klibs compiled for the project") { hasCompilableTargets.get() }
452456
}
453457

454458
private fun Project.mergeInferredKlibsUmbrellaTask(
@@ -464,6 +468,8 @@ private class KlibValidationPipelineBuilder(
464468
"into a single merged KLib ABI dump"
465469
dumpFileName = klibDumpFileName
466470
mergedFile = klibMergeDir.resolve(klibDumpFileName)
471+
val hasCompilableTargets = project.hasCompilableTargetsPredicate()
472+
onlyIf("There are no dumps to merge") { hasCompilableTargets.get() }
467473
}
468474

469475
private fun Project.mergeKlibsUmbrellaTask(
@@ -475,6 +481,8 @@ private class KlibValidationPipelineBuilder(
475481
"different targets into a single merged KLib ABI dump"
476482
dumpFileName = klibDumpFileName
477483
mergedFile = klibMergeDir.resolve(klibDumpFileName)
484+
val hasCompilableTargets = project.hasCompilableTargetsPredicate()
485+
onlyIf("There are no dumps to merge") { hasCompilableTargets.get() }
478486
}
479487

480488
fun Project.bannedTargets(): Set<String> {
@@ -499,30 +507,22 @@ private class KlibValidationPipelineBuilder(
499507

500508
val supportedTargetsProvider = supportedTargets()
501509
kotlin.targets.matching { it.emitsKlib }.configureEach { currentTarget ->
502-
val mainCompilations = currentTarget.mainCompilations
503-
if (mainCompilations.none()) {
504-
return@configureEach
505-
}
510+
val mainCompilation = currentTarget.mainCompilationOrNull ?: return@configureEach
506511

507512
val targetName = currentTarget.targetName
508513
val targetConfig = TargetConfig(project, extension, targetName, intermediateFilesConfig)
509514
val apiBuildDir = targetConfig.apiDir.map { project.layout.buildDirectory.asFile.get().resolve(it) }.get()
510515
val targetSupported = targetIsSupported(currentTarget)
511516
// If a target is supported, the workflow is simple: create a dump, then merge it along with other dumps.
512517
if (targetSupported) {
513-
mainCompilations.all {
514-
val buildTargetAbi = configureKlibCompilation(
515-
it, extension, targetConfig,
516-
apiBuildDir
517-
)
518-
mergeTask.configure {
519-
it.addInput(targetName, apiBuildDir)
520-
it.dependsOn(buildTargetAbi)
521-
}
522-
mergeInferredTask.configure {
523-
it.addInput(targetName, apiBuildDir)
524-
it.dependsOn(buildTargetAbi)
525-
}
518+
val buildTargetAbi = configureKlibCompilation(mainCompilation, extension, targetConfig, apiBuildDir)
519+
mergeTask.configure {
520+
it.addInput(targetName, apiBuildDir)
521+
it.dependsOn(buildTargetAbi)
522+
}
523+
mergeInferredTask.configure {
524+
it.addInput(targetName, apiBuildDir)
525+
it.dependsOn(buildTargetAbi)
526526
}
527527
return@configureEach
528528
}
@@ -534,9 +534,12 @@ private class KlibValidationPipelineBuilder(
534534
}
535535
// The actual merge will happen here, where we'll try to infer a dump for the unsupported target and merge
536536
// it with other supported target dumps.
537-
val proxy = unsupportedTargetDumpProxy(klibApiDir, targetConfig,
537+
val proxy = unsupportedTargetDumpProxy(
538+
mainCompilation,
539+
klibApiDir, targetConfig,
538540
extractUnderlyingTarget(currentTarget),
539-
apiBuildDir, supportedTargetsProvider)
541+
apiBuildDir, supportedTargetsProvider
542+
)
540543
mergeInferredTask.configure {
541544
it.addInput(targetName, apiBuildDir)
542545
it.dependsOn(proxy)
@@ -555,18 +558,20 @@ private class KlibValidationPipelineBuilder(
555558

556559
private fun Project.targetIsSupported(target: KotlinTarget): Boolean {
557560
if (bannedTargets().contains(target.targetName)) return false
558-
return when(target) {
561+
return when (target) {
559562
is KotlinNativeTarget -> HostManager().isEnabled(target.konanTarget)
560563
else -> true
561564
}
562565
}
563566

567+
// Compilable targets supported by the host compiler
564568
private fun Project.supportedTargets(): Provider<Set<String>> {
565569
val banned = bannedTargets() // for testing only
566570
return project.provider {
567571
val hm = HostManager()
568572
project.kotlinMultiplatform.targets.matching { it.emitsKlib }
569573
.asSequence()
574+
.filter { it.mainCompilationOrNull?.hasAnySources() == true }
570575
.filter {
571576
if (it is KotlinNativeTarget) {
572577
hm.isEnabled(it.konanTarget) && it.targetName !in banned
@@ -579,6 +584,14 @@ private class KlibValidationPipelineBuilder(
579584
}
580585
}
581586

587+
// Returns a predicate that checks if there are any compilable targets
588+
private fun Project.hasCompilableTargetsPredicate(): Provider<Boolean> {
589+
return project.provider {
590+
project.kotlinMultiplatform.targets.matching { it.emitsKlib }
591+
.asSequence()
592+
.any { it.mainCompilationOrNull?.hasAnySources() == true }
593+
}
594+
}
582595

583596
private fun Project.configureKlibCompilation(
584597
compilation: KotlinCompilation<KotlinCommonOptions>,
@@ -590,11 +603,9 @@ private class KlibValidationPipelineBuilder(
590603
val buildTask = project.task<KotlinKlibAbiBuildTask>(targetConfig.apiTaskName("Build")) {
591604
target = targetConfig.targetName!!
592605
// Do not enable task for empty umbrella modules
593-
isEnabled =
594-
klibAbiCheckEnabled(
595-
projectName,
596-
extension
597-
) && compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } }
606+
isEnabled = klibAbiCheckEnabled(projectName, extension)
607+
val hasSourcesPredicate = compilation.hasAnySourcesPredicate()
608+
onlyIf { hasSourcesPredicate.get() }
598609
// 'group' is not specified deliberately, so it will be hidden from ./gradlew tasks
599610
description = "Builds Kotlin KLib ABI dump for 'main' compilations of $projectName. " +
600611
"Complementary task and shouldn't be called manually"
@@ -620,6 +631,7 @@ private class KlibValidationPipelineBuilder(
620631
}
621632

622633
private fun Project.unsupportedTargetDumpProxy(
634+
compilation: KotlinCompilation<KotlinCommonOptions>,
623635
klibApiDir: Provider<File>,
624636
targetConfig: TargetConfig,
625637
underlyingTarget: String,
@@ -629,6 +641,8 @@ private class KlibValidationPipelineBuilder(
629641
val targetName = targetConfig.targetName!!
630642
return project.task<KotlinKlibInferAbiForUnsupportedTargetTask>(targetConfig.apiTaskName("Infer")) {
631643
isEnabled = klibAbiCheckEnabled(project.name, extension)
644+
val hasSourcesPredicate = compilation.hasAnySourcesPredicate()
645+
onlyIf { hasSourcesPredicate.get() }
632646
description = "Try to infer the dump for unsupported target $targetName using dumps " +
633647
"generated for supported targets."
634648
group = "other"
@@ -676,10 +690,18 @@ private fun extractUnderlyingTarget(target: KotlinTarget): String {
676690
private val Project.kotlinMultiplatform
677691
get() = extensions.getByName("kotlin") as KotlinMultiplatformExtension
678692

679-
private val KotlinTarget.mainCompilations
680-
get() = compilations.matching { it.name == "main" }
693+
private val KotlinTarget.mainCompilationOrNull: KotlinCompilation<KotlinCommonOptions>?
694+
get() = compilations.firstOrNull { it.name == KotlinCompilation.MAIN_COMPILATION_NAME }
681695

682696
private val Project.jvmDumpFileName: String
683697
get() = "$name.api"
684698
private val Project.klibDumpFileName: String
685699
get() = "$name.klib.api"
700+
701+
private fun KotlinCompilation<KotlinCommonOptions>.hasAnySources(): Boolean = allKotlinSourceSets.any {
702+
it.kotlin.srcDirs.any(File::exists)
703+
}
704+
705+
private fun KotlinCompilation<KotlinCommonOptions>.hasAnySourcesPredicate(): Provider<Boolean> = project.provider {
706+
this.hasAnySources()
707+
}

src/main/kotlin/KotlinKlibMergeAbiTask.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ internal abstract class KotlinKlibMergeAbiTask : DefaultTask() {
5454
internal fun merge() {
5555
KlibDump().apply {
5656
targetToFile.forEach { (targetName, dumpDir) ->
57-
merge(dumpDir.resolve(dumpFileName), targetName)
57+
val dumpFile = dumpDir.resolve(dumpFileName)
58+
if (dumpFile.exists()) {
59+
merge(dumpFile, targetName)
60+
}
5861
}
5962
}.saveTo(mergedFile)
6063
}

0 commit comments

Comments
 (0)