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");
