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