1 /*
<lambda>null2  * Copyright 2021 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 org.gradle.api.GradleException
20 import org.gradle.api.Project
21 import org.gradle.api.provider.Provider
22 import org.gradle.api.services.BuildService
23 import org.gradle.api.services.BuildServiceParameters
24 import org.tomlj.Toml
25 import org.tomlj.TomlParseResult
26 import org.tomlj.TomlTable
27 
28 /** Loads Library groups and versions from a specified TOML file. */
29 abstract class LibraryVersionsService : BuildService<LibraryVersionsService.Parameters> {
30     interface Parameters : BuildServiceParameters {
31         var tomlFileName: String
32         var tomlFileContents: Provider<String>
33     }
34 
35     private val parsedTomlFile: TomlParseResult by lazy {
36         val result = Toml.parse(parameters.tomlFileContents.get())
37         if (result.hasErrors()) {
38             val issues =
39                 result.errors().joinToString(separator = "\n") {
40                     "${parameters.tomlFileName}:${it.position()}: ${it.message}"
41                 }
42             throw Exception("${parameters.tomlFileName} file has issues.\n$issues")
43         }
44         result
45     }
46 
47     private fun getTable(key: String): TomlTable {
48         return parsedTomlFile.getTable(key)
49             ?: throw GradleException("Library versions toml file is missing [$key] table")
50     }
51 
52     // map from name of constant to Version
53     val libraryVersions: Map<String, Version> by lazy {
54         val versions = getTable("versions")
55         versions.keySet().associateWith { versionName ->
56             val versionValue = versions.getString(versionName)!!
57             Version.parseOrNull(versionValue)
58                 ?: throw GradleException(
59                     "$versionName does not match expected format - $versionValue"
60                 )
61         }
62     }
63 
64     // map of library groups keyed by their variable name in the toml file
65     val libraryGroups: Map<String, LibraryGroup> by lazy {
66         val result = mutableMapOf<String, LibraryGroup>()
67         for (association in libraryGroupAssociations) {
68             result[association.declarationName] = association.libraryGroup
69         }
70         result
71     }
72 
73     // map of library groups keyed by group name
74     val libraryGroupsByGroupId: Map<String, LibraryGroup> by lazy {
75         val result = mutableMapOf<String, LibraryGroup>()
76         for (association in libraryGroupAssociations) {
77             // Check for duplicate groups
78             val groupId = association.libraryGroup.group
79             val existingAssociation = result[groupId]
80             if (existingAssociation != null) {
81                 if (
82                     existingAssociation.atomicGroupVersion != null &&
83                         association.libraryGroup.atomicGroupVersion != null &&
84                         existingAssociation.group !in ALLOWED_ATOMIC_GROUP_EXCEPTIONS
85                 ) {
86                     throw GradleException(
87                         "Multiple atomic groups defined with the same Maven group ID: $groupId"
88                     )
89                 }
90                 if (association.overrideIncludeInProjectPaths.isEmpty()) {
91                     throw GradleException(
92                         "Duplicate library group $groupId defined in " +
93                             "${association.declarationName} does not set overrideInclude. " +
94                             "Declarations beyond the first can only have an effect if they set " +
95                             "overrideInclude"
96                     )
97                 }
98             } else {
99                 result[groupId] = association.libraryGroup
100             }
101         }
102         result
103     }
104 
105     // map from project name to group override if applicable
106     val overrideLibraryGroupsByProjectPath: Map<String, LibraryGroup> by lazy {
107         val result = mutableMapOf<String, LibraryGroup>()
108         for (association in libraryGroupAssociations) {
109             for (overridePath in association.overrideIncludeInProjectPaths) {
110                 result[overridePath] = association.libraryGroup
111             }
112         }
113         result
114     }
115 
116     private val libraryGroupAssociations: List<LibraryGroupAssociation> by lazy {
117         val groups = getTable("groups")
118 
119         fun readGroupVersion(groupDefinition: TomlTable, groupName: String, key: String): Version? {
120             val versionRef = groupDefinition.getString(key) ?: return null
121             if (!versionRef.startsWith(VersionReferencePrefix)) {
122                 throw GradleException(
123                     "Group entry $key is expected to start with $VersionReferencePrefix"
124                 )
125             }
126             // name without `versions.`
127             val atomicGroupVersionName = versionRef.removePrefix(VersionReferencePrefix)
128             return libraryVersions[atomicGroupVersionName]
129                 ?: error(
130                     "Group entry $groupName specifies $atomicGroupVersionName, but such version " +
131                         "doesn't exist"
132                 )
133         }
134         groups.keySet().sorted().map { name ->
135             // get group name
136             val groupDefinition = groups.getTable(name)!!
137             val groupName = groupDefinition.getString("group")!!
138 
139             // get group version, if any
140             val atomicGroupVersion =
141                 readGroupVersion(
142                     groupDefinition = groupDefinition,
143                     groupName = groupName,
144                     key = AtomicGroupVersion
145                 )
146             val overrideApplyToProjects =
147                 (groupDefinition.getArray("overrideInclude")?.toList() ?: listOf()).map {
148                     it as String
149                 }
150 
151             val group = LibraryGroup(groupName, atomicGroupVersion)
152             LibraryGroupAssociation(name, group, overrideApplyToProjects)
153         }
154     }
155 
156     companion object {
157         internal fun registerOrGet(project: Project): Provider<LibraryVersionsService> {
158             val tomlFileName = "libraryversions.toml"
159             val toml = project.lazyReadFile(tomlFileName)
160 
161             return project.gradle.sharedServices.registerIfAbsent(
162                 "libraryVersionsService",
163                 LibraryVersionsService::class.java
164             ) { spec ->
165                 spec.parameters.tomlFileName = tomlFileName
166                 spec.parameters.tomlFileContents = toml
167             }
168         }
169     }
170 }
171 
172 // a LibraryGroupSpec knows how to associate a LibraryGroup with the appropriate projects
173 private data class LibraryGroupAssociation(
174     // the name of the variable to which it is assigned in the toml file
175     val declarationName: String,
176     // the group
177     val libraryGroup: LibraryGroup,
178     // the paths of any additional projects that this group should be assigned to
179     val overrideIncludeInProjectPaths: List<String>
180 )
181 
182 private const val VersionReferencePrefix = "versions."
183 private const val AtomicGroupVersion = "atomicGroupVersion"
184 
185 // Maven groups that should be skipped for atomic duplication checks. Do not add further entries.
186 // TODO(b/401002936, b/401000219, b/401003097, b/401005632): Remove groups from this list
187 private val ALLOWED_ATOMIC_GROUP_EXCEPTIONS =
188     listOf(
189         "androidx.camera",
190         "androidx.compose.material3",
191         "androidx.lifecycle",
192         "androidx.tracing"
193     )
194