Skip to content

Fix Maven 3.9.12 + Develocity compatibility: declare checkPrerequisites override#855

Merged
agrasth merged 5 commits into
jfrog:masterfrom
galusben:fix-develocity-checkPrerequisites
May 6, 2026
Merged

Fix Maven 3.9.12 + Develocity compatibility: declare checkPrerequisites override#855
agrasth merged 5 commits into
jfrog:masterfrom
galusben:fix-develocity-checkPrerequisites

Conversation

@galusben
Copy link
Copy Markdown
Contributor

@galusben galusben commented May 4, 2026

Summary

  • Maven 3.9.12 renamed DefaultMavenPluginManager#checkRequiredMavenVersion to checkPrerequisites. The Develocity Maven extension reflectively looks the new method up via Class.getDeclaredMethod on the Plexus-bound DefaultMavenPluginManager implementation, which in this project is ArtifactoryEclipsePluginManager. getDeclaredMethod does not walk the superclass, so the lookup throws NoSuchMethodException and blocks any Maven >= 3.9.12 build that also loads Develocity's extension.
  • PR Fix Maven 3.9.12 compatibility: Add prerequisitesCheckers requirement #844 (in v2.43.6) added the prerequisitesCheckers Plexus field requirement, which addressed the NPE variant on Maven 3.9.12 — but did not declare the method, so the Develocity path stayed broken.
  • This PR bumps maven-core build dep from 3.8.6 to 3.9.12 so the override compiles cleanly, declares checkPrerequisites on the subclass (delegating to super), and adds a regression unit test that mirrors Develocity's exact reflective lookup.

Failing stack trace this fixes (from a real CI log)

java.lang.NoSuchMethodException: org.jfrog.build.extractor.maven.resolver.ArtifactoryEclipsePluginManager.checkPrerequisites(org.apache.maven.plugin.descriptor.PluginDescriptor)
    at java.lang.Class.getDeclaredMethod (Class.java:2422)
    at com.gradle.maven.cache.extension.g.b.checkPrerequisites (SourceFile:117)

Repro confirmed locally

Loading the master HEAD uber jar and running Class.forName("…ArtifactoryEclipsePluginManager").getDeclaredMethod("checkPrerequisites", PluginDescriptor.class) reproduces the exact same NoSuchMethodException. With this branch, the lookup returns the declared method and the exception goes away.

Test plan

  • All tests have passed. If this feature is not already covered by the tests, new tests have been added.
  • New ArtifactoryEclipsePluginManagerTest.testCheckPrerequisitesIsDeclaredOnSubclass mirrors Develocity's exact reflective contract — fails on master, passes here.
  • ./gradlew :build-info-extractor-maven3:compileJava :build-info-extractor-maven3:test clean against JDK 11 + the bumped maven-core 3.9.12 build dep.
  • Suggested follow-up (not in this PR): extend .github/workflows/maven-compatibility-test.yml to install Develocity's Maven extension in the test pom and fail on NoSuchMethodException, so this regression class is caught by the nightly job.

🤖 Generated with Claude Code

@galusben
Copy link
Copy Markdown
Contributor Author

galusben commented May 4, 2026

Root cause and where this should be fixed

Three parties are involved; each one is doing something a little brittle.

1. Maven made a backwards-incompatible API change in a patch release.
Verified with javap on maven-core:

  • 3.9.11 declares public void checkRequiredMavenVersion(PluginDescriptor)
  • 3.9.12 renames it to public void checkPrerequisites(PluginDescriptor) (the new name reflects the related extension point added in 3.9.12 — prerequisitesCheckers).

2. The Develocity Maven extension uses Class.getDeclaredMethod(...) for the lookup. getDeclaredMethod only returns methods physically declared on the class you call it on; it does not walk the superclass chain. Class.getMethod(...) would walk inheritance for public methods.

3. ArtifactoryEclipsePluginManager is registered (via Plexus components.xml) as the implementation of DefaultMavenPluginManager. Develocity therefore reflects against this subclass, not Maven's class. The subclass only directly declares setupExtensionsRealm, so every other method is inherited — and getDeclaredMethod("checkPrerequisites", …) on the subclass throws NoSuchMethodException.

