1 /*
<lambda>null2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
18 
19 package androidx.build.binarycompatibilityvalidator
20 
21 import androidx.build.AndroidXMultiplatformExtension
22 import androidx.build.Version
23 import androidx.build.addToBuildOnServer
24 import androidx.build.addToCheckTask
25 import androidx.build.checkapi.ApiType
26 import androidx.build.checkapi.getBcvFileDirectory
27 import androidx.build.checkapi.getBuiltBcvFileDirectory
28 import androidx.build.checkapi.getRequiredCompatibilityApiFileFromDir
29 import androidx.build.checkapi.shouldWriteVersionedApiFile
30 import androidx.build.getLibraryByName
31 import androidx.build.metalava.UpdateApiTask
32 import androidx.build.uptodatedness.cacheEvenIfNoOutputs
33 import androidx.build.version
34 import com.android.utils.appendCapitalized
35 import kotlinx.validation.KlibDumpMetadata
36 import kotlinx.validation.KotlinKlibAbiBuildTask
37 import kotlinx.validation.KotlinKlibExtractAbiTask
38 import kotlinx.validation.KotlinKlibMergeAbiTask
39 import kotlinx.validation.api.klib.KlibSignatureVersion
40 import kotlinx.validation.api.klib.KlibTarget
41 import kotlinx.validation.api.klib.konanTargetNameMapping
42 import kotlinx.validation.toKlibTarget
43 import org.gradle.api.DefaultTask
44 import org.gradle.api.Project
45 import org.gradle.api.Task
46 import org.gradle.api.artifacts.Configuration
47 import org.gradle.api.file.ConfigurableFileCollection
48 import org.gradle.api.file.Directory
49 import org.gradle.api.file.RegularFile
50 import org.gradle.api.file.RegularFileProperty
51 import org.gradle.api.provider.Provider
52 import org.gradle.api.tasks.TaskProvider
53 import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
54 import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
55 import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME
56 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
57 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
58 import org.jetbrains.kotlin.konan.target.HostManager
59 
60 private const val GENERATE_NAME = "generateAbi"
61 private const val CHECK_NAME = "checkAbi"
62 private const val CHECK_RELEASE_NAME = "checkAbiRelease"
63 private const val UPDATE_NAME = "updateAbi"
64 private const val EXTRACT_NAME = "extractAbi"
65 private const val EXTRACT_RELEASE_NAME = "extractAbiRelease"
66 private const val IGNORE_CHANGES_NAME = "ignoreAbiChanges"
67 
68 private const val KLIB_DUMPS_DIRECTORY = "klib"
69 private const val KLIB_MERGE_DIRECTORY = "merged"
70 private const val KLIB_EXTRACTED_DIRECTORY = "extracted"
71 private const val NATIVE_SUFFIX = "native"
72 internal const val CURRENT_API_FILE_NAME = "current.txt"
73 private const val IGNORE_FILE_NAME = "current.ignore"
74 private const val ABI_GROUP_NAME = "abi"
75 
76 class BinaryCompatibilityValidation(
77     val project: Project,
78     private val kotlinMultiplatformExtension: KotlinMultiplatformExtension
79 ) {
80     private val projectVersion: Version = project.version()
81 
82     fun setupBinaryCompatibilityValidatorTasks() =
83         project.afterEvaluate {
84             val androidXMultiplatformExtension =
85                 project.extensions.getByType(AndroidXMultiplatformExtension::class.java)
86             if (!androidXMultiplatformExtension.enableBinaryCompatibilityValidator) {
87                 return@afterEvaluate
88             }
89             val checkAll: TaskProvider<Task> = project.tasks.register(CHECK_NAME)
90             val updateAll: TaskProvider<Task> = project.tasks.register(UPDATE_NAME)
91             configureKlibTasks(project, checkAll, updateAll)
92             project.tasks.named("check").configure { it.dependsOn(checkAll) }
93             project.addToCheckTask(checkAll)
94             project.addToBuildOnServer(checkAll)
95             if (HostManager.hostIsMac) {
96                 project.tasks.named("updateApi", UpdateApiTask::class.java) {
97                     it.dependsOn(updateAll)
98                 }
99             }
100         }
101 
102     private fun configureKlibTasks(
103         project: Project,
104         checkAll: TaskProvider<Task>,
105         updateAll: TaskProvider<Task>
106     ) {
107         if (kotlinMultiplatformExtension.nativeTargets().isEmpty()) {
108             return
109         }
110         val runtimeClasspath: ConfigurableFileCollection =
111             project.files(project.prepareKlibValidationClasspath())
112         val projectVersion: Version = project.version()
113         val projectAbiDir = project.getBcvFileDirectory().dir(NATIVE_SUFFIX)
114         val currentIgnoreFile = projectAbiDir.file(IGNORE_FILE_NAME)
115         val buildAbiDir = project.getBuiltBcvFileDirectory().map { it.dir(NATIVE_SUFFIX) }
116 
117         val klibDumpDir = project.layout.buildDirectory.dir(KLIB_DUMPS_DIRECTORY)
118         val klibMergeFile =
119             klibDumpDir.map { it.dir(KLIB_MERGE_DIRECTORY) }.map { it.file(CURRENT_API_FILE_NAME) }
120         val klibExtractedFileDir = klibDumpDir.map { it.dir(KLIB_EXTRACTED_DIRECTORY) }
121 
122         val generateAbi = project.generateAbiTask(klibMergeFile, runtimeClasspath)
123         val generatedAndMergedApiFile: Provider<RegularFileProperty> =
124             generateAbi.map { it.mergedApiFile }
125         val updateKlibAbi =
126             project.updateKlibAbiTask(
127                 projectAbiDir,
128                 generatedAndMergedApiFile,
129                 projectVersion.toString(),
130                 runtimeClasspath
131             )
132 
133         val extractKlibAbi =
134             project.extractKlibAbiTask(projectAbiDir, klibExtractedFileDir, runtimeClasspath)
135         val extractedProjectFile = extractKlibAbi.map { it.outputAbiFile }
136         val checkKlibAbi = project.checkKlibAbiTask(extractedProjectFile, generatedAndMergedApiFile)
137         val checkKlibAbiRelease =
138             project.checkKlibAbiReleaseTask(
139                 generatedAndMergedApiFile,
140                 projectAbiDir,
141                 klibExtractedFileDir,
142                 currentIgnoreFile,
143                 runtimeClasspath
144             )
145 
146         updateKlibAbi.configure { update ->
147             checkKlibAbiRelease?.let { check -> update.dependsOn(check) }
148         }
149         updateAll.configure { it.dependsOn(updateKlibAbi) }
150         checkAll.configure { checkTask ->
151             checkTask.dependsOn(checkKlibAbi)
152             checkKlibAbiRelease?.let { releaseCheck -> checkTask.dependsOn(releaseCheck) }
153         }
154 
155         // add each target as an input to the merge task
156         project.configureKlibTargets(generateAbi, buildAbiDir, runtimeClasspath)
157     }
158 
159     /* Check that the current ABI definition is up to date. */
160     private fun Project.checkKlibAbiTask(
161         projectApiFile: Provider<RegularFileProperty>,
162         generatedApiFile: Provider<RegularFileProperty>
163     ) =
164         project.tasks.register(
165             CHECK_NAME.appendCapitalized(NATIVE_SUFFIX),
166             CheckAbiEquivalenceTask::class.java
167         ) {
168             it.checkedInDump = projectApiFile
169             it.builtDump = generatedApiFile
170             it.group = ABI_GROUP_NAME
171             it.cacheEvenIfNoOutputs()
172         }
173 
174     /* Check that the current ABI definition is compatible with most recently released version */
175     private fun Project.checkKlibAbiReleaseTask(
176         mergedApiFile: Provider<RegularFileProperty>,
177         klibApiDir: Directory,
178         klibExtractDir: Provider<Directory>,
179         ignoreFile: RegularFile,
180         runtimeClasspath: ConfigurableFileCollection
181     ) =
182         project.getRequiredCompatibilityAbiLocation(NATIVE_SUFFIX)?.let { requiredCompatFile ->
183             val extractReleaseTask =
184                 project.tasks.register(EXTRACT_RELEASE_NAME, KotlinKlibExtractAbiTask::class.java) {
185                     it.strictValidation.set(HostManager.hostIsMac)
186                     it.targetsToRemove.set(
187                         project.provider {
188                             unsupportedNativeTargetNames().map { targetName ->
189                                 KlibTarget(targetName)
190                             }
191                         }
192                     )
193                     it.inputAbiFile.set(klibApiDir.file(requiredCompatFile.name))
194                     it.outputAbiFile.set(klibExtractDir.map { it.file(requiredCompatFile.name) })
195                     it.runtimeClasspath.from(runtimeClasspath)
196                     (it as DefaultTask).group = ABI_GROUP_NAME
197                 }
198             project.tasks.register(IGNORE_CHANGES_NAME, IgnoreAbiChangesTask::class.java) {
199                 it.currentApiDump.set(mergedApiFile.map { fileProperty -> fileProperty.get() })
200                 it.previousApiDump.set(
201                     extractReleaseTask.map { extract -> extract.outputAbiFile.get() }
202                 )
203                 it.ignoreFile.set(ignoreFile)
204                 it.runtimeClasspath.from(runtimeClasspath)
205             }
206             project.tasks.register(CHECK_RELEASE_NAME, CheckAbiIsCompatibleTask::class.java) {
207                 it.currentApiDump.set(mergedApiFile.map { fileProperty -> fileProperty.get() })
208                 it.previousApiDump.set(
209                     extractReleaseTask.map { extract -> extract.outputAbiFile.get() }
210                 )
211                 it.projectVersion = provider { projectVersion.toString() }
212                 it.referenceVersion =
213                     extractReleaseTask.map { extract ->
214                         extract.outputAbiFile.get().asFile.nameWithoutExtension
215                     }
216                 it.ignoreFile.set(ignoreFile)
217                 it.group = ABI_GROUP_NAME
218                 it.dependsOn(extractReleaseTask)
219                 it.runtimeClasspath.from(runtimeClasspath)
220                 it.cacheEvenIfNoOutputs()
221             }
222         }
223 
224     /* Updates the current abi file as well as the versioned abi file if appropriate */
225     private fun Project.updateKlibAbiTask(
226         klibApiDir: Directory,
227         mergedKlibFile: Provider<RegularFileProperty>,
228         projectVersion: String,
229         runtimeClasspath: ConfigurableFileCollection
230     ) =
231         project.tasks.register(
232             UPDATE_NAME.appendCapitalized(NATIVE_SUFFIX),
233             UpdateAbiTask::class.java
234         ) {
235             it.outputDir.set(klibApiDir)
236             it.inputApiLocation.set(mergedKlibFile.map { fileProperty -> fileProperty.get() })
237             it.version.set(projectVersion)
238             it.shouldWriteVersionedApiFile.set(project.shouldWriteVersionedApiFile())
239             it.group = ABI_GROUP_NAME
240             it.unsupportedNativeTargetNames.set(unsupportedNativeTargetNames())
241             it.runtimeClasspath.from(runtimeClasspath)
242         }
243 
244     /**
245      * Extracts the targets that are supported on the current machine from the current file in the
246      * project directory so they can be validated with checkAbi. For example on linux, extract all
247      * current non-mac targets from the dump.
248      */
249     private fun Project.extractKlibAbiTask(
250         klibApiDir: Directory,
251         extractDir: Provider<Directory>,
252         runtimeClasspath: ConfigurableFileCollection
253     ) =
254         project.tasks.register(EXTRACT_NAME, KotlinKlibExtractAbiTask::class.java) {
255             it.strictValidation.set(HostManager.hostIsMac)
256             it.targetsToRemove.set(
257                 project.provider {
258                     unsupportedNativeTargetNames().map { targetName -> KlibTarget(targetName) }
259                 }
260             )
261             it.inputAbiFile.set(klibApiDir.file(CURRENT_API_FILE_NAME))
262             it.outputAbiFile.set(extractDir.map { it.file(CURRENT_API_FILE_NAME) })
263             it.runtimeClasspath.from(runtimeClasspath)
264             (it as DefaultTask).group = ABI_GROUP_NAME
265         }
266 
267     /* Merge target specific dumps into single file located in [mergeDir] */
268     private fun Project.generateAbiTask(
269         mergeFile: Provider<RegularFile>,
270         runtimeClasspath: ConfigurableFileCollection
271     ) =
272         project.tasks.register(GENERATE_NAME, KotlinKlibMergeAbiTask::class.java) {
273             it.mergedApiFile.set(mergeFile)
274             it.runtimeClasspath.from(runtimeClasspath)
275             (it as DefaultTask).group = ABI_GROUP_NAME
276         }
277 
278     private fun Project.configureKlibTargets(
279         mergeTask: TaskProvider<KotlinKlibMergeAbiTask>,
280         abiBuildDir: Provider<Directory>,
281         runtimeClasspath: ConfigurableFileCollection
282     ) {
283         val generatedDumps = objects.setProperty(KlibDumpMetadata::class.java)
284         mergeTask.configure { it.dumps.addAll(generatedDumps) }
285         kotlinMultiplatformExtension.nativeTargets().configureEach { currentTarget ->
286             val mainCompilation =
287                 currentTarget.compilations.findByName(MAIN_COMPILATION_NAME) ?: return@configureEach
288 
289             val target = currentTarget.toKlibTarget()
290 
291             val isEnabled =
292                 currentTarget is KotlinNativeTarget &&
293                     HostManager().isEnabled(currentTarget.konanTarget)
294             if (isEnabled) {
295                 val buildTargetAbi =
296                     configureKlibCompilation(
297                         mainCompilation,
298                         target,
299                         abiBuildDir.map { it.dir(target.targetName) },
300                         runtimeClasspath,
301                     )
302                 generatedDumps.add(
303                     KlibDumpMetadata(
304                         target,
305                         objects.fileProperty().also {
306                             it.set(buildTargetAbi.flatMap { it.outputAbiFile })
307                         }
308                     )
309                 )
310             }
311         }
312     }
313 
314     private fun supportedNativeTargetNames(): Set<String> {
315         val hostManager = HostManager()
316         return kotlinMultiplatformExtension
317             .nativeTargets()
318             .filter { hostManager.isEnabled(it.konanTarget) }
319             .map { it.klibTargetName() }
320             .toSet()
321     }
322 
323     private fun allNativeTargetNames(): Set<String> =
324         kotlinMultiplatformExtension.nativeTargets().map { it.klibTargetName() }.toSet()
325 
326     private fun unsupportedNativeTargetNames(): Set<String> =
327         allNativeTargetNames() - supportedNativeTargetNames()
328 
329     private fun Project.configureKlibCompilation(
330         compilation: KotlinCompilation<*>,
331         target: KlibTarget,
332         outputFileDir: Provider<Directory>,
333         runtimeClasspath: ConfigurableFileCollection
334     ): TaskProvider<KotlinKlibAbiBuildTask> {
335         val buildTask =
336             tasks.register(
337                 GENERATE_NAME.appendCapitalized(target.targetName),
338                 KotlinKlibAbiBuildTask::class.java
339             ) {
340                 it.nonPublicMarkers.addAll(nonPublicMarkers)
341                 it.target.set(target)
342                 it.klibFile.from(compilation.output.classesDirs)
343                 it.signatureVersion.set(KlibSignatureVersion.LATEST)
344                 it.outputAbiFile.set(outputFileDir.map { it.file(CURRENT_API_FILE_NAME) })
345                 it.runtimeClasspath.from(runtimeClasspath)
346                 (it as DefaultTask).group = ABI_GROUP_NAME
347             }
348         return buildTask
349     }
350 }
351 
Projectnull352 private fun Project.getRequiredCompatibilityAbiLocation(suffix: String) =
353     getRequiredCompatibilityApiFileFromDir(
354         project.getBcvFileDirectory().dir(suffix).asFile,
355         project.version(),
356         ApiType.CLASSAPI
357     )
358 
359 private fun KotlinMultiplatformExtension.nativeTargets() =
360     targets.withType(KotlinNativeTarget::class.java).matching {
361         it.platformType == KotlinPlatformType.native
362     }
363 
klibTargetNamenull364 private fun KotlinNativeTarget.klibTargetName(): String =
365     KlibTarget(targetName, konanTargetNameMapping[konanTarget.name]!!).toString()
366 
367 private fun Project.prepareKlibValidationClasspath(): Configuration {
368     return project.configurations.detachedConfiguration(
369         project.dependencies.create(getLibraryByName("kotlinCompilerEmbeddable"))
370     )
371 }
372 
373 // Not ideal to have a list instead of a pattern to match but this is all the API supports right now
374 // https://github.com/Kotlin/binary-compatibility-validator/issues/280
375 private val nonPublicMarkers =
376     setOf(
377         "androidx.annotation.Experimental",
378         "androidx.benchmark.BenchmarkState.Companion.ExperimentalExternalReport",
379         "androidx.benchmark.ExperimentalBenchmarkConfigApi",
380         "androidx.benchmark.ExperimentalBenchmarkStateApi",
381         "androidx.benchmark.ExperimentalBlackHoleApi",
382         "androidx.benchmark.macro.ExperimentalMacrobenchmarkApi",
383         "androidx.benchmark.macro.ExperimentalMetricApi",
384         "androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi",
385         "androidx.benchmark.perfetto.ExperimentalPerfettoTraceProcessorApi",
386         "androidx.camera.core.ExperimentalUseCaseApi",
387         "androidx.car.app.annotations.ExperimentalCarApi",
388         "androidx.compose.animation.ExperimentalAnimationApi",
389         "androidx.compose.animation.ExperimentalSharedTransitionApi",
390         "androidx.compose.animation.core.ExperimentalAnimatableApi",
391         "androidx.compose.animation.core.ExperimentalAnimationSpecApi",
392         "androidx.compose.animation.core.ExperimentalTransitionApi",
393         "androidx.compose.animation.core.InternalAnimationApi",
394         "androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
395         "androidx.compose.foundation.gestures.ExperimentalTapGestureDetectorBehaviorApi",
396         "androidx.compose.foundation.ExperimentalFoundationApi",
397         "androidx.compose.foundation.InternalFoundationApi",
398         "androidx.compose.foundation.layout.ExperimentalLayoutApi",
399         "androidx.compose.material.ExperimentalMaterialApi",
400         "androidx.compose.runtime.ExperimentalComposeApi",
401         "androidx.compose.runtime.ExperimentalComposeRuntimeApi",
402         "androidx.compose.runtime.InternalComposeApi",
403         "androidx.compose.runtime.InternalComposeTracingApi",
404         "androidx.compose.ui.ExperimentalComposeUiApi",
405         "androidx.compose.ui.InternalComposeUiApi",
406         "androidx.compose.ui.input.pointer.util.ExperimentalVelocityTrackerApi",
407         "androidx.compose.ui.node.InternalCoreApi",
408         "androidx.compose.ui.test.ExperimentalTestApi",
409         "androidx.compose.ui.test.InternalTestApi",
410         "androidx.compose.ui.text.ExperimentalTextApi",
411         "androidx.compose.ui.text.InternalTextApi",
412         "androidx.compose.ui.unit.ExperimentalUnitApi",
413         "androidx.constraintlayout.compose.ExperimentalMotionApi",
414         "androidx.core.telecom.util.ExperimentalAppActions",
415         "androidx.credentials.ExperimentalDigitalCredentialApi",
416         "androidx.glance.ExperimentalGlanceApi",
417         "androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi",
418         "androidx.health.connect.client.ExperimentalDeduplicationApi",
419         "androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi",
420         "androidx.ink.authoring.ExperimentalLatencyDataApi",
421         "androidx.ink.brush.ExperimentalInkCustomBrushApi",
422         "androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi",
423         "androidx.paging.ExperimentalPagingApi",
424         "androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures.RegisterSourceOptIn",
425         "androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures.Ext8OptIn",
426         "androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures.Ext10OptIn",
427         "androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures.Ext11OptIn",
428         "androidx.privacysandbox.ads.adservices.common.ExperimentalFeatures.Ext12OptIn",
429         "androidx.privacysandbox.ui.core.ExperimentalFeatures.DelegatingAdapterApi",
430         "androidx.room.ExperimentalRoomApi",
431         "androidx.room.compiler.processing.ExperimentalProcessingApi",
432         "androidx.tv.foundation.ExperimentalTvFoundationApi",
433         "androidx.wear.compose.foundation.ExperimentalWearFoundationApi",
434         "androidx.wear.compose.material.ExperimentalWearMaterialApi",
435         "androidx.window.core.ExperimentalWindowApi",
436     )
437 
438 const val NEW_ISSUE_URL = "https://b.corp.google.com/issues/new?component=1102332"
439