This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/master by this push:
     new 4fc2ea08d4 [MNG-8615] [MNG-8616] Maven core extensions handling 
improvements (#2147)
4fc2ea08d4 is described below

commit 4fc2ea08d4f7cd94126bc1b70d9ba59ff91c0420
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Thu Mar 13 13:50:59 2025 +0100

    [MNG-8615] [MNG-8616] Maven core extensions handling improvements (#2147)
    
    It all started with MNG-8615 to not swallow DI problems while loading core 
extensions. Then tried to add origin as well, but it turns out there is lack of 
context. Then turned out models are not location tracking. Then came MNG-8616 
as Maven was too rigid and did not apply precedence for extensions making 
project hopping problematic, if not impossible...
    
    Changes:
    * do not swallow DI issues happened during core extension loading; belly up 
instead.
    * report which extension caused DI issue
    * make core extension models and IO location tracking
    * alter CLI api to carry all extensions "by origin" (project, user, 
installation)
    * parser should be plain dumb, all it does is load extensions in precedence 
order and validates them (validity: they must be GA unique)
    * DI capsule performs now "selection" of extensions (based on precedence) 
and loads them as before
    * finally: we have now DEBUG logs which all extensions were considered and 
which were effectively loaded, something I missed a lot
    * new UTs revealed MavenInvokerUT problem: ClassWorlds were not cleaned up 
properly
    
    Behavioural change since 4.0.0-rc-3:
    * conflict within same source (same file) makes Maven fail - as before
    * conflict spanning across sources is warning only; precedence is applied 
to select extension to be loaded
    
    ---
    
    https://issues.apache.org/jira/browse/MNG-8615
    https://issues.apache.org/jira/browse/MNG-8616
---
 api/maven-api-cli/pom.xml                          |  2 +
 .../org/apache/maven/api/cli/CoreExtensions.java   | 48 +++++++++++
 .../org/apache/maven/api/cli/InvokerRequest.java   | 11 ++-
 .../org/apache/maven/api/cli/ParserRequest.java    |  2 +-
 .../maven/api/cli/extensions/package-info.java     |  2 -
 .../main/java/org/apache/maven/cli/MavenCli.java   |  5 +-
 impl/maven-cli/pom.xml                             |  1 +
 .../extensions/BootstrapCoreExtensionManager.java  |  8 +-
 .../LoadedCoreExtension.java}                      | 19 ++---
 .../maven/cling/invoker/BaseInvokerRequest.java    |  8 +-
 .../org/apache/maven/cling/invoker/BaseParser.java | 98 +++++++++++++---------
 .../cling/invoker/ContainerCapsuleFactory.java     |  3 +-
 ...suleFactory.java => CoreExtensionSelector.java} | 12 ++-
 .../apache/maven/cling/invoker/LookupInvoker.java  |  7 +-
 .../invoker/PlexusContainerCapsuleFactory.java     | 73 ++++++++++------
 .../invoker/PrecedenceCoreExtensionSelector.java   | 96 +++++++++++++++++++++
 .../cling/invoker/mvn/MavenInvokerRequest.java     |  4 +-
 .../invoker/mvnenc/EncryptInvokerRequest.java      |  4 +-
 .../cling/invoker/mvnsh/ShellInvokerRequest.java   |  4 +-
 .../maven/cling/invoker/mvn/MavenInvokerTest.java  | 81 ++++++++++++++++--
 .../cling/invoker/mvn/MavenInvokerTestSupport.java | 10 ++-
 .../mvn/resident/ResidentMavenInvokerTest.java     |  7 +-
 22 files changed, 387 insertions(+), 118 deletions(-)

diff --git a/api/maven-api-cli/pom.xml b/api/maven-api-cli/pom.xml
index fd096191b1..92517b0c99 100644
--- a/api/maven-api-cli/pom.xml
+++ b/api/maven-api-cli/pom.xml
@@ -59,6 +59,8 @@
             <template>model.vm</template>
           </templates>
           <params>
+            <param>locationTracking=true</param>
+            <param>generateLocationClasses=true</param>
             <param>packageModelV4=org.apache.maven.api.cli.extensions</param>
             
<param>packageToolV4=org.apache.maven.cli.internal.extension.io</param>
           </params>
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/CoreExtensions.java 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/CoreExtensions.java
new file mode 100644
index 0000000000..8206b26b0a
--- /dev/null
+++ 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/CoreExtensions.java
@@ -0,0 +1,48 @@
+/*
+ * 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.api.cli;
+
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.maven.api.Constants;
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.cli.extensions.CoreExtension;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Represents the list of core extensions configured at one source. The list 
is validated (are GA unique), but no
+ * other logic than that is applied.
+ *
+ * @since 4.0.0
+ * @param source The source file of core extensions, is never {@code null}.
+ * @param coreExtensions The configured core extensions, is never {@code 
null}. Contents of list is guaranteed to be unique by GA.
+ *
+ * @see Constants#MAVEN_PROJECT_EXTENSIONS
+ * @see Constants#MAVEN_USER_EXTENSIONS
+ * @see Constants#MAVEN_INSTALLATION_EXTENSIONS
+ */
+@Experimental
+public record CoreExtensions(Path source, List<CoreExtension> coreExtensions) {
+    public CoreExtensions(Path source, List<CoreExtension> coreExtensions) {
+        this.source = requireNonNull(source, "source");
+        this.coreExtensions = requireNonNull(coreExtensions, "coreExtensions");
+    }
+}
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java
index 3df660c130..e9b2a480ed 100644
--- 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java
+++ 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java
@@ -28,7 +28,6 @@
 import org.apache.maven.api.annotations.Experimental;
 import org.apache.maven.api.annotations.Immutable;
 import org.apache.maven.api.annotations.Nonnull;