In Maven 3.9.11 the same anti-pattern would have applied to checkRequiredMavenVersion, but Develocity wasn't reflecting on that method back then. They presumably added the reflective hook because of the 3.9.12 rename, and the moment they did, the latent issue with our subclass surfaced.

Why fix it here

Develocity could harden the reflective lookup (use getMethod, or fall back when getDeclaredMethod misses). That's the more durable fix and we should share this PR with them. But:

  • We don't ship Develocity, so we can't unblock our users by waiting on a Develocity release.
  • The change is small and self-contained: a MethodHandle lookup at class-load time plus a thin override that delegates to whichever method name the running Maven actually has, plus a regression test.
  • It mirrors the precedent set by Fix Maven 3.9.12 compatibility: Add prerequisitesCheckers requirement #844 (in v2.43.6), which kept up with the field side of the same Maven 3.9.12 reorg (prerequisitesCheckers). This PR is the missing companion piece for the method side.

If/when Develocity loosens their lookup, this override becomes a no-op safety net rather than load-bearing.

@galusben galusben added the safe to test Approve running integration tests on a pull request label May 4, 2026
@github-actions github-actions Bot removed the safe to test Approve running integration tests on a pull request label May 4, 2026
@galusben galusben added the safe to test Approve running integration tests on a pull request label May 4, 2026
@github-actions github-actions Bot removed the safe to test Approve running integration tests on a pull request label May 4, 2026

// Declared so Class.getDeclaredMethod on this subclass finds it —
// getDeclaredMethod does not walk the superclass chain.
@Override
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkPrerequisites doesn't exist in DefaultMavenPluginManager before Maven 3.9.12. On Maven 3.8.x without Develocity this is fine (method is never called). But with Develocity, getDeclaredMethod now finds this override and invokes it — causing super.checkPrerequisites() to throw NoSuchMethodError at runtime.
This replaces one error with another for users still on Maven 3.8.x + Develocity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 6a24180. Switched to a MethodHandle.findSpecial lookup resolved at class-load time so the bytecode no longer embeds a hard invokespecial reference to a method that may not exist on the parent.

  • Maven ≥ 3.9.12: handle is non-null → our override delegates to super.
  • Maven < 3.9.12 (3.8.x / 3.9.0 – 3.9.11): static initializer catches NoSuchMethodException from findSpecial, handle stays null, the override returns a no-op. No NoSuchMethodError at invoke time.

Verified locally by loading the same uber jar against both Maven 3.9.11 maven-core and 3.9.12 maven-core on the classpath:

=== Maven 3.9.11 (no checkPrerequisites on parent) ===
STEP 1 OK: getDeclaredMethod returned ...checkPrerequisites...
STEP 2 OK: checkPrerequisites returned without NoSuchMethodError

=== Maven 3.9.12 (parent has checkPrerequisites) ===
STEP 1 OK: getDeclaredMethod returned ...checkPrerequisites...
STEP 2 OK: super was invoked, threw NullPointerException (expected on 3.9.12+ with empty descriptor)

Comment on lines +8 to +9
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test runs under TestNG (@org.testng.annotations.Test) but uses JUnit 4 assertions. This only works incidentally because TestNG has an optional transitive dependency on junit:4.13.2. Please use TestNG's own assertions instead:

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6a24180 — swapped to TestNG's assertEquals / assertNotNull and flipped the argument order to TestNG's (actual, expected) convention. Note: the existing ArtifactoryResolutionTest in the same package uses JUnit assertions too, so we may want a follow-up to clean that up — but I left it out of this PR's scope.

Comment thread build.gradle
jdomVersion = '2.0.6.1'
jsrVersion = '3.0.2'
mavenVersion = '3.8.6'
mavenVersion = '3.9.12'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mavenVersion is used in 3 places, not just maven-core:

  // build.gradle lines 492-503
  "org.apache.maven:maven-repository-metadata:$mavenVersion",  // bumped 3.8.6 → 3.9.12
  "org.apache.maven:maven-model:$mavenVersion",                // bumped 3.8.6 → 3.9.12
  ...
  implementation("org.apache.maven:maven-core:$mavenVersion")  // bumped 3.8.6 → 3.9.12

