Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
45 changes: 45 additions & 0 deletions build-logic/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
pluginManagement {
repositories {
mavenLocal()
if (settings.extra.has("gradlePluginProxy")) {
maven {
url = uri(settings.extra["gradlePluginProxy"] as String)
isAllowInsecureProtocol = true
}
}
if (settings.extra.has("mavenRepositoryProxy")) {
maven {
url = uri(settings.extra["mavenRepositoryProxy"] as String)
isAllowInsecureProtocol = true
}
}
gradlePluginPortal()
mavenCentral()
}
}

dependencyResolutionManagement {
repositories {
mavenLocal()
if (settings.extra.has("mavenRepositoryProxy")) {
maven {
url = uri(settings.extra["mavenRepositoryProxy"] as String)
isAllowInsecureProtocol = true
}
}
gradlePluginPortal()
mavenCentral()
// Hosts the Gradle Tooling API artifact, which the smoke-test plugin uses to
// run nested Gradle builds without committing per-application wrappers.
maven {
url = uri("https://repo.gradle.org/gradle/libs-releases")
content {
includeGroup("org.gradle")
}
}
}
}

rootProject.name = "build-logic"

include(":smoke-test")
29 changes: 29 additions & 0 deletions build-logic/smoke-test/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
plugins {
`java-gradle-plugin`
`kotlin-dsl`
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8)
}
}

dependencies {
implementation("org.gradle:gradle-tooling-api:8.14.5")
runtimeOnly("org.slf4j:slf4j-simple:1.7.36")
}

