Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ gradlePlugin {
id = "dd-trace-java.instrumentation-naming"
implementationClass = "datadog.gradle.plugin.naming.InstrumentationNamingPlugin"
}

create("frgaal-test-compiler") {
id = "dd-trace-java.frgaal-test-compiler"
implementationClass = "datadog.gradle.plugin.frgaal.FrgaalCompilerPlugin"
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package datadog.gradle.plugin.frgaal

import datadog.gradle.plugin.frgaal.FrgaalCompilerPlugin.Companion.SOURCE_VERSION
import datadog.gradle.plugin.frgaal.FrgaalCompilerPlugin.Companion.TARGET_VERSION
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.kotlin.dsl.withType
import org.gradle.plugins.ide.idea.model.IdeaLanguageLevel
import org.gradle.plugins.ide.idea.model.IdeaModel
import java.io.File
import java.util.Locale

/**
* Compiles test sources with the [Frgaal](https://frgaal.org) compiler so they may use modern Java
* syntax (Java 17 source level) while still producing Java 8 bytecode that runs on every JVM the
* agent supports.
*
* Caveats — read before rolling this out widely:
* - **Sugar only.** Only syntax that desugars to Java 8 bytecode is safe: text blocks, `var`, switch
* expressions, `instanceof` patterns. Features that need newer *runtime* classes (records, sealed
* classes, pattern matching for `switch`) compile but fail at runtime on a Java 8 target. Treat the
* 17 source level as "nicer syntax", not "all of Java 17".
* - **No incremental compilation.** Forking with a custom `javac` executable opts out of Gradle's
* incremental Java compiler, so affected test source sets always recompile in full. Acceptable for
* a handful of modules; measure before applying repo-wide.
Comment on lines +25 to +27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Given the number of tests we have, this could be a noticeable performance regression.

* - **Frgaal runs on the Gradle daemon JDK** ([SOURCE_VERSION] source, [TARGET_VERSION] target).
*/
class FrgaalCompilerPlugin : Plugin<Project> {
override fun apply(project: Project) {
configureIdeaLanguageLevel(project)

val frgaalCompiler = project.configurations.create("frgaalCompiler")
project.dependencies.add(frgaalCompiler.name, "org.frgaal:compiler:$FRGAAL_VERSION")

val isWindows = System.getProperty("os.name").lowercase(Locale.ROOT).contains("win")
val frgaalJavaHome = project.layout.buildDirectory.dir("frgaal-java-home")
val frgaalJavacWrapper = frgaalJavaHome.map { it.file(if (isWindows) "bin/javac.bat" else "bin/javac") }

val writeFrgaalJavacWrapper = project.tasks.register("writeFrgaalJavacWrapper") {
inputs.files(frgaalCompiler)
outputs.dir(frgaalJavaHome)

doLast {
val binDir = File(frgaalJavaHome.get().asFile, "bin")
binDir.mkdirs()

val realJava = File(System.getProperty("java.home"), if (isWindows) "bin/java.exe" else "bin/java").absolutePath
val javacWrapper = frgaalJavacWrapper.get().asFile
// When forkOptions.executable points at <frgaalJavaHome>/bin/javac, Gradle treats
// <frgaalJavaHome> as a Java installation and validates it by running <home>/bin/java -version
// (this happens on JDK 8 daemons, where gradle/java_no_deps.gradle resolves a javaCompiler
// toolchain instead of using --release). So the fake home must expose a working java that
// delegates to the real, probe-able JDK — otherwise the build fails before javac even runs.
val javaWrapper = File(binDir, if (isWindows) "java.bat" else "java")
if (isWindows) {
javaWrapper.writeText(windowsJavaScript(realJava))
javacWrapper.writeText(windowsWrapperScript(realJava, frgaalCompiler.asPath))
} else {
javaWrapper.writeText(unixJavaScript(realJava))
javacWrapper.writeText(unixWrapperScript(realJava, frgaalCompiler.asPath))
javaWrapper.setExecutable(true)
javacWrapper.setExecutable(true)
}
}
}

// Registered from afterEvaluate so this configureEach runs *after* the shared compiler config in
// gradle/java_no_deps.gradle (which sets options.release in its own configureEach). configureEach
// actions run at task realization in registration order, so registering last lets us win and
// clear the release flag — Frgaal needs source > target, which --release forbids.
project.afterEvaluate {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: project.afterEvaluate looks wrong, at this point I'm not sure if there's a better approach with frgaal.

project.tasks.withType<JavaCompile>().configureEach {
if (isTestJavaCompileTask(name)) {
dependsOn(writeFrgaalJavacWrapper)
sourceCompatibility = SOURCE_VERSION.toString()
targetCompatibility = TARGET_VERSION.toString()
options.release.set(null as Int?)
options.isFork = true
options.forkOptions.executable = frgaalJavacWrapper.get().asFile.absolutePath
Comment thread
AlexeyKuznetsov-DD marked this conversation as resolved.
if (!options.compilerArgs.contains("-Xlint:-options")) {
options.compilerArgs.add("-Xlint:-options")
}
}
}
}
}

/**
* Tell IntelliJ (via its Gradle import) that the module accepts Java 17 source while still
* targeting Java 8 bytecode, matching what Frgaal does for the test compile tasks. Without this
* the IDE imports the module's language level/SDK from the `java` extension (Java 8) and flags
* text blocks and other modern syntax as errors.
*/
private fun configureIdeaLanguageLevel(project: Project) {
project.pluginManager.apply("idea")
project.extensions.configure<IdeaModel>("idea") {
module.jdkName = SOURCE_VERSION.majorVersion
module.languageLevel = IdeaLanguageLevel("JDK_${SOURCE_VERSION.majorVersion}")
module.targetBytecodeVersion = TARGET_VERSION
}
}
Comment on lines +90 to +103
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Wouldn;t this bump the language level of the main source set as well ?


private fun isTestJavaCompileTask(taskName: String): Boolean {
return taskName == "compileTestJava" ||
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: this is covered by the check below

(taskName.startsWith("compile") && taskName.endsWith("TestJava"))
}

/** Minimal `java` that forwards to the real JDK so Gradle's installation probe succeeds. */
private fun unixJavaScript(realJava: String): String {
return """
|#!/bin/sh
|exec ${shQuote(realJava)} "${'$'}@"
|
""".trimMargin()
}

private fun windowsJavaScript(realJava: String): String {
return """
|@echo off
|"$realJava" %*
|exit /b %ERRORLEVEL%
|
""".trimMargin()
}

private fun unixWrapperScript(javaExecutable: String, frgaalClasspath: String): String {
return """
|#!/usr/bin/env bash
|set -euo pipefail
|
|jvm_args=()
|javac_args=()
|for arg in "${'$'}@"; do
| if [[ "${'$'}arg" == -J* ]]; then
| jvm_args+=("${'$'}{arg:2}")
| else
| javac_args+=("${'$'}arg")
| fi
|done
|
|exec ${shQuote(javaExecutable)} "${'$'}{jvm_args[@]}" -cp ${shQuote(frgaalClasspath)} org.frgaal.Main "${'$'}{javac_args[@]}"
|
""".trimMargin()
}

private fun windowsWrapperScript(javaExecutable: String, frgaalClasspath: String): String {
// -J-prefixed args go to the JVM; everything else (including @argfiles) goes to Frgaal.
return """
|@echo off
|setlocal enabledelayedexpansion
|set "JVM_ARGS="
|set "JAVAC_ARGS="
|:frgaal_parse
|if "%~1"=="" goto frgaal_run
|set "frgaal_raw=%~1"
|if "!frgaal_raw:~0,2!"=="-J" (
| set "JVM_ARGS=!JVM_ARGS! !frgaal_raw:~2!"
|) else (
| set "JAVAC_ARGS=!JAVAC_ARGS! %1"
|)
|shift
|goto frgaal_parse
|:frgaal_run
|"$javaExecutable" !JVM_ARGS! -cp "$frgaalClasspath" org.frgaal.Main !JAVAC_ARGS!
|exit /b %ERRORLEVEL%
|
""".trimMargin()
}

private fun shQuote(value: String): String {
return "'" + value.replace("'", "'\"'\"'") + "'"
}

companion object {
private const val FRGAAL_VERSION = "25.0.0"
private val SOURCE_VERSION = JavaVersion.VERSION_17
private val TARGET_VERSION = JavaVersion.VERSION_1_8
}
}
1 change: 1 addition & 0 deletions dd-trace-core/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id 'me.champeau.jmh'
id 'dd-trace-java.version-file'
id 'dd-trace-java.frgaal-test-compiler'
}

