Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,28 @@ internal object MuzzleMavenRepoUtils {
private val backoffDelaysSeconds = listOf(5L, 10L, 30L)

/**
* Remote repositories used to query version ranges and fetch dependencies.
* Remote repositories used by Aether to query version ranges.
*
* This intentionally reads the environment on each access: Gradle daemons can
* be reused across builds with different MAVEN_REPOSITORY_PROXY values.
* be reused across builds with different proxy values. Aether queries metadata
* from all repositories, preferring the first for versions present in both.
*/
@JvmStatic
fun defaultMuzzleRepos(): List<RemoteRepository> {
val central = RemoteRepository.Builder("central", "default", "https://repo1.maven.org/maven2/").build()
val mavenProxyUrl = System.getenv("MAVEN_REPOSITORY_PROXY")
return if (mavenProxyUrl == null) {
listOf(central)
} else {
val proxy = RemoteRepository.Builder("central-proxy", "default", mavenProxyUrl).build()
// TODO: temporary hack for Maven Central rate limiting
listOf(proxy /*, central*/)
fun defaultMuzzleRepos(): List<RemoteRepository> =
defaultMuzzleRepos(System.getenv("MAVEN_REPOSITORY_PROXY"), System.getenv("MUZZLE_MAVEN_REPOSITORY_PROXY"))

internal fun defaultMuzzleRepos(mavenProxyUrl: String?, muzzleProxyUrl: String? = null): List<RemoteRepository> =
listOfNotNull(
muzzleProxyUrl?.takeUnless { it.isBlank() }?.let {
RemoteRepository.Builder("muzzle-proxy", "default", it).build()
},
mavenProxyUrl?.takeUnless { it.isBlank() }?.let {
RemoteRepository.Builder("central-proxy", "default", it).build()
}
).distinctBy { it.url }.ifEmpty {
// Avoid a direct Maven Central fallback while its rate limiting affects CI.
listOf(RemoteRepository.Builder("central", "default", "https://repo1.maven.org/maven2/").build())
}
}

/**
* Create new RepositorySystem for muzzle's Maven/Aether resolutions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import org.eclipse.aether.util.version.GenericVersionScheme
import org.gradle.api.GradleException
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable
import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
Expand Down Expand Up @@ -115,31 +113,73 @@ class MuzzleMavenRepoUtilsTest {
.hasMessageContaining("Backoff:\n disabled")
}

// The two tests below are mutually exclusive: MAVEN_REPOSITORY_PROXY is read from the real
// environment (defaultMuzzleRepos deliberately does not take it as a parameter), so each of
// them covers the branch its environment can reach -- unset locally, set in CI.

@Test
@DisabledIfEnvironmentVariable(
named = "MAVEN_REPOSITORY_PROXY",
matches = ".*",
disabledReason = "A mirror is configured; the proxy variant of this test covers that case"
)
fun `defaultMuzzleRepos is Maven Central alone when no proxy is configured`() {
assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos().map { it.id to it.url })
assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(null).map { it.id to it.url })
.containsExactly("central" to MAVEN_CENTRAL_URL)
}

@Test
fun `defaultMuzzleRepos prefers the Muzzle proxy over the shared proxy`() {
val sharedProxy = "https://maven.example.com/repository/"
val muzzleProxy = "https://muzzle.example.com/repository/"

assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(sharedProxy, muzzleProxy).map { it.id to it.url })
.containsExactly(
"muzzle-proxy" to muzzleProxy,
"central-proxy" to sharedProxy
)
}

@Test
fun `defaultMuzzleRepos preserves the shared proxy when no Muzzle proxy is configured`() {
val customProxy = "https://maven.example.com/repository/"

assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(customProxy).map { it.id to it.url })
.containsExactly("central-proxy" to customProxy)
}

@Test
fun `defaultMuzzleRepos allows a Muzzle proxy without a shared proxy`() {
val muzzleProxy = "https://muzzle.example.com/repository/"

assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(null, muzzleProxy).map { it.id to it.url })
.containsExactly("muzzle-proxy" to muzzleProxy)
}

@Test
fun `defaultMuzzleRepos ignores blank proxy values`() {
assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos("", " ").map { it.id to it.url })
.containsExactly("central" to MAVEN_CENTRAL_URL)
}

// TODO: Re-enable after removing the temporary Maven Central rate limiting workaround.
@Test
@Disabled("Temporarily using the configured proxy without a Maven Central fallback")
@EnabledIfEnvironmentVariable(named = "MAVEN_REPOSITORY_PROXY", matches = ".*")
fun `defaultMuzzleRepos queries the configured proxy before Maven Central`() {
val proxyUrl = System.getenv("MAVEN_REPOSITORY_PROXY")

// Central stays in the list as a fallback, but the proxy is consulted first.
assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos().map { it.id to it.url })
.containsExactly("central-proxy" to proxyUrl, "central" to MAVEN_CENTRAL_URL)
fun `defaultMuzzleRepos does not query the same proxy twice`() {
val proxy = "https://maven.example.com/repository/"

assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(proxy, proxy).map { it.id to it.url })
.containsExactly("muzzle-proxy" to proxy)
}

@Test
fun `resolveVersionRange uses the shared proxy when the Muzzle proxy has no metadata`() {
val sharedRepo = publishAndGetRepo("com.example", "mylib", listOf("1.0.0"))
val emptyRepo = MavenRepoFixture(File(tempDir, "empty"))
val directive = MuzzleDirective().apply {
group = "com.example"
module = "mylib"
versions = "[1.0,)"
}

val result = MuzzleMavenRepoUtils.resolveVersionRange(
directive,
system,
newSession(),
MuzzleMavenRepoUtils.defaultMuzzleRepos(sharedRepo.url, emptyRepo.repoUrl),
enableBackoffRetries = false
)

assertThat(result.versions.map { it.toString() }).containsExactly("1.0.0")
}

@Test
Expand Down
13 changes: 13 additions & 0 deletions docs/how_to_work_with_gradle.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,19 @@ However, via the command line, `buildSrc/` tests are disabled unless opted in wi
Repository proxies configured through `MAVEN_REPOSITORY_PROXY` or `GRADLE_PLUGIN_PROXY` are
propagated to TestKit builds through `repository-proxy.init.gradle.kts`.

### Muzzle Repository Proxies

Muzzle's Aether version discovery uses `MAVEN_REPOSITORY_PROXY`, or Maven Central when unset.
Set `MUZZLE_MAVEN_REPOSITORY_PROXY` to add a preferred endpoint for Muzzle while retaining
`MAVEN_REPOSITORY_PROXY` as a second route. Blank values are ignored; identical URLs are queried once.
These settings do not change Gradle's dependency repositories.

Aether queries metadata from all configured repositories and combines the versions it finds;
this is not sequential failover. A successful route can supply versions when another fails,
but resolution can still wait for the failing route to time out.
For a Fabric endpoint, enable it only where DNS, routing, and the Gradle JVM's certificate trust
are configured. Fabric is not enabled by default.

### How Gradle Compiles Build Scripts

During the **Configuration phase**, Gradle doesn't simply execute build scripts top-to-bottom. Instead, it first extracts and processes certain special blocks before compiling the rest of the script. This is necessary because Gradle needs to know which plugins to apply before it can understand the DSL extensions they provide.
Expand Down
Loading