Skip to content

Commit 321e2f2

Browse files
Cache Gradle distributions with MASS fallback
1 parent 1f390b7 commit 321e2f2

20 files changed

Lines changed: 938 additions & 152 deletions

File tree

.gitlab-ci.yml

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,6 @@ default:
251251
# Apache Maven Wrapper supports MVNW_REPOURL for repository-manager downloads:
252252
# https://maven.apache.org/tools/wrapper/#Using_a_Maven_Repository_Manager
253253
- export MVNW_REPOURL=${MAVEN_REPOSITORY_PROXY%/}
254-
# Route Gradle distribution download through MASS pull-through cache
255-
- |
256-
mass_read_host="${MASS_READ_URL#https://}"
257-
mass_read_host="${mass_read_host%/}"
258-
sed -i "/^distributionUrl=/ s|services.gradle.org|${mass_read_host}/internal/artifact/services.gradle.org|" gradle/wrapper/gradle-wrapper.properties
259254
- mkdir -p .mvn/caches
260255
# Redirect Spotless's Equo/Solstice P2 cache into the project tree so it is captured by the GitLab cache.
261256
# Solstice (https://github.com/equodev/equo-ide) defaults to ~/.m2/repository/dev/equo/p2-data, which is outside $CI_PROJECT_DIR.
@@ -271,6 +266,8 @@ default:
271266
# GitLab's cache helper restores .gradle as root, but we run as non-root-user (uid 1001),
272267
# and Gradle does `chmod 700 .gradle` on startup which requires user ownership.
273268
- sudo chown -R 1001:1001 .gradle
269+
# Bootstrap Gradle through MASS with upstream fallback.
270+
- bootstrap_gradle_distribution
274271
after_script:
275272
- *cgroup_info
276273
- *container_info
@@ -818,6 +815,7 @@ muzzle-dep-report:
818815
- dependency-lib
819816
unprotect: true
820817
before_script:
818+
- source .gitlab/gitlab-utils.sh
821819
- git config --global --add safe.directory "$CI_PROJECT_DIR"
822820
# Akka token added to SSM from https://account.akka.io/token
823821
- export ORG_GRADLE_PROJECT_akkaRepositoryToken=$(aws ssm get-parameter --region us-east-1 --name ci.dd-trace-java.akka_repo_token --with-decryption --query "Parameter.Value" --out text)
@@ -838,17 +836,13 @@ muzzle-dep-report:
838836
# Apache Maven Wrapper supports MVNW_REPOURL for repository-manager downloads:
839837
# https://maven.apache.org/tools/wrapper/#Using_a_Maven_Repository_Manager
840838
- export MVNW_REPOURL=${MAVEN_REPOSITORY_PROXY%/}
841-
# Route Gradle distribution download through MASS pull-through cache
842-
- |
843-
mass_read_host="${MASS_READ_URL#https://}"
844-
mass_read_host="${mass_read_host%/}"
845-
sed -i "/^distributionUrl=/ s|services.gradle.org|${mass_read_host}/internal/artifact/services.gradle.org|" gradle/wrapper/gradle-wrapper.properties
846839
- *normalize_node_index
847840
- *prepare_test_env
848841
# Disable CDS in forked JVMs to avoid SIGSEGVs on Linux arm64.
849842
- export GRADLE_OPTS="-Dorg.gradle.jvmargs='-Xshare:off -Xms$GRADLE_MEMORY_MIN -Xmx$GRADLE_MEMORY_MAX -XX:ErrorFile=/tmp/hs_err_pid%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp -Djava.util.prefs.userRoot=/tmp/.java/.userPrefs-${CI_JOB_ID}' -Ddatadog.forkedMinHeapSize=128M -Ddatadog.forkedMaxHeapSize=1024M"
850843
- export GRADLE_ARGS="--build-cache --stacktrace --no-daemon --parallel --max-workers=$GRADLE_WORKERS"
851-
- ./gradlew --version
844+
# Bootstrap Gradle through MASS with upstream fallback.
845+
- bootstrap_gradle_distribution
852846
script:
853847
- *gitlab_base_ref_params
854848
- ./gradlew $GRADLE_TARGET $GRADLE_PARAMS -PtestJvm=$testJvm -Pslot=$CI_NODE_INDEX/$CI_NODE_TOTAL $GRADLE_ARGS --continue