-import org.apache.maven.api.cli.extensions.CoreExtension;
 import org.apache.maven.api.services.Lookup;
 import org.apache.maven.api.services.MessageBuilderFactory;
 
@@ -172,12 +171,16 @@ default Optional<OutputStream> stdErr() {
     }
 
     /**
-     * Returns a list of core extensions, if configured in the 
.mvn/extensions.xml file.
+     * Returns a list of core extensions from all sources, that were 
discovered and loaded. Each instance of
+     * {@link CoreExtensions} is validated, but the list elements may have 
overlapping elements, that requires
+     * some logic to sort out (like precedence).
+     * <p>
+     * The list of {@link CoreExtensions} if present, is in precedence order.
      *
-     * @return an {@link Optional} containing the list of core extensions, or 
empty if not configured
+     * @return an {@link Optional} containing the {@link CoreExtensions}, or 
empty if not configured
      */
     @Nonnull
-    Optional<List<CoreExtension>> coreExtensions();
+    Optional<List<CoreExtensions>> coreExtensions();
 
     /**
      * Returns the options associated with this invocation request.
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java
index 5a5913b766..ea30233fc1 100644
--- 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java
+++ 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/ParserRequest.java
@@ -317,7 +317,7 @@ public ParserRequest build() {
                     command,
                     commandName,
                     List.copyOf(args),
-                    logger,
+                    lookup.lookupOptional(Logger.class).orElse(logger),
                     messageBuilderFactory,
                     lookup,
                     cwd,
diff --git 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/extensions/package-info.java
 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/extensions/package-info.java
index 7b4aafe562..65b9a662b5 100644
--- 
a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/extensions/package-info.java
+++ 
b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/extensions/package-info.java
@@ -19,8 +19,6 @@
 
 /**
  * Provides support for Maven core extensions configuration and management.
- * Core extensions can be configured through {@code .mvn/extensions.xml} in 
the project
- * base directory to enhance Maven's capabilities during build execution.
  *
  * <p>This package contains classes for:</p>
  * <ul>
diff --git 
a/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java 
b/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
index 979b4a523a..69c83a1714 100644
--- a/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
+++ b/compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java
@@ -60,6 +60,7 @@
 import org.apache.maven.Maven;
 import org.apache.maven.api.Constants;
 import org.apache.maven.api.cli.extensions.CoreExtension;
+import org.apache.maven.api.cli.extensions.InputSource;
 import org.apache.maven.api.services.MessageBuilder;
 import org.apache.maven.api.services.MessageBuilderFactory;
 import org.apache.maven.building.FileSource;
@@ -858,7 +859,9 @@ private List<CoreExtension> 
readCoreExtensionsDescriptor(String extensionsFile)
             Path extensionsPath = Path.of(extensionsFile);
             if (Files.exists(extensionsPath)) {
                 try (InputStream is = Files.newInputStream(extensionsPath)) {
-                    return new CoreExtensionsStaxReader().read(is, 
true).getExtensions();
+                    return new CoreExtensionsStaxReader()
+                            .read(is, true, new InputSource(extensionsFile))
+                            .getExtensions();
                 }
             }
         }
diff --git a/impl/maven-cli/pom.xml b/impl/maven-cli/pom.xml
index cb68e12289..97076b1cd6 100644
--- a/impl/maven-cli/pom.xml
+++ b/impl/maven-cli/pom.xml
@@ -261,6 +261,7 @@ under the License.
           <params>
             <param>packageModelV4=org.apache.maven.api.cli.extensions</param>
             
<param>packageToolV4=org.apache.maven.cling.internal.extension.io</param>
+            <param>locationTracking=true</param>
           </params>
           <velocityBasedir>${project.basedir}/../../src/mdo</velocityBasedir>
         </configuration>
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
index d0630453df..89dd0e0c0e 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java
@@ -132,7 +132,7 @@ public BootstrapCoreExtensionManager(
         this.interpolator = interpolator;
     }
 
-    public List<CoreExtensionEntry> loadCoreExtensions(
+    public List<LoadedCoreExtension> loadCoreExtensions(
             MavenExecutionRequest request, Set<String> providedArtifacts, 
List<CoreExtension> extensions)
             throws Exception {
         try (CloseableSession repoSession = repositorySystemSessionFactory
@@ -150,14 +150,14 @@ public List<CoreExtensionEntry> loadCoreExtensions(
         }
     }
 
-    private List<CoreExtensionEntry> resolveCoreExtensions(
+    private List<LoadedCoreExtension> resolveCoreExtensions(
             RepositorySystemSession repoSession,
             List<RemoteRepository> repositories,
             Set<String> providedArtifacts,
             List<CoreExtension> configuration,
             UnaryOperator<String> interpolator)
             throws Exception {
-        List<CoreExtensionEntry> extensions = new ArrayList<>();
+        List<LoadedCoreExtension> extensions = new ArrayList<>();
 
         DependencyFilter dependencyFilter = new 
ExclusionsDependencyFilter(providedArtifacts);
 
@@ -165,7 +165,7 @@ private List<CoreExtensionEntry> resolveCoreExtensions(
             List<Artifact> artifacts =
                     resolveExtension(extension, repoSession, repositories, 
dependencyFilter, interpolator);
             if (!artifacts.isEmpty()) {
-                extensions.add(createExtension(extension, artifacts));
+                extensions.add(new LoadedCoreExtension(extension, 
createExtension(extension, artifacts)));
             }
         }
 
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/LoadedCoreExtension.java
similarity index 64%
copy from 
impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
copy to 
impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/LoadedCoreExtension.java
index 1953ad233c..6679b60e41 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/LoadedCoreExtension.java
@@ -16,19 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.maven.cling.invoker;
+package org.apache.maven.cling.extensions;
 
-import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.cli.extensions.CoreExtension;
+import org.apache.maven.extension.internal.CoreExtensionEntry;
 
 /**
- * Container capsule factory.
- *
- * @param <C> The context type.
+ * Represents a core extension that has been selected to be loaded.
+ * @param coreExtension The extension configuration entry with location 
tracking.
+ * @param entry The loaded entry with descriptor.
  */