description = 'dd-trace-core'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,46 +137,48 @@ void actualConfigCommitWithServiceAndOrgLevelConfigs() throws Exception {
// Add org level config (priority 1) - should set service mapping
updater.accept(
orgKey,
("{\n"
+ " \"service_target\": {\n"
+ " \"service\": \"*\",\n"
+ " \"env\": \"*\"\n"
+ " },\n"
+ " \"lib_config\": {\n"
+ " \"tracing_service_mapping\": [{\n"
+ " \"from_key\": \"org-service\",\n"
+ " \"to_name\": \"org-mapped\"\n"
+ " }],\n"
+ " \"tracing_sampling_rate\": 0.7\n"
+ " }\n"
+ "}")
"""
{
"service_target": {
"service": "*",
"env": "*"
},
"lib_config": {
"tracing_service_mapping": [{
"from_key": "org-service",
"to_name": "org-mapped"
}],
"tracing_sampling_rate": 0.7
}
}"""
.getBytes(StandardCharsets.UTF_8),
null);
// Add service level config (priority 4) - should override service mapping and add header tags
updater.accept(
serviceKey,
("{\n"
+ " \"service_target\": {\n"
+ " \"service\": \"test-service\",\n"
+ " \"env\": \"*\"\n"
+ " },\n"
+ " \"lib_config\": {\n"
+ " \"tracing_service_mapping\": [{\n"
+ " \"from_key\": \"service-specific\",\n"
+ " \"to_name\": \"service-mapped\"\n"
+ " }],\n"
+ " \"tracing_header_tags\": [{\n"
+ " \"header\": \"X-Custom-Header\",\n"
+ " \"tag_name\": \"custom.header\"\n"
+ " }],\n"
+ " \"tracing_sampling_rate\": 1.3,\n"
+ " \"data_streams_transaction_extractors\": [{\n"
+ " \"name\": \"test\",\n"
+ " \"type\": \"unknown\",\n"
+ " \"value\": \"value\"\n"
+ " }]\n"
+ " }\n"
+ "}")
"""
{
"service_target": {
"service": "test-service",
"env": "*"
},
"lib_config": {
"tracing_service_mapping": [{
"from_key": "service-specific",
"to_name": "service-mapped"
}],
"tracing_header_tags": [{
"header": "X-Custom-Header",
"tag_name": "custom.header"
}],
"tracing_sampling_rate": 1.3,
"data_streams_transaction_extractors": [{
"name": "test",
"type": "unknown",
"value": "value"
}]
}
}"""
.getBytes(StandardCharsets.UTF_8),
null);
// Commit both configs
Expand Down Expand Up @@ -251,29 +253,31 @@ void twoOrgLevelsConfigSettingDifferentFlagsWorks() throws Exception {
// Add org level config with ApmTracing enabled
updater.accept(
orgConfig1Key,
("{\n"
+ " \"service_target\": {\n"
+ " \"service\": \"*\",\n"
+ " \"env\": \"*\"\n"
+ " },\n"
+ " \"lib_config\": {\n"
+ " \"tracing_enabled\": true\n"
+ " }\n"
+ "}")
"""
{
"service_target": {
"service": "*",
"env": "*"
},
"lib_config": {
"tracing_enabled": true
}
}"""
.getBytes(StandardCharsets.UTF_8),
null);
// Add second org level config with DataStreams enabled
updater.accept(
orgConfig2Key,
("{\n"
+ " \"service_target\": {\n"
+ " \"service\": \"*\",\n"
+ " \"env\": \"*\"\n"
+ " },\n"
+ " \"lib_config\": {\n"
+ " \"data_streams_enabled\": true\n"
+ " }\n"
+ "}")
"""
{
"service_target": {
"service": "*",
"env": "*"
},
"lib_config": {
"data_streams_enabled": true
}
}"""
.getBytes(StandardCharsets.UTF_8),
null);
// Commit both configs
Expand Down