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