11/* * Common setup for manual instrumentation of libraries and javaagent instrumentation. */
22
33import io.opentelemetry.instrumentation.gradle.OtelPropsExtension
4+ import io.opentelemetry.javaagent.muzzle.AcceptableVersions
45
56plugins {
67 `java- library`
@@ -26,36 +27,63 @@ val otelProps = the<OtelPropsExtension>()
2627 * described above.
2728 *
2829 * - latestDepTestLibrary: A dependency on a library for testing when testing of latest dependency
29- * version is enabled. This dependency will be added as-is to testImplementation, but only if
30- * -PtestLatestDeps=true. The version will not be modified but it will be given highest
31- * precedence. Use this to restrict the latest version dependency from the default `+`, for
32- * example to restrict to just a major version by specifying `2.+`.
30+ * version is enabled. This dependency will be added to testImplementation only if
31+ * -PtestLatestDeps=true. Its version overrides the `latest.release` from library/testLibrary
32+ * via resolutionStrategy.eachDependency (which takes highest precedence in Gradle). Use this
33+ * to restrict the latest version to a specific range, e.g. `2.+` to stay on major version 2 .
3334 */
35+
36+ val resolveLatestDeps = gradle.startParameter.projectProperties[" resolveLatestDeps" ] == " true"
37+ val pinLatestDeps = otelProps.testLatestDeps && ! resolveLatestDeps
38+
39+ fun getPinnedVersions (): Map <String , String > {
40+ if (! pinLatestDeps) return emptyMap()
41+ val key = " latestDepPinnedVersions"
42+ if (! rootProject.extra.has(key)) {
43+ val file = rootProject.file(" .github/config/latest-dep-versions.json" )
44+ if (! file.exists()) {
45+ throw GradleException (" Pinned latest-dep versions file is missing: ${file} ." )
46+ }
47+ @Suppress(" UNCHECKED_CAST" )
48+ rootProject.extra[key] = groovy.json.JsonSlurper ().parse(file) as Map <String , String >
49+ }
50+ @Suppress(" UNCHECKED_CAST" )
51+ return rootProject.extra[key] as Map <String , String >
52+ }
53+
54+ fun lookupPinnedVersion (group : String? , name : String , version : String? ): String? {
55+ if (! pinLatestDeps || group == null ) return null
56+ val pinned = getPinnedVersions()
57+ return if (version == " latest.release" ) {
58+ pinned[" $group :$name #+" ]
59+ } else if (version != null && version.contains(" +" )) {
60+ val rangeKey = " $group :$name #$version "
61+ val rangeVersion = pinned[rangeKey]
62+ if (rangeVersion != null ) {
63+ rangeVersion
64+ } else {
65+ // Range-specific key is missing from the pinned versions JSON.
66+ // Do NOT fall back to the base key because it could be a different major version
67+ // (e.g. base key resolves to 4.x but the range "2.+" expects 2.x).
68+ // Run resolveLatestDepVersions to populate the missing key.
69+ throw GradleException (
70+ " Pinned version missing for range key \" $rangeKey \" . " +
71+ " Run ./gradlew resolveLatestDepVersions -PtestLatestDeps=true -PresolveLatestDeps=true " +
72+ " to regenerate .github/config/latest-dep-versions.json"
73+ )
74+ }
75+ } else {
76+ null
77+ }
78+ }
79+
3480@CacheableRule
3581abstract class TestLatestDepsRule : ComponentMetadataRule {
3682 override fun execute (context : ComponentMetadataContext ) {
37- val version = context.details.id.version
38- if (version.contains(" -alpha" , true )
39- || version.contains(" -beta" , true )
40- || version.contains(" -rc" , true )
41- || version.contains(" .rc" , true )
42- || version.contains(" -m" , true ) // e.g. spring milestones are published to grails repo
43- || version.contains(" .m" , true ) // e.g. lettuce
44- || version.contains(" .alpha" , true ) // e.g. netty
45- || version.contains(" .beta" , true ) // e.g. hibernate
46- || version.contains(" .cr" , true ) // e.g. hibernate
47- || version.endsWith(" -nf-execution" ) // graphql
48- || GIT_SHA_PATTERN .matches(version) // graphql
49- || DATETIME_PATTERN .matches(version) // graphql
50- ) {
83+ if (! AcceptableVersions .isStable(context.details.id.version)) {
5184 context.details.status = " milestone"
5285 }
5386 }
54-
55- companion object {
56- private val GIT_SHA_PATTERN = Regex (" ^.*-[0-9a-f]{7,}$" )
57- private val DATETIME_PATTERN = Regex (" ^\\ d{4}-\\ d{2}-\\ d{2}T\\ d{2}-\\ d{2}-\\ d{2}.*$" )
58- }
5987}
6088
6189configurations {
@@ -74,14 +102,20 @@ configurations {
74102
75103 val testImplementation by getting
76104
105+ // Collect latestDepTestLibrary overrides so we can apply them via resolutionStrategy.
106+ // This map is populated during configuration and read during resolution.
107+ val latestDepTestLibraryOverrides = mutableMapOf<String , String >()
108+
77109 listOf (library, testLibrary).forEach { configuration ->
78110 // We use whenObjectAdded and copy into the real configurations instead of extension to allow
79111 // mutating the version for latest dep tests.
80112 configuration.dependencies.whenObjectAdded {
81113 val dep = copy()
82114 if (otelProps.testLatestDeps) {
115+ val extDep = this as ExternalDependency
116+ val pinnedVersion = lookupPinnedVersion(extDep.group, extDep.name, " latest.release" )
83117 (dep as ExternalDependency ).version {
84- require(" latest.release" )
118+ require(pinnedVersion ? : " latest.release" )
85119 }
86120 }
87121 testImplementation.dependencies.add(dep)
@@ -94,12 +128,31 @@ configurations {
94128 }
95129 }
96130
131+ // latestDepTestLibrary lets modules restrict the latest version to a specific range
132+ // (e.g. "3.+" to stay on major version 3). These constraints must beat the
133+ // require("latest.release") from library/testLibrary above.
134+ //
135+ // We can't use strictly() on the dependency itself because it conflicts with require()
136+ // from library/testLibrary (Gradle rejects conflicting strict/require constraints).
137+ // We can't use prefer() on library/testLibrary either because prefer() is too weak and
138+ // loses against the original library() version from compileOnly.
139+ //
140+ // Instead, we collect latestDepTestLibrary versions here and apply them via
141+ // resolutionStrategy.eachDependency below, which overrides all other constraints.
97142 latestDepTestLibrary.dependencies.whenObjectAdded {
98143 val dep = copy()
99144 val declaredVersion = dep.version
100145 if (declaredVersion != null ) {
146+ val extDep = this as ExternalDependency
147+ val pinnedVersion = lookupPinnedVersion(extDep.group, extDep.name, declaredVersion)
148+ val resolvedVersion = pinnedVersion ? : declaredVersion
149+ // Record the override; the actual version forcing happens in eachDependency below.
150+ // We use require() here (not strictly()) because strictly() would conflict with
151+ // the require() from library/testLibrary for the same artifact. The eachDependency
152+ // callback enforces the final version regardless.
153+ latestDepTestLibraryOverrides[" ${extDep.group} :${extDep.name} " ] = resolvedVersion
101154 (dep as ExternalDependency ).version {
102- strictly(declaredVersion )
155+ require(resolvedVersion )
103156 }
104157 }
105158 testImplementation.dependencies.add(dep)
@@ -108,6 +161,37 @@ configurations {
108161 named(" compileOnly" ) {
109162 extendsFrom(library)
110163 }
164+
165+ // Apply version overrides via resolutionStrategy which takes highest precedence.
166+ // This handles two cases:
167+ // 1. latestDepTestLibraryOverrides: modules that restrict latest deps to a version range
168+ // (e.g. spring-boot-starter-test:3.+ while latest is 4.x). These must override
169+ // the require("latest.release") from library/testLibrary.
170+ // 2. pinLatestDeps pinned versions: when pinning is enabled, any dependency still using
171+ // "latest.release" or "+" versions (e.g. transitive deps) gets pinned to a concrete
172+ // version from the JSON file.
173+ if (otelProps.testLatestDeps) {
174+ // Only apply to test-related configurations, not build tool configurations like Zinc
175+ // (the Scala compiler). Overriding scala-library in Zinc's configuration breaks compilation.
176+ configureEach {
177+ if (isCanBeResolved && (name.startsWith(" test" ) || name.startsWith(" latestDepTest" ))) {
178+ resolutionStrategy.eachDependency {
179+ // latestDepTestLibrary overrides take priority over pinned versions
180+ val override = latestDepTestLibraryOverrides[" ${requested.group} :${requested.name} " ]
181+ if (override != null ) {
182+ useVersion(override )
183+ return @eachDependency
184+ }
185+ if (pinLatestDeps) {
186+ val pinnedVersion = lookupPinnedVersion(requested.group, requested.name, requested.version)
187+ if (pinnedVersion != null ) {
188+ useVersion(pinnedVersion)
189+ }
190+ }
191+ }
192+ }
193+ }
194+ }
111195}
112196
113197if (otelProps.testLatestDeps) {
0 commit comments