diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5290d93abd0..f5eec1cf07b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -829,7 +829,7 @@ test_smoke_graalvm: extends: .test_job tags: [ "arch:amd64" ] variables: - GRADLE_TARGET: "stageMainDist :dd-smoke-test:spring-boot-3.0-native:test" + GRADLE_TARGET: "stageMainDist :dd-smoke-test:spring-boot-3.0-native:test :dd-smoke-test:quarkus-native:test" CACHE_TYPE: "smoke" CI_NO_SPLIT: "true" NON_DEFAULT_JVMS: "true" diff --git a/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/NestedGradleBuild.kt b/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/NestedGradleBuild.kt index e2c1c34f09f..3ee09d12c41 100644 --- a/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/NestedGradleBuild.kt +++ b/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/NestedGradleBuild.kt @@ -7,12 +7,14 @@ import org.gradle.api.file.FileTree import org.gradle.api.file.RegularFile import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.IgnoreEmptyDirectories import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Nested import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.PathSensitive @@ -37,6 +39,7 @@ import javax.inject.Inject * `-P=` and tracked as a task input so the nested build re-runs * when the upstream jar changes. */ +@CacheableTask abstract class NestedGradleBuild @Inject constructor( private val objects: ObjectFactory, javaToolchains: JavaToolchainService, @@ -49,6 +52,7 @@ abstract class NestedGradleBuild @Inject constructor( languageVersion.set(JavaLanguageVersion.of(DEFAULT_NESTED_JAVA_VERSION)) }, ) + buildCacheEnabled.convention(false) } @get:Internal @@ -74,6 +78,26 @@ abstract class NestedGradleBuild @Inject constructor( @get:Input abstract val buildArguments: ListProperty + /** + * Whether to enable the build cache in the nested Gradle invocation. + * Gradle's org.gradle.caching flag is resolved from many sources (project, + * init, gradle user home, environment, command line) and any of them silently + * enables the build cache for nested builds. For this reasons it defaults to `false`. + * Opt in only when the inner plugin chain keys its cached outputs on everything that + * varies between runs (e.g. Quarkus's native-image does not track `GRAALVM_HOME`). + * `--build-cache` / `--no-build-cache` is passed explicitly either way. + */ + @get:Input + abstract val buildCacheEnabled: Property + + /** + * Extra environment variables for the nested Gradle daemon. Merged on top of the outer + * process environment — set a key to override an inherited value. The nested build script + * sees these via `System.getenv()` like any normal environment variable. + */ + @get:Input + abstract val environment: MapProperty + @get:Nested abstract val projectJars: ListProperty @@ -104,6 +128,7 @@ abstract class NestedGradleBuild @Inject constructor( val daemonJavaHome = javaLauncher.get().metadata.installationPath.asFile val args = buildList { + add(if (buildCacheEnabled.get()) "--build-cache" else "--no-build-cache") add("-PappBuildDir=${appBuildDirFile.absolutePath}") projectJars.get().forEach { entry -> add("-P${entry.propertyName.get()}=${entry.file.get().asFile.absolutePath}") @@ -115,11 +140,16 @@ abstract class NestedGradleBuild @Inject constructor( .useGradleVersion(gradleVersion.get()) .forProjectDirectory(appDir) + val extraEnv = environment.get() + val mergedEnv: Map? = + if (extraEnv.isEmpty()) null else System.getenv() + extraEnv + connector.connect().use { connection -> connection.newBuild() .forTasks(*tasksToRun.get().toTypedArray()) .withArguments(args) .setJavaHome(daemonJavaHome) + .apply { if (mergedEnv != null) setEnvironmentVariables(mergedEnv) } .setStandardOutput(System.out) .setStandardError(System.err) .run() diff --git a/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/SmokeTestAppExtension.kt b/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/SmokeTestAppExtension.kt index ccb5ee838db..1e1e2c476b5 100644 --- a/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/SmokeTestAppExtension.kt +++ b/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/SmokeTestAppExtension.kt @@ -97,6 +97,8 @@ abstract class SmokeTestAppExtension @Inject constructor( javaLauncher.set(this@SmokeTestAppExtension.javaLauncher) tasksToRun.set(nestedTasks) buildArguments.set(spec.buildArguments) + environment.set(spec.environment) + buildCacheEnabled.set(spec.buildCacheEnabled) projectJars.set(this@SmokeTestAppExtension.projectJars) } @@ -117,16 +119,36 @@ abstract class SmokeTestAppExtension @Inject constructor( * lazily — no `evaluationDependsOn` is needed. */ fun projectJar(propertyName: String, sourceProject: Project) { + val cfg = createExtraJarConfiguration(propertyName) + project.dependencies.add(cfg.name, sourceProject) + addProjectJarFromConfiguration(propertyName, cfg) + } + + /** + * Forward a non-default artifact configuration from [sourceProject]. Use this when the + * upstream project exposes its build output under a configuration other than the default + * (e.g. `shadowJar`). + */ + fun projectJar(propertyName: String, sourceProject: Project, configuration: String) { + val cfg = createExtraJarConfiguration(propertyName) + project.dependencies.add( + cfg.name, + project.dependencies.project( + mapOf("path" to sourceProject.path, "configuration" to configuration), + ), + ) + addProjectJarFromConfiguration(propertyName, cfg) + } + + private fun createExtraJarConfiguration(propertyName: String): Configuration { val configurationName = "smokeTestAppExtraJar" + propertyName.replaceFirstChar { it.titlecase(Locale.ROOT) } - val cfg = project.configurations.maybeCreate(configurationName).apply { + return project.configurations.maybeCreate(configurationName).apply { isCanBeConsumed = false isCanBeResolved = true isTransitive = false description = "Jar artifact forwarded as -P$propertyName into the smoke-test nested build" } - project.dependencies.add(configurationName, sourceProject) - addProjectJarFromConfiguration(propertyName, cfg) } /** @@ -161,6 +183,11 @@ abstract class SmokeTestAppExtension @Inject constructor( /** DSL describing the nested-build invocation for one smoke-test application. */ abstract class ApplicationSpec @Inject constructor() { + + init { + buildCacheEnabled.convention(false) + } + /** Outer task name; the nested daemon runs the same task by default. */ abstract val taskName: Property @@ -176,12 +203,30 @@ abstract class ApplicationSpec @Inject constructor() { /** Extra arguments passed to the nested Gradle invocation. */ abstract val buildArguments: ListProperty + /** + * Extra environment variables exposed to the nested Gradle daemon. Merged on top of the + * outer process environment — entries here override any inherited values with the same key. + * Use this for nested tooling that reads `JAVA_HOME`, `GRAALVM_HOME`, etc. from the env. + */ + abstract val environment: MapProperty + /** * Additional system properties to forward to every `Test` task, keyed by property name with * values resolved against `applicationBuildDir`. Use this for smoke tests that need more * than the single primary artifact path (e.g. a separately unpacked server install). */ abstract val additionalSystemProperties: MapProperty + + /** + * Whether to enable the build cache in the nested Gradle invocation. + * Gradle's org.gradle.caching flag is resolved from many sources (project, + * init, gradle user home, environment, command line) and any of them silently + * enables the build cache for nested builds. For this reasons it defaults to `false`. + * Opt in only when the inner plugin chain keys its cached outputs on everything that + * varies between runs (e.g. Quarkus's native-image does not track `GRAALVM_HOME`). + * `--build-cache` / `--no-build-cache` is passed explicitly either way. + */ + abstract val buildCacheEnabled: Property } /** diff --git a/build-logic/smoke-test/src/test/kotlin/datadog/buildlogic/smoketest/SmokeTestAppEndToEndTest.kt b/build-logic/smoke-test/src/test/kotlin/datadog/buildlogic/smoketest/SmokeTestAppEndToEndTest.kt index b3ad87bbaf3..09cf32dd5f8 100644 --- a/build-logic/smoke-test/src/test/kotlin/datadog/buildlogic/smoketest/SmokeTestAppEndToEndTest.kt +++ b/build-logic/smoke-test/src/test/kotlin/datadog/buildlogic/smoketest/SmokeTestAppEndToEndTest.kt @@ -6,6 +6,9 @@ import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import java.io.File import java.nio.file.Path @@ -160,10 +163,153 @@ class SmokeTestAppEndToEndTest { assertThat(result.task(":customBuild")?.outcome).isEqualTo(TaskOutcome.SUCCESS) } - private fun writeOuterSettings() { + /** + * `buildCacheEnabled` defaults to `false` and is plumbed through to the nested daemon as + * an explicit `--no-build-cache` / `--build-cache` argument. The inner build records + * `gradle.startParameter.isBuildCacheEnabled` so we can assert the value the daemon + * actually received, regardless of any inner `gradle.properties`. + */ + @ParameterizedTest(name = "{0}") + @MethodSource("buildCacheFlagCases") + fun `buildCacheEnabled controls the inner --build-cache flag`( + scenario: String, + dslLine: String, + expectedFlag: String, + ) { + writeOuterSettings() + outerBuild.writeText( + """ + plugins { + java + id("dd-trace-java.smoke-test-app") + } + + smokeTestApp { + javaLauncher.set( + javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(${currentMajorJdk()})) } + ) + application { + taskName.set("recordCacheFlag") + artifactPath.set("cache-flag.txt") + sysProperty.set("cache.flag.path") + $dslLine + } + } + """.trimIndent(), + ) + writeInnerSettings() + writeInnerBuild( + """ + tasks.register("recordCacheFlag") { + val out = layout.buildDirectory.file("cache-flag.txt") + outputs.file(out) + val flag = gradle.startParameter.isBuildCacheEnabled + doLast { + out.get().asFile.writeText(flag.toString()) + } + } + """.trimIndent(), + ) + + val result = runner("recordCacheFlag").build() + + assertThat(result.task(":recordCacheFlag")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + val flagFile = File(projectDir.toFile(), "build/application/cache-flag.txt") + assertThat(flagFile).exists() + assertThat(flagFile.readText().trim()).isEqualTo(expectedFlag) + } + + /** + * Exercises the outer `NestedGradleBuild` `@CacheableTask` end-to-end. The `identical` + * case verifies the task is actually cacheable (FROM_CACHE on a re-run with a wiped + * output dir). The two `env-change` cases verify that the resolved value of an + * `environment` Provider participates in the cache key — covering both first-class + * `providers.gradleProperty(...)` and the closure-based `providers.provider(Callable { … })` + * pattern used by `quarkus-native` for `GRAALVM_HOME`. + */ + @ParameterizedTest(name = "{0}") + @MethodSource("envCacheKeyCases") + fun `outer cache key reflects environment changes`( + scenario: String, + extraOuterImports: String, + extraOuterPreamble: String, + envDslLine: String, + firstRunPropertyValue: String, + secondRunPropertyValue: String, + expectedSecondOutcome: TaskOutcome, + ) { + writeOuterSettings(withLocalBuildCache = true) + outerBuild.writeText( + """ + $extraOuterImports + plugins { + java + id("dd-trace-java.smoke-test-app") + } + + $extraOuterPreamble + + smokeTestApp { + javaLauncher.set( + javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(${currentMajorJdk()})) } + ) + application { + taskName.set("buildJar") + artifactPath.set("libs/sample.jar") + sysProperty.set("sample.path") + $envDslLine + } + } + """.trimIndent(), + ) + writeInnerSettings() + writeInnerBuild( + """ + tasks.register("buildJar") { + archiveFileName.set("sample.jar") + from(file("src")) + } + """.trimIndent(), + ) + File(applicationDir, "src").mkdir() + File(applicationDir, "src/hello.txt").writeText("hi") + + val firstArgs = listOfNotNull( + "buildJar", + "--build-cache", + firstRunPropertyValue.takeIf { it.isNotEmpty() }?.let { "-PenvValue=$it" }, + ).toTypedArray() + val first = runner(*firstArgs).build() + assertThat(first.task(":buildJar")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + + // Wipe the output dir so the cache must serve it on the next run. + File(projectDir.toFile(), "build/application").deleteRecursively() + + val secondArgs = listOfNotNull( + "buildJar", + "--build-cache", + secondRunPropertyValue.takeIf { it.isNotEmpty() }?.let { "-PenvValue=$it" }, + ).toTypedArray() + val second = runner(*secondArgs).build() + assertThat(second.task(":buildJar")?.outcome).isEqualTo(expectedSecondOutcome) + } + + private fun writeOuterSettings(withLocalBuildCache: Boolean = false) { + val cacheBlock = if (withLocalBuildCache) { + """ + buildCache { + local { + directory = file("${'$'}{rootDir}/build-cache") + } + } + """.trimIndent() + } else { + "" + } outerSettings.writeText( """ rootProject.name = "smoke-test-app-fixture" + $cacheBlock """.trimIndent(), ) } @@ -201,4 +347,53 @@ class SmokeTestAppEndToEndTest { System.getProperty("java.specification.version").let { if (it.startsWith("1.")) it.substring(2).toInt() else it.toInt() } + + companion object { + @JvmStatic + fun buildCacheFlagCases(): List = listOf( + // (scenario name, DSL line added to the `application { … }` block, expected + // `gradle.startParameter.isBuildCacheEnabled` value seen by the nested daemon) + Arguments.of("default off", "", "false"), + Arguments.of("explicit true", "buildCacheEnabled.set(true)", "true"), + ) + + @JvmStatic + fun envCacheKeyCases(): List = listOf( + // (scenario, extra imports, extra preamble before smokeTestApp, env DSL line, + // first-run -PenvValue, second-run -PenvValue, expected outcome of second run) + Arguments.of( + "identical inputs hit the cache", + "", + "", + "", + "", + "", + TaskOutcome.FROM_CACHE, + ), + Arguments.of( + "gradleProperty env change misses the cache", + "", + "", + """environment.put("MARKER_VAR", providers.gradleProperty("envValue"))""", + "alpha", + "beta", + TaskOutcome.SUCCESS, + ), + Arguments.of( + // Mirrors the `quarkus-native` wiring: an eagerly-resolved script-level value + // flows into `providers.provider(Callable { … })` before being put into the + // `environment` MapProperty. + "Provider-Callable env change misses the cache", + "import java.util.concurrent.Callable\nimport org.gradle.api.provider.Provider", + """ + val envValue: String = (project.findProperty("envValue") as String?) ?: "default" + val markerProvider: Provider = providers.provider(Callable { "resolved-${'$'}envValue" }) + """.trimIndent(), + """environment.put("MARKER_VAR", markerProvider)""", + "alpha", + "beta", + TaskOutcome.SUCCESS, + ), + ) + } } diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsExtension.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsExtension.kt index 03b8c533d61..a5575eb77f2 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsExtension.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsExtension.kt @@ -37,6 +37,15 @@ interface TestJvmConstraintsExtension { */ val allowReflectiveAccessToJdk: Property + /** + * Require the JDK running the test (or the daemon JVM, when no `testJvm` is selected) to + * be `native-image` capable — i.e. a GraalVM-flavoured distribution that ships + * `lib/svm/bin/native-image`. Tasks running on JDKs that don't satisfy this requirement + * are skipped via `onlyIf`, matching the gating model used by [minJavaVersion] and + * [includeJdk]. Defaults to `false` (no native-image requirement). + */ + val nativeImageCapable: Property + companion object { const val TEST_JVM_CONSTRAINTS = "testJvmConstraints" } diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsUtils.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsUtils.kt index a8c5ddf69f6..efdc3662f6d 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsUtils.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsUtils.kt @@ -2,6 +2,10 @@ package datadog.gradle.plugin.testJvmConstraints import org.gradle.api.JavaVersion import org.gradle.api.logging.Logging +import org.gradle.jvm.toolchain.JavaLauncher +import java.io.File +import java.nio.file.Files +import java.nio.file.Path private val logger = Logging.getLogger("TestJvmConstraintsUtils") @@ -28,6 +32,32 @@ internal fun TestJvmConstraintsExtension.isTestJvmAllowed(testJvmSpec: TestJvmSp return true } +/** + * When [TestJvmConstraintsExtension.nativeImageCapable] is `true`, verify the chosen test + * launcher ships the `native-image` tool. The actual binary lives under `lib/svm/bin/` on + * a GraalVM distribution; `bin/native-image` may be a symlink to it (or a `.cmd` shim on + * Windows), so probe all three. Returns `true` when the requirement is unset or satisfied, + * so the check is a safe no-op for tasks that haven't opted in. + */ +internal fun TestJvmConstraintsExtension.isNativeImageCapableTestJvm(launcher: JavaLauncher): Boolean { + if (!nativeImageCapable.getOrElse(false)) return true + return hasNativeImage(launcher.metadata.installationPath.asFile.toPath()) +} + +/** + * Daemon-side counterpart to [isNativeImageCapableTestJvm], used when no `testJvm` was + * selected — checks the running daemon's `java.home`. Same no-op semantics. + */ +internal fun TestJvmConstraintsExtension.isNativeImageCapableDaemon(): Boolean { + if (!nativeImageCapable.getOrElse(false)) return true + return hasNativeImage(File(System.getProperty("java.home")).toPath()) +} + +private fun hasNativeImage(installPath: Path): Boolean = + Files.exists(installPath.resolve("lib/svm/bin/native-image")) || + Files.exists(installPath.resolve("bin/native-image")) || + Files.exists(installPath.resolve("bin/native-image.cmd")) + private fun TestJvmConstraintsExtension.withinAllowedRange(currentJvmVersion: JavaVersion): Boolean { val definedMin = minJavaVersion.isPresent val definedMax = maxJavaVersion.isPresent diff --git a/buildSrc/src/main/kotlin/dd-trace-java.test-jvm-constraints.gradle.kts b/buildSrc/src/main/kotlin/dd-trace-java.test-jvm-constraints.gradle.kts index aa4724183b0..2ec68ed89c4 100644 --- a/buildSrc/src/main/kotlin/dd-trace-java.test-jvm-constraints.gradle.kts +++ b/buildSrc/src/main/kotlin/dd-trace-java.test-jvm-constraints.gradle.kts @@ -3,6 +3,8 @@ import datadog.gradle.plugin.testJvmConstraints.TestJvmConstraintsExtension import datadog.gradle.plugin.testJvmConstraints.TestJvmConstraintsExtension.Companion.TEST_JVM_CONSTRAINTS import datadog.gradle.plugin.testJvmConstraints.TestJvmSpec import datadog.gradle.plugin.testJvmConstraints.isJavaVersionAllowed +import datadog.gradle.plugin.testJvmConstraints.isNativeImageCapableDaemon +import datadog.gradle.plugin.testJvmConstraints.isNativeImageCapableTestJvm import datadog.gradle.plugin.testJvmConstraints.isTestJvmAllowed plugins { @@ -30,6 +32,7 @@ tasks.withType().configureEach { inputs.property("$TEST_JVM_CONSTRAINTS.forceJdk", taskExtension.forceJdk) inputs.property("$TEST_JVM_CONSTRAINTS.minJavaVersion", taskExtension.minJavaVersion).optional(true) inputs.property("$TEST_JVM_CONSTRAINTS.maxJavaVersion", taskExtension.maxJavaVersion).optional(true) + inputs.property("$TEST_JVM_CONSTRAINTS.nativeImageCapable", taskExtension.nativeImageCapable).optional(true) extensions.add(TEST_JVM_CONSTRAINTS, taskExtension) @@ -61,13 +64,19 @@ fun Test.conditionalJvmArgs( private fun Test.configureTestJvm(extension: TestJvmConstraintsExtension) { if (testJvmSpec.javaTestLauncher.isPresent) { javaLauncher = testJvmSpec.javaTestLauncher - onlyIf("Allowed or forced JDK") { + onlyIf("Test JDK is allowed or forced JDK") { extension.isTestJvmAllowed(testJvmSpec) } + onlyIf("Test JDK is native-image capable") { + extension.isNativeImageCapableTestJvm(testJvmSpec.javaTestLauncher.get()) + } } else { - onlyIf("Is current Daemon JVM allowed") { + onlyIf("Current Daemon JVM within allowed version range") { extension.isJavaVersionAllowed(JavaVersion.current()) } + onlyIf("Current Daemon JVM is native-image capable") { + extension.isNativeImageCapableDaemon() + } } // temporary workaround when using Java16+: some tests require reflective access to java.lang/java.util @@ -125,4 +134,5 @@ private fun Test.configureConventions( taskExtension.allowReflectiveAccessToJdk.convention(projectExtension.allowReflectiveAccessToJdk .orElse(providers.provider { project.findProperty("allowReflectiveAccessToJdk") as? Boolean }) ) + taskExtension.nativeImageCapable.convention(projectExtension.nativeImageCapable) } diff --git a/dd-smoke-tests/quarkus-native/application/build.gradle b/dd-smoke-tests/quarkus-native/application/build.gradle index 50e12545671..8b9c83da46d 100644 --- a/dd-smoke-tests/quarkus-native/application/build.gradle +++ b/dd-smoke-tests/quarkus-native/application/build.gradle @@ -25,3 +25,22 @@ dependencies { } } +if (hasProperty('agentJar')) { + // The Quarkus Gradle plugin reads `quarkus.native.additional-build-args` from + // MicroProfile Config, which honours system properties. Setting it at script + // evaluation time is equivalent to passing `-Dquarkus.native.additional-build-args=...` + // on the gradle invocation, but keeps the agent jar path resolution inside this + // script (where `agentJar` is forwarded by the outer smoke-test plugin). + // + // Note, however, the way these system properties are set in this nested project + // are not properly tracked by this nested gradle build cache, it needs to be tracked + // from the outside (see smokeTestApp). + final agentJar = property('agentJar') + System.setProperty( + 'quarkus.native.additional-build-args', + "-J-javaagent:${agentJar}," + + "-J-Ddatadog.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd'T'HH:mm:ss.SSS'Z [dd.trace]'," + + "-J-Ddd.profiling.enabled=true," + + "-march=native" + ) +} diff --git a/dd-smoke-tests/quarkus-native/build.gradle b/dd-smoke-tests/quarkus-native/build.gradle index 89d43864d70..447be38bcd7 100644 --- a/dd-smoke-tests/quarkus-native/build.gradle +++ b/dd-smoke-tests/quarkus-native/build.gradle @@ -1,81 +1,78 @@ +import java.util.concurrent.Callable import java.util.regex.Pattern +plugins { + id 'dd-trace-java.smoke-test-app' +} + apply from: "$rootDir/gradle/java.gradle" +description = 'Quarkus Native Smoke Tests.' + dependencies { testImplementation project(':dd-smoke-tests') testImplementation libs.bundles.jmc } -// Check 'testJvm' gradle command parameter is GraalVM (e.g., -PtestJvm=graalvm21), -// if not nothing is done + +// Quarkus 3.12.1's native-image step requires at least Mandrel/GraalVM 23.1 (JDK 21-based) +// No `maxJavaVersion` as it currently runs on GraalVM25 +testJvmConstraints { + minJavaVersion = JavaVersion.VERSION_21 + nativeImageCapable = true +} + +// The following makes `quarkusNativeBuild` only run when Gradle is passed `-PtestJvm=graalvm21` or later. def testJvm = gradle.startParameter.projectProperties.getOrDefault('testJvm', '') def graalvmMatcher = Pattern.compile("graalvm([0-9]+)").matcher(testJvm?.toLowerCase(Locale.ROOT) ?: '') def testGraalvmVersion = graalvmMatcher.find() ? Integer.parseInt(graalvmMatcher.group(1)) : -1 +def isSupportedGraalvm = testGraalvmVersion >= 21 +Provider graalvmHome = providers.provider({ + if (!isSupportedGraalvm) { + throw new GradleException("Set -PtestJvm=graalvm (N >= 21) to build the native image for this smoke test.") + } + getLazyJavaHomeFor(testGraalvmVersion, true).toString() +} as Callable) -if (testGraalvmVersion >= 17) { - // GraalVM with native-image capability for native compilation - def graalvmHome = getLazyJavaHomeFor(testGraalvmVersion, true) - // For GraalVM 21+ we need Java 17+ to run Gradle, for earlier versions use GraalVM itself - def gradleJavaHome = testGraalvmVersion >= 21 ? getLazyJavaHomeFor(17) : graalvmHome - // Configure build directory for application - def appDir = "$projectDir/application" - def appBuildDir = "$buildDir/application" - def isWindows = System.getProperty('os.name').toLowerCase().contains('win') - def gradlewCommand = isWindows ? 'gradlew.bat' : 'gradlew' - - // Define the task that builds the project - tasks.register('quarkusNativeBuild', Exec) { - workingDir "$appDir" - environment += [ - 'GRADLE_OPTS' : "-Dorg.gradle.jvmargs='-Xmx512M'", - 'JAVA_HOME' : gradleJavaHome, - 'GRAALVM_HOME': graalvmHome - ] - commandLine( - "$rootDir/${gradlewCommand}", - 'build', - '--no-daemon', +smokeTestApp { + application { + taskName = 'quarkusNativeBuild' + nestedTasks = ['build'] + artifactPath = 'quarkus-native-smoketest--runner' + sysProperty = 'datadog.smoketest.quarkus.native.executable' + buildArguments.addAll( '--max-workers=4', - "-Dquarkus.native.enabled=true", - "-Dquarkus.package.jar.enabled=false", - "-PappBuildDir=$appBuildDir", - "-PapiJar=${project(':dd-trace-api').tasks.jar.archiveFile.get()}", - "-Dquarkus.native.additional-build-args=-J-javaagent:${project(':dd-java-agent').tasks.shadowJar.archiveFile.get()}," + - "-J-Ddatadog.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd'T'HH:mm:ss.SSS'Z [dd.trace]'," + - "-J-Ddd.profiling.enabled=true,-march=native" + '-Dquarkus.native.enabled=true', + '-Dquarkus.package.jar.enabled=false', ) - outputs.cacheIf { true } - outputs.dir(appBuildDir) - .withPropertyName('nativeApplication') - inputs.files(fileTree(appDir) { - include '**/*' - exclude '.gradle/**' - }).withPropertyName('application') - .withPathSensitivity(PathSensitivity.RELATIVE) - inputs.file(project(':dd-trace-api').tasks.jar.archiveFile.get()).withPropertyName('apiJar') - inputs.file(project(':dd-java-agent').tasks.shadowJar.archiveFile.get()).withPropertyName('agentJar') + environment.put('GRAALVM_HOME', graalvmHome) } + projectJar('apiJar', project(':dd-trace-api')) + projectJar('agentJar', project(':dd-java-agent'), 'shadow') +} - tasks.named('quarkusNativeBuild') { - dependsOn project(':dd-trace-api').tasks.named("jar") // Use dev @Trace annotation - dependsOn project(':dd-java-agent').tasks.named('shadowJar') // Use dev agent - } +tasks.named('quarkusNativeBuild') { + onlyIf("Requires -PtestJvm=graalvm (N >= 21)") { isSupportedGraalvm } +} - tasks.named('compileTestGroovy') { - dependsOn 'quarkusNativeBuild' - outputs.upToDateWhen { - !quarkusNativeBuild.didWork - } +tasks.named('compileTestGroovy', GroovyCompile) { + dependsOn 'quarkusNativeBuild' + onlyIf { isSupportedGraalvm } + outputs.upToDateWhen { + !tasks.named('quarkusNativeBuild').get().didWork } +} - tasks.withType(Test).configureEach { - jvmArgs "-Ddatadog.smoketest.quarkus.native.executable=$appBuildDir/quarkus-native-smoketest--runner" - jvmArgs "-Ddd.profiling.enabled=true" - } -} else { - tasks.withType(Test).configureEach { - enabled = false - } +tasks.withType(Test).configureEach { + // Skip when no supported GraalVM was selected — `testJvmConstraints` only checks for a + // native-image-capable launcher, so a daemon JVM that happens to be GraalVM 21 would + // Keep the tests tied to the explicit GraalVM selection used to build the native + // executable. Without this, `testJvmConstraints` may allow the `Test` task when the + // daemon itself is native-image capable, even though `quarkusNativeBuild` was skipped. + onlyIf("Requires -PtestJvm=graalvm (N >= 21)") { isSupportedGraalvm } + // The test JVM is only a harness that starts the native executable, so run it with + // the default project toolchain instead of the selected GraalVM. + javaLauncher = javaToolchains.launcherFor(java.toolchain) + jvmArgs "-Ddd.profiling.enabled=true" } spotless { diff --git a/dd-smoke-tests/spring-boot-3.0-native/application/build.gradle b/dd-smoke-tests/spring-boot-3.0-native/application/build.gradle index 59a758aa5bc..01eb6cecb8e 100644 --- a/dd-smoke-tests/spring-boot-3.0-native/application/build.gradle +++ b/dd-smoke-tests/spring-boot-3.0-native/application/build.gradle @@ -17,6 +17,13 @@ if (hasProperty('appBuildDir')) { buildDir = property('appBuildDir') } +// Spring Boot 3.0's bundled ASM 9.4 only parses class files up to major 64 (Java 20), so +// target release 17 here regardless of the daemon JDK — keeps the inner app's class files +// at major 61 and avoids needing a separate Java 17 toolchain for the nested build. +tasks.withType(JavaCompile).configureEach { + options.release = 17 +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' if (hasProperty('apiJar')) { diff --git a/dd-smoke-tests/spring-boot-3.0-native/build.gradle b/dd-smoke-tests/spring-boot-3.0-native/build.gradle index e0dda4c6db6..c5d11f7b431 100644 --- a/dd-smoke-tests/spring-boot-3.0-native/build.gradle +++ b/dd-smoke-tests/spring-boot-3.0-native/build.gradle @@ -1,5 +1,10 @@ +import java.util.concurrent.Callable import java.util.regex.Pattern +plugins { + id 'dd-trace-java.smoke-test-app' +} + apply from: "$rootDir/gradle/java.gradle" description = 'Spring Boot 3.0 Native Smoke Tests.' @@ -9,77 +14,67 @@ dependencies { testImplementation libs.bundles.jmc } -// Check 'testJvm' gradle command parameter is GraalVM (e.g., -PtestJvm=graalvm21), -// if not nothing is done +// The nested build compiles the application with `--release 17`, so Spring Boot 3.0's +// build-time classpath scanners don't see class files from the selected GraalVM release. +testJvmConstraints { + minJavaVersion = JavaVersion.VERSION_17 + nativeImageCapable = true +} + +// The following makes `springNativeBuild` only run when Gradle is passed `-PtestJvm=graalvm17` or later. def testJvm = gradle.startParameter.projectProperties.getOrDefault('testJvm', '') def graalvmMatcher = Pattern.compile("graalvm([0-9]+)").matcher(testJvm?.toLowerCase(Locale.ROOT) ?: '') def testGraalvmVersion = graalvmMatcher.find() ? Integer.parseInt(graalvmMatcher.group(1)) : -1 - -if (testGraalvmVersion >= 17) { - // GraalVM with native-image capability for native compilation - def graalvmHome = getLazyJavaHomeFor(testGraalvmVersion, true) - // For GraalVM 21+ we need Java 17+ to run Gradle, for earlier versions use GraalVM itself - def gradleJavaHome = testGraalvmVersion >= 21 ? getLazyJavaHomeFor(17) : graalvmHome - // Configure build directory for application - def appDir = "$projectDir/application" - def appBuildDir = "$buildDir/application" - def isWindows = System.getProperty('os.name').toLowerCase().contains('win') - def gradlewCommand = isWindows ? 'gradlew.bat' : 'gradlew' - - // Define the task that builds the project - tasks.register('springNativeBuild', Exec) { - workingDir "$appDir" - environment += [ - 'GRADLE_OPTS': "-Dorg.gradle.jvmargs='-Xmx1024M'", - 'JAVA_HOME': gradleJavaHome, - 'GRAALVM_HOME': graalvmHome, - 'DD_TRACE_METHODS' : 'datadog.smoketest.springboot.controller.WebController[sayHello]', - 'NATIVE_IMAGE_DEPRECATED_BUILDER_SANITATION' : 'true' - ] - commandLine( - "$rootDir/${gradlewCommand}", - 'nativeCompile', - '--no-daemon', - '--max-workers=4', - "-PappBuildDir=$appBuildDir", - "-PapiJar=${project(':dd-trace-api').tasks.jar.archiveFile.get()}", - "-PagentPath=${project(':dd-java-agent').tasks.shadowJar.archiveFile.get()}", - "-Pprofiler=true" - ) - outputs.cacheIf { true } - outputs.dir(appBuildDir) - .withPropertyName('nativeApplication') - inputs.files(fileTree(appDir) { - include '**/*' - exclude '.gradle/**' - }).withPropertyName('application') - .withPathSensitivity(PathSensitivity.RELATIVE) - inputs.file(project(':dd-trace-api').tasks.jar.archiveFile.get()).withPropertyName('apiJar') - inputs.file(project(':dd-java-agent').tasks.shadowJar.archiveFile.get()).withPropertyName('agentJar') +def isSupportedGraalvm = testGraalvmVersion in 17..21 +Provider graalvmHome = providers.provider({ + if (!isSupportedGraalvm) { +def isSupportedGraalvm = testGraalvmVersion >= 17 +Provider graalvmHome = providers.provider({ + if (!isSupportedGraalvm) { + throw new GradleException("Set -PtestJvm=graalvm (N >= 17) to build the native image for this smoke test.") } + getLazyJavaHomeFor(testGraalvmVersion, true).toString() +} as Callable) - springNativeBuild { - dependsOn project(':dd-trace-api').tasks.named("jar") // Use dev @Trace annotation - dependsOn project(':dd-java-agent').tasks.named('shadowJar') // Use dev agent +smokeTestApp { + application { + taskName = 'springNativeBuild' + nestedTasks = ['nativeCompile'] + artifactPath = 'native/nativeCompile/native-3.0-smoketest' + sysProperty = 'datadog.smoketest.spring.native.executable' + buildArguments.addAll('--max-workers=4', '-Pprofiler=true') + environment.put('GRAALVM_HOME', graalvmHome) + environment.put('DD_TRACE_METHODS', 'datadog.smoketest.springboot.controller.WebController[sayHello]') + environment.put('NATIVE_IMAGE_DEPRECATED_BUILDER_SANITATION', 'true') } + projectJar('apiJar', project(':dd-trace-api')) + projectJar('agentPath', project(':dd-java-agent'), 'shadow') +} - tasks.named('compileTestGroovy') { - dependsOn 'springNativeBuild' - outputs.upToDateWhen { - !springNativeBuild.didWork - } - } +tasks.named('springNativeBuild') { + // spring-boot native 3.0 don't work on later JVM feature version + onlyIf("Requires -PtestJvm=graalvm (N >= 17)") { isSupportedGraalvm } +} - tasks.withType(Test).configureEach { - jvmArgs "-Ddatadog.smoketest.spring.native.executable=$appBuildDir/native/nativeCompile/native-3.0-smoketest" - jvmArgs "-Ddd.profiling.enabled=true" - } -} else { - tasks.withType(Test).configureEach { - enabled = false +tasks.named('compileTestGroovy', GroovyCompile) { + dependsOn 'springNativeBuild' + onlyIf { isSupportedGraalvm } + outputs.upToDateWhen { + !tasks.named('springNativeBuild').get().didWork } } +tasks.withType(Test).configureEach { + // Keep the tests tied to the explicit GraalVM selection used to build the native + // executable. Without this, `testJvmConstraints` may allow the Test task when the + // daemon itself is native-image capable, even though `springNativeBuild` was skipped. + onlyIf("Requires -PtestJvm=graalvm (N >= 17)") { isSupportedGraalvm } + // The test JVM is only a harness that starts the native executable, so run it with + // the default project toolchain instead of the selected GraalVM. + javaLauncher = javaToolchains.launcherFor(java.toolchain) + jvmArgs "-Ddd.profiling.enabled=true" +} + spotless { java { target "**/*.java"