.gitlab/gitlab-utils.sh

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,51 @@ function gitlab_section_end () {
1616
echo -e "section_end:`date +%s`:${section_title}\r\e[0K"
1717
}
1818

19+
# Bootstrap Gradle through MASS, retrying upstream because the wrapper supports only one URL.
20+
# The pinned checksum applies to either source. Call after `.gradle` and the Gradle environment are
21+
# prepared.
22+
function bootstrap_gradle_distribution () {
23+
local props="gradle/wrapper/gradle-wrapper.properties"
24+
local upstream_props="/tmp/gradle-wrapper.upstream.properties"
25+
local mass_read_host
26+
27+
if [ -z "${MASS_READ_URL:-}" ]; then
28+
./gradlew --version
29+
return
30+
fi
31+
32+
mass_read_host="${MASS_READ_URL#https://}"
33+
mass_read_host="${mass_read_host%/}"
34+
35+
# Derive upstream even if an earlier call already routed the URL through MASS.
36+
sed "/^distributionUrl=/ s|${mass_read_host}/internal/artifact/||" "$props" > "$upstream_props"
37+
38+
if grep -q "^distributionUrl=.*${mass_read_host}" "$props"; then
39+
# Avoid nesting the MASS prefix.
40+
echo "Gradle distribution is already routed through ${mass_read_host}"
41+
else
42+
# Redirect because GNU and BSD `sed -i` differ.
43+
sed "/^distributionUrl=/ s|services.gradle.org|${mass_read_host}/internal/artifact/services.gradle.org|" "$props" > "$props.mass" &&
44+
mv "$props.mass" "$props"
45+
fi
46+
47+
if ./gradlew --version; then
48+
rm -f "$upstream_props"
49+
return
50+
fi
51+
52+
if ! grep -q "^distributionUrl=" "$upstream_props"; then
53+
echo -e "${TEXT_RED}Gradle distribution bootstrap failed and no upstream distributionUrl could be derived from ${props} to fall back to${TEXT_CLEAR}" >&2
54+
return 1
55+
fi
56+
57+
# Keep upstream: changing the URL changes the wrapper cache key and would download twice.
58+
echo -e "${TEXT_YELLOW}MASS_FALLBACK gradle-distribution: ${MASS_READ_URL} could not serve the Gradle distribution, retrying via services.gradle.org${TEXT_CLEAR}" >&2
59+
cp "$upstream_props" "$props"
60+
rm -f "$upstream_props"
61+
./gradlew --version
62+
}
63+
1964
# A subset of ansi color/formatting codes https://misc.flogisoft.com/bash/tip_colors_and_formatting
2065
export TEXT_RED="\e[31m"
2166
export TEXT_GREEN="\e[32m"

build-logic/conventions/build.gradle.kts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
plugins {
22
`kotlin-dsl`
3+
`jvm-test-suite`
34
}
45

56
java {
@@ -12,3 +13,17 @@ kotlin {
1213
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8)
1314
}
1415
}
16+
17+
@Suppress("UnstableApiUsage")
18+
testing {
19+
suites {
20+
named<JvmTestSuite>("test") {
21+
useJUnitJupiter(libs.versions.junit5)
22+
dependencies {
23+
implementation(libs.junit.jupiter)
24+
implementation(libs.junit.jupiter.engine)
25+
implementation(libs.assertj.core)
26+
}
27+
}
28+
}
29+
}

