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