blob: 60b7e3b8aa3b45744b279fd2f8a32aec5bfc2af4 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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.
*/
import com.github.spotbugs.snom.SpotBugsTask
import com.github.vlsi.gradle.crlf.CrLfSpec
import com.github.vlsi.gradle.crlf.LineEndings
import com.github.vlsi.gradle.crlf.filter
import com.github.vlsi.gradle.git.FindGitAttributes
import com.github.vlsi.gradle.git.dsl.gitignore
import com.github.vlsi.gradle.properties.dsl.lastEditYear
import com.github.vlsi.gradle.properties.dsl.props
import com.github.vlsi.gradle.release.RepositoryType
import net.ltgt.gradle.errorprone.errorprone
import org.ajoberstar.grgit.Grgit
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.sonarqube.gradle.SonarQubeProperties
plugins {
java
kotlin("jvm") apply false
jacoco
checkstyle
id("org.jetbrains.gradle.plugin.idea-ext") apply false
id("org.nosphere.apache.rat")
id("com.github.autostyle")
id("com.github.spotbugs")
id("net.ltgt.errorprone") apply false
id("org.sonarqube")
id("com.github.vlsi.crlf")
id("com.github.vlsi.gradle-extensions")
id("com.github.vlsi.ide")
id("com.github.vlsi.stage-vote-release")
publishing
}
ide {
copyrightToAsf()
ideaInstructionsUri =
uri("https://github.com/apache/jmeter/blob/master/CONTRIBUTING.md#intellij")
doNotDetectFrameworks("android", "jruby")
}
fun Project.boolProp(name: String) =
findProperty(name)
// Project properties include tasks, extensions, etc, and we want only String properties
// We don't want to use "task" as a boolean property
?.let { it as? String }
?.equals("false", ignoreCase = true)?.not()
// Release candidate index
val String.v: String get() = rootProject.extra["$this.version"] as String
version = "jmeter".v + releaseParams.snapshotSuffix
val displayVersion by extra {
version.toString() +
if (releaseParams.release.get()) {
""
} else {
// Append 7 characters of Git commit id for snapshot version
val grgit: Grgit? by project
grgit?.let { " " + it.head().abbreviatedId }
}
}
println("Building JMeter $version")
fun reportsForHumans() = !(System.getenv()["CI"]?.toBoolean() ?: boolProp("CI") ?: false)
val lastEditYear by extra(lastEditYear().toString())
// This task scans the project for gitignore / gitattributes, and that is reused for building
// source/binary artifacts with the appropriate eol/executable file flags
// It enables to automatically exclude patterns from .gitignore
val gitProps by tasks.registering(FindGitAttributes::class) {
// Scanning for .gitignore and .gitattributes files in a task avoids doing that
// when distribution build is not required (e.g. code is just compiled)
root.set(rootDir)
}
val rat by tasks.getting(org.nosphere.apache.rat.RatTask::class) {
gitignore(gitProps)
verbose.set(true)
// Note: patterns are in non-standard syntax for RAT, so we use exclude(..) instead of excludeFile
exclude(rootDir.resolve(".ratignore").readLines())
}
tasks.validateBeforeBuildingReleaseArtifacts {
dependsOn(rat)
}
releaseArtifacts {
fromProject(":src:dist")
previewSite {
into("rat")
from(rat) {
filteringCharset = "UTF-8"
// XML is not really interesting for now
exclude("rat-report.xml")
// RAT reports have absolute paths, and we don't want to expose them
filter { str: String -> str.replace(rootDir.absolutePath, "") }
}
}
}
releaseParams {
tlp.set("JMeter")
releaseTag.set("rel/v${project.version}")
rcTag.set(rc.map { "v${project.version}-rc$it" })
svnDist {
// All the release versions are put under release/jmeter/{source,binary}
releaseFolder.set("release/jmeter")
releaseSubfolder.apply {
put(Regex("_src\\."), "source")
put(Regex("."), "binaries")
}
staleRemovalFilters {
excludes.add(Regex("release/.*/HEADER\\.html"))
}
}
nexus {
if (repositoryType.get() == RepositoryType.PROD) {
// org.apache.jmeter at repository.apache.org
stagingProfileId.set("4d29c092016673")
}
}
}
val jacocoReport by tasks.registering(JacocoReport::class) {
group = "Coverage reports"
description = "Generates an aggregate report from all subprojects"
}
val jacocoEnabled by extra {
(boolProp("coverage") ?: false) || gradle.startParameter.taskNames.any { it.contains("jacoco") }
}
// Do not enable spotbugs by default. Execute it only when -Pspotbugs is present
val enableSpotBugs = props.bool("spotbugs", default = false)
val enableErrorprone by props()
val ignoreSpotBugsFailures by props()
val skipCheckstyle by props()
val skipAutostyle by props()
val werror by props(true) // treat javac warnings as errors
// Allow to skip building source/binary distributions
val skipDist by extra {
boolProp("skipDist") ?: false
}
// Inherited from stage-vote-release-plugin: skipSign, useGpgCmd
allprojects {
apply(plugin = "com.github.vlsi.gradle-extensions")
if (project.path != ":src") {
tasks.register<DependencyInsightReportTask>("allDependencyInsight") {
group = HelpTasksPlugin.HELP_GROUP
description =
"Shows insights where the dependency is used. For instance: allDependencyInsight --configuration compile --dependency org.jsoup:jsoup"
}
}
}
sonarqube {
properties {
// See https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner+for+Gradle#AnalyzingwithSonarQubeScannerforGradle-Configureanalysisproperties
property("sonar.sourceEncoding", "UTF-8")
val projectName = "JMeter"
property("sonar.projectName", projectName)
property("sonar.projectKey", System.getenv()["SONAR_PROJECT_KEY"] ?: projectName)
property("sonar.organization", System.getenv()["SONAR_ORGANIZATION"] ?: "apache")
property("sonar.projectVersion", project.version.toString())
property("sonar.host.url", System.getenv()["SONAR_HOST_URL"] ?: "http://localhost:9000")
property("sonar.login", System.getenv()["SONAR_LOGIN"] ?: "")
property("sonar.password", System.getenv()["SONAR_PASSWORD"] ?: "")
property("sonar.links.homepage", "https://jmeter.apache.org")
property("sonar.links.ci", "https://builds.apache.org/job/JMeter-trunk/")
property("sonar.links.scm", "https://jmeter.apache.org/svnindex.html")
property("sonar.links.issue", "https://jmeter.apache.org/issues.html")
}
}
fun SonarQubeProperties.add(name: String, valueProvider: () -> String) {
properties.getOrPut(name) { mutableSetOf<Any>() }
.also {
@Suppress("UNCHECKED_CAST")
(it as MutableCollection<Any>).add(object {
// SonarQube calls toString when converting properties to values
// (see SonarQubeProperties), so we use that to emulate "lazy properties"
override fun toString() = valueProvider()
})
}
}
if (jacocoEnabled) {
val mergedCoverage = jacocoReport.get().reports.xml.outputLocation.toString()
// For every module we pass merged coverage report
// That enables to see ":src:core" lines covered even in case they are covered from
// "batch tests"
subprojects {
if (File(projectDir, "src/main").exists()) {
apply(plugin = "org.sonarqube")
sonarqube {
properties {
property("sonar.coverage.jacoco.xmlReportPaths", mergedCoverage)
}
}
}
}
tasks.sonarqube {
dependsOn(jacocoReport)
}
}
if (enableSpotBugs) {
// By default sonarqube does not depend on spotbugs
val sonarqubeTask = tasks.sonarqube
// See https://jira.sonarsource.com/browse/SONARGRADL-59
// Unfortunately, report paths must be specified manually for now
allprojects {
if (!File(projectDir, "src/main").exists()) {
return@allprojects
}
val spotBugTasks = tasks.withType<SpotBugsTask>().matching {
// We don't send spotbugs for test classes
!it.name.endsWith("Test")
}
sonarqubeTask {
dependsOn(spotBugTasks)
}
apply(plugin = "org.sonarqube")
sonarqube {
properties {
spotBugTasks.configureEach {
add("sonar.java.spotbugs.reportPaths") {
// Note: report is created with lower-case xml, and then
// the created entry MUST be retrieved as upper-case XML
reports.named("XML").get().destination.toString()
}
}
}
}
}
}
fun com.github.autostyle.gradle.BaseFormatExtension.license() {
licenseHeader(rootProject.ide.licenseHeader) {
copyrightStyle("bat", com.github.autostyle.generic.DefaultCopyrightStyle.REM)
copyrightStyle("cmd", com.github.autostyle.generic.DefaultCopyrightStyle.REM)
addBlankLineAfter.set(true)
}
trimTrailingWhitespace()
endWithNewline()
}
allprojects {
group = "org.apache.jmeter"
version = rootProject.version
repositories {
// RAT and Autostyle dependencies
mavenCentral()
}
// JMeter ClassFinder parses "class.path" and tries to find jar names there,
// so we should produce jars without versions names for now
// version = rootProject.version
if (!skipAutostyle) {
apply(plugin = "com.github.autostyle")
autostyle {
kotlinGradle {
license()
ktlint("ktlint".v)
}
format("configs") {
filter {
include("**/*.sh", "**/*.bsh", "**/*.cmd", "**/*.bat")
include("**/*.properties", "**/*.yml")
include("**/*.xsd", "**/*.xsl", "**/*.xml")
// Autostyle does not support gitignore yet https://github.com/autostyle/autostyle/issues/13
exclude("out/**")
if (project == rootProject) {
exclude(rootDir.resolve(".ratignore").readLines())
exclude("gradlew*")
// Generated by batch tests. It ignores log4j2.xml, however it is not that important
// The configuration will be removed when Autostyle will use .gitignore
exclude("bin/*.xml")
} else {
exclude("bin/**")
}
}
license()
}
format("markdown") {
filter.include("**/*.md")
endWithNewline()
}
}
}
plugins.withType<JavaPlugin> {
// We don't intend to resolve that configuration
// It is in line with further Gradle versions: https://github.com/gradle/gradle/issues/8585
dependencies {
configurations {
compileOnly(platform(project(":src:bom")))
}
}
apply<IdeaPlugin>()
apply<EclipsePlugin>()
if (!skipCheckstyle) {
apply<CheckstylePlugin>()
checkstyle {
toolVersion = "checkstyle".v
configProperties = mapOf(
"cache_file" to buildDir.resolve("checkstyle/cacheFile")
)
}
val sourceSets: SourceSetContainer by project
if (sourceSets.isNotEmpty()) {
val checkstyleTasks = tasks.withType<Checkstyle>()
checkstyleTasks.configureEach {
// Checkstyle 8.26 does not need classpath, see https://github.com/gradle/gradle/issues/14227
classpath = files()
}
tasks.register("checkstyleAll") {
dependsOn(checkstyleTasks)
}
tasks.register("checkstyle") {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Executes Checkstyle verifications"
dependsOn("checkstyleAll")
dependsOn("autostyleCheck")
}
// Autostyle produces more meaningful error messages, so we ensure it is executed before Checkstyle
if (!skipAutostyle) {
for (s in sourceSets.names) {
tasks.named("checkstyle" + s.capitalize()) {
mustRunAfter("autostyleApply")
mustRunAfter("autostyleCheck")
}
}
}
}
}
apply(plugin = "com.github.spotbugs")
spotbugs {
toolVersion.set("spotbugs".v)
ignoreFailures.set(ignoreSpotBugsFailures)
}
if (!skipAutostyle) {
autostyle {
java {
license()
importOrder("static ", "java.", "javax", "org", "net", "com", "")
removeUnusedImports()
indentWithSpaces(4)
}
}
}
tasks.register("style") {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Formats code (license header, import order, whitespace at end of line, ...) and executes Checkstyle verifications"
if (!skipAutostyle) {
dependsOn("autostyleApply")
}
if (!skipCheckstyle) {
dependsOn("checkstyleAll")
}
}
if (enableErrorprone) {
apply(plugin = "net.ltgt.errorprone")
dependencies {
"errorprone"("com.google.errorprone:error_prone_core:${"errorprone".v}")
"annotationProcessor"("com.google.guava:guava-beta-checker:1.0")
}
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.addAll(listOf("-Xmaxerrs", "10000", "-Xmaxwarns", "10000"))
options.errorprone {
disableWarningsInGeneratedCode.set(true)
errorproneArgs.add("-XepExcludedPaths:.*/javacc/.*")
disable(
"ComplexBooleanConstant",
"EqualsGetClass",
"InlineMeSuggester",
"OperatorPrecedence",
"MutableConstantField",
// "ReferenceEquality",
"SameNameButDifferent",
"TypeParameterUnusedInFormals"
)
// Analyze issues, and enable the check
disable(
"MissingSummary",
"EmptyBlockTag",
"BigDecimalEquals",
"StringSplitter"
)
}
}
}
}
plugins.withId("groovy") {
if (!skipAutostyle) {
autostyle {
groovy {
license()
importOrder("static ", "java.", "javax", "org", "net", "com", "")
indentWithSpaces(4)
}
}
}
}
plugins.withId("org.jetbrains.kotlin.jvm") {
configure<KotlinJvmProjectExtension> {
// Require explicit access modifiers and require explicit types for public APIs.
// See https://kotlinlang.org/docs/whatsnew14.html#explicit-api-mode-for-library-authors
if (props.bool("kotlin.explicitApi", default = true)) {
explicitApi()
}
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
if (!name.startsWith("compileTest")) {
apiVersion = "kotlin.api".v
}
}
}
if (!skipAutostyle) {
autostyle {
kotlin {
license()
trimTrailingWhitespace()
ktlint("ktlint".v)
endWithNewline()
}
}
}
}
plugins.withType<JacocoPlugin> {
the<JacocoPluginExtension>().toolVersion = "jacoco".v
val testTasks = tasks.withType<Test>()
val javaExecTasks = tasks.withType<JavaExec>()
.matching { it.name != "runGui" }
// This configuration must be postponed since JacocoTaskExtension might be added inside
// configure block of a task (== before this code is run). See :src:dist-check:createBatchTask
afterEvaluate {
for (t in arrayOf(testTasks, javaExecTasks)) {
t.configureEach {
extensions.findByType<JacocoTaskExtension>()?.apply {
// Do not collect coverage when not asked (e.g. via jacocoReport or -Pcoverage)
isEnabled = jacocoEnabled
// We don't want to collect coverage for third-party classes
includes?.add("org.apache.jmeter.*")
includes?.add("org.apache.jorphan.*")
includes?.add("org.apache.commons.cli.*")
}
}
}
}
jacocoReport {
// Note: this creates a lazy collection
// Some of the projects might fail to create a file (e.g. no tests or no coverage),
// So we check for file existence. Otherwise JacocoMerge would fail
val execFiles =
files(testTasks, javaExecTasks).filter { it.exists() && it.name.endsWith(".exec") }
executionData(execFiles)
}
tasks.withType<JacocoReport>().configureEach {
reports {
html.required.set(reportsForHumans())
xml.required.set(!reportsForHumans())
}
}
// Add each project to combined report
configure<SourceSetContainer> {
val mainCode = main.get()
jacocoReport.configure {
additionalSourceDirs.from(mainCode.allJava.srcDirs)
sourceDirectories.from(mainCode.allSource.srcDirs)
// IllegalStateException: Can't add different class with same name: module-info
// https://github.com/jacoco/jacoco/issues/858
classDirectories.from(
mainCode.output.asFileTree.matching {
exclude("module-info.class")
}
)
}
}
}
tasks.withType<AbstractArchiveTask>().configureEach {
// Ensure builds are reproducible
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
dirMode = "775".toInt(8)
fileMode = "664".toInt(8)
}
plugins.withType<JavaPlugin> {
// This block is executed right after `java` plugin is added to a project
java {
sourceCompatibility = JavaVersion.VERSION_1_8
consistentResolution {
useCompileClasspathVersions()
}
}
repositories {
mavenCentral()
}
tasks {
withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
options.compilerArgs.add("-Xlint:deprecation")
if (werror) {
options.compilerArgs.add("-Werror")
}
}
withType<ProcessResources>().configureEach {
filteringCharset = "UTF-8"
eachFile {
if (name.endsWith(".properties")) {
filteringCharset = "UTF-8"
// apply native2ascii conversion since Java 8 expects properties to have ascii symbols only
filter(org.apache.tools.ant.filters.EscapeUnicode::class)
filter(LineEndings.LF)
} else if (name.endsWith(".dtd") || name.endsWith(".svg") ||
name.endsWith(".txt")
) {
filter(LineEndings.LF)
}
}
}
afterEvaluate {
// Add default license/notice when missing (e.g. see :src:config that overrides LICENSE)
withType<Jar>().configureEach {
CrLfSpec(LineEndings.LF).run {
into("META-INF") {
filteringCharset = "UTF-8"
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
// Note: we need "generic Apache-2.0" text without third-party items
// So we use the text from $rootDir/config/ since source distribution
// contains altered text at $rootDir/LICENSE
textFrom("$rootDir/config/LICENSE")
textFrom("$rootDir/NOTICE")
}
}
}
}
withType<Jar>().configureEach {
manifest {
attributes["Bundle-License"] = "Apache-2.0"
attributes["Specification-Title"] = "Apache JMeter"
attributes["Specification-Vendor"] = "Apache Software Foundation"
attributes["Implementation-Vendor"] = "Apache Software Foundation"
attributes["Implementation-Vendor-Id"] = "org.apache"
attributes["Implementation-Version"] = rootProject.version
}
}
withType<Test>().configureEach {
useJUnitPlatform()
testLogging {
exceptionFormat = TestExceptionFormat.FULL
showStandardStreams = true
}
outputs.cacheIf("test outcomes sometimes depends on third-party systems, so we should not cache it for now") {
false
}
// Pass the property to tests
fun passProperty(name: String, default: String? = null) {
val value = System.getProperty(name) ?: default
value?.let { systemProperty(name, it) }
}
System.getProperties().filter {
it.key.toString().startsWith("jmeter.properties.")
}.forEach {
systemProperty(it.key.toString().substring("jmeter.properties.".length), it.value)
}
passProperty("java.awt.headless")
passProperty("skip.test_TestDNSCacheManager.testWithCustomResolverAnd1Server")
passProperty("junit.jupiter.execution.parallel.enabled", "true")
passProperty("junit.jupiter.execution.timeout.default", "2 m")
}
withType<SpotBugsTask>().configureEach {
group = LifecycleBasePlugin.VERIFICATION_GROUP
if (enableSpotBugs) {
description = "$description (skipped by default, to enable it add -Pspotbugs)"
}
reports {
// xml goes for SonarQube, so we always create it just in case
create("xml")
if (reportsForHumans()) {
create("html")
}
}
enabled = enableSpotBugs
}
withType<Javadoc>().configureEach {
(options as StandardJavadocDocletOptions).apply {
noTimestamp.value = true
showFromProtected()
locale = "en"
docEncoding = "UTF-8"
charSet = "UTF-8"
encoding = "UTF-8"
docTitle = "Apache JMeter ${project.name} API"
windowTitle = "Apache JMeter ${project.name} API"
header = "<b>Apache JMeter</b>"
addStringOption("source", "8")
bottom =
"Copyright © 1998-$lastEditYear Apache Software Foundation. All Rights Reserved."
if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
addBooleanOption("html5", true)
links("https://docs.oracle.com/javase/9/docs/api/")
} else {
links("https://docs.oracle.com/javase/8/docs/api/")
}
}
}
}
}
}