diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/PluginDependenciesResolver.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/PluginDependenciesResolver.java
index b3f02e39f3af..20d8fb88c6fa 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/PluginDependenciesResolver.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/PluginDependenciesResolver.java
@@ -74,6 +74,12 @@ DependencyNode resolve(
/**
* Resolves the runtime dependencies of the specified core extension (as {@link Plugin} as GAV carrier).
+ *
+ * The default implementation delegates to
+ * {@link #resolvePlugin(Plugin, Artifact, DependencyFilter, List, RepositorySystemSession)} so that
+ * implementations written against Maven 4.0.0-rc-5 and earlier keep working. Implementations should override
+ * this method: unlike the default, a dedicated implementation is expected to read the extension's artifact
+ * descriptor first, applying relocations and dependency validation.
*
* @param plugin The plugin for which to resolve the dependencies, must not be {@code null}.
* @param dependencyFilter A filter to exclude artifacts from resolution (but not collection), may be {@code null}.
@@ -83,15 +89,21 @@ DependencyNode resolve(
* @throws PluginResolutionException If any dependency could not be resolved.
* @since 3.10.0
*/
- DependencyResult resolveCoreExtensionAndFlatten(
+ default DependencyResult resolveCoreExtensionAndFlatten(
Plugin plugin,
DependencyFilter dependencyFilter,
List repositories,
RepositorySystemSession session)
- throws PluginResolutionException;
+ throws PluginResolutionException {
+ return resolvePlugin(plugin, null, dependencyFilter, repositories, session);
+ }
/**
* Resolves the runtime dependencies of the specified plugin.
+ *
+ * Implementations must not delegate to
+ * {@link #resolvePluginAndFlatten(Plugin, Artifact, DependencyFilter, List, RepositorySystemSession)} without
+ * also overriding it: the default implementation of that method delegates back here.
*
* @param plugin The plugin for which to resolve the dependencies, must not be {@code null}.
* @param artifact The plugin's main artifact, may be {@code null}.
@@ -114,6 +126,10 @@ DependencyResult resolvePlugin(
/**
* Resolves the runtime dependencies of the specified plugin.
+ *
+ * The default implementation delegates to
+ * {@link #resolvePlugin(Plugin, Artifact, DependencyFilter, List, RepositorySystemSession)}, which this method
+ * supersedes, so that implementations written against Maven 4.0.0-rc-5 and earlier keep working.
*
* @param plugin The plugin for which to resolve the dependencies, must not be {@code null}.
* @param pluginArtifact The plugin's main artifact, may be {@code null}.
@@ -124,11 +140,13 @@ DependencyResult resolvePlugin(
* @throws PluginResolutionException If any dependency could not be resolved.
* @since 3.10.0
*/
- DependencyResult resolvePluginAndFlatten(
+ default DependencyResult resolvePluginAndFlatten(
Plugin plugin,
Artifact pluginArtifact,
DependencyFilter dependencyFilter,
List repositories,
RepositorySystemSession session)
- throws PluginResolutionException;
+ throws PluginResolutionException {
+ return resolvePlugin(plugin, pluginArtifact, dependencyFilter, repositories, session);
+ }
}
diff --git a/impl/maven-core/src/test/java/org/apache/maven/plugin/internal/PluginDependenciesResolverDefaultMethodsTest.java b/impl/maven-core/src/test/java/org/apache/maven/plugin/internal/PluginDependenciesResolverDefaultMethodsTest.java
new file mode 100644
index 000000000000..2d5d933b572e
--- /dev/null
+++ b/impl/maven-core/src/test/java/org/apache/maven/plugin/internal/PluginDependenciesResolverDefaultMethodsTest.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.plugin.internal;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.apache.maven.model.Plugin;
+import org.apache.maven.plugin.PluginResolutionException;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.DependencyRequest;
+import org.eclipse.aether.resolution.DependencyResult;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * {@link PluginDependenciesResolver} is documented as internal, but tools that embed Maven — most visibly
+ * IntelliJ IDEA's {@code Maven40PluginDependenciesResolver} — implement it out of tree and are compiled against
+ * one Maven version while running against another. When {@code resolveCoreExtensionAndFlatten} and
+ * {@code resolvePluginAndFlatten} were forward-ported from Maven 3.10.0 as abstract methods, every such
+ * implementation started failing with {@code AbstractMethodError} as soon as core invoked them.
+ *
+ * These tests pin the compatibility contract: an implementation providing only the methods that existed before
+ * the forward port must remain a legal implementation, and the two newer methods must stay {@code default}.
+ */
+class PluginDependenciesResolverDefaultMethodsTest {
+
+ private static final DependencyResult RESULT = new DependencyResult(new DependencyRequest());
+
+ /**
+ * Implements exactly the method set of the pre-forward-port interface — nothing more. This class failing to
+ * compile is the regression: it means the two newer methods went back to being abstract.
+ */
+ private static final class LegacyResolver implements PluginDependenciesResolver {
+
+ private Plugin plugin;
+ private Artifact pluginArtifact;
+ private DependencyFilter dependencyFilter;
+ private List repositories;
+ private RepositorySystemSession session;
+
+ @Override
+ public Artifact resolve(Plugin plugin, List repositories, RepositorySystemSession session) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public org.eclipse.aether.graph.DependencyNode resolve(
+ Plugin plugin,
+ Artifact pluginArtifact,
+ DependencyFilter dependencyFilter,
+ List repositories,
+ RepositorySystemSession session) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DependencyResult resolvePlugin(
+ Plugin plugin,
+ Artifact pluginArtifact,
+ DependencyFilter dependencyFilter,
+ List repositories,
+ RepositorySystemSession session) {
+ this.plugin = plugin;
+ this.pluginArtifact = pluginArtifact;
+ this.dependencyFilter = dependencyFilter;
+ this.repositories = repositories;
+ this.session = session;
+ return RESULT;
+ }
+ }
+
+ @Test
+ void resolvePluginAndFlattenDelegatesToResolvePlugin() throws PluginResolutionException {
+ LegacyResolver resolver = new LegacyResolver();
+ Plugin plugin = new Plugin();
+ Artifact artifact = new DefaultArtifact("g:a:1.0");
+ DependencyFilter filter = (node, parents) -> true;
+ List repositories = List.of();
+
+ assertSame(RESULT, resolver.resolvePluginAndFlatten(plugin, artifact, filter, repositories, null));
+
+ assertSame(plugin, resolver.plugin);
+ assertSame(artifact, resolver.pluginArtifact);
+ assertSame(filter, resolver.dependencyFilter);
+ assertSame(repositories, resolver.repositories);
+ assertNull(resolver.session);
+ }
+
+ @Test
+ void resolveCoreExtensionAndFlattenDelegatesToResolvePlugin() throws PluginResolutionException {
+ LegacyResolver resolver = new LegacyResolver();
+ Plugin plugin = new Plugin();
+ DependencyFilter filter = (node, parents) -> true;
+ List repositories = List.of();
+
+ assertSame(RESULT, resolver.resolveCoreExtensionAndFlatten(plugin, filter, repositories, null));
+
+ assertSame(plugin, resolver.plugin);
+ assertNull(resolver.pluginArtifact, "the extension's main artifact is resolved from the plugin GAV");
+ assertSame(filter, resolver.dependencyFilter);
+ assertSame(repositories, resolver.repositories);
+ }
+
+ /**
+ * Guards the property that actually broke IntelliJ IDEA: these methods are invoked on implementations compiled
+ * against an older Maven, so they must carry an implementation in the interface itself. Turning either back into
+ * an abstract method reintroduces {@code AbstractMethodError} for every out-of-tree implementation.
+ */
+ @Test
+ void newerMethodsAreDefaultMethods() throws NoSuchMethodException {
+ Method resolveCoreExtensionAndFlatten = PluginDependenciesResolver.class.getMethod(
+ "resolveCoreExtensionAndFlatten",
+ Plugin.class,
+ DependencyFilter.class,
+ List.class,
+ RepositorySystemSession.class);
+ Method resolvePluginAndFlatten = PluginDependenciesResolver.class.getMethod(
+ "resolvePluginAndFlatten",
+ Plugin.class,
+ Artifact.class,
+ DependencyFilter.class,
+ List.class,
+ RepositorySystemSession.class);
+
+ assertTrue(
+ resolveCoreExtensionAndFlatten.isDefault(),
+ "resolveCoreExtensionAndFlatten must stay a default method");
+ assertTrue(resolvePluginAndFlatten.isDefault(), "resolvePluginAndFlatten must stay a default method");
+ }
+}
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh13068LegacyPluginDependenciesResolverTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh13068LegacyPluginDependenciesResolverTest.java
new file mode 100644
index 000000000000..69030eb3c593
--- /dev/null
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh13068LegacyPluginDependenciesResolverTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.it;
+
+import java.io.File;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Regression test for apache/maven#13068.
+ * The forward port in apache/maven#12335 added
+ * {@code resolveCoreExtensionAndFlatten} and {@code resolvePluginAndFlatten} to
+ * {@code PluginDependenciesResolver} as abstract methods in 4.0.0-rc-6.
+ *
+ * That interface is documented as internal, but it is the only hook Maven offers for influencing plugin
+ * resolution, and every major Java IDE overrides it: IntelliJ IDEA, Eclipse m2e and NetBeans. Because such
+ * implementations live out of tree and are compiled against one Maven while running on another, adding
+ * abstract methods turns every plugin resolution into an {@code AbstractMethodError}.
+ *
+ * This test builds a core extension against maven-core 4.0.0-rc-5 — the last release before those methods
+ * existed — installs it, and then runs a build that uses it. If the newer interface methods are abstract,
+ * the build fails before any goal executes.
+ */
+class MavenITgh13068LegacyPluginDependenciesResolverTest extends AbstractMavenIntegrationTestCase {
+
+ MavenITgh13068LegacyPluginDependenciesResolverTest() {
+ // resolvePluginAndFlatten / resolveCoreExtensionAndFlatten were introduced in 4.0.0-rc-6
+ super("[4.0.0-rc-6,)");
+ }
+
+ @Test
+ void legacyImplementationStillWorks() throws Exception {
+ File testDir = extractResources("/gh-13068-legacy-plugin-dependencies-resolver");
+
+ Verifier extensionVerifier = newVerifier(new File(testDir, "extension").getPath());
+ extensionVerifier.deleteArtifacts("org.apache.maven.its.gh-13068");
+ extensionVerifier.addCliArgument("install");
+ extensionVerifier.execute();
+ extensionVerifier.verifyErrorFreeLog();
+
+ Verifier clientVerifier = newVerifier(new File(testDir, "client").getPath());
+ clientVerifier.setAutoclean(false);
+ clientVerifier.addCliArgument("clean");
+ clientVerifier.execute();
+ clientVerifier.verifyErrorFreeLog();
+
+ // the extension must actually have displaced the default component, otherwise this test
+ // would pass without ever exercising the interface
+ clientVerifier.verifyTextInLog("[gh-13068] legacy PluginDependenciesResolver installed");
+ clientVerifier.verifyTextInLog("[gh-13068] resolvePlugin reached for maven-clean-plugin");
+ }
+}
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java b/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java
index cf56bd54a4a4..2a14fc57c3e6 100644
--- a/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/TestSuiteOrdering.java
@@ -103,6 +103,7 @@ public TestSuiteOrdering() {
* the tests are to finishing. Newer tests are also more likely to fail, so this is
* a fail fast technique as well.
*/
+ suite.addTestSuite(MavenITgh13068LegacyPluginDependenciesResolverTest.class);
suite.addTestSuite(MavenITmdep0590ClassifiedPomArtifactFromReactorTest.class);
suite.addTestSuite(MavenITgh12660BomVersionFromImportedBomTest.class);
suite.addTestSuite(MavenITgh11346DependencyManagementOverrideTest.class);
diff --git a/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/client/.mvn/extensions.xml b/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/client/.mvn/extensions.xml
new file mode 100644
index 000000000000..da9e208285ed
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/client/.mvn/extensions.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ org.apache.maven.its.gh-13068
+ legacy-plugin-dependencies-resolver
+ 1.0.0-SNAPSHOT
+
+
diff --git a/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/client/pom.xml b/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/client/pom.xml
new file mode 100644
index 000000000000..853bc1e170b7
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/client/pom.xml
@@ -0,0 +1,33 @@
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.gh-13068
+ client
+ 1.0.0-SNAPSHOT
+ jar
+
+ Maven Integration Test :: gh-13068 :: client
+
+ Builds with a core extension that overrides PluginDependenciesResolver with a pre-4.0.0-rc-6
+ implementation. Resolving any plugin routes through that extension.
+
+
diff --git a/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/extension/pom.xml b/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/extension/pom.xml
new file mode 100644
index 000000000000..f92eb8a5b22a
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/extension/pom.xml
@@ -0,0 +1,79 @@
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.gh-13068
+ legacy-plugin-dependencies-resolver
+ 1.0.0-SNAPSHOT
+ jar
+
+ Maven Integration Test :: legacy-plugin-dependencies-resolver
+
+ A core extension that overrides PluginDependenciesResolver implementing only the method set that
+ existed up to Maven 4.0.0-rc-5. It is deliberately compiled against maven-core 4.0.0-rc-5, the last
+ release before resolveCoreExtensionAndFlatten and resolvePluginAndFlatten were added, so that it
+ reproduces the situation every out-of-tree implementation is in: built against one Maven, run on another.
+
+
+
+ 17
+ UTF-8
+
+
+
+
+ org.apache.maven
+ maven-core
+ 4.0.0-rc-5
+ provided
+
+
+ javax.inject
+ javax.inject
+ 1
+ provided
+
+
+ org.eclipse.sisu
+ org.eclipse.sisu.inject
+ 1.1.0
+ provided
+
+
+
+
+
+
+ org.eclipse.sisu
+ sisu-maven-plugin
+ 1.1.0
+
+
+ index-project
+
+ main-index
+
+
+
+
+
+
+
diff --git a/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/extension/src/main/java/org/apache/maven/its/gh13068/LegacyPluginDependenciesResolver.java b/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/extension/src/main/java/org/apache/maven/its/gh13068/LegacyPluginDependenciesResolver.java
new file mode 100644
index 000000000000..8755935d9369
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-13068-legacy-plugin-dependencies-resolver/extension/src/main/java/org/apache/maven/its/gh13068/LegacyPluginDependenciesResolver.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.its.gh13068;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.util.List;
+
+import org.apache.maven.model.Plugin;
+import org.apache.maven.plugin.PluginResolutionException;
+import org.apache.maven.plugin.internal.DefaultPluginDependenciesResolver;
+import org.apache.maven.plugin.internal.PluginDependenciesResolver;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.graph.DependencyFilter;
+import org.eclipse.aether.graph.DependencyNode;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.DependencyResult;
+import org.eclipse.sisu.Priority;
+
+/**
+ * A decorating {@link PluginDependenciesResolver} that implements only the methods which existed up to
+ * Maven 4.0.0-rc-5, delegating everything to the default implementation. This mirrors how IDEs embed
+ * Maven, most visibly IntelliJ IDEA's {@code Maven40PluginDependenciesResolver}.
+ *
+ * Compiled against maven-core 4.0.0-rc-5 (see this module's POM) and run against the Maven under test,
+ * so the newer interface methods have no implementation here. They must therefore be resolved through
+ * {@code default} methods on the interface; if they are abstract, plugin resolution dies with
+ * {@code AbstractMethodError} before a single goal runs.
+ */
+@Named
+@Singleton
+@Priority(10)
+public class LegacyPluginDependenciesResolver implements PluginDependenciesResolver {
+
+ private final PluginDependenciesResolver delegate;
+
+ @Inject
+ public LegacyPluginDependenciesResolver(DefaultPluginDependenciesResolver delegate) {
+ this.delegate = delegate;
+ System.out.println("[gh-13068] legacy PluginDependenciesResolver installed");
+ }
+
+ @Override
+ public Artifact resolve(Plugin plugin, List repositories, RepositorySystemSession session)
+ throws PluginResolutionException {
+ return delegate.resolve(plugin, repositories, session);
+ }
+
+ @Override
+ public DependencyNode resolve(
+ Plugin plugin,
+ Artifact pluginArtifact,
+ DependencyFilter dependencyFilter,
+ List repositories,
+ RepositorySystemSession session)
+ throws PluginResolutionException {
+ return delegate.resolve(plugin, pluginArtifact, dependencyFilter, repositories, session);
+ }
+
+ @Override
+ public DependencyResult resolvePlugin(
+ Plugin plugin,
+ Artifact pluginArtifact,
+ DependencyFilter dependencyFilter,
+ List repositories,
+ RepositorySystemSession session)
+ throws PluginResolutionException {
+ System.out.println("[gh-13068] resolvePlugin reached for " + plugin.getArtifactId());
+ return delegate.resolvePlugin(plugin, pluginArtifact, dependencyFilter, repositories, session);
+ }
+}