jvmti-access/build.gradle.kts (200 lines of code) (raw):

import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage import com.bmuschko.gradle.docker.tasks.image.DockerExistingImage import com.github.dockerjava.api.async.ResultCallback import com.github.dockerjava.api.command.CreateContainerResponse import com.github.dockerjava.api.command.WaitContainerResultCallback import com.github.dockerjava.api.model.* import java.io.IOException import java.util.* plugins { id("elastic-otel.library-packaging-conventions") id("elastic-otel.sign-and-publish-conventions") alias(catalog.plugins.dockerJavaApplication) } description = "Library for exposing JVMTI and JNI functionality to Java" // we use Java 7 for this project so that it can be reused in the old elastic-apm-agent // Subsequently, the newest Java compiler we can use is java 17 java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } tasks { compileJava { options.release.set(7) } } tasks.withType<Test>().configureEach { // the check:jni flag helps unconver potential pitfalls in native code during runtime // it is not required for running the tests, but it is very useful jvmArgs("-Xcheck:jni") } val jniSrcDir = file("src/main/jni") val jniBuildDir: Directory = layout.buildDirectory.dir("jni").get() sourceSets { main { resources { srcDir(jniBuildDir) } } } val sharedCompilerArgs = "-std=c++17 -fno-rtti -fno-exceptions -O2 -ftls-model=global-dynamic -fPIC -Wall -Werror -Wextra -shared" val nativeTargets = listOf( NativeTarget( "darwin-arm64.so", "jni_darwin.Dockerfile", "-arch arm64 $sharedCompilerArgs" ), NativeTarget( "darwin-x64.so", "jni_darwin.Dockerfile", "-arch x86_64 $sharedCompilerArgs" ), //On linux we statically link libstdc++ and libgcc to maximize portability NativeTarget( "linux-arm64.so", "jni_linux_arm64.Dockerfile", "-static-libstdc++ -static-libgcc -mtls-dialect=desc $sharedCompilerArgs" ), NativeTarget( "linux-x64.so", "jni_linux_x64.Dockerfile", "-static-libstdc++ -static-libgcc -mtls-dialect=gnu2 $sharedCompilerArgs" ), NativeTarget( "linux-musl-arm64.so", "jni_linux_musl_arm64.Dockerfile", "-static-libstdc++ -static-libgcc -mtls-dialect=desc $sharedCompilerArgs" ), NativeTarget( "linux-musl-x64.so", "jni_linux_musl_x64.Dockerfile", "-static-libstdc++ -static-libgcc -mtls-dialect=gnu2 $sharedCompilerArgs" ) ) task("buildJavaIncludesImage", DockerBuildImage::class) { dockerFile.set(file("jni-build/java_includes.Dockerfile")) inputDir.set(file("jni-build")) images.add("elastic_jni_build_java_includes:latest") } val compileJniTask = task("compileJni") compileJniTask.group = "jni" tasks.processResources { dependsOn(compileJniTask) } tasks.sourcesJar { //sources jar doesn't need the generated native libraries exclude("elastic-jvmti") } nativeTargets.forEach { val taskSuffix = it.getTaskSuffix(); val createImageTask = task("buildCompilerImage$taskSuffix", DockerBuildImage::class) { dependsOn("buildJavaIncludesImage") dockerFile.set(file("jni-build/"+it.dockerfile)) inputDir.set(file("jni-build")) } val artifactCompileTask = task("compileJni$taskSuffix", DockerRun::class) { dependsOn(createImageTask) //compileJava generates the JNI-headers from native methods dependsOn(tasks.compileJava) val artifactName = "elastic-jvmti-${it.artifactSuffix}" val actualOutputDir = jniBuildDir.asFile.resolve("elastic-jvmti") val artifactFile = actualOutputDir.resolve(artifactName) val generatedHeadersDir = layout.buildDirectory.get().dir("generated/sources/headers/java/main") inputs.dir(jniSrcDir) inputs.dir(generatedHeadersDir) outputs.file(artifactFile) doFirst { actualOutputDir.mkdirs() if (artifactFile.exists()) { artifactFile.delete() } } targetImageId { createImageTask.imageId.get() } binds.put(jniSrcDir.absolutePath, "/jni_src") binds.put(generatedHeadersDir.asFile.absolutePath, "/jni_headers") binds.put(actualOutputDir.absolutePath, "/jni_dest") val args = "${it.compilerArgs} -I /jni_headers -I /jni_src -o /jni_dest/$artifactName /jni_src/*.cpp" envVars.put("BUILD_ARGS", args) } compileJniTask.dependsOn(artifactCompileTask) } class NativeTarget(val artifactSuffix: String, val dockerfile: String, val compilerArgs: String) { fun getTaskSuffix() : String { var suffix = artifactSuffix; //remove file suffix if(suffix.contains('.')) { suffix = suffix.substring(0, suffix.lastIndexOf('.')) } //replace kebab-case with upper CamelCase var result = ""; for (segment in suffix.split("-")) { result += Character.toUpperCase(segment[0]); result += segment.substring(1); } return result; } } /** * Custom task combining creating, running and cleaning up a container. */ open class DockerRun : DockerExistingImage() { @get:Optional @get:Input val envVars: MapProperty<String, String> = project.objects.mapProperty( String::class.java, String::class.java ) @get:Optional @get:Input val binds: MapProperty<String, String> = project.objects.mapProperty( String::class.java, String::class.java ) @Throws(IOException::class) override fun runRemoteCommand() { logger.debug("Creating container") val container = createContainer() try { logger.debug("Starting container with ID '${container.id}'.") dockerClient.startContainerCmd(container.id).exec() logger.debug("Following logs of container with ID '${container.id}'.") followContainerLogs(container) val containerWait = dockerClient.waitContainerCmd(container.id) val exitCode = containerWait.exec(WaitContainerResultCallback()).awaitStatusCode() logger.debug("Container exited with code $exitCode") if(exitCode != 0) { throw GradleException("Container exited with status code $exitCode, check the logs for details") } } finally { dockerClient.removeContainerCmd(container.id) .withForce(true) .exec() } } private fun createContainer(): CreateContainerResponse { val createContainerCommand = dockerClient.createContainerCmd(imageId.get()) createContainerCommand.withEnv( envVars.get().entries.stream() .map { "${it.key}=${it.value}" } .toList() ) createContainerCommand.hostConfig.withBinds(binds.get().entries.stream() .map { "${it.key}:${it.value}" } .map(Bind::parse) .toList() ) return createContainerCommand.exec() } private fun followContainerLogs(container: CreateContainerResponse) { val logCommand = dockerClient.logContainerCmd(container.id) .withFollowStream(true) .withTailAll() .withStdErr(true) .withStdOut(true) logCommand.exec(object : ResultCallback.Adapter<Frame?>() { override fun onNext(frame: Frame?) { if (frame != null) { when (frame.streamType) { StreamType.STDOUT, StreamType.RAW -> logger.quiet( String(frame.payload).replaceFirst("/\\s+$/".toRegex(), "") ) StreamType.STDERR -> logger.error( String(frame.payload).replaceFirst("/\\s+$/".toRegex(), "") ) else -> {} } } super.onNext(frame) } }).awaitCompletion(); } }