1 /*
<lambda>null2  * Copyright (C) 2023 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 com.android.tools.metalava
18 
19 import com.android.build.api.dsl.Lint
20 import com.android.tools.metalava.buildinfo.CreateAggregateLibraryBuildInfoFileTask
21 import com.android.tools.metalava.buildinfo.CreateAggregateLibraryBuildInfoFileTask.Companion.CREATE_AGGREGATE_BUILD_INFO_FILES_TASK
22 import com.android.tools.metalava.buildinfo.addTaskToAggregateBuildInfoFileTask
23 import com.android.tools.metalava.buildinfo.configureBuildInfoTask
24 import java.io.File
25 import java.io.StringReader
26 import java.util.Properties
27 import org.gradle.api.JavaVersion
28 import org.gradle.api.Plugin
29 import org.gradle.api.Project
30 import org.gradle.api.artifacts.Configuration
31 import org.gradle.api.component.AdhocComponentWithVariants
32 import org.gradle.api.internal.tasks.testing.filter.DefaultTestFilter
33 import org.gradle.api.plugins.JavaPlugin
34 import org.gradle.api.plugins.JavaPluginExtension
35 import org.gradle.api.provider.Provider
36 import org.gradle.api.publish.PublishingExtension
37 import org.gradle.api.publish.maven.MavenPublication
38 import org.gradle.api.publish.maven.plugins.MavenPublishPlugin
39 import org.gradle.api.publish.tasks.GenerateModuleMetadata
40 import org.gradle.api.tasks.TaskProvider
41 import org.gradle.api.tasks.bundling.Zip
42 import org.gradle.api.tasks.testing.Test
43 import org.gradle.api.tasks.testing.logging.TestLogEvent
44 import org.gradle.kotlin.dsl.create
45 import org.gradle.kotlin.dsl.get
46 import org.gradle.kotlin.dsl.getByType
47 import org.jetbrains.kotlin.gradle.dsl.JvmTarget
48 import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
49 import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper
50 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
51 
52 class MetalavaBuildPlugin : Plugin<Project> {
53     override fun apply(project: Project) {
54         project.plugins.all { plugin ->
55             when (plugin) {
56                 is JavaPlugin -> {
57                     project.extensions.getByType<JavaPluginExtension>().apply {
58                         sourceCompatibility = JavaVersion.VERSION_17
59                         targetCompatibility = JavaVersion.VERSION_17
60                     }
61                 }
62                 is KotlinBasePluginWrapper -> {
63                     project.tasks.withType(KotlinCompile::class.java).configureEach { task ->
64                         task.compilerOptions.apply {
65                             jvmTarget.set(JvmTarget.JVM_17)
66                             apiVersion.set(KotlinVersion.KOTLIN_1_7)
67                             languageVersion.set(KotlinVersion.KOTLIN_1_7)
68                             allWarningsAsErrors.set(true)
69                         }
70                     }
71                 }
72                 is MavenPublishPlugin -> {
73                     configurePublishing(project)
74                 }
75             }
76         }
77 
78         configureLint(project)
79         configureTestTasks(project)
80         project.configureKtfmt()
81         project.version = project.getMetalavaVersion()
82         project.group = "com.android.tools.metalava"
83     }
84 
85     fun configureLint(project: Project) {
86         project.apply(mapOf("plugin" to "com.android.lint"))
87         project.extensions.getByType<Lint>().apply {
88             fatal.add("UastImplementation") // go/hide-uast-impl
89             fatal.add("KotlincFE10") // b/239982263
90             disable.add("UseTomlInstead") // not useful for this project
91             disable.add("GradleDependency") // not useful for this project
92             abortOnError = true
93             baseline = File("lint-baseline.xml")
94         }
95     }
96 
97     fun configureTestTasks(project: Project) {
98         val testTask = project.tasks.named("test", Test::class.java)
99 
100         val zipTask: TaskProvider<Zip> =
101             project.tasks.register("zipTestResults", Zip::class.java) { zip ->
102                 zip.destinationDirectory.set(
103                     File(getDistributionDirectory(project), "host-test-reports")
104                 )
105                 zip.archiveFileName.set(testTask.map { "${it.path}.zip" })
106                 zip.from(testTask.map { it.reports.junitXml.outputLocation.get() })
107             }
108 
109         testTask.configure { task ->
110             task as Test
111             task.jvmArgs = listOf(
112                 "--add-opens=java.base/java.lang=ALL-UNNAMED",
113                 // Needed for CustomizableParameterizedRunner
114                 "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED",
115             )
116 
117             task.doFirst {
118                 // Before running the tests update the filter.
119                 task.filter { testFilter ->
120                     testFilter as DefaultTestFilter
121 
122                     // The majority of Metalava tests are now parameterized, as they run against
123                     // multiple providers. As parameterized tests they include a suffix of `[....]`
124                     // after the method name that contains the arguments for those parameters. The
125                     // problem with parameterized tests is that the test name does not match the
126                     // method name so when running a specific test an IDE cannot just use the
127                     // method name in the test filter, it has to use a wildcard to match all the
128                     // instances of the test method. When IntelliJ runs a test that has
129                     // `@RunWith(org.junit.runners.Parameterized::class)` it will add `[*]` to the
130                     // end of the test filter to match all instances of that test method.
131                     // Unfortunately, that only applies to tests that explicitly use
132                     // `org.junit.runners.Parameterized` and the Metalava tests use their own
133                     // custom runner that uses `Parameterized` under the covers. Without the `[*]`,
134                     // any attempt to run a specific parameterized test method just results in an
135                     // error that "no tests matched".
136                     //
137                     // This code avoids that by checking the patterns that have been provided on the
138                     // command line and adding a wildcard. It cannot add `[*]` as that would cause
139                     // a "no tests matched" error for non-parameterized tests and while most tests
140                     // in Metalava are parameterized, some are not. Also, it is necessary to be able
141                     // to run a specific instance of a test with a specific set of arguments.
142                     //
143                     // This code adds a `*` to the end of the pattern if it does not already end
144                     // with a `*` or a `\]`. i.e.:
145                     // * "pkg.ClassTest" will become "pkg.ClassTest*". That does run the risk of
146                     //   matching other classes, e.g. "ClassTestOther" but they are unlikely to
147                     //   exist and can be renamed if it becomes an issue.
148                     // * "pkg.ClassTest.method" will become "pkg.ClassTest.method*". That does run
149                     //   the risk of running other non-parameterized methods, e.g.
150                     //   "pkg.ClassTest.methodWithSuffix" but again they can be renamed if it
151                     //   becomes an issue.
152                     // * "pkg.ClassTest.method[*]" will be unmodified and will match any
153                     //   parameterized instance of the method.
154                     // * "pkg.ClassTest.method[a,b]" will be unmodified and will match a specific
155                     //   parameterized instance of the method.
156                     val commandLineIncludePatterns = testFilter.commandLineIncludePatterns
157                     if (commandLineIncludePatterns.isNotEmpty()) {
158                         val transformedPatterns = commandLineIncludePatterns.map { pattern ->
159 
160                             if (!pattern.endsWith("]") && !pattern.endsWith("*")) {
161                                 "$pattern*"
162                             } else {
163                                 pattern
164                             }
165                         }
166                         testFilter.setCommandLineIncludePatterns(transformedPatterns)
167                     }
168                 }
169             }
170 
171             task.maxParallelForks =
172                 (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1
173             task.testLogging.events =
174                 hashSetOf(
175                     TestLogEvent.FAILED,
176                     TestLogEvent.STANDARD_OUT,
177                     TestLogEvent.STANDARD_ERROR
178                 )
179             task.finalizedBy(zipTask)
180             if (isBuildingOnServer()) task.ignoreFailures = true
181         }
182     }
183 
184     fun configurePublishing(project: Project) {
185         val projectRepo = project.layout.buildDirectory.dir("repo")
186         val archiveTaskProvider =
187             configurePublishingArchive(
188                 project,
189                 publicationName,
190                 repositoryName,
191                 getBuildId(),
192                 getDistributionDirectory(project),
193                 projectRepo,
194             )
195 
196         project.extensions.getByType<PublishingExtension>().apply {
197             publications { publicationContainer ->
198                 publicationContainer.create<MavenPublication>(publicationName) {
199                     val javaComponent = project.components["java"] as AdhocComponentWithVariants
200                     // Disable publishing of test fixtures as we consider them internal
201                     project.configurations.findByName("testFixturesApiElements")?.let {
202                         javaComponent.withVariantsFromConfiguration(it) { it.skip() }
203                     }
204                     project.configurations.findByName("testFixturesRuntimeElements")?.let {
205                         javaComponent.withVariantsFromConfiguration(it) { it.skip() }
206                     }
207                     from(javaComponent)
208                     suppressPomMetadataWarningsFor("testFixturesApiElements")
209                     suppressPomMetadataWarningsFor("testFixturesRuntimeElements")
210                     pom { pom ->
211                         pom.licenses { spec ->
212                             spec.license { license ->
213                                 license.name.set("The Apache License, Version 2.0")
214                                 license.url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
215                             }
216                         }
217                         pom.developers { spec ->
218                             spec.developer { developer ->
219                                 developer.name.set("The Android Open Source Project")
220                             }
221                         }
222                         pom.scm { scm ->
223                             scm.connection.set(
224                                 "scm:git:https://android.googlesource.com/platform/tools/metalava"
225                             )
226                             scm.url.set("https://android.googlesource.com/platform/tools/metalava/")
227                         }
228                     }
229 
230                     val buildInfoTask =
231                         configureBuildInfoTask(
232                             project,
233                             this,
234                             isBuildingOnServer(),
235                             getDistributionDirectory(project),
236                             archiveTaskProvider
237                         )
238                     project.addTaskToAggregateBuildInfoFileTask(buildInfoTask)
239                 }
240             }
241             repositories { handler ->
242                 handler.maven { repository ->
243                     repository.url =
244                         project.uri(
245                             "file://${
246                                 getDistributionDirectory(project).canonicalPath
247                             }/repo/m2repository"
248                         )
249                 }
250                 handler.maven { repository ->
251                     repository.name = repositoryName
252                     repository.url = project.uri(projectRepo)
253                 }
254             }
255         }
256 
257         // Add a buildId into Gradle Metadata file so we can tell which build it is from.
258         project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task ->
259             val outDirProvider = project.providers.environmentVariable("DIST_DIR")
260             task.inputs.property("buildOutputDirectory", outDirProvider).optional(true)
261             task.doLast {
262                 val metadata = (it as GenerateModuleMetadata).outputFile.asFile.get()
263                 val text = metadata.readText()
264                 val buildId = outDirProvider.orNull?.let { File(it).name } ?: "0"
265                 metadata.writeText(
266                     text.replace(
267                         """"createdBy": {
268     "gradle": {""",
269                         """"createdBy": {
270     "gradle": {
271       "buildId:": "$buildId",""",
272                     )
273                 )
274             }
275         }
276     }
277 }
278 
versionnull279 internal fun Project.version(): Provider<String> {
280     @Suppress("UNCHECKED_CAST") // version is a VersionProviderWrapper set in MetalavaBuildPlugin
281     return (version as VersionProviderWrapper).versionProvider
282 }
283 
284 // https://github.com/gradle/gradle/issues/25971
285 private class VersionProviderWrapper(val versionProvider: Provider<String>) {
toStringnull286     override fun toString(): String {
287         return versionProvider.get()
288     }
289 }
290 
Projectnull291 private fun Project.getMetalavaVersion(): VersionProviderWrapper {
292     val contents =
293         providers.fileContents(
294             rootProject.layout.projectDirectory.file("version.properties")
295         )
296     return VersionProviderWrapper(
297         contents.asText.map {
298             val versionProps = Properties()
299             versionProps.load(StringReader(it))
300             versionProps["metalavaVersion"]!! as String
301         }
302     )
303 }
304 
305 /**
306  * The build server will copy the contents of the distribution directory and make it available for
307  * download.
308  */
getDistributionDirectorynull309 internal fun getDistributionDirectory(project: Project): File {
310     return if (System.getenv("DIST_DIR") != null) {
311         File(System.getenv("DIST_DIR"))
312     } else {
313         File(project.rootProject.projectDir, "../../out/dist")
314     }
315 }
316 
isBuildingOnServernull317 private fun isBuildingOnServer(): Boolean {
318     return System.getenv("OUT_DIR") != null && System.getenv("DIST_DIR") != null
319 }
320 
321 /**
322  * @return build id string for current build
323  *
324  * The build server does not pass the build id so we infer it from the last folder of the
325  * distribution directory name.
326  */
getBuildIdnull327 private fun getBuildId(): String {
328     return if (System.getenv("DIST_DIR") != null) File(System.getenv("DIST_DIR")).name else "0"
329 }
330 
331 private const val publicationName = "Metalava"
332 private const val repositoryName = "Dist"
333