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

davsclaus pushed a commit to branch CAMEL-26901-one-version
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 8ace784835ba6a8a947e0d4949582b0f68c301ba
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 12 22:46:23 2026 +0100

    CAMEL-26901: camel-jbang - camel run loads one version of each library
    
    The libraries already resolved for the class loader are passed as managed 
versions to the next resolutions, so a
    component loaded later gets the version already loaded (whatever the 
classifier, such as a native JAR of another
    platform), and the other version is not downloaded. The dependencies asked 
for directly keep their version.
    
    Before, 10 components (kafka, aws2-s3, sql, http, jms, mongodb, 
google-pubsub, azure-storage-blob, elasticsearch,
    infinispan) put 21 libraries on the classpath in more than one version, 
such as Netty in 4.1.127, 4.1.138 and
    4.1.139; now none.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  8 +++
 .../main/download/MavenDependencyDownloader.java   | 30 +++++++-
 .../camel/tooling/maven/MavenDownloader.java       | 18 +++++
 .../camel/tooling/maven/MavenDownloaderImpl.java   | 80 ++++++++++++++++++++++
 .../tooling/maven/MavenDownloaderImplTest.java     | 71 +++++++++++++++++++
 5 files changed, 205 insertions(+), 2 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index da6bbccf6431..ced6426a25b0 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -8385,3 +8385,11 @@ With `allowTemplateFromHeader=true`, a script from the 
`CamelLanguageScript` hea
 Before, with `cacheScript=true` it could replace the cached script of the 
endpoint, so later messages without the header
 ran it too. The header is now removed after use, so a later language endpoint 
in the route runs its own script instead
 of the header's. Set the header again before each language endpoint that 
should use it.
+
+=== camel-jbang - camel run loads one version of each library
+
+`camel run` downloads the JARs of each component when it is first used. A 
library that is already loaded is now used,
+in the version that was loaded first, for the components that are loaded 
later, as the `dependencyManagement` of a
+Maven project would. Before, a later component that depended on another 
version of the library added that version as
+well, so the classpath could hold, for example, three versions of Netty, and a 
class was loaded from whichever JAR
+came first. Use `--verbose` to see the JARs added for each dependency.
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
index 097e6772d13a..f721d208e7b9 100644
--- 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/download/MavenDependencyDownloader.java
@@ -29,6 +29,7 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Predicate;
 
 import javax.xml.parsers.DocumentBuilder;
@@ -71,6 +72,9 @@ public class MavenDependencyDownloader extends ServiceSupport 
implements Depende
     private final Set<DownloadListener> downloadListeners = new 
LinkedHashSet<>();
     private final Set<ArtifactDownloadListener> artifactDownloadListeners = 
new LinkedHashSet<>();
     private final Map<String, DownloadRecord> downloadRecords = new 
HashMap<>();
+    // the artifacts already resolved for the class loader 
(groupId:artifactId:classifier:extension -> artifact): their
+    // version is managed in the next resolutions, so a library is not loaded 
again in another version
+    private final Map<String, MavenGav> loaded = new ConcurrentHashMap<>();
     private KnownReposResolver knownReposResolver;
     private VersionResolver versionResolver;
     private boolean download = true;
@@ -339,7 +343,10 @@ public class MavenDependencyDownloader extends 
ServiceSupport implements Depende
                             a.getClassifier())
                     : null;
             List<MavenArtifact> artifacts = 
resolveDependenciesViaAether(parentGav, deps, extraRepositories,
-                    transitively, useApacheSnapshots, available);
+                    transitively, useApacheSnapshots, available, 
List.copyOf(loaded.values()));
+            for (MavenArtifact a : artifacts) {
+                loaded.putIfAbsent(managementKey(a.getGav()), a.getGav());
+            }
             List<File> files = new ArrayList<>();
             if (verbose) {
                 LOG.info("Dependencies: {} -> [{}]", gav, artifacts);
@@ -639,6 +646,10 @@ public class MavenDependencyDownloader extends 
ServiceSupport implements Depende
         mavenDownloader = mavenDownloaderImpl;
     }
 
