| /* |
| * Copyright (C) 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.metalava |
| |
| import org.gradle.api.tasks.bundling.Jar |
| import com.android.build.api.dsl.Lint |
| import com.android.tools.metalava.buildinfo.configureBuildInfoTask |
| import org.gradle.api.JavaVersion |
| import org.gradle.api.Plugin |
| import org.gradle.api.Project |
| import org.gradle.api.component.AdhocComponentWithVariants |
| import org.gradle.api.internal.tasks.testing.filter.DefaultTestFilter |
| import org.gradle.api.plugins.JavaPlugin |
| import org.gradle.api.plugins.JavaPluginExtension |
| import org.gradle.api.provider.Provider |
| import org.gradle.api.publish.PublishingExtension |
| import org.gradle.api.publish.maven.MavenPublication |
| import org.gradle.api.publish.maven.plugins.MavenPublishPlugin |
| import org.gradle.api.publish.tasks.GenerateModuleMetadata |
| import org.gradle.api.tasks.TaskProvider |
| import org.gradle.api.tasks.bundling.Zip |
| import org.gradle.api.tasks.testing.Test |
| import org.gradle.api.tasks.testing.logging.TestLogEvent |
| import org.gradle.kotlin.dsl.create |
| import org.gradle.kotlin.dsl.get |
| import org.gradle.kotlin.dsl.getByType |
| import org.gradle.kotlin.dsl.setEnvironment |
| import org.jetbrains.kotlin.gradle.dsl.JvmTarget |
| import org.jetbrains.kotlin.gradle.dsl.KotlinVersion |
| import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper |
| import org.jetbrains.kotlin.gradle.tasks.KotlinCompile |
| import java.io.File |
| import java.io.StringReader |
| import java.util.Properties |
| |
| class MetalavaBuildPlugin : Plugin<Project> { |
| override fun apply(project: Project) { |
| project.plugins.all { plugin -> |
| when (plugin) { |
| is JavaPlugin -> { |
| project.extensions.getByType<JavaPluginExtension>().apply { |
| sourceCompatibility = JavaVersion.VERSION_17 |
| targetCompatibility = JavaVersion.VERSION_17 |
| } |
| } |
| is KotlinBasePluginWrapper -> { |
| project.tasks.withType(KotlinCompile::class.java).configureEach { task -> |
| task.compilerOptions.apply { |
| jvmTarget.set(JvmTarget.JVM_17) |
| apiVersion.set(KotlinVersion.KOTLIN_2_0) |
| languageVersion.set(KotlinVersion.KOTLIN_2_0) |
| allWarningsAsErrors.set(true) |
| } |
| } |
| } |
| is MavenPublishPlugin -> { |
| configurePublishing(project) |
| } |
| } |
| } |
| |
| configureLint(project) |
| configureTestTasks(project) |
| project.configureKtfmt() |
| project.version = project.getMetalavaVersion() |
| project.group = "com.android.tools.metalava" |
| } |
| |
| private fun configureLint(project: Project) { |
| project.apply(mapOf("plugin" to "com.android.lint")) |
| project.extensions.getByType<Lint>().apply { |
| fatal.add("UastImplementation") // go/hide-uast-impl |
| fatal.add("KotlincFE10") // b/239982263 |
| disable.add("UseTomlInstead") // not useful for this project |
| disable.add("GradleDependency") // not useful for this project |
| abortOnError = true |
| baseline = File("lint-baseline.xml") |
| } |
| } |
| |
| private fun configureTestTasks(project: Project) { |
| val testTask = project.tasks.named("test", Test::class.java) |
| |
| val zipTask: TaskProvider<Zip> = |
| project.tasks.register("zipTestResults", Zip::class.java) { zip -> |
| zip.destinationDirectory.set( |
| File(getDistributionDirectory(project), "host-test-reports") |
| ) |
| zip.archiveFileName.set(testTask.map { "${it.path}.zip" }) |
| zip.from(testTask.map { it.reports.junitXml.outputLocation.get() }) |
| } |
| |
| testTask.configure { task -> |
| task as Test |
| task.jvmArgs = listOf( |
| "--add-opens=java.base/java.lang=ALL-UNNAMED", |
| // Needed for CustomizableParameterizedRunner |
| "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", |
| ) |
| |
| // Get the jar from the stub-annotations project. |
| val jarTask = project.findProject(":stub-annotations")!!.tasks.named("jar", Jar::class.java) |
| |
| // Add a dependency from this test task to the jar task of stub-annotations to make sure |
| // it is built before this is run. |
| task.dependsOn(jarTask) |
| |
| // Clear the environment before adding any custom variables. Avoids problems with |
| // inconsistent behavior when testing code that accesses environment variables, e.g. |
| // command line tools that use environment variables to determine whether to use colors |
| // in command line help. |
| task.setEnvironment() |
| |
| // Get the path to the stub-annotations jar and pass it to this in an environment |
| // variable. |
| val stubAnnotationsJar = jarTask.get().outputs.files.singleFile |
| task.environment.put( |
| "METALAVA_STUB_ANNOTATIONS_JAR", stubAnnotationsJar, |
| ) |
| |
| task.doFirst { |
| // Before running the tests update the filter. |
| task.filter { testFilter -> |
| testFilter as DefaultTestFilter |
| |
| // The majority of Metalava tests are now parameterized, as they run against |
| // multiple providers. As parameterized tests they include a suffix of `[....]` |
| // after the method name that contains the arguments for those parameters. The |
| // problem with parameterized tests is that the test name does not match the |
| // method name so when running a specific test an IDE cannot just use the |
| // method name in the test filter, it has to use a wildcard to match all the |
| // instances of the test method. When IntelliJ runs a test that has |
| // `@RunWith(org.junit.runners.Parameterized::class)` it will add `[*]` to the |
| // end of the test filter to match all instances of that test method. |
| // Unfortunately, that only applies to tests that explicitly use |
| // `org.junit.runners.Parameterized` and the Metalava tests use their own |
| // custom runner that uses `Parameterized` under the covers. Without the `[*]`, |
| // any attempt to run a specific parameterized test method just results in an |
| // error that "no tests matched". |
| // |
| // This code avoids that by checking the patterns that have been provided on the |
| // command line and adding a wildcard. It cannot add `[*]` as that would cause |
| // a "no tests matched" error for non-parameterized tests and while most tests |
| // in Metalava are parameterized, some are not. Also, it is necessary to be able |
| // to run a specific instance of a test with a specific set of arguments. |
| // |
| // This code adds a `*` to the end of the pattern if it does not already end |
| // with a `*` or a `\]`. i.e.: |
| // * "pkg.ClassTest" will become "pkg.ClassTest*". That does run the risk of |
| // matching other classes, e.g. "ClassTestOther" but they are unlikely to |
| // exist and can be renamed if it becomes an issue. |
| // * "pkg.ClassTest.method" will become "pkg.ClassTest.method*". That does run |
| // the risk of running other non-parameterized methods, e.g. |
| // "pkg.ClassTest.methodWithSuffix" but again they can be renamed if it |
| // becomes an issue. |
| // * "pkg.ClassTest.method[*]" will be unmodified and will match any |
| // parameterized instance of the method. |
| // * "pkg.ClassTest.method[a,b]" will be unmodified and will match a specific |
| // parameterized instance of the method. |
| val commandLineIncludePatterns = testFilter.commandLineIncludePatterns |
| if (commandLineIncludePatterns.isNotEmpty()) { |
| val transformedPatterns = commandLineIncludePatterns.map { pattern -> |
| |
| if (!pattern.endsWith("]") && !pattern.endsWith("*")) { |
| "$pattern*" |
| } else { |
| pattern |
| } |
| } |
| testFilter.setCommandLineIncludePatterns(transformedPatterns) |
| } |
| } |
| } |
| |
| task.maxParallelForks = |
| (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1 |
| task.testLogging.events = |
| hashSetOf( |
| TestLogEvent.FAILED, |
| TestLogEvent.STANDARD_OUT, |
| TestLogEvent.STANDARD_ERROR |
| ) |
| task.finalizedBy(zipTask) |
| if (isBuildingOnServer()) task.ignoreFailures = true |
| } |
| } |
| |
| private fun configurePublishing(project: Project) { |
| val projectRepo = project.layout.buildDirectory.dir("repo") |
| val archiveTaskProvider = |
| configurePublishingArchive( |
| project, |
| publicationName, |
| repositoryName, |
| getBuildId(), |
| getDistributionDirectory(project), |
| projectRepo, |
| ) |
| |
| project.extensions.getByType<PublishingExtension>().apply { |
| publications { publicationContainer -> |
| publicationContainer.create<MavenPublication>(publicationName) { |
| val javaComponent = project.components["java"] as AdhocComponentWithVariants |
| // Disable publishing of test fixtures as we consider them internal |
| project.configurations.findByName("testFixturesApiElements")?.let { |
| javaComponent.withVariantsFromConfiguration(it) { it.skip() } |
| } |
| project.configurations.findByName("testFixturesRuntimeElements")?.let { |
| javaComponent.withVariantsFromConfiguration(it) { it.skip() } |
| } |
| from(javaComponent) |
| suppressPomMetadataWarningsFor("testFixturesApiElements") |
| suppressPomMetadataWarningsFor("testFixturesRuntimeElements") |
| pom { pom -> |
| pom.licenses { spec -> |
| spec.license { license -> |
| license.name.set("The Apache License, Version 2.0") |
| license.url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") |
| } |
| } |
| pom.developers { spec -> |
| spec.developer { developer -> |
| developer.name.set("The Android Open Source Project") |
| } |
| } |
| pom.scm { scm -> |
| scm.connection.set( |
| "scm:git:https://android.googlesource.com/platform/tools/metalava" |
| ) |
| scm.url.set("https://android.googlesource.com/platform/tools/metalava/") |
| } |
| } |
| |
| configureBuildInfoTask( |
| project, |
| this, |
| isBuildingOnServer(), |
| getDistributionDirectory(project), |
| archiveTaskProvider |
| ) |
| } |
| } |
| repositories { handler -> |
| handler.maven { repository -> |
| repository.url = |
| project.uri( |
| "file://${ |
| getDistributionDirectory(project).canonicalPath |
| }/repo/m2repository" |
| ) |
| } |
| handler.maven { repository -> |
| repository.name = repositoryName |
| repository.url = project.uri(projectRepo) |
| } |
| } |
| } |
| |
| // Add a buildId into Gradle Metadata file so we can tell which build it is from. |
| project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task -> |
| val outDirProvider = project.providers.environmentVariable("DIST_DIR") |
| task.inputs.property("buildOutputDirectory", outDirProvider).optional(true) |
| task.doLast { |
| val metadata = (it as GenerateModuleMetadata).outputFile.asFile.get() |
| val text = metadata.readText() |
| val buildId = outDirProvider.orNull?.let { File(it).name } ?: "0" |
| metadata.writeText( |
| text.replace( |
| """"createdBy": { |
| "gradle": {""", |
| """"createdBy": { |
| "gradle": { |
| "buildId:": "$buildId",""", |
| ) |
| ) |
| } |
| } |
| } |
| } |
| |
| internal fun Project.version(): Provider<String> { |
| return (version as VersionProviderWrapper).versionProvider |
| } |
| |
| // https://github.com/gradle/gradle/issues/25971 |
| private class VersionProviderWrapper(val versionProvider: Provider<String>) { |
| override fun toString(): String { |
| return versionProvider.get() |
| } |
| } |
| |
| private fun Project.getMetalavaVersion(): VersionProviderWrapper { |
| val contents = |
| providers.fileContents( |
| isolated.rootProject.projectDirectory.file("version.properties") |
| ) |
| return VersionProviderWrapper( |
| contents.asText.map { |
| val versionProps = Properties() |
| versionProps.load(StringReader(it)) |
| versionProps["metalavaVersion"]!! as String |
| } |
| ) |
| } |
| |
| /** |
| * The build server will copy the contents of the distribution directory and make it available for |
| * download. |
| */ |
| internal fun getDistributionDirectory(project: Project): File { |
| return if (System.getenv("DIST_DIR") != null) { |
| File(System.getenv("DIST_DIR")) |
| } else { |
| File(project.rootProject.projectDir, "../../out/dist") |
| } |
| } |
| |
| private fun isBuildingOnServer(): Boolean { |
| return System.getenv("OUT_DIR") != null && System.getenv("DIST_DIR") != null |
| } |
| |
| /** |
| * @return build id string for current build |
| * |
| * The build server does not pass the build id so we infer it from the last folder of the |
| * distribution directory name. |
| */ |
| private fun getBuildId(): String { |
| return if (System.getenv("DIST_DIR") != null) File(System.getenv("DIST_DIR")).name else "0" |
| } |
| |
| private const val publicationName = "Metalava" |
| private const val repositoryName = "Dist" |