build-logic/conventions/src/main/kotlin/datadog/buildlogic/mass/MassExtension.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,16 @@ constructor(objects: ObjectFactory, providers: ProviderFactory) {
1111
val readUrl: Property<String> =
1212
objects.property(String::class.java).convention(providers.environmentVariable("MASS_READ_URL"))
1313

14-
fun artifactUrl(upstreamArtifactUrl: String): String {
15-
val massReadUrl = readUrl.orNull ?: return "https://$upstreamArtifactUrl"
14+
fun artifactUrl(upstreamArtifactUrl: String): String = artifactUrls(upstreamArtifactUrl).first()
15+
16+
/**
17+
* Ordered repository URLs: MASS, then upstream. Gradle advances only for missing artifacts, so
18+
* this covers MASS cache misses, not connection failures. Without MASS, returns only upstream.
19+
*/
20+
fun artifactUrls(upstreamArtifactUrl: String): List<String> {
21+
val upstreamUrl = "https://$upstreamArtifactUrl"
22+
val massReadUrl = readUrl.orNull?.takeIf { it.isNotBlank() } ?: return listOf(upstreamUrl)
1623
val baseUrl = if (massReadUrl.endsWith("/")) massReadUrl else "$massReadUrl/"
17-
return "${baseUrl}internal/artifact/$upstreamArtifactUrl"
24+
return listOf("${baseUrl}internal/artifact/$upstreamArtifactUrl", upstreamUrl)
1825
}
1926
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package datadog.buildlogic.mass
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.gradle.kotlin.dsl.newInstance
5+
import org.gradle.testfixtures.ProjectBuilder
6+
import org.junit.jupiter.api.Test
7+
8+
class MassExtensionTest {
9+
10+
private fun extension(massReadUrl: String?): MassExtension {
11+
val project = ProjectBuilder.builder().build()
12+
// Empty overrides the environment-backed convention.
13+
return project.objects.newInstance<MassExtension>().also { it.readUrl.set(massReadUrl ?: "") }
14+
}
15+
16+
@Test
17+
fun `routes artifacts through MASS and keeps the upstream host as a second repository`() {
18+
val mass = extension("https://mass.example")
19+
20+
assertThat(mass.artifactUrls("dlcdn.apache.org")).containsExactly(
21+
"https://mass.example/internal/artifact/dlcdn.apache.org",
22+
"https://dlcdn.apache.org",
23+
)
24+
assertThat(mass.artifactUrl("dlcdn.apache.org"))
25+
.isEqualTo("https://mass.example/internal/artifact/dlcdn.apache.org")
26+
}
27+
28+
@Test
29+
fun `tolerates a trailing slash on the MASS read url`() {
30+
val mass = extension("https://mass.example/")
31+
32+
assertThat(mass.artifactUrls("dlcdn.apache.org").first())
33+
.isEqualTo("https://mass.example/internal/artifact/dlcdn.apache.org")
34+
}
35+
36+
@Test
37+
fun `declares the upstream host only once when MASS is not configured`() {
38+
listOf(null, "", " ").forEach { unset ->
39+
val mass = extension(unset)
40+
41+
assertThat(mass.artifactUrls("dlcdn.apache.org"))
42+
.containsExactly("https://dlcdn.apache.org")
43+
assertThat(mass.artifactUrl("dlcdn.apache.org")).isEqualTo("https://dlcdn.apache.org")
44+
}
45+
}
46+
47+
@Test
48+
fun `keeps any path on the upstream artifact url`() {
49+
val mass = extension("https://mass.example")
50+
51+
assertThat(mass.artifactUrls("github.com/wildfly/wildfly/releases/download/")).containsExactly(
52+
"https://mass.example/internal/artifact/github.com/wildfly/wildfly/releases/download/",
53+
"https://github.com/wildfly/wildfly/releases/download/",
54+
)
55+
}
56+
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,26 @@ import java.net.URI
44

55
internal const val MASS_READ_URL_ENV = "MASS_READ_URL"
66

7+
/** Public Gradle distribution source. */
8+
internal const val UPSTREAM_DISTRIBUTIONS_BASE_URL = "https://services.gradle.org/distributions"
9+
710
internal fun gradleDistributionUri(massReadUrl: String, gradleVersion: String): URI {
811
val baseUrl = if (massReadUrl.endsWith("/")) massReadUrl else "$massReadUrl/"
912
return URI.create(
1013
"${baseUrl}internal/artifact/services.gradle.org/distributions/gradle-$gradleVersion-bin.zip",
1114
)
1215
}
16+
17+
internal fun upstreamGradleDistributionUri(gradleVersion: String): URI =
18+
URI.create("$UPSTREAM_DISTRIBUTIONS_BASE_URL/gradle-$gradleVersion-bin.zip")
19+
20+
/** Nested Gradle sources, preferring MASS and falling back upstream. */
21+
internal fun gradleDistributionUris(massReadUrl: String?, gradleVersion: String): List<URI> =
22+
if (massReadUrl.isNullOrBlank()) {
23+
listOf(upstreamGradleDistributionUri(gradleVersion))
24+
} else {
25+
listOf(
26+
gradleDistributionUri(massReadUrl, gradleVersion),
27+
upstreamGradleDistributionUri(gradleVersion),
28+
)
29+
}

0 commit comments

Comments
 (0)