Skip to content

Commit 635c6b8

Browse files
authored
Support Maven smoke test apps (#11695)
feat(smoke): support Maven smoke test apps The smoke-test-app plugin now has separate Gradle and Maven application entry points. OpenLiberty smoke tests can use the shared nested-build wiring instead of bespoke Exec tasks, while existing Gradle smoke-test apps keep the same task model. Maven apps still launch through the checked-in root mvnw instead of an installed mvn. Keeping mvnw preserves the Maven version pinned by .mvn/wrapper/maven-wrapper.properties and gives CI and local builds the same cross-platform launcher. CI no longer rewrites maven-wrapper.properties to use MASS. Apache Maven Wrapper documents MVNW_REPOURL as the repository-manager override, so GitLab exports it from MAVEN_REPOSITORY_PROXY and trims the trailing slash before mvnw appends the Maven distribution path: https://maven.apache.org/tools/wrapper/#Using_a_Maven_Repository_Manager The Maven path is covered by smoke-test plugin tests. ProjectBuilder verifies mavenApp task wiring, and the TestKit end-to-end test runs a fake mvnw with MAVEN_REPOSITORY_PROXY. It also asserts the nested Maven process receives MVNW_REPOURL. fix(smoke): address Maven app review feedback fix(smoke): make nested build timeouts opt-in Only wire Gradle stop and Maven build timeouts when the smoke app DSL sets them explicitly. Add Maven cache coverage that deletes the app output directory and verifies the jar is restored from the outer build cache. Mirror the output assertion for the existing Gradle cache test. Co-authored-by: brice.dutheil <brice.dutheil@datadoghq.com>
1 parent a723424 commit 635c6b8

37 files changed

Lines changed: 672 additions & 160 deletions

File tree

.gitlab-ci.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,9 @@ default:
242242
EOF
243243
- mkdir -p .gradle
244244
- export GRADLE_USER_HOME=$(pwd)/.gradle
245-
# replace maven central part by MAVEN_REPOSITORY_PROXY in .mvn/wrapper/maven-wrapper.properties
246-
- sed -i "s|https://repo.maven.apache.org/maven2/|$MAVEN_REPOSITORY_PROXY|g" .mvn/wrapper/maven-wrapper.properties
245+
# Apache Maven Wrapper supports MVNW_REPOURL for repository-manager downloads:
246+
# https://maven.apache.org/tools/wrapper/#Using_a_Maven_Repository_Manager
247+
- export MVNW_REPOURL=${MAVEN_REPOSITORY_PROXY%/}
247248
# Route Gradle distribution download through MASS pull-through cache
248249
- |
249250
mass_read_host="${MASS_READ_URL#https://}"
@@ -784,7 +785,9 @@ muzzle-dep-report:
784785
# and Gradle does `chmod 700 .gradle` on startup which requires user ownership.
785786
- sudo chown -R 1001:1001 .gradle
786787
- export GRADLE_USER_HOME=$(pwd)/.gradle
787-
- sed -i "s|https://repo.maven.apache.org/maven2/|$MAVEN_REPOSITORY_PROXY|g" .mvn/wrapper/maven-wrapper.properties
788+
# Apache Maven Wrapper supports MVNW_REPOURL for repository-manager downloads:
789+
# https://maven.apache.org/tools/wrapper/#Using_a_Maven_Repository_Manager
790+
- export MVNW_REPOURL=${MAVEN_REPOSITORY_PROXY%/}
788791
- *normalize_node_index
789792
- *prepare_test_env
790793
# Disable CDS in forked JVMs to avoid SIGSEGVs on Linux arm64.

build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/NestedGradleBuild.kt

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ abstract class NestedGradleBuild @Inject constructor(
113113
@get:Input
114114
abstract val buildCacheEnabled: Property<Boolean>
115115

116+
/** Timeout, in seconds, for stopping the nested Gradle daemon after the build. */
117+
@get:Input
118+
@get:Optional
119+
abstract val stopTimeoutSeconds: Property<Long>
120+
116121
/**
117122
* Extra environment variables for the nested Gradle daemon. Merged on top of the outer process
118123
* environment; Gradle launcher variables are reserved by this task so nested builds do not
@@ -236,9 +241,17 @@ abstract class NestedGradleBuild @Inject constructor(
236241
}
237242

238243
val process = processBuilder.start()
239-
if (!process.waitFor(GRADLE_STOP_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
244+
val timeoutSeconds = stopTimeoutSeconds.orNull
245+
val completed =
246+
if (timeoutSeconds == null) {
247+
process.waitFor()
248+
true
249+
} else {
250+
process.waitFor(timeoutSeconds, TimeUnit.SECONDS)
251+
}
252+
if (!completed) {
240253
process.destroyForcibly()
241-
logger.warn("Timed out while stopping nested Gradle daemon")
254+
logger.warn("Timed out after {} seconds while stopping nested Gradle daemon", timeoutSeconds)
242255
return
243256
}
244257
val exitCode = process.exitValue()
@@ -270,13 +283,6 @@ abstract class NestedGradleBuild @Inject constructor(
270283
file.parentFile?.name == "bin"
271284
}
272285

273-
private fun gradleExecutableName(): String =
274-
if (System.getProperty("os.name").lowercase().contains("windows")) {
275-
"gradle.bat"
276-
} else {
277-
"gradle"
278-
}
279-
280286
private fun createGradleUserHome(): File {
281287
val directory = temporaryDir.resolve("gradle-user-home")
282288
deleteGradleUserHome(directory)
@@ -294,8 +300,13 @@ abstract class NestedGradleBuild @Inject constructor(
294300
}
295301
}
296302

297-
private companion object {
298-
const val GRADLE_STOP_TIMEOUT_SECONDS = 30L
303+
companion object {
304+
internal fun gradleExecutableName(osName: String = System.getProperty("os.name")): String =
305+
if (isWindows(osName)) {
306+
"gradle.bat"
307+
} else {
308+
"gradle"
309+
}
299310
}
300311
}
301312

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package datadog.buildlogic.smoketest
2+
3+
import org.gradle.api.DefaultTask
4+
import org.gradle.api.GradleException
5+
import org.gradle.api.file.DirectoryProperty
6+
import org.gradle.api.file.FileTree
7+
import org.gradle.api.file.RegularFileProperty
8+
import org.gradle.api.model.ObjectFactory
9+
import org.gradle.api.provider.ListProperty
10+
import org.gradle.api.provider.MapProperty
11+
import org.gradle.api.provider.Property
12+
import org.gradle.api.tasks.CacheableTask
13+
import org.gradle.api.tasks.IgnoreEmptyDirectories
14+
import org.gradle.api.tasks.Input
15+
import org.gradle.api.tasks.InputFile
16+
import org.gradle.api.tasks.InputFiles
17+
import org.gradle.api.tasks.Internal
18+
import org.gradle.api.tasks.Nested
19+
import org.gradle.api.tasks.Optional
20+
import org.gradle.api.tasks.OutputDirectory
21+
import org.gradle.api.tasks.PathSensitive
22+
import org.gradle.api.tasks.PathSensitivity
23+
import org.gradle.api.tasks.TaskAction
24+
import org.gradle.jvm.toolchain.JavaLanguageVersion
25+
import org.gradle.jvm.toolchain.JavaLauncher
26+
import org.gradle.jvm.toolchain.JavaToolchainService
27+
import java.io.File
28+
import java.util.concurrent.TimeUnit
29+
import javax.inject.Inject
30+
31+
/**
32+
* Runs a nested Maven build inside [applicationDir] via the root Maven wrapper.
33+
*
34+
* The nested build is expected to honour `-Dtarget.dir=<path>` and write outputs under that
35+
* directory so Gradle can track the artifact under [applicationBuildDir].
36+
*/
37+
@CacheableTask
38+
abstract class NestedMavenBuild @Inject constructor(
39+
private val objects: ObjectFactory,
40+
javaToolchains: JavaToolchainService,
41+
) : DefaultTask() {
42+
43+
init {
44+
javaLauncher.convention(
45+
javaToolchains.launcherFor {
46+
languageVersion.set(JavaLanguageVersion.of(DEFAULT_NESTED_JAVA_VERSION))
47+
},
48+
)
49+
goals.convention(listOf("package"))
50+
arguments.convention(emptyList())
51+
environment.convention(emptyMap())
52+
mavenOpts.convention("")
53+
useMavenLocalRepository.convention(false)
54+
}
55+
56+
@get:Internal
57+
abstract val applicationDir: DirectoryProperty
58+
59+
@get:InputFiles
60+
@get:IgnoreEmptyDirectories
61+
@get:PathSensitive(PathSensitivity.RELATIVE)
62+
val applicationSources: FileTree =
63+
objects.fileTree().from(applicationDir).matching {
64+
exclude("target/**")
65+
}
66+
67+
@get:OutputDirectory
68+
abstract val applicationBuildDir: DirectoryProperty
69+
70+
@get:InputFile
71+
@get:PathSensitive(PathSensitivity.RELATIVE)
72+
abstract val mavenExecutable: RegularFileProperty
73+
74+
@get:InputFiles
75+
@get:IgnoreEmptyDirectories
76+
@get:PathSensitive(PathSensitivity.RELATIVE)
77+
val mavenWrapperFiles: FileTree =
78+
objects.fileTree().from(project.rootProject.layout.projectDirectory.dir(".mvn/wrapper")).matching {
79+
include("maven-wrapper.properties", "maven-wrapper.jar")
80+
}
81+
82+
@get:Nested
83+
abstract val javaLauncher: Property<JavaLauncher>
84+
85+
@get:Input
86+
abstract val goals: ListProperty<String>
87+
88+
@get:Input
89+
abstract val arguments: ListProperty<String>
90+
91+
@get:Input
92+
abstract val environment: MapProperty<String, String>
93+
94+
@get:Input
95+
@get:Optional
96+
abstract val mavenOpts: Property<String>
97+
98+
@get:Input
99+
@get:Optional
100+
abstract val mavenRepositoryProxy: Property<String>
101+
102+
@get:Input
103+
abstract val useMavenLocalRepository: Property<Boolean>
104+
105+
/** Timeout, in seconds, for the nested Maven build process. */
106+
@get:Input
107+
@get:Optional
108+
abstract val buildTimeoutSeconds: Property<Long>
109+
110+
@get:Internal
111+
abstract val mavenLocalRepository: DirectoryProperty
112+
113+
@TaskAction
114+
fun runNestedBuild() {
115+
val appDir = applicationDir.get().asFile
116+
val appBuildDirFile = applicationBuildDir.get().asFile
117+
val targetDir = appBuildDirFile.resolve("target")
118+
val daemonJavaHome = javaLauncher.get().metadata.installationPath.asFile
119+
val command = mavenCommand(mavenExecutable.get().asFile).apply {
120+
add("-Dtarget.dir=${targetDir.absolutePath}")
121+
if (useMavenLocalRepository.get()) {
122+
add("-Dmaven.repo.local=${mavenLocalRepository.get().asFile.absolutePath}")
123+
}
124+
addAll(arguments.get())
125+
addAll(goals.get())
126+
}
127+
128+
val process = ProcessBuilder(command)
129+
.directory(appDir)
130+
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
131+
.redirectError(ProcessBuilder.Redirect.INHERIT)
132+
.apply {
133+
environment().apply {
134+
clear()
135+
putAll(nestedEnvironment(daemonJavaHome))
136+
}
137+
}
138+
.start()
139+
140+
try {
141+
val timeoutSeconds = buildTimeoutSeconds.orNull
142+
val completed =
143+
if (timeoutSeconds == null) {
144+
process.waitFor()
145+
true
146+
} else {
147+
process.waitFor(timeoutSeconds, TimeUnit.SECONDS)
148+
}
149+
if (!completed) {
150+
process.destroyForcibly()
151+
throw GradleException(
152+
"Nested Maven build timed out after $timeoutSeconds seconds: " +
153+
command.joinToString(" "),
154+
)
155+
}
156+
} catch (e: InterruptedException) {
157+
process.destroyForcibly()
158+
Thread.currentThread().interrupt()
159+
throw GradleException("Interrupted while running nested Maven build", e)
160+
}
161+
val exitCode = process.exitValue()
162+
if (exitCode != 0) {
163+
throw GradleException(
164+
"Nested Maven build failed with exit code $exitCode: ${command.joinToString(" ")}",
165+
)
166+
}
167+
}
168+
169+
private fun nestedEnvironment(daemonJavaHome: File): Map<String, String> {
170+
val env = System.getenv().toMutableMap()
171+
val repositoryProxy = mavenRepositoryProxy.orNull?.takeIf { it.isNotBlank() }
172+
if (repositoryProxy != null) {
173+
env["MAVEN_REPOSITORY_PROXY"] = repositoryProxy
174+
env.putIfAbsent("MVNW_REPOURL", repositoryProxy.trimEnd('/'))
175+
}
176+
val opts = mavenOpts.orNull
177+
if (!opts.isNullOrBlank()) {
178+
env["MAVEN_OPTS"] = opts
179+
}
180+
env["JAVA_HOME"] = daemonJavaHome.absolutePath
181+
env.putAll(environment.get())
182+
return env
183+
}
184+
185+
private fun mavenCommand(executable: File): MutableList<String> =
186+
if (isWindows()) {
187+
mutableListOf("cmd", "/c", executable.absolutePath)
188+
} else {
189+
mutableListOf(executable.absolutePath)
190+
}
191+
192+
companion object {
193+
internal fun mavenWrapperName(osName: String = System.getProperty("os.name")): String =
194+
if (isWindows(osName)) {
195+
"mvnw.cmd"
196+
} else {
197+
"mvnw"
198+
}
199+
200+
}
201+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package datadog.buildlogic.smoketest
2+
3+
import java.util.Locale
4+
5+
internal fun isWindows(osName: String = System.getProperty("os.name")): Boolean =
6+
osName.lowercase(Locale.ROOT).contains("windows")
7+

0 commit comments

Comments
 (0)