+    private static String managementKey(MavenGav gav) {
+        return gav.getGroupId() + ":" + gav.getArtifactId() + ":" + 
gav.getClassifier() + ":" + gav.getPackaging();
+    }
+
     /**
      * Whether the class loader the JARs are added to sees the classpath the 
JVM was started with, so what is on it is
      * not added (nor downloaded) again. A class loader without the 
application class loader as parent, such as for
@@ -695,9 +706,24 @@ public class MavenDependencyDownloader extends 
ServiceSupport implements Depende
             String parentGav,
             List<String> depIds, Set<String> extraRepositories,
             boolean transitively, boolean useApacheSnapshots, 
Predicate<MavenGav> available) {
+        return resolveDependenciesViaAether(parentGav, depIds, 
extraRepositories, transitively, useApacheSnapshots,
+                available, null);
+    }
+
+    /**
+     * @param available the artifacts that are already available (such as on 
the classpath), which are left out together
+     *                  with their dependencies, or null to resolve them all
+     * @param managed   the artifacts whose version is used for the transitive 
dependencies on them (such as the ones
+     *                  already loaded), or null
+     */
+    public List<MavenArtifact> resolveDependenciesViaAether(
+            String parentGav,
+            List<String> depIds, Set<String> extraRepositories,
+            boolean transitively, boolean useApacheSnapshots, 
Predicate<MavenGav> available,
+            Collection<MavenGav> managed) {
         try {
             return mavenDownloader.resolveArtifacts(parentGav, depIds, 
extraRepositories, transitively, useApacheSnapshots,
-                    available);
+                    available, managed);
         } catch (MavenResolutionException e) {
             String repos = (e.getRepositories() == null || 
e.getRepositories().isEmpty())
                     ? "(empty URL list)"
diff --git 
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
 
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
index f808d83cd36d..6f4b67a67f00 100644
--- 
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
+++ 
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloader.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.tooling.maven;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Predicate;
@@ -79,6 +80,23 @@ public interface MavenDownloader extends Service {
         return resolveArtifacts(rootGav, dependencyGAVs, extraRepositories, 
transitively, useApacheSnapshots);
     }
 
+    /**
+     * Resolves the artifacts as {@link #resolveArtifacts(String, List, Set, 
boolean, boolean, Predicate)} does, with
+     * the versions of the managed artifacts: a transitive dependency on one 
of them gets its version, as with the
+     * dependencyManagement of a Maven project. For example, the libraries 
already loaded, so a library is not resolved
+     * again in another version.
+     *
+     * @param managed the artifacts whose version is used for the transitive 
dependencies on them, or null
+     */
+    default List<MavenArtifact> resolveArtifacts(
+            String rootGav,
+            List<String> dependencyGAVs, Set<String> extraRepositories,
+            boolean transitively, boolean useApacheSnapshots, 
Predicate<MavenGav> available,
+            Collection<MavenGav> managed)
+            throws MavenResolutionException {
+        return resolveArtifacts(rootGav, dependencyGAVs, extraRepositories, 
transitively, useApacheSnapshots, available);
+    }
+
     /**
      * Overload for {@link MavenDownloader#resolveArtifacts(String, List, Set, 
boolean, boolean)}
      */
diff --git 
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
 
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
index 56dd570af226..e9321b1841bd 100644
--- 
a/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
+++ 
b/tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenDownloaderImpl.java
@@ -25,11 +25,14 @@ import java.net.URL;
 import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Predicate;
@@ -60,6 +63,8 @@ import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.collection.CollectRequest;
 import org.eclipse.aether.collection.DependencyCollectionContext;
+import org.eclipse.aether.collection.DependencyManagement;
+import org.eclipse.aether.collection.DependencyManager;
 import org.eclipse.aether.collection.DependencySelector;
 import org.eclipse.aether.graph.Dependency;
 import org.eclipse.aether.graph.DependencyFilter;
@@ -590,6 +595,17 @@ public class MavenDownloaderImpl extends ServiceSupport 
implements MavenDownload
             List<String> dependencyGAVs, Set<String> extraRepositories,
             boolean transitively, boolean useApacheSnapshots, 
Predicate<MavenGav> available)
             throws MavenResolutionException {
+        return resolveArtifacts(rootGav, dependencyGAVs, extraRepositories, 
transitively, useApacheSnapshots, available,
+                null);
+    }
+
+    @Override
+    public List<MavenArtifact> resolveArtifacts(
+            String rootGav,
+            List<String> dependencyGAVs, Set<String> extraRepositories,
+            boolean transitively, boolean useApacheSnapshots, 
Predicate<MavenGav> available,
+            Collection<MavenGav> managed)
+            throws MavenResolutionException {
 
         // When preferLocal is enabled, what is already in the local 
repository is used as is: SNAPSHOT
         // metadata is not checked again remotely, while anything missing is 
still downloaded
@@ -601,10 +617,74 @@ public class MavenDownloaderImpl extends ServiceSupport 
implements MavenDownload
                     session.getDependencySelector(), new 
AvailableDependencySelector(available)));
             session = availableSession;
         }
