1 /* <lambda>null2 * Copyright 2018 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 package androidx.build 18 19 import com.android.build.api.variant.LibraryAndroidComponentsExtension 20 import groovy.lang.Closure 21 import java.io.File 22 import javax.inject.Inject 23 import org.gradle.api.GradleException 24 import org.gradle.api.Project 25 import org.gradle.api.configuration.BuildFeatures 26 import org.gradle.api.plugins.ExtensionAware 27 import org.gradle.api.plugins.ExtensionContainer 28 import org.gradle.api.provider.Property 29 import org.gradle.api.provider.Provider 30 import org.gradle.api.provider.SetProperty 31 import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 32 33 /** Extension for [AndroidXImplPlugin] that's responsible for holding configuration options. */ 34 abstract class AndroidXExtension( 35 val project: Project, 36 @Suppress("unused", "PropertyName") // used from build.gradle 37 @JvmField 38 val LibraryVersions: Map<String, Version>, 39 @Suppress("unused") // used from build.gradle 40 @JvmField 41 val AllLibraryGroups: List<LibraryGroup>, 42 private val libraryGroupsByGroupId: Map<String, LibraryGroup>, 43 private val overrideLibraryGroupsByProjectPath: Map<String, LibraryGroup>, 44 private val allPossibleProjects: Provider<List<IncludedProject>>, 45 private val headShaProvider: () -> Provider<String>, 46 private val configureAarAsJarForConfigurationDelegate: (String) -> Unit, 47 ) : ExtensionAware, AndroidXConfiguration { 48 val mavenGroup: LibraryGroup? 49 val deviceTests = DeviceTests.register(project.extensions) 50 51 init { 52 // Always set a known default based on project path. see: b/302183954 53 setDefaultGroupFromProjectPath() 54 mavenGroup = chooseLibraryGroup() 55 chooseProjectVersion() 56 } 57 58 @get:Inject abstract val buildFeatures: BuildFeatures 59 60 fun isIsolatedProjectsEnabled(): Boolean { 61 return buildFeatures.isIsolatedProjectsEnabled() 62 } 63 64 /** 65 * Map of maven coordinates (e.g. "androidx.core:core") to a Gradle project path (e.g. 66 * ":core:core") 67 */ 68 val mavenCoordinatesToProjectPathMap: Map<String, String> by lazy { 69 val newProjectMap: MutableMap<String, String> = mutableMapOf() 70 allPossibleProjects.get().forEach { 71 val group = 72 overrideLibraryGroupsByProjectPath[it.gradlePath] 73 ?: getLibraryGroupFromProjectPath(it.gradlePath, null) 74 if (group != null) { 75 newProjectMap["${group.group}:${substringAfterLastColon(it.gradlePath)}"] = 76 it.gradlePath 77 } 78 } 79 newProjectMap 80 } 81 82 val name: Property<String> = project.objects.property(String::class.java) 83 84 /** The name for this artifact to be used in .pom files. */ 85 fun setName(newName: String) { 86 name.set(newName) 87 } 88 89 /** 90 * Maven version of the library. 91 * 92 * Note that, setting this is an error if the library group sets an atomic version. 93 */ 94 var mavenVersion: Version? = null 95 set(value) { 96 field = value 97 chooseProjectVersion() 98 } 99 100 var projectDirectlySpecifiesMavenVersion: Boolean = false 101 private set 102 103 fun getOtherProjectsInSameGroup(): List<IncludedProject> { 104 val ourGroup = chooseLibraryGroup() ?: return listOf() 105 val otherProjectsInSameGroup = 106 allPossibleProjects.get().filter { otherProject -> 107 if (otherProject.gradlePath == project.path) { 108 false 109 } else { 110 getLibraryGroupFromProjectPath(otherProject.gradlePath) == ourGroup 111 } 112 } 113 return otherProjectsInSameGroup 114 } 115 116 /** Returns a string explaining the value of mavenGroup */ 117 fun explainMavenGroup(): List<String> { 118 val explanationBuilder = mutableListOf<String>() 119 chooseLibraryGroup(explanationBuilder) 120 return explanationBuilder 121 } 122 123 private fun chooseLibraryGroup(explanationBuilder: MutableList<String>? = null): LibraryGroup? { 124 return getLibraryGroupFromProjectPath(project.path, explanationBuilder) 125 } 126 127 private fun substringBeforeLastColon(projectPath: String): String { 128 val lastColonIndex = projectPath.lastIndexOf(":") 129 return projectPath.substring(0, lastColonIndex) 130 } 131 132 private fun substringAfterLastColon(projectPath: String): String { 133 val lastColonIndex = projectPath.lastIndexOf(":") 134 return projectPath.substring(lastColonIndex + 1) 135 } 136 137 // gets the library group from the project path, including special cases 138 private fun getLibraryGroupFromProjectPath( 139 projectPath: String, 140 explanationBuilder: MutableList<String>? = null 141 ): LibraryGroup? { 142 val overridden = overrideLibraryGroupsByProjectPath[projectPath] 143 explanationBuilder?.add( 144 "Library group (in libraryversions.toml) having" + 145 " overrideInclude=[\"$projectPath\"] is $overridden" 146 ) 147 if (overridden != null) return overridden 148 149 val result = getStandardLibraryGroupFromProjectPath(projectPath, explanationBuilder) 150 if (result != null) return result 151 152 // samples are allowed to be nested deeper 153 if (projectPath.contains("samples")) { 154 val parentPath = substringBeforeLastColon(projectPath) 155 return getLibraryGroupFromProjectPath(parentPath, explanationBuilder) 156 } 157 return null 158 } 159 160 // simple function to get the library group from the project path, without special cases 161 private fun getStandardLibraryGroupFromProjectPath( 162 projectPath: String, 163 explanationBuilder: MutableList<String>? 164 ): LibraryGroup? { 165 // Get the text of the library group, something like "androidx.core" 166 val parentPath = substringBeforeLastColon(projectPath) 167 168 if (parentPath == "") { 169 explanationBuilder?.add("Parent path for $projectPath is empty") 170 return null 171 } 172 // convert parent project path to groupId 173 val groupIdText = 174 if (projectPath.startsWith(":external")) { 175 projectPath.replace(":external:", "") 176 } else { 177 "androidx.${parentPath.substring(1).replace(':', '.')}" 178 } 179 180 // get the library group having that text 181 val result = libraryGroupsByGroupId[groupIdText] 182 explanationBuilder?.add( 183 "Library group (in libraryversions.toml) having group=\"$groupIdText\" is $result" 184 ) 185 return result 186 } 187 188 /** 189 * Sets a group for the project based on its path. This ensures we always use a known value for 190 * the project group instead of what Gradle assigns by default. Furthermore, it also helps make 191 * them consistent between the main build and the playground builds. 192 */ 193 private fun setDefaultGroupFromProjectPath() { 194 project.group = 195 project.path 196 .split(":") 197 .filter { it.isNotEmpty() } 198 .dropLast(1) 199 .joinToString(separator = ".", prefix = "androidx.") 200 } 201 202 private fun chooseProjectVersion() { 203 val version: Version 204 val group: String? = mavenGroup?.group 205 val groupVersion: Version? = mavenGroup?.atomicGroupVersion 206 val mavenVersion: Version? = mavenVersion 207 if (mavenVersion != null) { 208 projectDirectlySpecifiesMavenVersion = true 209 if (groupVersion != null && !isGroupVersionOverrideAllowed()) { 210 throw GradleException( 211 "Cannot set mavenVersion (" + 212 mavenVersion + 213 ") for a project (" + 214 project + 215 ") whose mavenGroup already specifies forcedVersion (" + 216 groupVersion + 217 ")" 218 ) 219 } else { 220 verifyVersionExtraFormat(mavenVersion) 221 version = mavenVersion 222 } 223 } else { 224 projectDirectlySpecifiesMavenVersion = false 225 if (groupVersion != null) { 226 verifyVersionExtraFormat(groupVersion) 227 version = groupVersion 228 } else { 229 return 230 } 231 } 232 if (group != null) { 233 project.group = group 234 } 235 project.version = if (isSnapshotBuild()) version.copy(extra = "-SNAPSHOT") else version 236 versionIsSet = true 237 } 238 239 private fun verifyVersionExtraFormat(version: Version) { 240 val ALLOWED_EXTRA_PREFIXES = listOf("-alpha", "-beta", "-rc", "-dev", "-SNAPSHOT") 241 val extra = version.extra 242 if (extra != null) { 243 if (!version.isSnapshot()) { 244 if (ALLOWED_EXTRA_PREFIXES.any { extra.startsWith(it) }) { 245 for (potentialPrefix in ALLOWED_EXTRA_PREFIXES) { 246 if (extra.startsWith(potentialPrefix)) { 247 val secondExtraPart = extra.removePrefix(potentialPrefix) 248 if (secondExtraPart.toIntOrNull() == null) { 249 throw IllegalArgumentException( 250 "Version $version is not" + 251 " a properly formatted version, please ensure that " + 252 "$potentialPrefix is followed by a number only" 253 ) 254 } 255 } 256 } 257 } else { 258 throw IllegalArgumentException( 259 "Version $version is not a proper " + 260 "version, version suffixes following major.minor.patch should " + 261 "be one of ${ALLOWED_EXTRA_PREFIXES.joinToString(", ")}" 262 ) 263 } 264 } 265 } 266 } 267 268 private fun isGroupVersionOverrideAllowed(): Boolean { 269 // Grant an exception to the same-version-group policy for artifacts that haven't shipped a 270 // stable API surface, e.g. 1.0.0-alphaXX, to allow for rapid early-stage development. 271 val version = mavenVersion 272 return version != null && 273 version.major == 1 && 274 version.minor == 0 && 275 version.patch == 0 && 276 version.isAlpha() 277 } 278 279 /** Whether the version for this artifact has been set */ 280 var versionIsSet = false 281 private set 282 283 /** Description for this artifact to use in .pom files */ 284 var description: String? = null 285 /** The year when the development of this library started to use in .pom files */ 286 var inceptionYear: String? = null 287 288 /** The main license to add when publishing. Default is Apache 2. */ 289 var license: License = 290 License().apply { 291 name = "The Apache Software License, Version 2.0" 292 url = "http://www.apache.org/licenses/LICENSE-2.0.txt" 293 } 294 295 private var extraLicenses: MutableCollection<License> = ArrayList() 296 297 fun shouldPublish(): Boolean = type.publish.shouldPublish() 298 299 fun shouldRelease(): Boolean = type.publish.shouldRelease() 300 301 fun ifReleasing(action: () -> Unit) { 302 project.afterEvaluate { 303 if (shouldRelease()) { 304 action() 305 } 306 } 307 } 308 309 fun shouldPublishSbom(): Boolean { 310 if (isIsolatedProjectsEnabled()) return false 311 // IDE plugins are used by and ship inside Studio 312 return shouldPublish() || type == SoftwareType.IDE_PLUGIN 313 } 314 315 var doNotDocumentReason: String? = null 316 317 var type: SoftwareType = SoftwareType.UNSET 318 319 val failOnDeprecationWarnings = project.objects.property(Boolean::class.java).convention(true) 320 321 /** Whether this project should fail on javac compilation warnings */ 322 fun failOnDeprecationWarnings(enabled: Boolean) = failOnDeprecationWarnings.set(enabled) 323 324 /** 325 * Whether Kotlin Strict API mode is enabled, see 326 * [kotlin 1.4 release notes](https://kotlinlang.org/docs/whatsnew14.html#explicit-api-mode-for-library-authors) 327 */ 328 var legacyDisableKotlinStrictApiMode = false 329 330 var bypassCoordinateValidation = false 331 332 /** Whether Metalava should use K2 Kotlin front-end for source analysis */ 333 var metalavaK2UastEnabled = true 334 335 /** Whether the project has not yet been migrated to use JSpecify annotations. */ 336 var optOutJSpecify = false 337 338 val additionalDeviceTestApkKeys = mutableListOf<String>() 339 340 val additionalDeviceTestTags: MutableList<String> by lazy { 341 val tags = 342 when { 343 project.path.startsWith(":compose:") -> mutableListOf("compose") 344 project.path.startsWith(":privacysandbox:ads:") -> 345 mutableListOf("privacysandbox", "privacysandbox_ads") 346 project.path.startsWith(":privacysandbox:") -> mutableListOf("privacysandbox") 347 project.path.startsWith(":wear:") -> mutableListOf("wear") 348 else -> mutableListOf() 349 } 350 if (deviceTests.enableAlsoRunningOnPhysicalDevices) { 351 tags.add("all_run_on_physical_device") 352 } 353 return@lazy tags 354 } 355 356 fun shouldEnforceKotlinStrictApiMode(): Boolean { 357 return !legacyDisableKotlinStrictApiMode && type.checkApi is RunApiTasks.Yes 358 } 359 360 fun extraLicense(closure: Closure<Any>): License { 361 val license = project.configure(License(), closure) as License 362 extraLicenses.add(license) 363 return license 364 } 365 366 fun getExtraLicenses(): Collection<License> { 367 return extraLicenses 368 } 369 370 fun configureAarAsJarForConfiguration(name: String) { 371 configureAarAsJarForConfigurationDelegate(name) 372 } 373 374 fun getReferenceSha(): Provider<String> = headShaProvider() 375 376 /** 377 * Specify the version for Kotlin API compatibility mode used during Kotlin compilation. 378 * 379 * Changing this value will force clients to update their Kotlin compiler version, which may be 380 * disruptive. Library developers should only change this value if there is a strong reason to 381 * upgrade their Kotlin API version ahead of the rest of Jetpack. 382 */ 383 abstract val kotlinTarget: Property<KotlinTarget> 384 385 /** 386 * A list of test module names for the project. 387 * 388 * Includes both host and device tests. These names should match the ones in AnTS. 389 */ 390 abstract val testModuleNames: SetProperty<String> 391 392 override val kotlinApiVersion: Provider<KotlinVersion> 393 get() = kotlinTarget.map { it.apiVersion } 394 395 override val kotlinBomVersion: Provider<String> 396 get() = kotlinTarget.map { project.getVersionByName(it.catalogVersion) } 397 398 companion object { 399 const val DEFAULT_UNSPECIFIED_VERSION = "unspecified" 400 } 401 402 /** List of documentation samples projects for this project. */ 403 var samplesProjects: MutableCollection<Project> = mutableSetOf() 404 private set 405 406 /** 407 * Used to register a project that will be providing documentation samples for this project. Can 408 * only be called once so only one samples library can exist per library b/318840087. 409 */ 410 fun samples(samplesProject: Project) { 411 samplesProjects.add(samplesProject) 412 } 413 414 /** Adds golden image assets to Android test APKs to use for screenshot tests. */ 415 fun addGoldenImageAssets() { 416 project.extensions.findByType(LibraryAndroidComponentsExtension::class.java)?.onVariants { 417 variant -> 418 val subdirectory = project.path.replace(":", "/") 419 variant.androidTest 420 ?.sources 421 ?.assets 422 ?.addStaticSourceDirectory( 423 File(project.rootDir, "../../golden$subdirectory").absolutePath 424 ) 425 } 426 } 427 428 /** Locates a project by path. */ 429 // This method is needed for Gradle project isolation to avoid calls to parent projects due to 430 // androidx { samples(project(":foo")) } 431 // Without this method, the call above results into a call to the parent object, because 432 // AndroidXExtension has `val project: Project`, which from groovy `project` call within 433 // `androidx` block tries retrieves that project object and calls to look for :foo property 434 // on it, then checking all the parents for it. 435 fun project(name: String): Project = project.project(name) 436 437 /** 438 * Declare an optional project dependency on a project or its latest snapshot artifact. In AOSP 439 * builds this is a no-op and always returns a project reference 440 */ 441 fun projectOrArtifact(name: String): Any { 442 return if (!ProjectLayoutType.isPlayground(project)) { 443 // In AndroidX build, this is always enforced to the project 444 project.project(name) 445 } else { 446 // In Playground builds, they are converted to the latest SNAPSHOT artifact if the 447 // project is not included in that playground. 448 playgroundProjectOrArtifact(project.rootProject, name) 449 } 450 } 451 } 452 453 class License { 454 var name: String? = null 455 var url: String? = null 456 } 457 458 abstract class DeviceTests { 459 companion object { 460 private const val EXTENSION_NAME = "deviceTests" 461 registernull462 internal fun register(extensions: ExtensionContainer): DeviceTests { 463 return extensions.findByType(DeviceTests::class.java) 464 ?: extensions.create(EXTENSION_NAME, DeviceTests::class.java) 465 } 466 } 467 468 /** Whether this project's Android on device tests should be run in CI. */ 469 var enabled = true 470 /** The app project that this project's Android on device tests require to be able to run. */ 471 var targetAppProject: Project? = null 472 var targetAppVariant = "debug" 473 474 /** 475 * Whether this project's Android on device tests should also run on a physical Android device 476 * when run in CI. 477 */ 478 var enableAlsoRunningOnPhysicalDevices = false 479 } 480