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 33da53475d1..2c4f72244b5 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 @@ -56,6 +56,12 @@ abstract class NestedGradleBuild @Inject constructor( ) initScripts.convention(emptyList()) gradleProperties.convention(emptyMap()) + mavenRepositoryProxy.convention( + project.providers.gradleProperty(MAVEN_REPOSITORY_PROXY_PROPERTY), + ) + gradlePluginProxy.convention( + project.providers.gradleProperty(GRADLE_PLUGIN_PROXY_PROPERTY), + ) javaLauncher.convention( javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(DEFAULT_NESTED_JAVA_VERSION)) @@ -92,6 +98,23 @@ abstract class NestedGradleBuild @Inject constructor( @get:Input abstract val gradleProperties: MapProperty + /** + * Repository proxy to use for dependencies, defaulting to the `mavenRepositoryProxy` property of + * the owning build. + * + * A nested build runs against a throwaway Gradle user home, so it never sees the + * `gradle.properties` that configures the outer build. Forwarding this explicitly is what lets + * nested builds resolve for developers and CI machines that cannot reach Maven Central directly. + */ + @get:Input + @get:Optional + abstract val mavenRepositoryProxy: Property + + /** Plugin repository proxy, defaulting to the `gradlePluginProxy` property of the owning build. */ + @get:Input + @get:Optional + abstract val gradlePluginProxy: Property + @get:Nested abstract val javaLauncher: Property @@ -156,7 +179,9 @@ abstract class NestedGradleBuild @Inject constructor( val appBuildDirFile = applicationBuildDir.get().asFile val daemonJavaHome = javaLauncher.get().metadata.installationPath.asFile val gradleUserHomeDir = createGradleUserHome() - val initScriptFiles = writeInitScripts() + val declaredProperties = gradleProperties.get() + val proxyProperties = proxyProperties(declaredProperties) + val initScriptFiles = writeInitScripts(proxyProperties.isNotEmpty()) val args = buildList { initScriptFiles.forEach { script -> @@ -165,9 +190,14 @@ abstract class NestedGradleBuild @Inject constructor( } add(if (buildCacheEnabled.get()) "--build-cache" else "--no-build-cache") add("-PappBuildDir=${appBuildDirFile.absolutePath}") - gradleProperties.get().forEach { (name, value) -> + declaredProperties.forEach { (name, value) -> addGradleProperty(name, value) } + // Forward the proxies unless the caller already declared them, so that every nested build + // resolves the same way as the outer one. + proxyProperties + .filterKeys { !declaredProperties.containsKey(it) } + .forEach { (name, value) -> addGradleProperty(name, value) } projectJars.get().forEach { entry -> add("-P${entry.propertyName.get()}=${entry.file.get().asFile.absolutePath}") } @@ -269,12 +299,36 @@ abstract class NestedGradleBuild @Inject constructor( } } - private fun writeInitScripts(): List = - initScripts.get().mapIndexed { index, script -> + private fun proxyProperties(declaredProperties: Map): Map = + buildMap { + addProxyProperty( + MAVEN_REPOSITORY_PROXY_PROPERTY, + declaredProperties, + mavenRepositoryProxy.orNull, + ) + addProxyProperty( + GRADLE_PLUGIN_PROXY_PROPERTY, + declaredProperties, + gradlePluginProxy.orNull, + ) + } + + private fun writeInitScripts(hasProxy: Boolean): List { + val declared = initScripts.get() + // Nested build scripts are not required to read the proxy properties themselves, so inject the + // repositories as well. Skipped when the caller already declared this script. + val effective = + if (!hasProxy || declared.contains(PROXY_REPOSITORIES_INIT_SCRIPT)) { + declared + } else { + listOf(PROXY_REPOSITORIES_INIT_SCRIPT) + declared + } + return effective.mapIndexed { index, script -> temporaryDir.resolve("init-$index.init.gradle.kts").also { file -> file.writeText(script) } } + } private fun findGradleExecutable(gradleUserHomeDir: File): File? = gradleUserHomeDir.walkTopDown().firstOrNull { file -> @@ -310,12 +364,32 @@ abstract class NestedGradleBuild @Inject constructor( } } +private fun MutableMap.addProxyProperty( + name: String, + declaredProperties: Map, + defaultValue: String?, +) { + val value = + if (declaredProperties.containsKey(name)) { + declaredProperties[name] + } else { + defaultValue + } + value?.takeIf { it.isNotBlank() }?.let { put(name, it) } +} + private fun MutableList.addGradleProperty(name: String, value: String?) { if (!value.isNullOrBlank()) { add("-P$name=$value") } } +/** Gradle property naming the repository proxy to resolve dependencies through. */ +internal const val MAVEN_REPOSITORY_PROXY_PROPERTY = "mavenRepositoryProxy" + +/** Gradle property naming the repository proxy to resolve plugins through. */ +internal const val GRADLE_PLUGIN_PROXY_PROPERTY = "gradlePluginProxy" + internal val PROXY_REPOSITORIES_INIT_SCRIPT: String = NestedGradleBuild::class.java.getResource("proxy-repositories.init.gradle.kts") ?.readText() 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 a34ee321cbc..71175e894e0 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 @@ -67,10 +67,6 @@ abstract class SmokeTestAppExtension @Inject constructor( internal abstract val projectJars: ListProperty - internal abstract val initScripts: ListProperty - - internal abstract val gradleProperties: MapProperty - init { applicationDir.convention(project.layout.projectDirectory.dir("application")) applicationBuildDir.convention(project.layout.buildDirectory.dir("application")) @@ -84,27 +80,6 @@ abstract class SmokeTestAppExtension @Inject constructor( }, ) - val isCi = project.providers.environmentVariable("CI") - .map { it.equals("true", ignoreCase = true) } - .orElse(false) - initScripts.convention( - isCi.map { - if (it) { - listOf(PROXY_REPOSITORIES_INIT_SCRIPT) - } else { - emptyList() - } - }, - ) - gradleProperties.convention( - isCi.map { - if (it) { - proxyGradleProperties() - } else { - emptyMap() - } - }, - ) } /** @@ -268,20 +243,6 @@ abstract class SmokeTestAppExtension @Inject constructor( ) } - private fun proxyGradleProperties(): Map { - val properties = mutableMapOf() - addGradleProperty(properties, "gradlePluginProxy") - addGradleProperty(properties, "mavenRepositoryProxy") - return properties - } - - private fun addGradleProperty(properties: MutableMap, name: String) { - val value = project.providers.gradleProperty(name).orNull - if (!value.isNullOrBlank()) { - properties[name] = value - } - } - private fun rootMavenExecutable(): Provider = project.providers.provider { project.rootProject.layout.projectDirectory.file(NestedMavenBuild.mavenWrapperName()) diff --git a/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/SmokeTestAppPlugin.kt b/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/SmokeTestAppPlugin.kt index 30f2476debd..07bb11f0144 100644 --- a/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/SmokeTestAppPlugin.kt +++ b/build-logic/smoke-test/src/main/kotlin/datadog/buildlogic/smoketest/SmokeTestAppPlugin.kt @@ -3,7 +3,6 @@ package datadog.buildlogic.smoketest import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.create -import org.gradle.kotlin.dsl.withType /** * Exposes nested build task types plus a `smokeTestApp` extension that wires a smoke-test @@ -16,10 +15,6 @@ import org.gradle.kotlin.dsl.withType */ class SmokeTestAppPlugin : Plugin { override fun apply(project: Project) { - val extension = project.extensions.create("smokeTestApp") - project.tasks.withType().configureEach { - initScripts.convention(extension.initScripts) - gradleProperties.convention(extension.gradleProperties) - } + project.extensions.create("smokeTestApp") } } 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 086accd5ce3..6aa17ed4b7c 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 @@ -265,7 +265,7 @@ class SmokeTestAppEndToEndTest { } @Test - fun `init script prepends Maven proxy repositories without overriding project repositories`() { + fun `local proxy property prepends Maven proxy repositories exactly once`() { writeOuterSettings() val proxyRepository = projectDir.resolve("proxy-maven-repo").toFile() val projectRepository = projectDir.resolve("project-maven-repo").toFile() @@ -298,10 +298,12 @@ class SmokeTestAppEndToEndTest { inputs.files(configurations.compileClasspath) outputs.file(resolved) doLast { + val artifacts = configurations.compileClasspath.get() + .sortedBy { it.name } + .map { it.name + "=" + it.readText() } resolved.get().asFile.writeText( - configurations.compileClasspath.get() - .sortedBy { it.name } - .joinToString(System.lineSeparator()) { it.name + "=" + it.readText() } + (listOf("init-script-count=" + gradle.startParameter.initScripts.size) + artifacts) + .joinToString(System.lineSeparator()) ) } } @@ -311,18 +313,117 @@ class SmokeTestAppEndToEndTest { val result = runner( "resolveRepositories", "-PmavenRepositoryProxy=${proxyRepository.toURI()}", - environment = mapOf("CI" to "true"), + environment = mapOf("CI" to "false"), ).build() assertThat(result.task(":resolveRepositories")?.outcome).isEqualTo(TaskOutcome.SUCCESS) val resolvedFile = applicationOutput("resolved-repositories.txt") assertThat(resolvedFile).exists() assertThat(resolvedFile.readLines()).containsExactly( + "init-script-count=1", "project-only-1.0.jar=project-only", "shared-1.0.jar=proxy", ) } + @Test + fun `proxy declared in gradleProperties also injects repository init script`() { + writeOuterSettings() + writeSmokeTestAppBuild( + smokeTestGradleApplication( + taskName = "recordProxy", + artifactPath = "proxy.txt", + sysProperty = "proxy.path", + ), + extraImports = "import datadog.buildlogic.smoketest.NestedGradleBuild", + extraPreamble = """ + tasks.withType().configureEach { + mavenRepositoryProxy.set("") + gradleProperties.put("mavenRepositoryProxy", "https://declared.example") + } + """, + ) + writeInnerSettings() + writeInnerBuild(recordProxyTask()) + + val result = runner( + "recordProxy", + environment = mapOf("CI" to "false"), + ).build() + + assertThat(result.task(":recordProxy")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(applicationOutput("proxy.txt").readLines()).containsExactly( + "proxy=https://declared.example", + "init-script-count=1", + ) + } + + @Test + fun `explicit task proxy overrides outer proxy on CI`() { + writeOuterSettings() + writeSmokeTestAppBuild( + smokeTestGradleApplication( + taskName = "recordProxy", + artifactPath = "proxy.txt", + sysProperty = "proxy.path", + ), + extraImports = "import datadog.buildlogic.smoketest.NestedGradleBuild", + extraPreamble = """ + tasks.withType().configureEach { + mavenRepositoryProxy.set("https://task.example") + } + """, + ) + writeInnerSettings() + writeInnerBuild(recordProxyTask()) + + val result = runner( + "recordProxy", + "-PmavenRepositoryProxy=https://outer.example", + environment = mapOf("CI" to "true"), + ).build() + + assertThat(result.task(":recordProxy")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(applicationOutput("proxy.txt").readLines()).containsExactly( + "proxy=https://task.example", + "init-script-count=1", + ) + } + + @Test + fun `outer cache key reflects repository proxy changes`() { + writeOuterSettings(withLocalBuildCache = true) + writeSmokeTestAppBuild( + smokeTestGradleApplication( + taskName = "recordProxy", + artifactPath = "proxy.txt", + sysProperty = "proxy.path", + ), + ) + writeInnerSettings() + writeInnerBuild(recordProxyTask()) + + val first = runner( + "recordProxy", + "--build-cache", + "-PmavenRepositoryProxy=https://first.example", + ).build() + assertThat(first.task(":recordProxy")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + + applicationBuildDir.deleteRecursively() + + val second = runner( + "recordProxy", + "--build-cache", + "-PmavenRepositoryProxy=https://second.example", + ).build() + assertThat(second.task(":recordProxy")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(applicationOutput("proxy.txt").readLines()).containsExactly( + "proxy=https://second.example", + "init-script-count=1", + ) + } + /** * `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 @@ -570,6 +671,22 @@ class SmokeTestAppEndToEndTest { ) } + private fun recordProxyTask(): String = + """ + tasks.register("recordProxy") { + val out = layout.buildDirectory.file("proxy.txt") + outputs.file(out) + doLast { + out.get().asFile.writeText( + listOf( + "proxy=" + project.findProperty("mavenRepositoryProxy"), + "init-script-count=" + gradle.startParameter.initScripts.size, + ).joinToString(System.lineSeparator()) + ) + } + } + """.trimIndent() + private fun applicationOutput(relativePath: String): File = applicationBuildDir.resolve(relativePath) diff --git a/build-logic/smoke-test/src/test/kotlin/datadog/buildlogic/smoketest/SmokeTestAppPluginTest.kt b/build-logic/smoke-test/src/test/kotlin/datadog/buildlogic/smoketest/SmokeTestAppPluginTest.kt index 010af208667..cb235bd8b02 100644 --- a/build-logic/smoke-test/src/test/kotlin/datadog/buildlogic/smoketest/SmokeTestAppPluginTest.kt +++ b/build-logic/smoke-test/src/test/kotlin/datadog/buildlogic/smoketest/SmokeTestAppPluginTest.kt @@ -184,26 +184,23 @@ class SmokeTestAppPluginTest { } @Test - fun `manual NestedGradleBuild task receives smokeTestApp conventions`() { + fun `manual NestedGradleBuild task exposes independent repository proxy inputs`() { val project = ProjectBuilder.builder().build() project.apply() project.plugins.apply("dd-trace-java.smoke-test-app") - val extension = project.extensions.getByType() - extension.initScripts.set(listOf("init-script")) - extension.gradleProperties.set( - mapOf("mavenRepositoryProxy" to "https://repo.example"), - ) - val task = project.tasks.register("customBuild", NestedGradleBuild::class.java) { applicationDir.set(project.layout.projectDirectory.dir("application")) applicationBuildDir.set(project.layout.buildDirectory.dir("application")) tasksToRun.set(listOf("buildJar")) + mavenRepositoryProxy.set("https://repo.example") + gradlePluginProxy.set("https://plugins.example") }.get() - assertThat(task.initScripts.get()).containsExactly("init-script") - assertThat(task.gradleProperties.get()) - .containsEntry("mavenRepositoryProxy", "https://repo.example") + assertThat(task.mavenRepositoryProxy.get()).isEqualTo("https://repo.example") + assertThat(task.gradlePluginProxy.get()).isEqualTo("https://plugins.example") + assertThat(task.initScripts.get()).isEmpty() + assertThat(task.gradleProperties.get()).isEmpty() } @Test