+        if (managed != null && !managed.isEmpty() && transitively) {
+            // the versions of the managed artifacts, whatever their classifier
+            Map<String, String> versions = new HashMap<>();
+            for (MavenGav gav : managed) {
+                versions.putIfAbsent(gav.getGroupId() + ":" + 
gav.getArtifactId(), gav.getVersion());
+            }
+            DefaultRepositorySystemSession managedSession = new 
DefaultRepositorySystemSession(session);
+            managedSession.setDependencyManager(
+                    new 
ManagedVersionDependencyManager(session.getDependencyManager(), versions, 0));
+            session = managedSession;
+        }
         return doResolveArtifacts(rootGav, dependencyGAVs, extraRepositories,
                 transitively, useApacheSnapshots, session);
     }
 
+    /**
+     * Gives the transitive dependencies on the managed artifacts their 
version, whatever their classifier (such as a
+     * native JAR of another platform), as the dependencyManagement of a Maven 
project would. The dependencies asked for
+     * directly keep their version.
+     */
+    static final class ManagedVersionDependencyManager implements 
DependencyManager {
+
+        private final DependencyManager delegate;
+        private final Map<String, String> versions;
+        private final int depth;
+
+        ManagedVersionDependencyManager(DependencyManager delegate, 
Map<String, String> versions, int depth) {
+            this.delegate = delegate;
+            this.versions = versions;
+            this.depth = depth;
+        }
+
+        @Override
+        public DependencyManagement manageDependency(Dependency dependency) {
+            DependencyManagement management = delegate != null ? 
delegate.manageDependency(dependency) : null;
+            // depth 1 are the dependencies asked for
+            if (depth >= 2) {
+                Artifact a = dependency.getArtifact();
+                String version = versions.get(a.getGroupId() + ":" + 
a.getArtifactId());
+                if (version != null && !version.equals(a.getVersion())) {
+                    if (management == null) {
+                        management = new DependencyManagement();
+                    }
+                    management.setVersion(version);
+                }
+            }
+            return management;
+        }
+
+        @Override
+        public DependencyManager 
deriveChildManager(DependencyCollectionContext context) {
+            DependencyManager child = delegate != null ? 
delegate.deriveChildManager(context) : null;
+            return new ManagedVersionDependencyManager(child, versions, depth 
+ 1);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            // the collector caches by dependency manager
+            return o instanceof ManagedVersionDependencyManager other && depth 
== other.depth
+                    && versions == other.versions && Objects.equals(delegate, 
other.delegate);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(delegate, depth, 
System.identityHashCode(versions));
+        }
+    }
+
     /**
      * Leaves out the dependencies that are already available, such as on the 
classpath, together with their own
      * dependencies.
diff --git 
a/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
 
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
index 599983902a36..23fc8b73a362 100644
--- 
a/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
+++ 
b/tooling/camel-tooling-maven/src/test/java/org/apache/camel/tooling/maven/MavenDownloaderImplTest.java
@@ -1030,6 +1030,61 @@ class MavenDownloaderImplTest {
         }
     }
 
+    @Test
+    void testManagedVersionIsUsedForTransitiveDependencies() throws Exception {
+        // app -> lib 2.0.0, while lib 1.0.0 is already loaded
+        File repo = new File(tempDir, "managed-repo");
+        writeJar(repo, "app", "1.0.0",
+                """
+                          <dependencies>
+                            
<dependency><groupId>org.example.fixture</groupId><artifactId>lib</artifactId><version>2.0.0</version></dependency>
+                          </dependencies>
+                        """);
+        writeJar(repo, "app-native", "1.0.0",
+                """
+                          <dependencies>
+                            
<dependency><groupId>org.example.fixture</groupId><artifactId>lib</artifactId><version>2.0.0</version><classifier>linux-aarch_64</classifier></dependency>
+                          </dependencies>
+                        """);
+        writeJar(repo, "lib", "1.0.0", "");
+        writeJar(repo, "lib", "2.0.0", "");
+        File native1 = new File(repo, 
"org/example/fixture/lib/1.0.0/lib-1.0.0-linux-aarch_64.jar");
+        Files.writeString(native1.toPath(), "native");
+        Files.writeString(new File(repo, 
"org/example/fixture/lib/2.0.0/lib-2.0.0-linux-aarch_64.jar").toPath(), 
"native");
+        File localRepo = new File(tempDir, "managed-local-repo");
+
+        try (MavenDownloaderImpl downloader = new MavenDownloaderImpl()) {
+            downloader.setMavenCentralEnabled(false);
+            downloader.setMavenApacheSnapshotEnabled(false);
+            downloader.setMavenSettingsLocation("false");
+            downloader.setRepos(repo.toURI().toString());
+            downloader.build();
+            MavenDownloader customDownloader = 
downloader.customize(localRepo.getAbsolutePath(), 5000, 5000);
+            List<MavenGav> managed = 
List.of(MavenGav.parseGav("org.example.fixture:lib:1.0.0"));
+
+            List<MavenArtifact> artifacts = 
customDownloader.resolveArtifacts(null,
+                    List.of("org.example.fixture:app:1.0.0"), null, true, 
false, null, managed);
+
+            assertEquals(List.of("org.example.fixture:app:1.0.0", 
"org.example.fixture:lib:1.0.0"),
+                    artifacts.stream().map(a -> 
a.getGav().toString()).toList());
+            // the other version is not downloaded
+            assertFalse(new File(localRepo, 
"org/example/fixture/lib/2.0.0").exists());
+
+            // also for another classifier than the one loaded (such as a 
native JAR of another platform)
+            artifacts = customDownloader.resolveArtifacts(null, 
List.of("org.example.fixture:app-native:1.0.0"), null,
+                    true, false, null, managed);
+            assertEquals(List.of("org.example.fixture:app-native:1.0.0", 
"org.example.fixture:lib:1.0.0"),
+                    artifacts.stream().map(a -> 
a.getGav().toString()).toList());
+            assertEquals("linux-aarch_64", 
artifacts.get(1).getGav().getClassifier());
+
+            // a dependency asked for directly keeps its version
+            artifacts = customDownloader.resolveArtifacts(null, 
List.of("org.example.fixture:lib:2.0.0"), null, true,
+                    false, null, managed);
+            assertEquals(List.of("org.example.fixture:lib:2.0.0"),
+                    artifacts.stream().map(a -> 
a.getGav().toString()).toList());
+        }
+    }
+
     private static String dependencies(String... artifactIds) {
         StringBuilder sb = new StringBuilder("  <dependencies>\n");
         for (String artifactId : artifactIds) {
@@ -1039,6 +1094,22 @@ class MavenDownloaderImplTest {
         return sb.append("  </dependencies>\n").toString();
     }
 
+    private static void writeJar(File repo, String artifactId, String version, 
String body) throws Exception {
+        File dir = new File(repo, "org/example/fixture/" + artifactId + "/" + 
version);
+        assertTrue(dir.mkdirs() || dir.isDirectory());
+        String pom = """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <project xmlns="http://maven.apache.org/POM/4.0.0";>
+                  <modelVersion>4.0.0</modelVersion>
+                  <groupId>org.example.fixture</groupId>
+                  <artifactId>%s</artifactId>
+                  <version>%s</version>
+                %s</project>
+                """.formatted(artifactId, version, body);
+        Files.writeString(new File(dir, artifactId + "-" + version + 
".pom").toPath(), pom);
+        Files.writeString(new File(dir, artifactId + "-" + version + 
".jar").toPath(), artifactId);
+    }
+
     private static void writeJar(File repo, String artifactId, String body) 
throws Exception {
         writePom(repo, artifactId, body);
         File dir = new File(repo, "org/example/fixture/" + artifactId + 
"/1.0.0");

Reply via email to