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