-public interface ContainerCapsuleFactory<C extends LookupContext> {
-    /**
-     * Creates container capsule.
-     */
-    @Nonnull
-    ContainerCapsule createContainerCapsule(LookupInvoker<C> invoker, C 
context) throws Exception;
-}
+public record LoadedCoreExtension(CoreExtension coreExtension, 
CoreExtensionEntry entry) {}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java
index 9cb49cb27d..caef621191 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java
@@ -25,9 +25,9 @@
 
 import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.cli.CoreExtensions;
 import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.ParserRequest;
-import org.apache.maven.api.cli.extensions.CoreExtension;
 
 import static java.util.Objects.requireNonNull;
 
@@ -41,7 +41,7 @@ public abstract class BaseInvokerRequest implements 
InvokerRequest {
     private final Map<String, String> systemProperties;
     private final Path topDirectory;
     private final Path rootDirectory;
-    private final List<CoreExtension> coreExtensions;
+    private final List<CoreExtensions> coreExtensions;
 
     @SuppressWarnings("ParameterNumber")
     public BaseInvokerRequest(
@@ -54,7 +54,7 @@ public BaseInvokerRequest(
             @Nonnull Map<String, String> systemProperties,
             @Nonnull Path topDirectory,
             @Nullable Path rootDirectory,
-            @Nullable List<CoreExtension> coreExtensions) {
+            @Nullable List<CoreExtensions> coreExtensions) {
         this.parserRequest = requireNonNull(parserRequest);
         this.parsingFailed = parsingFailed;
         this.cwd = requireNonNull(cwd);
@@ -114,7 +114,7 @@ public Optional<Path> rootDirectory() {
     }
 
     @Override
-    public Optional<List<CoreExtension>> coreExtensions() {
+    public Optional<List<CoreExtensions>> coreExtensions() {
         return Optional.ofNullable(coreExtensions);
     }
 }
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
index 7c2737be98..d7e7e869c1 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java
@@ -34,14 +34,18 @@
 import java.util.Objects;
 import java.util.Properties;
 import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
 
 import org.apache.maven.api.Constants;
 import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.cli.CoreExtensions;
 import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.Options;
 import org.apache.maven.api.cli.Parser;
 import org.apache.maven.api.cli.ParserRequest;
 import org.apache.maven.api.cli.extensions.CoreExtension;
+import org.apache.maven.api.cli.extensions.InputLocation;
+import org.apache.maven.api.cli.extensions.InputSource;
 import org.apache.maven.api.services.Interpolator;
 import org.apache.maven.cling.internal.extension.io.CoreExtensionsStaxReader;
 import org.apache.maven.cling.props.MavenPropertiesLoader;
@@ -79,7 +83,9 @@ public LocalContext(ParserRequest parserRequest) {
         @Nullable
         public Path rootDirectory;
 
-        public List<CoreExtension> extensions;
+        @Nullable
+        public List<CoreExtensions> extensions;
+
         public Options options;
 
         public Map<String, String> extraInterpolationSource() {
@@ -425,54 +431,48 @@ protected Map<String, String> 
populateUserProperties(LocalContext context) {
 
     protected abstract Options assembleOptions(List<Options> parsedOptions);
 
-    protected List<CoreExtension> readCoreExtensionsDescriptor(LocalContext 
context) {
-        String installationExtensionsFile = 
context.userProperties.get(Constants.MAVEN_INSTALLATION_EXTENSIONS);
-        ArrayList<CoreExtension> installationExtensions = new 
ArrayList<>(readCoreExtensionsDescriptorFromFile(
-                
context.installationDirectory.resolve(installationExtensionsFile)));
-
-        String userExtensionsFile = 
context.userProperties.get(Constants.MAVEN_USER_EXTENSIONS);
-        ArrayList<CoreExtension> userExtensions = new ArrayList<>(
-                
readCoreExtensionsDescriptorFromFile(context.userHomeDirectory.resolve(userExtensionsFile)));
-
-        String projectExtensionsFile = 
context.userProperties.get(Constants.MAVEN_PROJECT_EXTENSIONS);
-        ArrayList<CoreExtension> projectExtensions =
-                new 
ArrayList<>(readCoreExtensionsDescriptorFromFile(context.cwd.resolve(projectExtensionsFile)));
-
-        // merge these 3 but check for GA uniqueness; we don't want to load up 
same extension w/ different Vs
-        HashMap<String, String> gas = new HashMap<>();
-        ArrayList<String> conflicts = new ArrayList<>();
-
-        ArrayList<CoreExtension> coreExtensions =
-                new ArrayList<>(installationExtensions.size() + 
userExtensions.size() + projectExtensions.size());
-        coreExtensions.addAll(mergeExtensions(installationExtensions, 
installationExtensionsFile, gas, conflicts));
-        coreExtensions.addAll(mergeExtensions(userExtensions, 
userExtensionsFile, gas, conflicts));
-        coreExtensions.addAll(mergeExtensions(projectExtensions, 
projectExtensionsFile, gas, conflicts));
-
-        if (!conflicts.isEmpty()) {
-            throw new IllegalStateException("Extension conflicts: " + 
String.join("; ", conflicts));
+    /**
+     * Important: This method must return list of {@link CoreExtensions} in 
precedence order.
+     */
+    protected List<CoreExtensions> readCoreExtensionsDescriptor(LocalContext 
context) {
+        ArrayList<CoreExtensions> result = new ArrayList<>();
+        Path file;
+        List<CoreExtension> loaded;
+
+        // project
+        file = 
context.cwd.resolve(context.userProperties.get(Constants.MAVEN_PROJECT_EXTENSIONS));
+        loaded = readCoreExtensionsDescriptorFromFile(file);
+        if (!loaded.isEmpty()) {
+            result.add(new CoreExtensions(file, loaded));
         }
 
-        return coreExtensions;
-    }
+        // user
+        file = 
context.userHomeDirectory.resolve(context.userProperties.get(Constants.MAVEN_USER_EXTENSIONS));
+        loaded = readCoreExtensionsDescriptorFromFile(file);
+        if (!loaded.isEmpty()) {
+            result.add(new CoreExtensions(file, loaded));
+        }
 
-    private List<CoreExtension> mergeExtensions(
-            List<CoreExtension> extensions, String extensionsSource, 
Map<String, String> gas, List<String> conflicts) {
-        for (CoreExtension extension : extensions) {
-            String ga = extension.getGroupId() + ":" + 
extension.getArtifactId();
-            if (gas.containsKey(ga)) {
-                conflicts.add(ga + " from " + extensionsSource + " already 
specified in " + gas.get(ga));
-            } else {
-                gas.put(ga, extensionsSource);
-            }
+        // installation
+        file = context.installationDirectory.resolve(
+                
context.userProperties.get(Constants.MAVEN_INSTALLATION_EXTENSIONS));
+        loaded = readCoreExtensionsDescriptorFromFile(file);
+        if (!loaded.isEmpty()) {
+            result.add(new CoreExtensions(file, loaded));
         }
-        return extensions;
+
+        return result.isEmpty() ? null : List.copyOf(result);
     }
 
     protected List<CoreExtension> readCoreExtensionsDescriptorFromFile(Path 
extensionsFile) {
         try {
             if (extensionsFile != null && Files.exists(extensionsFile)) {
                 try (InputStream is = Files.newInputStream(extensionsFile)) {
-                    return new CoreExtensionsStaxReader().read(is, 
true).getExtensions();
+                    return validateCoreExtensionsDescriptorFromFile(
+                            extensionsFile,
+                            List.copyOf(new CoreExtensionsStaxReader()
+                                    .read(is, true, new 
InputSource(extensionsFile.toString()))
+                                    .getExtensions()));
                 }
             }
             return List.of();
@@ -480,4 +480,24 @@ protected List<CoreExtension> 
readCoreExtensionsDescriptorFromFile(Path extensio
             throw new IllegalArgumentException("Failed to parse extensions 
file: " + extensionsFile, e);
         }
     }
+
+    protected List<CoreExtension> validateCoreExtensionsDescriptorFromFile(
+            Path extensionFile, List<CoreExtension> coreExtensions) {
+        Map<String, List<InputLocation>> gasLocations = new HashMap<>();
+        for (CoreExtension coreExtension : coreExtensions) {
+            String ga = coreExtension.getGroupId() + ":" + 
coreExtension.getArtifactId();
+            InputLocation location = coreExtension.getLocation("");
+            gasLocations.computeIfAbsent(ga, k -> new 
ArrayList<>()).add(location);
+        }
+        if (gasLocations.values().stream().noneMatch(l -> l.size() > 1)) {
+            return coreExtensions;
+        }
+        throw new IllegalStateException("Extension conflicts in file " + 
extensionFile + ": "
+                + gasLocations.entrySet().stream()
+                        .map(e -> e.getKey() + " defined on lines "
+                                + e.getValue().stream()
+                                        .map(l -> 
String.valueOf(l.getLineNumber()))
+                                        .collect(Collectors.joining(", ")))
+                        .collect(Collectors.joining("; ")));
+    }
 }
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
index 1953ad233c..e044f441ed 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
@@ -30,5 +30,6 @@ public interface ContainerCapsuleFactory<C extends 
LookupContext> {
      * Creates container capsule.
      */
     @Nonnull
-    ContainerCapsule createContainerCapsule(LookupInvoker<C> invoker, C 
context) throws Exception;
+    ContainerCapsule createContainerCapsule(
+            LookupInvoker<C> invoker, C context, CoreExtensionSelector<C> 
coreExtensionSelector) throws Exception;
 }
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CoreExtensionSelector.java
similarity index 67%
copy from 
impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
copy to 
impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CoreExtensionSelector.java
index 1953ad233c..e45f12aeaf 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/ContainerCapsuleFactory.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/CoreExtensionSelector.java
@@ -18,17 +18,21 @@
  */
 package org.apache.maven.cling.invoker;
 
+import java.util.List;
+
 import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.cli.InvokerRequest;
+import org.apache.maven.api.cli.extensions.CoreExtension;
 
 /**
- * Container capsule factory.
+ * Core extension selector: selects which entries to load from {@link 
InvokerRequest#coreExtensions()}.
  *
  * @param <C> The context type.
  */
-public interface ContainerCapsuleFactory<C extends LookupContext> {
+public interface CoreExtensionSelector<C extends LookupContext> {
     /**
-     * Creates container capsule.
+     * Selects core extensions to be loaded from list of all sources detected.
      */
     @Nonnull
-    ContainerCapsule createContainerCapsule(LookupInvoker<C> invoker, C 
context) throws Exception;
+    List<CoreExtension> selectCoreExtensions(LookupInvoker<C> invoker, C 
context);
 }
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
index abd90ebe9f..ccdfa43700 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java
@@ -503,7 +503,8 @@ protected void preCommands(C context) throws Exception {
 
     protected void container(C context) throws Exception {
         if (context.lookup == null) {
-            context.containerCapsule = 
createContainerCapsuleFactory().createContainerCapsule(this, context);
+            context.containerCapsule = createContainerCapsuleFactory()
+                    .createContainerCapsule(this, context, 
createCoreExtensionSelector());
             context.closeables.add(context::closeContainer);
             context.lookup = context.containerCapsule.getLookup();
         } else {
@@ -511,6 +512,10 @@ protected void container(C context) throws Exception {
         }
     }
 
+    protected CoreExtensionSelector<C> createCoreExtensionSelector() {
+        return new PrecedenceCoreExtensionSelector<>();
+    }
+
     protected ContainerCapsuleFactory<C> createContainerCapsuleFactory() {
         return new PlexusContainerCapsuleFactory<>();
     }
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java
index 98c8618872..15e4630c9a 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java
@@ -31,13 +31,13 @@
 import com.google.inject.Module;
 import org.apache.maven.api.Constants;
 import org.apache.maven.api.ProtoSession;
-import org.apache.maven.api.cli.InvokerRequest;
 import org.apache.maven.api.cli.Logger;
 import org.apache.maven.api.cli.extensions.CoreExtension;
 import org.apache.maven.api.services.MessageBuilderFactory;
 import org.apache.maven.api.services.SettingsBuilder;
 import org.apache.maven.cling.extensions.BootstrapCoreExtensionManager;
 import org.apache.maven.cling.extensions.ExtensionConfigurationModule;
+import org.apache.maven.cling.extensions.LoadedCoreExtension;
 import org.apache.maven.cling.logging.Slf4jLoggerManager;
 import org.apache.maven.di.Injector;
 import org.apache.maven.execution.DefaultMavenExecutionRequest;
@@ -60,6 +60,7 @@
 import org.codehaus.plexus.logging.LoggerManager;
 import org.slf4j.ILoggerFactory;
 
+import static java.util.Objects.requireNonNull;
 import static org.apache.maven.cling.invoker.Utils.toPlexusLoggingLevel;
 
 /**
@@ -69,20 +70,33 @@
  */
 public class PlexusContainerCapsuleFactory<C extends LookupContext> implements 
ContainerCapsuleFactory<C> {
     @Override
-    public ContainerCapsule createContainerCapsule(LookupInvoker<C> invoker, C 
context) throws Exception {
+    public ContainerCapsule createContainerCapsule(
+            LookupInvoker<C> invoker, C context, CoreExtensionSelector<C> 
coreExtensionSelector) throws Exception {
+        requireNonNull(invoker, "invoker");
+        requireNonNull(context, "context");
+        requireNonNull(coreExtensionSelector, "coreExtensionSelector");
         return new PlexusContainerCapsule(
-                context, Thread.currentThread().getContextClassLoader(), 
container(invoker, context));
+                context,
+                Thread.currentThread().getContextClassLoader(),
+                container(invoker, context, coreExtensionSelector));
     }
 
-    protected DefaultPlexusContainer container(LookupInvoker<C> invoker, C 
context) throws Exception {
-        ClassWorld classWorld = invoker.protoLookup.lookup(ClassWorld.class);
+    protected DefaultPlexusContainer container(
+            LookupInvoker<C> invoker, C context, CoreExtensionSelector<C> 
coreExtensionSelector) throws Exception {
+        ClassWorld classWorld = 
requireNonNull(invoker.protoLookup.lookup(ClassWorld.class), "classWorld");
         ClassRealm coreRealm = classWorld.getClassRealm("plexus.core");
         List<Path> extClassPath = parseExtClasspath(context);
         CoreExtensionEntry coreEntry = 
CoreExtensionEntry.discoverFrom(coreRealm);
-        List<CoreExtensionEntry> extensions =
-                loadCoreExtensions(invoker, context, coreRealm, 
coreEntry.getExportedArtifacts());
+        List<LoadedCoreExtension> loadedExtensions = loadCoreExtensions(
+                invoker,
+                context,
+                coreRealm,
+                coreEntry.getExportedArtifacts(),
+                coreExtensionSelector.selectCoreExtensions(invoker, context));
+        List<CoreExtensionEntry> loadedExtensionsEntries =
+                
loadedExtensions.stream().map(LoadedCoreExtension::entry).toList();
         ClassRealm containerRealm =
-                setupContainerRealm(context.logger, classWorld, coreRealm, 
extClassPath, extensions);
+                setupContainerRealm(context.logger, classWorld, coreRealm, 
extClassPath, loadedExtensionsEntries);
         ContainerConfiguration cc = new DefaultContainerConfiguration()
                 .setClassWorld(classWorld)
                 .setRealm(containerRealm)
@@ -95,8 +109,8 @@ protected DefaultPlexusContainer container(LookupInvoker<C> 
invoker, C context)
 
         CoreExports exports = new CoreExports(
                 containerRealm,
-                collectExportedArtifacts(coreEntry, extensions),
-                collectExportedPackages(coreEntry, extensions));
+                collectExportedArtifacts(coreEntry, loadedExtensionsEntries),
+                collectExportedPackages(coreEntry, loadedExtensionsEntries));
         Thread.currentThread().setContextClassLoader(containerRealm);
         DefaultPlexusContainer container = new DefaultPlexusContainer(cc, 
getCustomModule(context, exports));
 
@@ -113,24 +127,35 @@ protected DefaultPlexusContainer 
container(LookupInvoker<C> invoker, C context)
             }
             return value;
         };
-        for (CoreExtensionEntry extension : extensions) {
+        List<Throwable> failures = new ArrayList<>();
+        for (LoadedCoreExtension extension : loadedExtensions) {
             container.discoverComponents(
-                    extension.getClassRealm(),
+                    extension.entry().getClassRealm(),
                     new AbstractModule() {
                         @Override
                         protected void configure() {
                             try {
-                                
container.lookup(Injector.class).discover(extension.getClassRealm());
+                                container
+                                        .lookup(Injector.class)
+                                        
.discover(extension.entry().getClassRealm());
                             } catch (Throwable e) {
-                                context.logger.warn("Maven DI failure", e);
+                                failures.add(new IllegalStateException(
+                                        "Injection failure in "
+                                                + 
extension.coreExtension().getId(),
+                                        e));
                             }
                         }
                     },
                     new 
SessionScopeModule(container.lookup(SessionScope.class)),
                     new 
MojoExecutionScopeModule(container.lookup(MojoExecutionScope.class)),
-                    new ExtensionConfigurationModule(extension, 
extensionSource));
+                    new ExtensionConfigurationModule(extension.entry(), 
extensionSource));
+        }
+        if (!failures.isEmpty()) {
+            IllegalStateException mavenDiFailed = new IllegalStateException(
+                    "Maven dependency injection failed for at least one of the 
registered core extension");
+            failures.forEach(mavenDiFailed::addSuppressed);
+            throw mavenDiFailed;
         }
-
         
container.getLoggerManager().setThresholds(toPlexusLoggingLevel(context.loggerLevel));
         customizeContainer(context, container);
 
@@ -241,16 +266,16 @@ protected ClassRealm setupContainerRealm(
         return coreRealm;
     }
 
-    protected List<CoreExtensionEntry> loadCoreExtensions(
-            LookupInvoker<C> invoker, C context, ClassRealm containerRealm, 
Set<String> providedArtifacts)
+    protected List<LoadedCoreExtension> loadCoreExtensions(
+            LookupInvoker<C> invoker,
+            C context,
+            ClassRealm containerRealm,
+            Set<String> providedArtifacts,
+            List<CoreExtension> extensions)
             throws Exception {
-        InvokerRequest invokerRequest = context.invokerRequest;
-        if (invokerRequest.coreExtensions().isEmpty()
-                || invokerRequest.coreExtensions().get().isEmpty()) {
-            return Collections.emptyList();
+        if (extensions.isEmpty()) {
+            return List.of();
         }
-
-        List<CoreExtension> extensions = invokerRequest.coreExtensions().get();
         ContainerConfiguration cc = new DefaultContainerConfiguration()
                 .setClassWorld(containerRealm.getWorld())
                 .setRealm(containerRealm)
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PrecedenceCoreExtensionSelector.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PrecedenceCoreExtensionSelector.java
new file mode 100644
index 0000000000..6edfde98ee
--- /dev/null
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PrecedenceCoreExtensionSelector.java
@@ -0,0 +1,96 @@
+/*
+ * 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.cling.invoker;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.maven.api.cli.CoreExtensions;
+import org.apache.maven.api.cli.InvokerRequest;
+import org.apache.maven.api.cli.extensions.CoreExtension;
+import org.apache.maven.api.cli.extensions.InputLocation;
+
+public class PrecedenceCoreExtensionSelector<C extends LookupContext> 
implements CoreExtensionSelector<C> {
+    @Override
+    public List<CoreExtension> selectCoreExtensions(LookupInvoker<C> invoker, 
C context) {
+        Optional<List<CoreExtensions>> coreExtensions = 
context.invokerRequest.coreExtensions();
+        if (coreExtensions.isEmpty() || coreExtensions.get().isEmpty()) {
+            return List.of();
+        }
+
+        return selectCoreExtensions(
+                context, 
context.invokerRequest.coreExtensions().orElseThrow());
+    }
+
+    /**
+     * Selects extensions to load discovered from various sources by 
precedence ("first wins"), as
+     * {@link InvokerRequest#coreExtensions()} is in precedence order. Also 
reports conflicts, if any.
+     * Finally, at DEBUG level reports configured vs selected extensions.
+     */
+    protected List<CoreExtension> selectCoreExtensions(C context, 
List<CoreExtensions> configuredCoreExtensions) {
+        context.logger.debug("Configured core extensions (in precedence 
order):");
+        for (CoreExtensions source : configuredCoreExtensions) {
+            context.logger.debug("* Source file: " + source.source());
+            for (CoreExtension extension : source.coreExtensions()) {
+                context.logger.debug("  - " + extension.getId() + " -> " + 
formatLocation(extension.getLocation("")));
+            }
+        }
+
+        LinkedHashMap<String, CoreExtension> selectedExtensions = new 
LinkedHashMap<>();
+        List<String> conflicts = new ArrayList<>();
+        for (CoreExtensions coreExtensions : configuredCoreExtensions) {
+            for (CoreExtension coreExtension : 
coreExtensions.coreExtensions()) {
+                String key = coreExtension.getGroupId() + ":" + 
coreExtension.getArtifactId();
+                CoreExtension conflict = selectedExtensions.putIfAbsent(key, 
coreExtension);
+                if (conflict != null) {
+                    conflicts.add(String.format(
+                            "Conflicting extension %s: %s vs %s",
+                            key,
+                            formatLocation(conflict.getLocation("")),
+                            formatLocation(coreExtension.getLocation(""))));
+                }
+            }
+        }
+        if (!conflicts.isEmpty()) {
+            context.logger.warn("Found " + conflicts.size() + " extension 
conflict(s):");
+            for (String conflict : conflicts) {
+                context.logger.warn("* " + conflict);
+            }
+            context.logger.warn("");
+            context.logger.warn(
+                    "Order of core extensions precedence is project > user > 
installation. Selected extensions are:");
+            for (CoreExtension extension : selectedExtensions.values()) {
+                context.logger.warn(
+                        "* " + extension.getId() + " configured in " + 
formatLocation(extension.getLocation("")));
+            }
+        }
+
+        context.logger.debug("Selected core extensions (in loading order):");
+        for (CoreExtension source : selectedExtensions.values()) {
+            context.logger.debug("* " + source.getId() + ": " + 
formatLocation(source.getLocation("")));
+        }
+        return List.copyOf(selectedExtensions.values());
+    }
+
+    protected String formatLocation(InputLocation location) {
+        return location.getSource().getLocation() + ":" + 
location.getLineNumber();
+    }
+}
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvokerRequest.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvokerRequest.java
index efd5e9ecec..2df07aec03 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvokerRequest.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvokerRequest.java
@@ -23,8 +23,8 @@
 import java.util.Map;
 
 import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.cli.CoreExtensions;
 import org.apache.maven.api.cli.ParserRequest;
-import org.apache.maven.api.cli.extensions.CoreExtension;
 import org.apache.maven.api.cli.mvn.MavenOptions;
 import org.apache.maven.cling.invoker.BaseInvokerRequest;
 
@@ -47,7 +47,7 @@ public MavenInvokerRequest(
             Map<String, String> systemProperties,
             Path topDirectory,
             Path rootDirectory,
-            List<CoreExtension> coreExtensions,
+            List<CoreExtensions> coreExtensions,
             MavenOptions options) {
         super(
                 parserRequest,
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvokerRequest.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvokerRequest.java
index 2b2900b598..f2847b624f 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvokerRequest.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnenc/EncryptInvokerRequest.java
@@ -23,8 +23,8 @@
 import java.util.Map;
 
 import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.cli.CoreExtensions;
 import org.apache.maven.api.cli.ParserRequest;
-import org.apache.maven.api.cli.extensions.CoreExtension;
 import org.apache.maven.api.cli.mvnenc.EncryptOptions;
 import org.apache.maven.cling.invoker.BaseInvokerRequest;
 
@@ -44,7 +44,7 @@ public EncryptInvokerRequest(
             Map<String, String> systemProperties,
             Path topDirectory,
             Path rootDirectory,
-            List<CoreExtension> coreExtensions,
+            List<CoreExtensions> coreExtensions,
             EncryptOptions options) {
         super(
                 parserRequest,
diff --git 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvokerRequest.java
 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvokerRequest.java
index 9268b9ed30..5a841a9658 100644
--- 
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvokerRequest.java
+++ 
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnsh/ShellInvokerRequest.java
@@ -23,8 +23,8 @@
 import java.util.Map;
 
 import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.cli.CoreExtensions;
 import org.apache.maven.api.cli.ParserRequest;
-import org.apache.maven.api.cli.extensions.CoreExtension;
 import org.apache.maven.api.cli.mvnsh.ShellOptions;
 import org.apache.maven.cling.invoker.BaseInvokerRequest;
 
@@ -44,7 +44,7 @@ public ShellInvokerRequest(
             Map<String, String> systemProperties,
             Path topDirectory,
             Path rootDirectory,
-            List<CoreExtension> coreExtensions,
+            List<CoreExtensions> coreExtensions,
             ShellOptions options) {
         super(
                 parserRequest,
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTest.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTest.java
index 9bc896c030..de8b4fdf7b 100644
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTest.java
+++ 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTest.java
@@ -37,6 +37,7 @@
 import org.junit.jupiter.api.io.CleanupMode;
 import org.junit.jupiter.api.io.TempDir;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -47,10 +48,9 @@
 @Order(200)
 public class MavenInvokerTest extends MavenInvokerTestSupport {
     @Override
-    protected Invoker createInvoker() {
-        return new MavenInvoker(ProtoLookup.builder()
-                .addMapping(ClassWorld.class, new ClassWorld("plexus.core", 
ClassLoader.getSystemClassLoader()))
-                .build());
+    protected Invoker createInvoker(ClassWorld classWorld) {
+        return new MavenInvoker(
+                ProtoLookup.builder().addMapping(ClassWorld.class, 
classWorld).build());
     }
 
     @Override
@@ -66,8 +66,59 @@ void defaultFs(
         invoke(cwd, userHome, List.of("verify"), List.of());
     }
 
+    /**
+     * Same source (user or project extensions.xml) must not contain same GA 
with different V.
+     */
     @Test
-    void conflictingExtensions(
+    void conflictingExtensionsFromSameSource(
+            @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path cwd,
+            @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path userHome)
+            throws Exception {
+        String projectExtensionsXml =
+                """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <extensions>
+                    <extension>
+                        <groupId>io.takari.maven</groupId>
+                        <artifactId>takari-smart-builder</artifactId>
+                        <version>1.0.2</version>
+                    </extension>
+                    <extension>
+                        <groupId>io.takari.maven</groupId>
+                        <artifactId>takari-smart-builder</artifactId>
+                        <version>1.0.1</version>
+                    </extension>
+                </extensions>
+                """;
+        Path dotMvn = cwd.resolve(".mvn");
+        Files.createDirectories(dotMvn);
+        Path projectExtensions = dotMvn.resolve("extensions.xml");
+        Files.writeString(projectExtensions, projectExtensionsXml);
+
+        String userExtensionsXml =
+                """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <extensions>
+                    <extension>
+                        <groupId>io.takari.maven</groupId>
+                        <artifactId>takari-smart-builder</artifactId>
+                        <version>1.0.2</version>
+                    </extension>
+                </extensions>
+                """;
+        Path userConf = userHome.resolve(".m2");
+        Files.createDirectories(userConf);
+        Path userExtensions = userConf.resolve("extensions.xml");
+        Files.writeString(userExtensions, userExtensionsXml);
+
+        assertThrows(InvokerException.class, () -> invoke(cwd, userHome, 
List.of("validate"), List.of()));
+    }
+
+    /**
+     * In case of conflict spanning different sources, precedence is applied: 
project > user > installation.
+     */
+    @Test
+    void conflictingExtensionsFromDifferentSource(
             @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path cwd,
             @TempDir(cleanup = CleanupMode.ON_SUCCESS) Path userHome)
             throws Exception {
@@ -76,9 +127,9 @@ void conflictingExtensions(
                 <?xml version="1.0" encoding="UTF-8"?>
                 <extensions>
                     <extension>
-                        <groupId>eu.maveniverse.maven.mimir</groupId>
-                        <artifactId>extension3</artifactId>
-                        <version>0.3.4</version>
+                        <groupId>io.takari.maven</groupId>
+                        <artifactId>takari-smart-builder</artifactId>
+                        <version>1.0.2</version>
                     </extension>
                 </extensions>
                 """;
@@ -92,7 +143,19 @@ void conflictingExtensions(
         Path userExtensions = userConf.resolve("extensions.xml");
         Files.writeString(userExtensions, extensionsXml);
 
-        assertThrows(InvokerException.class, () -> invoke(cwd, userHome, 
List.of("validate"), List.of()));
+        // this should not throw
+        assertDoesNotThrow(() -> invoke(cwd, userHome, List.of("validate"), 
List.of()));
+        // but warn
+
+        // [main] WARNING 
org.apache.maven.cling.invoker.PlexusContainerCapsuleFactory - Found 1 
extension conflict(s):
+        // [main] WARNING 
org.apache.maven.cling.invoker.PlexusContainerCapsuleFactory - * Conflicting 
extension
+        // eu.maveniverse.maven.mimir:extension3: 
/tmp/junit-191051426131307150/.mvn/extensions.xml:3 vs
+        // /tmp/junit-16591192886395443631/.m2/extensions.xml:3
+        // [main] WARNING 
org.apache.maven.cling.invoker.PlexusContainerCapsuleFactory -
+        // [main] WARNING 
org.apache.maven.cling.invoker.PlexusContainerCapsuleFactory - Order of core 
extensions
+        // precedence is project > user > installation. Selected extensions 
are:
+        // [main] WARNING 
org.apache.maven.cling.invoker.PlexusContainerCapsuleFactory - *
+        // eu.maveniverse.maven.mimir:extension3:0.3.4 configured in 
/tmp/junit-191051426131307150/.mvn/extensions.xml:3
     }
 
     @Test
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java
index 7f2db7c3ee..ba9246c639 100644
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java
+++ 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java
@@ -32,6 +32,7 @@
 import org.apache.maven.api.cli.Parser;
 import org.apache.maven.api.cli.ParserRequest;
 import org.apache.maven.jline.JLineMessageBuilderFactory;
+import org.codehaus.plexus.classworlds.ClassWorld;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
@@ -99,7 +100,8 @@ protected Map<String, String> invoke(Path cwd, Path 
userHome, Collection<String>
 
         HashMap<String, String> logs = new HashMap<>();
         Parser parser = createParser();
-        try (Invoker invoker = createInvoker()) {
+        try (ClassWorld classWorld = createClassWorld();
+                Invoker invoker = createInvoker(classWorld)) {
             for (String goal : goals) {
                 ByteArrayOutputStream stdout = new ByteArrayOutputStream();
                 ByteArrayOutputStream stderr = new ByteArrayOutputStream();
@@ -134,7 +136,11 @@ protected Map<String, String> invoke(Path cwd, Path 
userHome, Collection<String>
         return logs;
     }
 
-    protected abstract Invoker createInvoker();
+    protected ClassWorld createClassWorld() {
+        return new ClassWorld("plexus.core", 
ClassLoader.getSystemClassLoader());
+    }
+
+    protected abstract Invoker createInvoker(ClassWorld classWorld);
 
     protected abstract Parser createParser();
 }
diff --git 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenInvokerTest.java
 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenInvokerTest.java
index 7ecbcee663..8636316280 100644
--- 
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenInvokerTest.java
+++ 
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenInvokerTest.java
@@ -43,10 +43,9 @@
 public class ResidentMavenInvokerTest extends MavenInvokerTestSupport {
 
     @Override
-    protected Invoker createInvoker() {
-        return new ResidentMavenInvoker(ProtoLookup.builder()
-                .addMapping(ClassWorld.class, new ClassWorld("plexus.core", 
ClassLoader.getSystemClassLoader()))
-                .build());
+    protected Invoker createInvoker(ClassWorld classWorld) {
+        return new ResidentMavenInvoker(
+                ProtoLookup.builder().addMapping(ClassWorld.class, 
classWorld).build());
     }
 
     @Override

Reply via email to