blob: ed40ea3deed44451bb5b1e2309d574bd86408686 [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.vlsi.gradle.crlf.CrLfSpec
import com.github.vlsi.gradle.crlf.LineEndings
import com.github.vlsi.gradle.git.FindGitAttributes
import com.github.vlsi.gradle.git.dsl.gitignore
import com.github.vlsi.gradle.properties.dsl.props
import org.gradle.api.internal.TaskOutputsInternal
import kotlin.math.absoluteValue
plugins {
id("build-logic.build-params")
id("com.github.vlsi.crlf")
id("com.github.vlsi.stage-vote-release")
id("build-logic.jvm-library")
}
var jars = arrayOf(
":src:bshclient",
":src:launcher",
":src:components",
":src:core",
// ":src:examples",
":src:functions",
":src:jorphan",
":src:protocol:bolt",
":src:protocol:ftp",
":src:protocol:http",
":src:protocol:java",
":src:protocol:jdbc",
":src:protocol:jms",
":src:protocol:junit",
":src:protocol:ldap",
":src:protocol:mail",
":src:protocol:mongodb",
":src:protocol:native",
":src:protocol:tcp"
)
// https://github.com/gradle/gradle/pull/16627
inline fun <reified T : Named> AttributeContainer.attribute(attr: Attribute<T>, value: String) =
attribute(attr, objects.named<T>(value))
// isCanBeConsumed = false ==> other modules must not use the configuration as a dependency
val buildDocs by configurations.creating {
isCanBeConsumed = false
}
val generatorJar by configurations.creating {
isCanBeConsumed = false
}
val junitSampleJar by configurations.creating {
isCanBeConsumed = false
isTransitive = false
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, Category.LIBRARY)
attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, LibraryElements.JAR)
attribute(Usage.USAGE_ATTRIBUTE, Usage.JAVA_RUNTIME)
attribute(Bundling.BUNDLING_ATTRIBUTE, Bundling.EXTERNAL)
}
}
val binLicense by configurations.creating {
isCanBeConsumed = false
}
val srcLicense by configurations.creating {
isCanBeConsumed = false
}
// Note: you can inspect final classpath (list of jars in the binary distribution) via
// gw dependencies --configuration runtimeClasspath
dependencies {
for (p in jars) {
api(project(p))
}
binLicense(project(":src:licenses", "binLicense"))
srcLicense(project(":src:licenses", "srcLicense"))
generatorJar(project(":src:generator", "archives"))
junitSampleJar(project(":src:protocol:junit-sample"))
buildDocs(platform(projects.src.bomThirdparty))
buildDocs("org.apache.velocity:velocity")
buildDocs("commons-lang:commons-lang")
buildDocs("org.apache.commons:commons-collections4")
buildDocs("org.jdom:jdom")
}
tasks.clean {
// copyLibs uses Sync task, so it can't predict all the possible output files (e.g. from previous executions)
// So we register patterns to remove explicitly
delete(fileTree("$rootDir/bin") { include("ApacheJMeter.jar") })
delete(fileTree("$rootDir/lib") { include("*.jar") })
delete(fileTree("$rootDir/lib/ext") { include("ApacheJMeter*.jar") })
delete(fileTree("$rootDir/lib/junit") { include("test.jar") })
}
// Libs are populated dynamically since we can't get the full set of dependencies
// before we execute all the build scripts
val libs = copySpec {
// Third-party dependencies + jorphan.jar
}
val libsExt = copySpec {
// Apache JMeter jars
}
val binLibs = copySpec {
// ApacheJMeter.jar launcher
}
// Splits jar dependencies to "lib", "lib/ext", and "bin" folders
val populateLibs by tasks.registering {
dependsOn(configurations.runtimeClasspath)
doLast {
val deps = configurations.runtimeClasspath.get().resolvedConfiguration.resolvedArtifacts
// This ensures project exists, if project is renamed, names should be corrected here as wells
val launcherProject = projects.src.launcher.dependencyProject.path
val bshclientProject = projects.src.bshclient.dependencyProject.path
val jorphanProject = projects.src.jorphan.dependencyProject.path
listOf(libs, libsExt, binLibs).forEach {
it.fileMode = "644".toInt(8)
it.dirMode = "755".toInt(8)
}
for (dep in deps) {
val compId = dep.id.componentIdentifier
if (compId !is ProjectComponentIdentifier || !compId.build.isCurrentBuild) {
// Move all non-JMeter jars to lib folder
libs.from(dep.file)
continue
}
// JMeter jars are spread across $root/bin, $root/libs, and $root/libs/ext
// for historical reasons
when (compId.projectPath) {
launcherProject -> binLibs
jorphanProject, bshclientProject -> libs
else -> libsExt
}.from(dep.file) {
// Remove version from the file name
rename { dep.name + "." + dep.extension }
}
}
}
}
val updateExpectedJars by props()
val verifyReleaseDependencies by tasks.registering {
description = "Verifies if binary release archive contains the expected set of external jars"
group = LifecycleBasePlugin.VERIFICATION_GROUP
dependsOn(configurations.runtimeClasspath)
val expectedLibs = file("src/dist/expected_release_jars.csv")
inputs.file(expectedLibs)
inputs.property("updateExpectedJars", updateExpectedJars)
val actualLibs = layout.buildDirectory.file("dist/expected_release_jars.csv")
outputs.file(actualLibs)
val ignoreJarsMismatch = version.toString().endsWith("-SNAPSHOT")
if (ignoreJarsMismatch || updateExpectedJars) {
// The task does not fail in case of -SNAPSHOT version, so we make the task never UP-TO-DATE
// in that case. Otherwise, the task never executes on the second request, even if the user runs with
// -PupdateExpectedJars
outputs.upToDateWhen { false }
}
doLast {
val caseInsensitive: Comparator<String> = compareBy(String.CASE_INSENSITIVE_ORDER, { it })
val deps = configurations.runtimeClasspath.get().resolvedConfiguration.resolvedArtifacts
val libs = deps.asSequence()
.filter {
val compId = it.id.componentIdentifier
compId !is ProjectComponentIdentifier || !compId.build.isCurrentBuild
}
.map { it.file.name to it.file.length() }
.sortedWith(compareBy(caseInsensitive) { it.first })
.associate { it }
val expected = expectedLibs.readLines().asSequence()
.filter { "," in it }
.map {
val (length, name) = it.split(",", limit = 2)
name to length.toLong()
}
.associate { it }
if (libs == expected) {
return@doLast
}
val sb = StringBuilder()
sb.append("External dependencies differ (you could update ${expectedLibs.relativeTo(rootDir)} if you run $path -PupdateExpectedJars):")
val sizeBefore = expected.values.sum()
val sizeAfter = libs.values.sum()
if (sizeBefore != sizeAfter) {
sb.append("\n $sizeBefore => $sizeAfter bytes")
sb.append(" (${if (sizeAfter > sizeBefore) "+" else "-"}${(sizeAfter - sizeBefore).absoluteValue} byte")
if ((sizeAfter - sizeBefore).absoluteValue > 1) {
sb.append("s")
}
sb.append(")")
}
if (libs.size != expected.size) {
sb.append("\n ${expected.size} => ${libs.size} files")
sb.append(" (${if (libs.size > expected.size) "+" else "-"}${(libs.size - expected.size).absoluteValue})")
}
sb.appendLine()
for (dep in (libs.keys + expected.keys).sortedWith(caseInsensitive)) {
val old = expected[dep]
val new = libs[dep]
if (old == new) {
continue
}
sb.append("\n")
if (old != null) {
sb.append("-").append(old.toString().padStart(8))
} else {
sb.append("+").append(new.toString().padStart(8))
}
sb.append(" ").append(dep)
}
val newline = System.getProperty("line.separator")
val actualLibsFile = actualLibs.get().asFile
actualLibsFile.writeText(
libs.map { "${it.value},${it.key}" }.joinToString(newline, postfix = newline)
)
if (updateExpectedJars) {
println("Updating ${expectedLibs.relativeTo(rootDir)}")
actualLibsFile.copyTo(expectedLibs, overwrite = true)
} else if (ignoreJarsMismatch) {
// Renovate requires self-hosted runner for executing postUpgradeTasks,
// so we can't make Renovate to update expected_release_jars.csv at the moment
logger.lifecycle(sb.toString())
} else {
throw GradleException(sb.toString())
}
}
}
tasks.check {
dependsOn(verifyReleaseDependencies)
}
// This adds dependency on "populateLibs" task
// This makes uses of these copySpecs transparently depend on the builder task
libs.from(populateLibs)
libsExt.from(populateLibs)
binLibs.from(populateLibs)
val copyLibs by tasks.registering(Sync::class) {
// Can't use $rootDir since Gradle somehow reports .gradle/caches/ as "always modified"
rootSpec.into("$rootDir/lib")
with(libs)
preserve {
// Sync does not really know which files it copied during previous times, so
// it just removes everything it sees.
// We configure it to keep txt files that should be present there (the files come from Git source tree)
include("**/*.txt")
// Keep jars in lib/ext so developers don't have to re-install the plugins again and again
include("ext/*.jar")
exclude("ext/ApacheJMeter*.jar")
}
into("ext") {
with(libsExt)
from(files(generatorJar)) {
rename { "ApacheJMeter_generator.jar" }
}
}
into("junit") {
from(files(junitSampleJar)) {
rename { "test.jar" }
}
}
}
val copyBinLibs by tasks.registering(Copy::class) {
// Can't use $rootDir since Gradle somehow reports .gradle/caches/ as "always modified"
rootSpec.into("$rootDir/bin")
with(binLibs)
// :src:config:jar conflicts with copyBinLibs on bin, bin/templates, bin/report-template folders
// so we add explicit ordering
mustRunAfter(":src:config:jar")
}
val createDist by tasks.registering {
group = LifecycleBasePlugin.BUILD_GROUP
description = "Copies JMeter jars and dependencies to projectRoot/lib/ folder"
dependsOn(copyLibs)
dependsOn(copyBinLibs)
}
// This task scans the project for gitignore / gitattributes, and that is reused for building
// source/binary artifacts with the appropriate eol/executable file flags
val gitProps by rootProject.tasks.existing(FindGitAttributes::class)
fun createAnakiaTask(
taskName: String,
baseDir: String,
extension: String = ".html",
style: String,
velocityProperties: String,
projectFile: String,
excludes: Array<String>,
includes: Array<String>
): TaskProvider<Task> {
val outputDir = layout.buildDirectory.dir("docs/$taskName").get().asFile
val prepareProps = tasks.register("prepareProperties$taskName") {
// AnakiaTask can't use relative paths, and it forbids ../, so we create a dedicated
// velocity.properties file that contains absolute path
inputs.file(velocityProperties)
val outputProps = layout.buildDirectory.file("docProps/$taskName/velocity.properties").get().asFile
outputs.file(outputProps)
doLast {
// Unfortunately, Velocity does not use Java properties format.
// For instance, Properties escape : as \:, however Velocity does not understand that.
// Thus it tries to use c\:\path\to\workspace which does not work
val p = `java.util`.Properties()
file(velocityProperties).reader().use {
p.load(it)
}
p["resource.loader"] = "file"
p["file.resource.loader.path"] = baseDir
p["file.resource.loader.class"] = "org.apache.velocity.runtime.resource.loader.FileResourceLoader"
val specials = Regex("""([,\\])""")
val lines = p.entries
.map { (it.key as String) + "=" + ((it.value as String).replace(specials, """\\$1""")) }
.sorted()
file(outputProps).apply {
parentFile.run { isDirectory || mkdirs() } || throw IllegalStateException("Unable to create directory $parentFile")
writer().use {
it.appendLine("# Auto-generated from $velocityProperties to pass absolute path to Velocity")
for (line in lines) {
it.appendLine(line)
}
}
}
}
}
return tasks.register(taskName) {
inputs.file("$baseDir/$style").withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("styleDir")
inputs.file("$baseDir/$projectFile").withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("projectDir")
inputs.files(
fileTree(baseDir) {
include(*includes)
exclude(*excludes)
}
).withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("baseDir")
inputs.property("extension", extension)
outputs.dir(outputDir)
outputs.cacheIf { true }
dependsOn(prepareProps)
doLast {
ant.withGroovyBuilder {
"taskdef"(
"name" to "anakia",
"classname" to "org.apache.velocity.anakia.AnakiaTask",
"classpath" to buildDocs.asPath
)
"anakia"(
"basedir" to baseDir,
"destdir" to outputDir,
"extension" to extension,
"style" to style,
"projectFile" to projectFile,
"excludes" to excludes.joinToString(" "),
"includes" to includes.joinToString(" "),
"lastModifiedCheck" to "true",
"velocityPropertiesFile" to prepareProps.get().outputs.files.singleFile
)
}
}
}
}
val xdocs = "$rootDir/xdocs"
fun CopySpec.docCssAndImages() {
from(xdocs) {
include(".htaccess")
include("css/**")
include("images/**")
}
}
fun CopySpec.manuals() {
from(xdocs) {
include("demos/**")
include("extending/jmeter_tutorial.pdf")
include("usermanual/**/*.pdf")
}
}
fun CopySpec.printableDocumentation() {
into("docs") {
docCssAndImages()
}
into("printable_docs") {
from(buildPrintableDoc)
manuals()
}
}
val buildPrintableDoc = createAnakiaTask(
"buildPrintableDoc", baseDir = xdocs,
style = "stylesheets/site_printable.vsl",
velocityProperties = "$xdocs/velocity.properties",
projectFile = "stylesheets/printable_project.xml",
excludes = arrayOf("**/stylesheets/**", "extending.xml", "extending/*.xml"),
includes = arrayOf("**/*.xml")
)
val previewPrintableDocs by tasks.registering(Copy::class) {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Creates preview of a printable documentation to build/docs/printable_preview"
into(layout.buildDirectory.dir("docs/printable_preview"))
CrLfSpec().run {
gitattributes(gitProps)
printableDocumentation()
}
}
val lastEditYear: String by rootProject.extra
fun xslt(
subdir: String,
outputDir: String,
includes: Array<String> = arrayOf("*.xml"),
excludes: Array<String> = arrayOf("extending.xml")
) {
val relativePath = if (subdir.isEmpty()) "." else ".."
ant.withGroovyBuilder {
"xslt"(
"style" to "$xdocs/stylesheets/website-style.xsl",
"basedir" to "$xdocs/$subdir",
"destdir" to "$outputDir/$subdir",
"excludes" to excludes.joinToString(" "),
"includes" to includes.joinToString(" ")
) {
"param"("name" to "relative-path", "expression" to relativePath)
"param"("name" to "subdir", "expression" to subdir)
"param"("name" to "year", "expression" to lastEditYear)
}
}
}
val processSiteXslt by tasks.registering {
val outputDir = layout.buildDirectory.dir("siteXslt").get().asFile
inputs.files(xdocs).withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("xdocs")
inputs.property("year", lastEditYear)
outputs.dir(outputDir)
outputs.cacheIf { true }
doLast {
for (f in (outputs as TaskOutputsInternal).previousOutputFiles) {
f.delete()
}
for (i in arrayOf("", "usermanual", "localising")) {
xslt(i, outputDir.absolutePath)
}
}
}
fun CopySpec.siteLayout() {
// TODO: generate doap_JMeter.rdf
from("$xdocs/download_jmeter.cgi")
into("api") {
javadocs()
}
from(processSiteXslt)
docCssAndImages()
manuals()
}
// See https://github.com/gradle/gradle/issues/10960
val previewSiteDir = layout.buildDirectory.dir("site")
val previewSite by tasks.registering(Sync::class) {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Creates preview of a site to build/docs/site"
into(previewSiteDir)
CrLfSpec().run {
gitattributes(gitProps)
siteLayout()
}
doLast {
println("Site preview synchronized to ${previewSiteDir.get().file("index.html")}")
}
}
val distributionGroup = "distribution"
val baseFolder = "apache-jmeter-${rootProject.version}"
fun CopySpec.javadocs() = from(javadocAggregate)
fun CopySpec.excludeLicenseFromSourceRelease() {
// Source release has "/licenses" folder with licenses for third-party dependencies
// It is populated by "dependencyLicenses" above,
// so we ignore the folder when building source releases
exclude("licenses/**")
exclude("LICENSE")
}
fun CrLfSpec.binaryLayout() = copySpec {
gitattributes(gitProps)
into(baseFolder) {
// Note: license content is taken from "/build/..", so gitignore should not be used
// Note: this is a "license + third-party licenses", not just Apache-2.0
// Note: files(...) adds both "files" and "dependency"
from(files(binLicense))
from(rootDir) {
gitignore(gitProps)
exclude("bin/testfiles")
exclude("bin/rmi_keystore.jks")
include("bin/**")
include("lib/ext/**")
include("lib/junit/**")
include("extras/**")
include("README.md")
excludeLicenseFromSourceRelease()
}
into("bin") {
with(binLibs)
}
into("lib") {
with(libs)
into("ext") {
with(libsExt)
}
}
printableDocumentation()
into("docs/api") {
javadocs()
}
}
}
fun CrLfSpec.sourceLayout() = copySpec {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
gitattributes(gitProps)
into(baseFolder) {
// Note: license content is taken from "/build/..", so gitignore should not be used
// Note: this is a "license + third-party licenses", not just Apache-2.0
// Note: files(...) adds both "files" and "dependency"
from(files(srcLicense))
// Include all the source files
from(rootDir) {
gitignore(gitProps)
excludeLicenseFromSourceRelease()
}
}
}
val javadocAggregate by tasks.registering(Javadoc::class) {
group = JavaBasePlugin.DOCUMENTATION_GROUP
description = "Generates aggregate javadoc for all the artifacts"
val sourceSets = jars.map { project(it).sourceSets.main }
classpath = files(sourceSets.map { set -> set.map { it.output + it.compileClasspath } })
// Aggregate javadoc needs to include generated JMeterVersion class
// So we use delay computation of source files
setSource(sourceSets.map { set -> set.map { it.allJava } })
setDestinationDir(layout.buildDirectory.dir("docs/javadocAggregate").get().asFile)
}
// Generates distZip, distTar, distZipSource, and distTarSource tasks
// The archives and checksums are put to build/distributions
for (type in listOf("binary", "source")) {
if (buildParameters.skipDist) {
break
}
for (archive in listOf(Zip::class, Tar::class)) {
val taskName = "dist${archive.simpleName}${type.replace("binary", "").replaceFirstChar { it.titlecaseChar() }}"
val archiveTask = tasks.register(taskName, archive) {
val eol = if (archive == Tar::class) LineEndings.LF else LineEndings.CRLF
group = distributionGroup
description = "Creates $type distribution with $eol line endings for text files"
if (this is Tar) {
compression = Compression.GZIP
}
// dist task excludes jar files from bin/, and lib/ however Gradle does not see that
// So we add an artificial dependency
mustRunAfter(copyBinLibs)
mustRunAfter(copyLibs)
mustRunAfter(":src:dist-check:copyExtraTestLibs")
// Gradle does not track "filters" as archive/copy task dependencies,
// So a mere change of a file attribute won't trigger re-execution of a task
// So we add a custom property to re-execute the task in case attributes change
inputs.property("gitproperties", gitProps.map { it.props.attrs.toString() })
// Gradle defaults to the following pattern, and JMeter was using apache-jmeter-5.1_src.zip
// [baseName]-[appendix]-[version]-[classifier].[extension]
archiveBaseName.set("apache-jmeter-${rootProject.version}${if (type == "source") "_src" else ""}")
// Discard project version since we want it to be added before "_src"
archiveVersion.set("")
CrLfSpec(eol).run {
wa1191SetInputs(gitProps)
with(if (type == "source") sourceLayout() else binaryLayout())
}
}
releaseArtifacts {
artifact(archiveTask)
}
}
}
releaseArtifacts {
previewSite(previewSite) {
into("site") {
from(previewSiteDir)
}
}
}
val runGui by tasks.registering(JavaExec::class) {
group = "Development"
description = "Builds and starts JMeter GUI"
dependsOn(createDist)
buildParameters.testJdk?.let {
javaLauncher.set(javaToolchains.launcherFor(it))
}
workingDir = File(project.rootDir, "bin")
mainClass.set("org.apache.jmeter.NewDriver")
classpath("$rootDir/bin/ApacheJMeter.jar")
jvmArgs("-Xss256k")
jvmArgs("-XX:MaxMetaspaceSize=256m")
val osName = System.getProperty("os.name")
if (osName.contains(Regex("mac os x|darwin|osx", RegexOption.IGNORE_CASE))) {
jvmArgs("-Xdock:name=JMeter")
jvmArgs("-Xdock:icon=$rootDir/xdocs/images/jmeter_square.png")
jvmArgs("-Dapple.laf.useScreenMenuBar=true")
jvmArgs("-Dapple.eawt.quitStrategy=CLOSE_ALL_WINDOWS")
}
fun passProperty(name: String, default: String? = null) {
val value = System.getProperty(name) ?: default
value?.let { systemProperty(name, it) }
}
passProperty("java.awt.headless")
val props = System.getProperties()
@Suppress("UNCHECKED_CAST")
for (e in props.propertyNames() as `java.util`.Enumeration<String>) {
// Pass -Djmeter.* and -Ddarklaf.* properties to the JMeter process
if (e.startsWith("jmeter.") || e.startsWith("darklaf.")) {
passProperty(e)
}
if (e == "darklaf.native") {
systemProperty("darklaf.decorations", "true")
systemProperty("darklaf.allowNativeCode", "true")
}
}
}