gradlePlugin {
plugins {
create("smoke-test-app") {
id = "dd-trace-java.smoke-test-app"
implementationClass = "datadog.buildlogic.smoketest.SmokeTestAppPlugin"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package datadog.buildlogic.smoketest

import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity

/**
* A jar produced by the root build that needs to be forwarded into a [NestedGradleBuild].
*
* At execution time the task adds `-P${propertyName}=<absolute path of file>` to the nested
* Gradle invocation, so the inner build script can pick it up via `findProperty(...)`.
*/
abstract class NestedBuildProjectJar {

@get:Input
abstract val propertyName: Property<String>

@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val file: RegularFileProperty
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package datadog.buildlogic.smoketest

import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
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.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.Nested
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.jvm.toolchain.JavaLauncher
import org.gradle.tooling.GradleConnector
import javax.inject.Inject

/**
* Runs a nested Gradle build inside [applicationDir] via the Gradle Tooling API.
*
* Lets a smoke test pin a Gradle version (typically older than the root build) and a Java
* toolchain for the nested daemon, without committing per-application `gradlew` wrappers.
*
* The nested build script is expected to honour `-PappBuildDir=<path>` and redirect its
* `buildDir` to that path so the artifact lands in [applicationBuildDir]. Project artifacts
* from the root build can be forwarded via [projectJar]; each entry is passed as
* `-P<propertyName>=<absolute-path>` and tracked as a task input so the nested build re-runs
* when the upstream jar changes.
*/
abstract class NestedGradleBuild @Inject constructor(private val objects: ObjectFactory) :
DefaultTask() {

@get:Internal
abstract val applicationDir: DirectoryProperty

@get:InputFiles
@get:IgnoreEmptyDirectories
@get:PathSensitive(PathSensitivity.RELATIVE)
val applicationSources: FileTree =
objects.fileTree().from(applicationDir).matching {
exclude(".gradle/**", "build/**")
}

@get:Input
abstract val gradleVersion: Property<String>

@get:Nested
abstract val javaLauncher: Property<JavaLauncher>

@get:Input
abstract val tasksToRun: ListProperty<String>

@get:Input
abstract val buildArguments: ListProperty<String>

@get:Nested
abstract val projectJars: ListProperty<NestedBuildProjectJar>

@get:OutputDirectory
abstract val applicationBuildDir: DirectoryProperty

/** Forward a root-build jar as `-P<name>=<absolute path>` into the nested build. */
fun projectJar(name: String, file: Provider<RegularFile>) {
val entry = objects.newInstance(NestedBuildProjectJar::class.java)
entry.propertyName.set(name)
entry.file.set(file)
projectJars.add(entry)
}

/** Configure additional aspects of the nested build via a typed action. */
fun projectJar(action: Action<NestedBuildProjectJar>) {
val entry = objects.newInstance(NestedBuildProjectJar::class.java)
action.execute(entry)
projectJars.add(entry)
}

@TaskAction
fun runNestedBuild() {
val appDir = applicationDir.get().asFile
val appBuildDirFile = applicationBuildDir.get().asFile
val daemonJavaHome = javaLauncher.get().metadata.installationPath.asFile

val args = buildList {
add("-PappBuildDir=${appBuildDirFile.absolutePath}")
projectJars.get().forEach { entry ->
add("-P${entry.propertyName.get()}=${entry.file.get().asFile.absolutePath}")
}
addAll(buildArguments.get())
}

val connector = GradleConnector.newConnector()
.useGradleVersion(gradleVersion.get())
.forProjectDirectory(appDir)

connector.connect().use { connection ->
connection.newBuild()
.forTasks(*tasksToRun.get().toTypedArray())
.withArguments(args)
.setJavaHome(daemonJavaHome)
.setStandardOutput(System.out)
.setStandardError(System.err)
.run()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package datadog.buildlogic.smoketest

import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFile
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.TaskProvider
import org.gradle.api.tasks.testing.Test
import org.gradle.jvm.toolchain.JavaLauncher
import org.gradle.process.CommandLineArgumentProvider
import java.util.Locale
import javax.inject.Inject

/**
* Project extension that wires a [NestedGradleBuild] task for a smoke-test application.
*
* The plugin only contributes a task when the consumer calls [application]; if the extension
* stays unconfigured, the plugin is a no-op and consumers can register [NestedGradleBuild]
* directly.
*/
abstract class SmokeTestAppExtension @Inject constructor(private val project: Project) {

/** Gradle version used by the nested daemon. Defaults to the root build's version. */
abstract val gradleVersion: Property<String>

/** JDK used by the nested daemon. Required when calling [application]. */
abstract val javaLauncher: Property<JavaLauncher>

/** Directory containing the nested project's `settings.gradle` + sources. */
abstract val applicationDir: DirectoryProperty

/**
* Directory the nested build writes its outputs to. The nested build script is expected to
* honour `-PappBuildDir=<path>`; see the existing smoke-test inner builds for the pattern.
*/
abstract val applicationBuildDir: DirectoryProperty

internal abstract val projectJars: ListProperty<NestedBuildProjectJar>

init {
applicationDir.convention(project.layout.projectDirectory.dir("application"))
applicationBuildDir.convention(project.layout.buildDirectory.dir("application"))
gradleVersion.convention(project.gradle.gradleVersion)
}

/**
* Register the nested-build task and wire the produced artifact into every `Test` task as
* a system property. Calling this triggers task registration; consumers that prefer to
* register [NestedGradleBuild] manually can leave [application] uncalled.
*/
fun application(action: Action<ApplicationSpec>) {
require(javaLauncher.isPresent) {
"smokeTestApp.javaLauncher must be set before configuring application { ... }"
}
val spec = project.objects.newInstance(ApplicationSpec::class.java)
action.execute(spec)
val taskName = requireNotNull(spec.taskName.orNull) {
"smokeTestApp.application { taskName = ... } is required"
}
val artifactPath = requireNotNull(spec.artifactPath.orNull) {
"smokeTestApp.application { artifactPath = ... } is required"
}
val sysProperty = requireNotNull(spec.sysProperty.orNull) {
"smokeTestApp.application { sysProperty = ... } is required"
}
val nestedTasks = spec.nestedTasks.orNull?.takeIf { it.isNotEmpty() } ?: listOf(taskName)

val capturedJars = projectJars
val capturedAppDir = applicationDir
val capturedAppBuildDir = applicationBuildDir
val capturedGradleVersion = gradleVersion
val capturedJavaLauncher = javaLauncher
val capturedBuildArguments = spec.buildArguments

val taskProvider: TaskProvider<NestedGradleBuild> =
project.tasks.register(taskName, NestedGradleBuild::class.java) {
applicationDir.set(capturedAppDir)
applicationBuildDir.set(capturedAppBuildDir)
gradleVersion.set(capturedGradleVersion)
javaLauncher.set(capturedJavaLauncher)
tasksToRun.set(nestedTasks)
buildArguments.set(capturedBuildArguments)
projectJars.set(capturedJars)
}

val artifactProvider: Provider<RegularFile> = applicationBuildDir.file(artifactPath)
val extras = spec.additionalSystemProperties.get().mapValues { (_, relativePath) ->
applicationBuildDir.file(relativePath)
}
project.tasks.withType(Test::class.java).configureEach {
dependsOn(taskProvider)
jvmArgumentProviders.add(SmokeTestArgProvider(sysProperty, artifactProvider, extras))
}
}

/**
* Forward the default `jar` artifact from [sourceProject] into the nested build as
* `-P<propertyName>=<absolute path>`. The jar is consumed via a resolvable [Configuration],
* which both establishes the correct task dependency and lets Gradle resolve the artifact
* lazily — no `evaluationDependsOn` is needed.
*/
fun projectJar(propertyName: String, sourceProject: Project) {
val configurationName = "smokeTestAppExtraJar" +
propertyName.replaceFirstChar { it.titlecase(Locale.ROOT) }
val cfg = 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)
}

/**
* Lower-level overload for the rare case where the caller already has a provider of the
* file. The caller is responsible for the upstream task dependency.
*/
fun projectJar(propertyName: String, file: Provider<RegularFile>) {
val entry = project.objects.newInstance(NestedBuildProjectJar::class.java)
entry.propertyName.set(propertyName)
entry.file.set(file)
projectJars.add(entry)
}

private fun addProjectJarFromConfiguration(propertyName: String, cfg: Configuration) {
val entry = project.objects.newInstance(NestedBuildProjectJar::class.java)
entry.propertyName.set(propertyName)
// Configuration.elements yields a Provider that carries the producing task dependency, so
// wiring it into the task's @InputFile both tracks file contents and arranges build order.
entry.file.set(
cfg.elements.map { files ->
project.objects.fileProperty().fileValue(files.single().asFile).get()
}
)
projectJars.add(entry)
}
}

/** DSL describing the nested-build invocation for one smoke-test application. */
abstract class ApplicationSpec @Inject constructor() {
/** Outer task name; the nested daemon runs the same task by default. */
abstract val taskName: Property<String>

/** Path to the produced artifact, relative to `applicationBuildDir`. */
abstract val artifactPath: Property<String>

/** System property name set on Test tasks to point them at the produced artifact. */
abstract val sysProperty: Property<String>

/** Tasks run inside the nested build. Defaults to `[taskName]`. */
abstract val nestedTasks: ListProperty<String>

/** Extra arguments passed to the nested Gradle invocation. */
abstract val buildArguments: ListProperty<String>

/**
* 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<String, String>
}

private class SmokeTestArgProvider(
private val sysProperty: String,
private val artifact: Provider<RegularFile>,
private val extras: Map<String, Provider<RegularFile>>,
) : CommandLineArgumentProvider {
override fun asArguments(): Iterable<String> =
buildList {
add("-D$sysProperty=${artifact.get().asFile.absolutePath}")
extras.forEach { (key, value) ->
add("-D$key=${value.get().asFile.absolutePath}")
}
}
}
Loading
Loading