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); + } +}