PR intended to only bump maven-core to get access to checkPrerequisites, but it accidentally also bumped maven-repository-metadata and maven-model as a side effect since they share the same variable.
maybe we can split the variable:

  mavenVersion = '3.8.6'         // for maven-repository-metadata, maven-model
  mavenCoreVersion = '3.9.12'    // for maven-core only

wdyt @bhanurp ?

Copy link
Copy Markdown
Contributor Author

@galusben galusben May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep the single version for simplicity.
On some point you want to upgrade everything.

@galusben
Copy link
Copy Markdown
Contributor Author

galusben commented May 5, 2026

CI triage — none of the red checks are caused by this PR

Compared this PR's runs against the master HEAD CI run (24656256484, sha 013da9b0).

Pre-existing on master HEAD (and in 5 of 5 recent master pushes):

  • ❌ Distribution (3 OSes) — Bad credentials on setUp of *DistributionManagerTest
  • ❌ Go (3 OSes) — 401: Wrong username was used on GoExtractorTest.setUp
  • ❌ Gradle (ubuntu, windows) — UnexpectedBuildFailure running artifactoryPublish in testkit
  • ❌ Extractor (macos-latest) — also failed on master HEAD

Initially red on this PR but green on rerun (transient flakes, coincided with a GitHub Actions cache outage — "Our services aren't available right now"):

  • ✅ Docker — was unknown blob / gcr.io pull failures; now passing
  • ✅ Python (windows-latest) — was a contaminated pip venv on the windows runner; now passing

Stable green across both runs — the jobs this change actually exercises:

  • ✅ Maven (ubuntu / macos / windows)
  • ✅ Extractor (ubuntu / windows) — runs the new ArtifactoryEclipsePluginManagerTest regression test
  • ✅ Ivy (3 OSes), npm, NuGet, Python (ubuntu)

The mavenVersion bump (3.8.6 → 3.9.12) is consumed only by build-info-extractor-maven3. Distribution / Go / Gradle / Docker / pip don't pull maven-core and the test surface that touched our change (Maven matrix + maven3 unit tests) is fully green.

