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