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