galusben added a commit to galusben/build-info that referenced this pull request May 5, 2026
Concern raised by @agrasth (PR jfrog#855): with the previous direct super.checkPrerequisites()
call, bytecode embeds an invokespecial reference to DefaultMavenPluginManager.checkPrerequisites
that does not exist on Maven 3.8.x / 3.9.0 - 3.9.11. Develocity's reflective lookup now
finds our override on those runtimes too, invokes it, and the super call throws
NoSuchMethodError — replacing one error with another for users still on Maven 3.8.x +
Develocity.

Switch to a MethodHandle.findSpecial lookup resolved at class-load time:
- Maven >= 3.9.12: handle is non-null, our override delegates to super correctly.
- Maven < 3.9.12: handle stays null (NoSuchMethodException caught in static initializer);
  the override is a safe no-op; no NoSuchMethodError at invoke time.

Verified locally with the same uber jar against both Maven 3.9.11 and 3.9.12 maven-core
on the classpath.

Also: switch the test from JUnit assertions (working incidentally via TestNG's transitive
junit dep) to TestNG's own assertions, and swap arg order to match TestNG's
(actual, expected) convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@galusben galusben added the safe to test Approve running integration tests on a pull request label May 5, 2026
@github-actions github-actions Bot removed the safe to test Approve running integration tests on a pull request label May 5, 2026
@galusben galusben added the safe to test Approve running integration tests on a pull request label May 5, 2026
@galusben
Copy link
Copy Markdown
Contributor Author

galusben commented May 5, 2026

Update: also delegate on Maven < 3.9.12 (per @galusben suggestion)

@galusben raised: when called by Develocity on Maven < 3.9.12, the previous version of the override silently no-op'd — bypassing whatever prerequisites check the caller expected. New commit 97153646 makes the static initializer also resolve checkRequiredMavenVersion (the pre-3.9.12 name), so the check runs on every supported Maven version.

Subtle trap worth flagging

First attempt looked up the old name first, falling back to the new one. That hit a deprecation shim: on Maven 3.9.12 the MavenPluginManager interface keeps checkRequiredMavenVersion as a deprecated default method that delegates back to checkPrerequisites via virtual dispatch — which lands on our override — infinite recursion → StackOverflowError.

Verified with javap:

# 3.9.11 interface
public abstract void checkRequiredMavenVersion(PluginDescriptor)

# 3.9.12 interface
public default void checkRequiredMavenVersion(PluginDescriptor)   ← deprecation shim
public default void checkPrerequisites(PluginDescriptor)

Fix: try the new name first. On 3.9.12 we hit the real concrete impl on DefaultMavenPluginManager directly and never touch the shim. On < 3.9.12 the new name doesn't exist; we fall back to the old name, which is the real concrete impl on those versions.

End-to-end verification

Same uber jar, two Maven runtimes:

=== Maven 3.9.11 ===
STEP 1 OK: getDeclaredMethod returned ...checkPrerequisites...
STEP 2 super was invoked, threw NPE
   at DefaultMavenPluginManager.checkRequiredMavenVersion(DefaultMavenPluginManager.java:293)  ← real parent method
   at ArtifactoryEclipsePluginManager.checkPrerequisites(ArtifactoryEclipsePluginManager.java:78)

=== Maven 3.9.12 ===
STEP 1 OK: getDeclaredMethod returned ...checkPrerequisites...
STEP 2 super was invoked, threw NPE
   at DefaultMavenPluginManager.checkPrerequisites(DefaultMavenPluginManager.java:290)         ← real parent method
   at ArtifactoryEclipsePluginManager.checkPrerequisites(ArtifactoryEclipsePluginManager.java:78)

The NPE is from the parent touching uninitialized state on a stand-alone PluginDescriptor (outside Maven's container) — what matters is which parent method ran. On both versions, the call lands in the right place.

@github-actions github-actions Bot removed the safe to test Approve running integration tests on a pull request label May 5, 2026
@galusben galusben added the safe to test Approve running integration tests on a pull request label May 5, 2026
@github-actions github-actions Bot removed the safe to test Approve running integration tests on a pull request label May 5, 2026
agrasth pushed a commit that referenced this pull request May 5, 2026
Concern raised by @agrasth (PR #855): with the previous direct super.checkPrerequisites()
call, bytecode embeds an invokespecial reference to DefaultMavenPluginManager.checkPrerequisites
that does not exist on Maven 3.8.x / 3.9.0 - 3.9.11. Develocity's reflective lookup now
finds our override on those runtimes too, invokes it, and the super call throws
NoSuchMethodError — replacing one error with another for users still on Maven 3.8.x +
Develocity.

Switch to a MethodHandle.findSpecial lookup resolved at class-load time:
- Maven >= 3.9.12: handle is non-null, our override delegates to super correctly.
- Maven < 3.9.12: handle stays null (NoSuchMethodException caught in static initializer);
  the override is a safe no-op; no NoSuchMethodError at invoke time.

Verified locally with the same uber jar against both Maven 3.9.11 and 3.9.12 maven-core
on the classpath.

Also: switch the test from JUnit assertions (working incidentally via TestNG's transitive
junit dep) to TestNG's own assertions, and swap arg order to match TestNG's
(actual, expected) convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
galusben added a commit to galusben/build-info that referenced this pull request May 5, 2026
Concern raised by @agrasth (PR jfrog#855): with the previous direct super.checkPrerequisites()
call, bytecode embeds an invokespecial reference to DefaultMavenPluginManager.checkPrerequisites
that does not exist on Maven 3.8.x / 3.9.0 - 3.9.11. Develocity's reflective lookup now
finds our override on those runtimes too, invokes it, and the super call throws
NoSuchMethodError — replacing one error with another for users still on Maven 3.8.x +
Develocity.

Switch to a MethodHandle.findSpecial lookup resolved at class-load time:
- Maven >= 3.9.12: handle is non-null, our override delegates to super correctly.
- Maven < 3.9.12: handle stays null (NoSuchMethodException caught in static initializer);
  the override is a safe no-op; no NoSuchMethodError at invoke time.

Verified locally with the same uber jar against both Maven 3.9.11 and 3.9.12 maven-core
on the classpath.

Also: switch the test from JUnit assertions (working incidentally via TestNG's transitive
junit dep) to TestNG's own assertions, and swap arg order to match TestNG's
(actual, expected) convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@galusben galusben force-pushed the fix-develocity-checkPrerequisites branch from 4b82c53 to fbf0b01 Compare May 5, 2026 07:40
@galusben galusben added the safe to test Approve running integration tests on a pull request label May 5, 2026
@github-actions github-actions Bot removed the safe to test Approve running integration tests on a pull request label May 5, 2026
galusben and others added 4 commits May 5, 2026 10:43
…es override

Maven 3.9.12 renamed DefaultMavenPluginManager#checkRequiredMavenVersion to
checkPrerequisites. The Develocity Maven extension reflectively looks the new
method up via Class.getDeclaredMethod on the Plexus-bound DefaultMavenPluginManager
implementation, which is ArtifactoryEclipsePluginManager in this project.
getDeclaredMethod does not walk the superclass, so the lookup throws
NoSuchMethodException and blocks any Maven >= 3.9.12 build that also loads
Develocity's extension.

PR jfrog#844 (in v2.43.6) added the prerequisitesCheckers Plexus field requirement,
which addressed the NPE variant on Maven 3.9.12 but did not declare the method,
so the Develocity path stayed broken.

Bumps maven-core build dep from 3.8.6 to 3.9.12 so the override compiles cleanly,
and adds a regression unit test that mirrors Develocity's exact reflective lookup
(Class.getDeclaredMethod on the subclass).

Reproduced locally against Maven 3.9.12 + JDK 11 with the published v2.43.6 jar
and confirmed the same stack trace as the failing CI log; the fix makes the
NoSuchMethodException go away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the third-party-specific framing from the source and test comments —
the contract (declared on the subclass so getDeclaredMethod can find it) is
the load-bearing fact. The why-it-matters discussion lives on the PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Concern raised by @agrasth (PR jfrog#855): with the previous direct super.checkPrerequisites()
call, bytecode embeds an invokespecial reference to DefaultMavenPluginManager.checkPrerequisites
that does not exist on Maven 3.8.x / 3.9.0 - 3.9.11. Develocity's reflective lookup now
finds our override on those runtimes too, invokes it, and the super call throws
NoSuchMethodError — replacing one error with another for users still on Maven 3.8.x +
Develocity.

Switch to a MethodHandle.findSpecial lookup resolved at class-load time:
- Maven >= 3.9.12: handle is non-null, our override delegates to super correctly.
- Maven < 3.9.12: handle stays null (NoSuchMethodException caught in static initializer);
  the override is a safe no-op; no NoSuchMethodError at invoke time.

Verified locally with the same uber jar against both Maven 3.9.11 and 3.9.12 maven-core
on the classpath.

Also: switch the test from JUnit assertions (working incidentally via TestNG's transitive
junit dep) to TestNG's own assertions, and swap arg order to match TestNG's
(actual, expected) convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ia override

Previously, when Maven < 3.9.12 invoked our checkPrerequisites override (e.g. via
Develocity's reflective lookup), the MethodHandle was null and we silently no-op'd.
That weakened behavior on those runtimes — the prerequisites check the caller
expected to run was skipped.

Now the static initializer also looks up the older method name
(checkRequiredMavenVersion) so the check runs on every supported Maven version:

- Maven >= 3.9.12: handle resolves to DefaultMavenPluginManager#checkPrerequisites
  (the real concrete impl on the abstract class, not the deprecated default method
  on the interface — the order of the lookup matters: trying the new name first
  hits the concrete impl directly).
- Maven < 3.9.12 (3.8.x / 3.9.0 - 3.9.11): new name doesn't exist on the parent,
  fall back to checkRequiredMavenVersion which is a real concrete impl.
- If neither exists (no known Maven version): handle stays null, override is a
  safe no-op.

Verified locally with the same uber jar against both Maven 3.9.11 and 3.9.12
maven-core jars on the classpath; the call lands in the correct parent method on
each runtime (no recursion, no NoSuchMethodError, no silent skip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the local handle variable and collapse the multi-catch with
ExceptionUtils.rethrow (commons-lang3, already on the classpath). Same
runtime behavior — original exception type is preserved (no wrapping in
RuntimeException), and PluginIncompatibleException still propagates as a
real checked exception.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@galusben galusben force-pushed the fix-develocity-checkPrerequisites branch from fbf0b01 to be32cd5 Compare May 5, 2026 07:43
@agrasth agrasth merged commit 0ee6b74 into jfrog:master May 6, 2026
3 of 34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants