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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new b240602dbb1d CAMEL-26441: camel-launcher - The distribution runs the 
Camel CLI from the Spring Boot fat jar, which is slower and has no class data 
sharing (#28776)
b240602dbb1d is described below

commit b240602dbb1d91a1cfe5d454679b68a76fb6e06b
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 31 22:00:28 2026 +0100

    CAMEL-26441: camel-launcher - The distribution runs the Camel CLI from the 
Spring Boot fat jar, which is slower and has no class data sharing (#28776)
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .github/workflows/camel-launcher-native-exe.yml    |  18 +++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  20 ++++
 .../modules/ROOT/pages/camel-jbang-launcher.adoc   |  26 ++++-
 .../dsl/jbang/core/common/LauncherHelper.java      |  13 ++-
 .../dsl/jbang/core/common/LauncherHelperTest.java  |  10 ++
 .../camel-jbang-plugin-generate/pom.xml            |   5 +
 dsl/camel-jbang/camel-launcher/README.md           |   8 +-
 dsl/camel-jbang/camel-launcher/pom.xml             |  23 +++++
 .../src/main/assembly/launcher-content.xml         |  32 +++---
 .../src/main/assembly/launcher-jar.xml             |  34 ++++++
 .../camel/dsl/jbang/launcher/CamelLauncher.java    |   5 +-
 .../src/main/resources/bin/camel.bat               |  48 ++++++++-
 .../camel-launcher/src/main/resources/bin/camel.sh |  37 ++++++-
 .../dsl/jbang/launcher/CamelBatDiscoveryTest.java  |  91 ++++++++++++++--
 .../dsl/jbang/launcher/CamelShDiscoveryTest.java   | 114 ++++++++++++++++++++-
 .../apache/camel/dsl/jbang/launcher/FakeJava.java  |  19 +++-
 .../jbang/launcher/LauncherArchiveLayoutIT.java    |  99 ++++++++++++++++++
 .../camel/dsl/jbang/launcher/PackagePlanTest.java  |   5 +-
 18 files changed, 559 insertions(+), 48 deletions(-)

diff --git a/.github/workflows/camel-launcher-native-exe.yml 
b/.github/workflows/camel-launcher-native-exe.yml
index 910bc2fe423e..7a09a8963a51 100644
--- a/.github/workflows/camel-launcher-native-exe.yml
+++ b/.github/workflows/camel-launcher-native-exe.yml
@@ -239,6 +239,18 @@ jobs:
             exit 1
           fi
           echo "Both bin/camel-x64.exe and bin/camel-arm64.exe found in 
archive"
+      - name: Run the Camel CLI from the archive
+        run: |
+          # bin/camel.sh runs lib/camel-launcher-<version>.jar, whose manifest 
names the other JARs of lib/
+          ZIP=$(find dsl/camel-jbang/camel-launcher/target -maxdepth 1 -name 
'camel-launcher-*-bin.zip' ! -name '*-winget-bin.zip' -print -quit)
+          CLI_DIR="$RUNNER_TEMP/camel-cli"
+          rm -rf "$CLI_DIR" && mkdir -p "$CLI_DIR" && unzip -q "$ZIP" -d 
"$CLI_DIR"
+          CAMEL=$(find "$CLI_DIR" -path '*/bin/camel.sh' -print -quit)
+          export XDG_CACHE_HOME="$RUNNER_TEMP/camel-cli-cache"
+          # the first run creates the CDS archive of the command, the second 
one starts with it
+          sh "$CAMEL" version
+          sh "$CAMEL" version | grep -q 'Camel CLI version'
+          ls -l "$XDG_CACHE_HOME"/camel-cli/cds/*/version.jsa
       - name: Verify reproducible native executables and WinGet archive
         run: |
           HASH_FILE="$RUNNER_TEMP/winget-first-build.sha256"
@@ -273,10 +285,10 @@ jobs:
             diff <(unzip -v "$FIRST_BUILD"/*-winget-bin.zip) <(unzip -v 
"$ZIP") || true
 
             # The launcher JAR is itself an archive, so a CRC change above 
says nothing about which of
-            # its thousands of entries moved. Unpack both and diff one level 
down.
+            # its entries moved. Unpack both and diff one level down.
             echo "=== differing entries inside the launcher JAR ==="
-            unzip -qo "$FIRST_BUILD"/*-winget-bin.zip 
'*/bin/camel-launcher-*.jar' -d "$RUNNER_TEMP/jar-first"
-            unzip -qo "$ZIP" '*/bin/camel-launcher-*.jar' -d 
"$RUNNER_TEMP/jar-second"
+            unzip -qo "$FIRST_BUILD"/*-winget-bin.zip 
'*/lib/camel-launcher-*.jar' -d "$RUNNER_TEMP/jar-first"
+            unzip -qo "$ZIP" '*/lib/camel-launcher-*.jar' -d 
"$RUNNER_TEMP/jar-second"
             diff <(unzip -v "$(find "$RUNNER_TEMP/jar-first" -name '*.jar' 
-print -quit)") \
                  <(unzip -v "$(find "$RUNNER_TEMP/jar-second" -name '*.jar' 
-print -quit)") \
               | head -60 || true
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 881333655347..3dd6687f2d48 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
@@ -4339,6 +4339,26 @@ To find the dependencies, `camel export` boots the 
integration silently, as befo
   as `prod`, which fails on an insecure option) does not fail the export, nor 
does `camel.security.policy=fail`: the
   export only warns. Without `--profile` the dependencies are the same as 
before.
 
+=== camel-launcher - the distribution has the JARs in lib/
+
+The zip and tar of the Camel CLI (`camel-launcher-<version>-bin`, which 
Homebrew, SDKMAN, WinGet, Scoop, Chocolatey,
+the website installer and `camel self-update` install) no longer have the fat 
JAR in `bin/`. They have the JARs in
+`lib/`, with `lib/camel-launcher-<version>.jar`, whose manifest names the 
other JARs of `lib/` as its class path. The
+scripts in `bin/` run `java -jar lib/camel-launcher-<version>.jar`, so each 
command starts about 100 ms faster, and
+`camel --help` about 250 ms. A script or an image that runs the fat JAR from 
the extracted distribution runs
+`lib/camel-launcher-<version>.jar` instead, or the fat JAR from Maven Central, 
which has not changed.
+
+The JARs of `lib/` are in the order of the dependencies of `camel-launcher`, 
which is the class path its tests run
+with; the fat JAR had them in alphabetical order. `camel generate` no longer 
has `swagger-models` 2.2.52 next to
+`swagger-models-jakarta` 2.2.36, two JARs with the same classes.
+
+`bin/camel.sh` and `bin/camel.bat` turn on class data sharing: the JVM keeps 
the classes that a command loads in an
+archive, one per command and Camel version, in 
`${XDG_CACHE_HOME:-~/.cache}/camel-cli/cds` (`%LOCALAPPDATA%\camel-cli\cds`
+on Windows). The first run of a command creates its archive and takes longer 
(up to about a second), and the JVM
+creates it again when the JDK changes. `CAMEL_CDS=false` turns it off, and so 
do CDS or AOT options in `JAVA_OPTS`
+(such as `-Xshare:off`). The archives of a Camel version are not needed after 
an upgrade, and the directory can be
+removed at any time.
+
 === camel-once - the consumer fires on a Camel thread pool
 
 The once consumer now fires its route on a single thread scheduled executor 
that it creates through the
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-launcher.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-launcher.adoc
index 115843b935a9..80ec9294ad7c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-launcher.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-launcher.adoc
@@ -2,7 +2,7 @@
 
 *Available as of Camel 4.13*
 
-A self-contained executable JAR for running Camel CLI without JBang. Uses 
Spring Boot's nested JAR loader for fast startup and isolated classpath.
+The Camel CLI without JBang: a distribution with scripts to run it, and a 
self-contained executable JAR.
 
 Download from 
https://repo1.maven.org/maven2/org/apache/camel/camel-launcher/[Maven Central].
 
@@ -36,12 +36,32 @@ And then use the provided scripts:
 bin\camel.bat [command] [options]
 ----
 
+The distribution has the scripts in `bin/` and the JARs in `lib/`. The scripts 
run
+`java -jar lib/camel-launcher-<version>.jar`, whose manifest names the other 
JARs of `lib/` as its class path, which
+starts faster than the fat JAR.
+
+=== Class data sharing
+
+The scripts turn on class data sharing of the JVM: the classes that a command 
loads are kept in an archive, one per
+command and Camel version, so the command starts faster the next time. The 
first run of a command creates its archive,
+and takes longer, and the JVM creates it again when the JDK changes. The 
archives are in
+`${XDG_CACHE_HOME:-~/.cache}/camel-cli/cds` (`%LOCALAPPDATA%\camel-cli\cds` on 
Windows), and can be removed at any time.
+
+To turn it off, set `CAMEL_CDS=false`:
+
+[source,bash]
+----
+CAMEL_CDS=false camel version
+----
+
+CDS or AOT options in `JAVA_OPTS` (such as `-Xshare:off` or `-XX:AOTCache`) 
turn it off too. A JVM without class
+data sharing (such as OpenJ9) ignores the options.
+
 == Benefits
 
 - No JBang installation required
-- Faster startup (no dependency resolution, on-demand class loading)
+- Faster startup (no dependency resolution, class data sharing in the 
distribution)
 - Lower memory usage (only loads classes actually used)
-- No classpath conflicts (dependencies kept as separate nested JARs)
 - Pinned Camel version — upgrade by downloading a newer launcher
 
 == Limitations
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/LauncherHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/LauncherHelper.java
index c162aa2be6f1..eae29b4ff492 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/LauncherHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/LauncherHelper.java
@@ -67,9 +67,18 @@ public final class LauncherHelper {
         }
 
         // 2. Try to detect from code source location
+        return getJarPath(LauncherHelper.class);
+    }
+
+    /**
+     * Gets the path to the JAR file that has the class: the fat JAR when the 
class is in one.
+     *
+     * @param  type the class
+     * @return      the path, or {@code null} when it cannot be detected
+     */
+    public static String getJarPath(Class<?> type) {
         try {
-            URL location = LauncherHelper.class.getProtectionDomain()
-                    .getCodeSource().getLocation();
+            URL location = 
type.getProtectionDomain().getCodeSource().getLocation();
             if (location != null) {
                 return parseJarPath(location.toString());
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/LauncherHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/LauncherHelperTest.java
index 28801019de87..634b8c79a3a4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/LauncherHelperTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/LauncherHelperTest.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.dsl.jbang.core.common;
 
+import java.nio.file.Path;
+
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.EnabledOnOs;
 import org.junit.jupiter.api.condition.OS;
@@ -28,6 +30,14 @@ class LauncherHelperTest {
     // Real URL form: jar:nested:/outer.jar/!BOOT-INF/lib/inner.jar!/
     // The outer-jar boundary is /! (slash-bang), not !/ (bang-slash).
 
+    @Test
+    void theJarOfAClassIsTheJarThatHasIt() throws Exception {
+        // the launcher JAR of the distribution has the main class only, the 
classes of the CLI are in the other JARs
+        Path jar = 
Path.of(Test.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+
+        
assertThat(LauncherHelper.getJarPath(Test.class)).isEqualTo(jar.toString());
+    }
+
     @Test
     void parsesNestedJarUrlOnLinux() {
         String result = LauncherHelper.parseJarPath(
diff --git a/dsl/camel-jbang/camel-jbang-plugin-generate/pom.xml 
b/dsl/camel-jbang/camel-jbang-plugin-generate/pom.xml
index c010d5ec9f1d..922b7823e1aa 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-generate/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-plugin-generate/pom.xml
@@ -73,6 +73,11 @@
             <artifactId>openapi-generator</artifactId>
             <version>${openapi-generator-version}</version>
             <exclusions>
+                <!-- the same classes as swagger-models-jakarta above, of 
another version -->
+                <exclusion>
+                    <groupId>io.swagger.core.v3</groupId>
+                    <artifactId>swagger-models</artifactId>
+                </exclusion>
                 <exclusion>
                     <groupId>org.slf4j</groupId>
                     <artifactId>slf4j-api</artifactId>
diff --git a/dsl/camel-jbang/camel-launcher/README.md 
b/dsl/camel-jbang/camel-launcher/README.md
index 23e782f3526f..9d2150a0b46a 100644
--- a/dsl/camel-jbang/camel-launcher/README.md
+++ b/dsl/camel-jbang/camel-launcher/README.md
@@ -14,7 +14,8 @@ mvn clean package
 
 This will create:
 1. A self-executing JAR (`camel-launcher-<version>.jar`) in the `target` 
directory using Spring Boot's nested JAR structure
-2. Distribution archives (`camel-launcher-<version>-bin.zip` and 
`camel-launcher-<version>-bin.tar.gz`) in the `target` directory
+2. Distribution archives (`camel-launcher-<version>-bin.zip` and 
`camel-launcher-<version>-bin.tar.gz`) in the `target` directory,
+   with the scripts in `bin/` and the JARs in `lib/` (not the fat JAR)
 
 When `-Dcamel.exe.build=true` is enabled, Maven also creates a WinGet-only 
archive,
 `camel-launcher-<version>-winget-bin.zip`, containing the native Windows 
bootstraps built by
@@ -73,6 +74,11 @@ java -jar camel-launcher-<version>.jar run hello.java
    The native executables are confined to the WinGet-only archive. WinGet 
selects the executable matching the host
    architecture and exposes it as `camel.exe`.
 
+   The scripts run `java -jar lib/camel-launcher-<version>.jar`: a JAR with 
the classes of this module, whose manifest
+   names the other JARs of `lib/` as its class path 
(`src/main/assembly/launcher-jar.xml`). Classes of the class path,
+   unlike the classes of the nested JARs, can be kept in a class data sharing 
archive, which the scripts turn on (one
+   archive per command, in `${XDG_CACHE_HOME:-~/.cache}/camel-cli/cds`; 
`CAMEL_CDS=false` turns it off).
+
 ## Benefits
 
 - No need for JBang installation
diff --git a/dsl/camel-jbang/camel-launcher/pom.xml 
b/dsl/camel-jbang/camel-launcher/pom.xml
index 189db8fc5908..bf8cf66cbed4 100644
--- a/dsl/camel-jbang/camel-launcher/pom.xml
+++ b/dsl/camel-jbang/camel-launcher/pom.xml
@@ -332,6 +332,29 @@
                     <tarLongFileMode>posix</tarLongFileMode>
                 </configuration>
                 <executions>
+                    <!-- before the distributions, which put it in lib/ -->
+                    <execution>
+                        <id>launcher-jar</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <configuration>
+                            <descriptors combine.self="override">
+                                
<descriptor>src/main/assembly/launcher-jar.xml</descriptor>
+                            </descriptors>
+                            <attach>false</attach>
+                            <archive>
+                                <manifest>
+                                    
<mainClass>org.apache.camel.dsl.jbang.launcher.CamelLauncher</mainClass>
+                                    <addClasspath>true</addClasspath>
+                                    <!-- the names of the JARs in lib/ (see 
launcher-content.xml) -->
+                                    
<useUniqueVersions>false</useUniqueVersions>
+                                    
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                                </manifest>
+                            </archive>
+                        </configuration>
+                    </execution>
                     <execution>
                         <phase>package</phase>
                         <goals>
diff --git 
a/dsl/camel-jbang/camel-launcher/src/main/assembly/launcher-content.xml 
b/dsl/camel-jbang/camel-launcher/src/main/assembly/launcher-content.xml
index 296373b1c045..5ce9d9eb5b16 100644
--- a/dsl/camel-jbang/camel-launcher/src/main/assembly/launcher-content.xml
+++ b/dsl/camel-jbang/camel-launcher/src/main/assembly/launcher-content.xml
@@ -37,7 +37,27 @@
             </includes>
             <useProjectArtifact>false</useProjectArtifact>
         </dependencySet>
+        <!-- the class path of the launcher JAR: the same names as its 
manifest has (useUniqueVersions=false) -->
+        <dependencySet>
+            <outputDirectory>lib</outputDirectory>
+            <useProjectArtifact>false</useProjectArtifact>
+            <scope>runtime</scope>
+            
<outputFileNameMapping>${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}</outputFileNameMapping>
+            <includes>
+                <include>*:*:jar:*</include>
+            </includes>
+            <excludes>
+                <exclude>org.jolokia:jolokia-agent-jvm:jar:javaagent</exclude>
+            </excludes>
+        </dependencySet>
     </dependencySets>
+    <files>
+        <file>
+            
<source>${project.build.directory}/${project.build.finalName}-launcher.jar</source>
+            <outputDirectory>lib</outputDirectory>
+            <destName>camel-launcher-${project.version}.jar</destName>
+        </file>
+    </files>
     <fileSets>
         <fileSet>
             <directory>${project.basedir}</directory>
@@ -48,18 +68,6 @@
                 <include>NOTICE*</include>
             </includes>
         </fileSet>
-        <fileSet>
-            <directory>${project.build.directory}</directory>
-            <outputDirectory>bin</outputDirectory>
-            <includes>
-                <include>*.jar</include>
-            </includes>
-            <excludes>
-                <exclude>original-*.jar</exclude>
-                <exclude>*-sources.jar</exclude>
-                <exclude>*-javadoc.jar</exclude>
-            </excludes>
-        </fileSet>
         <fileSet>
             <directory>${project.basedir}/src/main/resources/bin</directory>
             <outputDirectory>bin</outputDirectory>
diff --git a/dsl/camel-jbang/camel-launcher/src/main/assembly/launcher-jar.xml 
b/dsl/camel-jbang/camel-launcher/src/main/assembly/launcher-jar.xml
new file mode 100644
index 000000000000..377ce25240b3
--- /dev/null
+++ b/dsl/camel-jbang/camel-launcher/src/main/assembly/launcher-jar.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0";
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 
http://maven.apache.org/xsd/assembly-2.1.0.xsd";>
+    <id>launcher</id>
+    <formats>
+        <format>jar</format>
+    </formats>
+    <includeBaseDirectory>false</includeBaseDirectory>
+    <fileSets>
+        <fileSet>
+            <directory>${project.build.outputDirectory}</directory>
+            <outputDirectory>/</outputDirectory>
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git 
a/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncher.java
 
b/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncher.java
index e4be9e7aad66..af7fef3a6611 100644
--- 
a/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncher.java
+++ 
b/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncher.java
@@ -34,8 +34,9 @@ public class CamelLauncher {
     public static void main(String... args) {
         System.setProperty(LauncherHelper.CAMEL_LAUNCHER_PROPERTY, "true");
 
-        // Resolve JAR path via the shared helper so all downstream code uses 
one implementation
-        String jarPath = LauncherHelper.getLauncherJarPath();
+        // The JAR of this class, which the CLI runs again in another process: 
the fat JAR, or the launcher JAR in lib/
+        // of the distribution, where the other classes are in the other JARs 
of lib/
+        String jarPath = LauncherHelper.getJarPath(CamelLauncher.class);
         if (jarPath != null) {
             System.setProperty(LauncherHelper.CAMEL_LAUNCHER_JAR_PROPERTY, 
jarPath);
         }
diff --git a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat 
b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat
index 83d883c93c6e..909cad68121d 100644
--- a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat
+++ b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat
@@ -49,13 +49,53 @@ set "JAVACMD=%CAMEL_JAVACMD%"
 @REM Set JVM options if specified
 if "%JAVA_OPTS%" == "" set JAVA_OPTS=-Xmx512m
 
-@REM Find the launcher JAR, excluding sources and javadoc
+@REM Find the launcher JAR in lib\ beside bin\, excluding sources and javadoc. 
Its manifest has the class path (the
+@REM other JARs in lib\).
 set "LAUNCHER_JAR="
-for %%i in ("%BASEDIR%\camel-launcher-*.jar") do (
+for %%i in ("%BASEDIR%\..\lib\camel-launcher-*.jar") do (
   echo %%~nxi| findstr /i /e /l /c:"-sources.jar" /c:"-javadoc.jar" > nul
   if errorlevel 1 if not defined LAUNCHER_JAR set "LAUNCHER_JAR=%%~fi"
 )
 
+@REM Class data sharing: the JVM keeps the classes that a command loads in an 
archive (one per command and Camel
+@REM version, in %LOCALAPPDATA%\camel-cli\cds), which it creates when the 
command runs the first time, or again when
+@REM the JDK changed, so the command starts faster from then on. 
CAMEL_CDS=false turns it off, and so do CDS or AOT
+@REM options of JAVA_OPTS. A JVM without CDS (OpenJ9) ignores the options.
+set "CDS_FILE="
+if /i "%CAMEL_CDS%"=="false" goto cdsDone
+if not defined LAUNCHER_JAR goto cdsDone
+if not defined LOCALAPPDATA goto cdsDone
+setlocal EnableDelayedExpansion
+set "_CDS_FILE="
+if not "!JAVA_OPTS:Xshare=!"=="!JAVA_OPTS!" goto cdsEnd
+if not "!JAVA_OPTS:SharedArchiveFile=!"=="!JAVA_OPTS!" goto cdsEnd
+if not "!JAVA_OPTS:AOTCache=!"=="!JAVA_OPTS!" goto cdsEnd
+if not "!JAVA_OPTS:AOTMode=!"=="!JAVA_OPTS!" goto cdsEnd
+@REM the archive of the command, when the first argument is the name of one 
(letters, digits and -)
+set "_CDS_CMD=camel"
+set "_CDS_ARG=%~1"
+if not defined _CDS_ARG goto cdsCommandDone
+if "!_CDS_ARG:~0,1!"=="-" goto cdsCommandDone
+@REM the dot is not removed, so the variable stays defined
+set "_CDS_REST=.!_CDS_ARG!"
+for %%c in (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 
7 8 9 -) do set "_CDS_REST=!_CDS_REST:%%c=!"
+if "!_CDS_REST!"=="." set "_CDS_CMD=!_CDS_ARG!"
+:cdsCommandDone
+for %%j in ("!LAUNCHER_JAR!") do set "_CDS_VERSION=%%~nj"
+set "_CDS_VERSION=!_CDS_VERSION:camel-launcher-=!"
+set "_CDS_DIR=!LOCALAPPDATA!\camel-cli\cds\!_CDS_VERSION!"
+@REM the JVM does not start when the directory of the archive is missing
+if not exist "!_CDS_DIR!\" mkdir "!_CDS_DIR!" 2> nul
+if not exist "!_CDS_DIR!\" goto cdsEnd
+set "_CDS_FILE=!_CDS_DIR!\!_CDS_CMD!.jsa"
+@REM the JVM does not replace an empty archive (one it stopped writing)
+for %%f in ("!_CDS_FILE!") do if exist "%%~f" if "%%~zf"=="0" del "%%~f" 2> nul
+:cdsEnd
+endlocal & set "CDS_FILE=%_CDS_FILE%"
+:cdsDone
+set "CDS_OPTS="
+if defined CDS_FILE set CDS_OPTS=-XX:+AutoCreateSharedArchive 
"-XX:SharedArchiveFile=%CDS_FILE%" "-Xlog:cds*=off"
+
 @REM Execute Camel CLI, preserving arguments and the child exit code
 for %%e in ("%JAVACMD%") do set "_JAVACMD_EXT=%%~xe"
 if /i "%_JAVACMD_EXT%"==".bat" goto executeJavaBatch
@@ -63,12 +103,12 @@ if /i "%_JAVACMD_EXT%"==".cmd" goto executeJavaBatch
 goto executeJavaNative
 
 :executeJavaBatch
-call "%JAVACMD%" %JAVA_OPTS% -jar "%LAUNCHER_JAR%" %*
+call "%JAVACMD%" %JAVA_OPTS% %CDS_OPTS% -jar "%LAUNCHER_JAR%" %*
 set ERROR_CODE=%ERRORLEVEL%
 goto javaExecuted
 
 :executeJavaNative
-"%JAVACMD%" %JAVA_OPTS% -jar "%LAUNCHER_JAR%" %*
+"%JAVACMD%" %JAVA_OPTS% %CDS_OPTS% -jar "%LAUNCHER_JAR%" %*
 set ERROR_CODE=%ERRORLEVEL%
 
 :javaExecuted
diff --git a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh 
b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh
index aac01cf18f02..22a26d496ac5 100644
--- a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh
+++ b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh
@@ -108,9 +108,11 @@ then
   REPO="$BASEDIR"
 fi
 
-# Find the launcher JAR, excluding sources and javadoc
+# Find the launcher JAR in lib/ beside bin/, excluding sources and javadoc. 
Its manifest has the class path (the other
+# JARs in lib/).
+LIBDIR=`cd "$BASEDIR/../lib" 2>/dev/null && pwd`
 LAUNCHER_JAR=
-for f in "$BASEDIR"/camel-launcher-*.jar; do
+for f in "$LIBDIR"/camel-launcher-*.jar; do
   case "$f" in *-sources.jar|*-javadoc.jar) continue ;; esac
   if [ -f "$f" ]; then
     LAUNCHER_JAR="$f"
@@ -118,9 +120,37 @@ for f in "$BASEDIR"/camel-launcher-*.jar; do
   fi
 done
 
+# Class data sharing: the JVM keeps the classes that a command loads in an 
archive (one per command and Camel version,
+# in the cache directory of the user), which it creates when the command runs 
the first time, or again when the JDK
+# changed, so the command starts faster from then on. CAMEL_CDS=false turns it 
off, and so do CDS or AOT options of
+# JAVA_OPTS. A JVM without CDS (OpenJ9) ignores the options.
+CDS_FILE=
+camel_cds_file() {
+  [ "$CAMEL_CDS" != "false" ] && [ -n "$LAUNCHER_JAR" ] || return 1
+  case "$JAVA_OPTS" in *Xshare*|*SharedArchiveFile*|*AOTCache*|*AOTMode*) 
return 1 ;; esac
+  _cds_cmd=camel
+  case "$1" in
+    [a-z]*) case "$1" in *[!a-z0-9-]*) ;; *) _cds_cmd="$1" ;; esac ;;
+  esac
+  _cds_version=`basename "$LAUNCHER_JAR" .jar`
+  _cds_version=${_cds_version#camel-launcher-}
+  _cds_cache=${XDG_CACHE_HOME:-${HOME:+$HOME/.cache}}
+  [ -n "$_cds_cache" ] || return 1
+  _cds_dir="$_cds_cache/camel-cli/cds/$_cds_version"
+  # the JVM does not start when the directory of the archive is missing
+  mkdir -p "$_cds_dir" 2>/dev/null && [ -w "$_cds_dir" ] || return 1
+  CDS_FILE="$_cds_dir/$_cds_cmd.jsa"
+  # the JVM does not replace an empty archive (one it stopped writing)
+  if [ -f "$CDS_FILE" ] && [ ! -s "$CDS_FILE" ]; then
+    rm -f "$CDS_FILE"
+  fi
+}
+camel_cds_file "$1" || CDS_FILE=
+
 # For Cygwin, switch paths to Windows format before running java
 if $cygwin; then
   [ -n "$LAUNCHER_JAR" ] && LAUNCHER_JAR=`cygpath --path --windows 
"$LAUNCHER_JAR"`
+  [ -n "$CDS_FILE" ] && CDS_FILE=`cygpath --path --windows "$CDS_FILE"`
   [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
   [ -n "$HOME" ] && HOME=`cygpath --path --windows "$HOME"`
   [ -n "$BASEDIR" ] && BASEDIR=`cygpath --path --windows "$BASEDIR"`
@@ -132,4 +162,5 @@ if [ -z "$JAVA_OPTS" ]; then
   JAVA_OPTS="-Xmx512m"
 fi
 
-exec "$JAVACMD" $JAVA_OPTS -jar "$LAUNCHER_JAR" "$@"
+exec "$JAVACMD" $JAVA_OPTS ${CDS_FILE:+-XX:+AutoCreateSharedArchive} 
${CDS_FILE:+"-XX:SharedArchiveFile=$CDS_FILE"} \
+  ${CDS_FILE:+"-Xlog:cds*=off"} -jar "$LAUNCHER_JAR" "$@"
diff --git 
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java
 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java
index a3a0a2dda757..ae1ee1b11144 100644
--- 
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java
+++ 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java
@@ -80,10 +80,10 @@ class CamelBatDiscoveryTest {
     @Test
     void skipsSourcesAndJavadocJars(@TempDir Path base) throws Exception {
         Path h = home(Files.createDirectories(base.resolve("launcher with 
spaces")));
-        Files.writeString(h.resolve("camel-launcher-9.9.9-sources.jar"), 
"sources");
-        Files.writeString(h.resolve("camel-launcher-zzz-sources.jar"), 
"sources");
-        Files.writeString(h.resolve("camel-launcher-9.9.9-javadoc.jar"), 
"javadoc");
-        Files.writeString(h.resolve("camel-launcher-zzz-javadoc.jar"), 
"javadoc");
+        
Files.writeString(FakeJava.lib(h).resolve("camel-launcher-9.9.9-sources.jar"), 
"sources");
+        
Files.writeString(FakeJava.lib(h).resolve("camel-launcher-zzz-sources.jar"), 
"sources");
+        
Files.writeString(FakeJava.lib(h).resolve("camel-launcher-9.9.9-javadoc.jar"), 
"javadoc");
+        
Files.writeString(FakeJava.lib(h).resolve("camel-launcher-zzz-javadoc.jar"), 
"javadoc");
         Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "JAVACMD");
         Map<String, String> env = isolatedEnvironment();
         env.put("JAVACMD", java.toString());
@@ -91,11 +91,84 @@ class CamelBatDiscoveryTest {
         FakeJava.Result r = FakeJava.run(h.resolve("camel.bat"), env, 
"hawtio");
 
         assertEquals(0, r.exitCode(), r.stderr());
-        
assertTrue(r.stdout().contains(h.resolve("camel-launcher-9.9.9.jar").toString()),
 r.stdout());
+        
assertTrue(r.stdout().contains(FakeJava.lib(h).resolve("camel-launcher-9.9.9.jar").toString()),
 r.stdout());
         assertFalse(r.stdout().contains("-sources.jar"), r.stdout());
         assertFalse(r.stdout().contains("-javadoc.jar"), r.stdout());
     }
 
+    @Test
+    void usesAClassDataSharingArchivePerCommand(@TempDir Path base) throws 
Exception {
+        Path h = home(base);
+        Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "ARGS");
+        Path localAppData = Files.createDirectories(base.resolve("local app 
data"));
+        Map<String, String> env = isolatedEnvironment();
+        env.put("JAVACMD", java.toString());
+        env.put("LOCALAPPDATA", localAppData.toString());
+
+        FakeJava.Result r = FakeJava.run(h.resolve("camel.bat"), env, 
"version");
+
+        Path archives = 
localAppData.resolve("camel-cli").resolve("cds").resolve("9.9.9");
+        assertEquals(0, r.exitCode(), r.stderr());
+        assertTrue(r.stdout().contains("-XX:+AutoCreateSharedArchive 
\"-XX:SharedArchiveFile="
+                                       + archives.resolve("version.jsa") + "\" 
\"-Xlog:cds*=off\" -jar "),
+                r.stdout());
+        assertTrue(Files.isDirectory(archives), "the JVM does not start 
without the directory of the archive");
+    }
+
+    @Test
+    void usesTheArchiveOfCamelWithoutACommand(@TempDir Path base) throws 
Exception {
+        Path h = home(base);
+        Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "ARGS");
+        Path localAppData = Files.createDirectories(base.resolve("lad"));
+        Map<String, String> env = isolatedEnvironment();
+        env.put("JAVACMD", java.toString());
+        env.put("LOCALAPPDATA", localAppData.toString());
+
+        FakeJava.Result r = FakeJava.run(h.resolve("camel.bat"), env, 
"--help");
+
+        assertTrue(r.stdout().contains(
+                "-XX:SharedArchiveFile=" + 
localAppData.resolve("camel-cli\\cds\\9.9.9\\camel.jsa") + "\""),
+                r.stdout());
+    }
+
+    @Test
+    void removesAnEmptyArchive(@TempDir Path base) throws Exception {
+        // the JVM does not replace an empty archive, which it leaves when it 
stops while writing one
+        Path h = home(base);
+        Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "ARGS");
+        Path localAppData = Files.createDirectories(base.resolve("lad"));
+        Path archive = 
Files.createDirectories(localAppData.resolve("camel-cli\\cds\\9.9.9")).resolve("version.jsa");
+        Files.createFile(archive);
+        Map<String, String> env = isolatedEnvironment();
+        env.put("JAVACMD", java.toString());
+        env.put("LOCALAPPDATA", localAppData.toString());
+
+        FakeJava.Result r = FakeJava.run(h.resolve("camel.bat"), env, 
"version");
+
+        assertTrue(r.stdout().contains("-XX:SharedArchiveFile=" + archive), 
r.stdout());
+        assertFalse(Files.exists(archive));
+    }
+
+    @Test
+    void noArchiveWhenTurnedOffOrJavaOptsHaveCdsOptions(@TempDir Path base) 
throws Exception {
+        Path h = home(base);
+        Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "ARGS");
+        Path localAppData = Files.createDirectories(base.resolve("lad"));
+        for (Map.Entry<String, String> setting : Map.of(
+                "CAMEL_CDS", "false",
+                "JAVA_OPTS", "-XX:AOTCache=app.aot").entrySet()) {
+            Map<String, String> env = isolatedEnvironment();
+            env.put("JAVACMD", java.toString());
+            env.put("LOCALAPPDATA", localAppData.toString());
+            env.put(setting.getKey(), setting.getValue());
+
+            FakeJava.Result r = FakeJava.run(h.resolve("camel.bat"), env, 
"version");
+
+            assertEquals(0, r.exitCode(), r.stderr());
+            assertFalse(r.stdout().contains("SharedArchiveFile"), setting + ": 
" + r.stdout());
+        }
+    }
+
     @Test
     void acceptsJava21ViaJavacmd(@TempDir Path base) throws Exception {
         Path h = home(base);
@@ -381,7 +454,7 @@ class CamelBatDiscoveryTest {
         assertTrue(r.stdout().contains("ARG_COUNT=5"), r.stdout());
         assertTrue(r.stdout().contains("ARG1=[-Xmx512m]"), r.stdout());
         assertTrue(r.stdout().contains("ARG2=[-jar]"), r.stdout());
-        assertTrue(r.stdout().contains("ARG3=[" + 
h.resolve("camel-launcher-9.9.9.jar") + "]"), r.stdout());
+        assertTrue(r.stdout().contains("ARG3=[" + 
FakeJava.lib(h).resolve("camel-launcher-9.9.9.jar") + "]"), r.stdout());
         assertTrue(r.stdout().contains("ARG4=[run]"), r.stdout());
         assertTrue(r.stdout().contains("ARG5=[my route.yaml]"), r.stdout());
         assertTrue(r.stdout().contains("ARG6=[]"), r.stdout());
@@ -563,10 +636,10 @@ class CamelBatDiscoveryTest {
         Path launcher = launcherHome.resolve("camel.bat");
         String source = Files.readString(launcher, StandardCharsets.UTF_8);
         String newline = source.contains("\r\n") ? "\r\n" : "\n";
-        String launchBlock = "call \"%JAVACMD%\" %JAVA_OPTS% -jar 
\"%LAUNCHER_JAR%\" %*" + newline
+        String launchBlock = "call \"%JAVACMD%\" %JAVA_OPTS% %CDS_OPTS% -jar 
\"%LAUNCHER_JAR%\" %*" + newline
                              + "set ERROR_CODE=%ERRORLEVEL%" + newline
                              + "goto javaExecuted";
-        String instrumentedBlock = "call \"%JAVACMD%\" %JAVA_OPTS% -jar 
\"%LAUNCHER_JAR%\" %*" + newline
+        String instrumentedBlock = "call \"%JAVACMD%\" %JAVA_OPTS% %CDS_OPTS% 
-jar \"%LAUNCHER_JAR%\" %*" + newline
                                    + "set ERROR_CODE=%ERRORLEVEL%" + newline
                                    + "echo LAUNCHER-RESUMED>\"" + sentinel + 
"\"" + newline
                                    + "goto javaExecuted";
@@ -581,7 +654,7 @@ class CamelBatDiscoveryTest {
         manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, 
"1.0");
         manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, 
CamelBatRealJavaMarker.class.getName());
         String entryName = CamelBatRealJavaMarker.class.getName().replace('.', 
'/') + ".class";
-        Path jar = launcherHome.resolve("camel-launcher-9.9.9.jar");
+        Path jar = 
FakeJava.lib(launcherHome).resolve("camel-launcher-9.9.9.jar");
         try (InputStream classBytes = 
CamelBatRealJavaMarker.class.getResourceAsStream("/" + entryName);
              JarOutputStream out = new 
JarOutputStream(Files.newOutputStream(jar), manifest)) {
             assertTrue(classBytes != null, "could not load marker class 
bytes");
diff --git 
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java
 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java
index c3413e75c929..f323abe0e62e 100644
--- 
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java
+++ 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java
@@ -60,10 +60,10 @@ class CamelShDiscoveryTest {
     @Test
     void skipsSourcesAndJavadocJars(@TempDir Path base) throws Exception {
         Path h = home(Files.createDirectories(base.resolve("launcher with 
spaces")));
-        Files.writeString(h.resolve("camel-launcher-9.9.9-sources.jar"), 
"sources");
-        Files.writeString(h.resolve("camel-launcher-zzz-sources.jar"), 
"sources");
-        Files.writeString(h.resolve("camel-launcher-9.9.9-javadoc.jar"), 
"javadoc");
-        Files.writeString(h.resolve("camel-launcher-zzz-javadoc.jar"), 
"javadoc");
+        
Files.writeString(FakeJava.lib(h).resolve("camel-launcher-9.9.9-sources.jar"), 
"sources");
+        
Files.writeString(FakeJava.lib(h).resolve("camel-launcher-zzz-sources.jar"), 
"sources");
+        
Files.writeString(FakeJava.lib(h).resolve("camel-launcher-9.9.9-javadoc.jar"), 
"javadoc");
+        
Files.writeString(FakeJava.lib(h).resolve("camel-launcher-zzz-javadoc.jar"), 
"javadoc");
         Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "JAVACMD");
         Map<String, String> env = new HashMap<>();
         env.put("LC_ALL", "C");
@@ -72,7 +72,7 @@ class CamelShDiscoveryTest {
         FakeJava.Result r = FakeJava.run(h.resolve("camel.sh"), env, "hawtio");
 
         assertEquals(0, r.exitCode(), r.stderr());
-        
assertTrue(r.stdout().contains(h.resolve("camel-launcher-9.9.9.jar").toString()),
 r.stdout());
+        
assertTrue(r.stdout().contains(FakeJava.lib(h).resolve("camel-launcher-9.9.9.jar").toString()),
 r.stdout());
         assertFalse(r.stdout().contains("-sources.jar"), r.stdout());
         assertFalse(r.stdout().contains("-javadoc.jar"), r.stdout());
     }
@@ -391,6 +391,110 @@ class CamelShDiscoveryTest {
         assertTrue(r.stdout().contains("|run|my route.yaml|"), r.stdout());
     }
 
+    @Test
+    void usesAClassDataSharingArchivePerCommand(@TempDir Path base) throws 
Exception {
+        Path h = home(base);
+        Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "ARGS");
+        Path cache = base.resolve("cache dir");
+        Map<String, String> env = new HashMap<>();
+        env.put("JAVACMD", java.toString());
+        env.put("XDG_CACHE_HOME", cache.toString());
+
+        FakeJava.Result r = FakeJava.run(h.resolve("camel.sh"), env, 
"version");
+
+        Path archives = cache.resolve("camel-cli/cds/9.9.9");
+        assertEquals(0, r.exitCode(), r.stderr());
+        
assertTrue(r.stdout().contains("|-XX:+AutoCreateSharedArchive|-XX:SharedArchiveFile="
 + archives.resolve("version.jsa")
+                                       + "|-Xlog:cds*=off|-jar|"),
+                r.stdout());
+        assertTrue(Files.isDirectory(archives), "the JVM does not start 
without the directory of the archive");
+    }
+
+    @Test
+    void usesTheArchiveOfCamelWithoutACommand(@TempDir Path base) throws 
Exception {
+        Path h = home(base);
+        Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "ARGS");
+        Path cache = base.resolve("cache");
+        Map<String, String> env = new HashMap<>();
+        env.put("JAVACMD", java.toString());
+        env.put("XDG_CACHE_HOME", cache.toString());
+
+        FakeJava.Result r = FakeJava.run(h.resolve("camel.sh"), env, "--help");
+
+        assertTrue(r.stdout().contains("-XX:SharedArchiveFile=" + 
cache.resolve("camel-cli/cds/9.9.9/camel.jsa") + "|"),
+                r.stdout());
+    }
+
+    @Test
+    void usesTheCacheDirectoryInTheHomeDirectory(@TempDir Path base) throws 
Exception {
+        Path h = home(base);
+        Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "ARGS");
+        Path userHome = Files.createDirectories(base.resolve("home"));
+        Map<String, String> env = new HashMap<>();
+        env.put("JAVACMD", java.toString());
+        env.put("HOME", userHome.toString());
+
+        FakeJava.Result r = FakeJava.run(h.resolve("camel.sh"), env, "get");
+
+        assertTrue(r.stdout().contains("-XX:SharedArchiveFile=" + 
userHome.resolve(".cache/camel-cli/cds/9.9.9/get.jsa")),
+                r.stdout());
+    }
+
+    @Test
+    void removesAnEmptyArchive(@TempDir Path base) throws Exception {
+        // the JVM does not replace an empty archive, which it leaves when it 
stops while writing one
+        Path h = home(base);
+        Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "ARGS");
+        Path cache = base.resolve("cache");
+        Path archive = 
Files.createDirectories(cache.resolve("camel-cli/cds/9.9.9")).resolve("version.jsa");
+        Files.createFile(archive);
+        Map<String, String> env = new HashMap<>();
+        env.put("JAVACMD", java.toString());
+        env.put("XDG_CACHE_HOME", cache.toString());
+
+        FakeJava.Result r = FakeJava.run(h.resolve("camel.sh"), env, 
"version");
+
+        assertTrue(r.stdout().contains("-XX:SharedArchiveFile=" + archive), 
r.stdout());
+        assertFalse(Files.exists(archive));
+    }
+
+    @Test
+    void noArchiveWhenTurnedOffOrJavaOptsHaveCdsOptions(@TempDir Path base) 
throws Exception {
+        Path h = home(base);
+        Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "ARGS");
+        Path cache = base.resolve("cache");
+        for (Map.Entry<String, String> setting : Map.of(
+                "CAMEL_CDS", "false",
+                "JAVA_OPTS", "-Xshare:off").entrySet()) {
+            Map<String, String> env = new HashMap<>();
+            env.put("JAVACMD", java.toString());
+            env.put("XDG_CACHE_HOME", cache.toString());
+            env.put(setting.getKey(), setting.getValue());
+
+            FakeJava.Result r = FakeJava.run(h.resolve("camel.sh"), env, 
"version");
+
+            assertEquals(0, r.exitCode(), r.stderr());
+            assertFalse(r.stdout().contains("SharedArchiveFile"), setting + ": 
" + r.stdout());
+        }
+    }
+
+    @Test
+    void noArchiveWithoutACacheDirectory(@TempDir Path base) throws Exception {
+        Path h = home(base);
+        Path java = FakeJava.writeFakeJava(base, "java", JAVA21, 0, "ARGS");
+        // a file where the cache directory would be
+        Path cache = Files.writeString(base.resolve("cache"), "not a 
directory");
+        Map<String, String> env = new HashMap<>();
+        env.put("JAVACMD", java.toString());
+        env.put("XDG_CACHE_HOME", cache.toString());
+
+        FakeJava.Result r = FakeJava.run(h.resolve("camel.sh"), env, 
"version");
+
+        assertEquals(0, r.exitCode(), r.stderr());
+        assertFalse(r.stdout().contains("SharedArchiveFile"), r.stdout());
+        assertTrue(r.stdout().contains("ARGS=-Xmx512m|-jar|"), r.stdout());
+    }
+
     @Test
     void defaultsHeapWhenJavaOptsUnset(@TempDir Path base) throws Exception {
         Path h = home(base);
diff --git 
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/FakeJava.java
 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/FakeJava.java
index 9ad3612ca1ea..97cd951a50b9 100644
--- 
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/FakeJava.java
+++ 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/FakeJava.java
@@ -86,16 +86,29 @@ final class FakeJava {
         return exe;
     }
 
+    /**
+     * A launcher home with the layout of the distribution: the script in bin/ 
and the launcher JAR in lib/.
+     *
+     * @return the bin/ directory, which has the script
+     */
     static Path newLauncherHome(Path base, Path scriptResource) throws 
IOException {
         Path home = Files.createTempDirectory(base, "launcher-home");
-        Path script = home.resolve(scriptResource.getFileName().toString());
+        Path bin = Files.createDirectories(home.resolve("bin"));
+        Path script = bin.resolve(scriptResource.getFileName().toString());
         Files.copy(scriptResource, script);
         if (!WINDOWS) {
             Files.setPosixFilePermissions(script, Set.of(OWNER_READ, 
OWNER_WRITE, OWNER_EXECUTE));
         }
         // A stub jar so the launcher's "camel-launcher-*.jar" glob resolves; 
fake java never reads it.
-        Files.writeString(home.resolve("camel-launcher-9.9.9.jar"), "stub");
-        return home;
+        
Files.writeString(Files.createDirectories(home.resolve("lib")).resolve("camel-launcher-9.9.9.jar"),
 "stub");
+        return bin;
+    }
+
+    /**
+     * The lib/ directory of a launcher home (see {@link 
#newLauncherHome(Path, Path)}).
+     */
+    static Path lib(Path bin) {
+        return bin.resolveSibling("lib");
     }
 
     static Result run(Path script, Map<String, String> env, String... args) 
throws Exception {
diff --git 
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/LauncherArchiveLayoutIT.java
 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/LauncherArchiveLayoutIT.java
new file mode 100644
index 000000000000..c257fd5685d7
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/LauncherArchiveLayoutIT.java
@@ -0,0 +1,99 @@
+/*
+ * 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.camel.dsl.jbang.launcher;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.jar.Attributes;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The layout of the launcher distributions: the launcher JAR in lib/, whose 
manifest names the other JARs in lib/ as
+ * its class path, and no JAR in bin/ but the Jolokia agent. Runs during 
{@code verify} when
+ * {@code -Dcamel.exe.build=true} is set, as {@link CamelLauncherNativeExeIT} 
does.
+ */
+@EnabledIfSystemProperty(named = "camel.exe.build", matches = "true")
+class LauncherArchiveLayoutIT {
+
+    private static final Path TARGET = Paths.get("target");
+
+    @Test
+    void theLauncherJarHasTheJarsOfLibAsItsClassPath() throws IOException {
+        List<Path> zips = binZips();
+        assertThat(zips).as("the public and the WinGet ZIP").hasSize(2);
+        for (Path zip : zips) {
+            try (ZipFile archive = new ZipFile(zip.toFile())) {
+                Set<String> lib = new TreeSet<>();
+                Set<String> binJars = new TreeSet<>();
+                ZipEntry launcher = null;
+                for (ZipEntry entry : archive.stream().toList()) {
+                    String[] path = entry.getName().split("/");
+                    if (entry.isDirectory() || path.length != 3) {
+                        continue;
+                    }
+                    if ("lib".equals(path[1])) {
+                        if (path[2].matches("camel-launcher-[^/]*\\.jar")) {
+                            launcher = entry;
+                        } else {
+                            lib.add(path[2]);
+                        }
+                    } else if ("bin".equals(path[1]) && 
path[2].endsWith(".jar")) {
+                        binJars.add(path[2]);
+                    }
+                }
+                assertThat(launcher).as("lib/camel-launcher-<version>.jar in " 
+ zip.getFileName()).isNotNull();
+                assertThat(binJars).as("the JARs in bin/ of " + 
zip.getFileName())
+                        .allMatch(jar -> jar.startsWith("jolokia-agent-jvm-") 
&& jar.endsWith("-javaagent.jar"));
+
+                Manifest manifest;
+                try (InputStream in = archive.getInputStream(launcher); 
JarInputStream jar = new JarInputStream(in)) {
+                    manifest = jar.getManifest();
+                }
+                Attributes attributes = manifest.getMainAttributes();
+                
assertThat(attributes.getValue(Attributes.Name.MAIN_CLASS)).isEqualTo(CamelLauncher.class.getName());
+                Set<String> classPath = new 
TreeSet<>(List.of(attributes.getValue(Attributes.Name.CLASS_PATH).split(" ")));
+                assertThat(classPath).as("the class path of the launcher JAR 
in " + zip.getFileName())
+                        .isEqualTo(lib);
+            }
+        }
+    }
+
+    private static List<Path> binZips() throws IOException {
+        try (Stream<Path> paths = Files.list(TARGET)) {
+            return new ArrayList<>(
+                    paths
+                            .filter(p -> 
p.getFileName().toString().matches("camel-launcher-.*-bin\\.zip"))
+                            .toList());
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanTest.java
 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanTest.java
index fe9d21de71f7..b9c10f78b7ff 100644
--- 
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanTest.java
+++ 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/PackagePlanTest.java
@@ -197,7 +197,10 @@ class PackagePlanTest {
         List<String> wingetIncludes = effectiveIncludes(wingetAssembly);
         assertTrue(wingetIncludes.contains("camel-x64.exe"), 
wingetIncludes.toString());
         assertTrue(wingetIncludes.contains("camel-arm64.exe"), 
wingetIncludes.toString());
-        assertTrue(wingetIncludes.contains("*.jar"), "the WinGet archive must 
retain the launcher JAR beside camel.bat");
+        assertTrue(wingetIncludes.contains("*:*:jar:*"), "the WinGet archive 
must carry the JARs of lib/");
+        assertEquals(List.of("camel-launcher-${project.version}.jar"),
+                
elementTexts(parseXml(MODULE_DIR.resolve("src/main/assembly/launcher-content.xml")),
 "destName"),
+                "camel.bat runs the launcher JAR in lib/");
         assertTrue(wingetIncludes.contains("camel.bat"),
                 "the native bootstrap delegates to camel.bat beside its 
resolved target");
         assertEquals(List.of("zip"), elementTexts(parseXml(wingetAssembly), 
"format"),

Reply via email to