Skip to content

[LAUNCHER] Fix nested jar url handling - #26039

Merged
Croway merged 2 commits into
apache:mainfrom
jvrubel:launcher-fix-nested-url
Sep 3, 2026
Merged

[LAUNCHER] Fix nested jar url handling#26039
Croway merged 2 commits into
apache:mainfrom
jvrubel:launcher-fix-nested-url

Conversation

@jvrubel

@jvrubel jvrubel commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fix LauncherHelper to handle Spring Boot 3.2+/4.x jar:nested: URL scheme

Problem

Camel-launcher is packaged as a Spring Boot executable fat JAR (Main-Class: org.springframework.boot.loader.launch.JarLauncher). Spring
Boot 3.2+/4.x uses a jar:nested: URL scheme for code source locations of classes loaded from nested library JARs:

jar:nested:/path/to/camel-launcher-4.22.0.jar/!BOOT-INF/lib/camel-jbang-core-4.22.0.jar!/

LauncherHelper.getLauncherJarPath() only handled jar:file: (Spring Boot 2.x / Maven Shade) and file: URL schemes. When it received a jar:nested: URL, it returned null.

This caused isRunningFromLauncher() to return false, so getCamelCommand() fell back to ["camel"] — a command that does not exist when running from the fat JAR. Any operation
that spawned a background process (e.g. camel run --background) failed with:

java.io.IOException: Cannot run program "camel": error=2, No such file or directory

Fix

Add a jar:nested: case to getLauncherJarPath(), before the existing jar:file: handler. The outer JAR path is extracted by splitting on /! (the separator between the outer JAR
and the nested path):

if (urlStr.startsWith("jar:nested:")) { String path = urlStr.substring("jar:nested:".length()); int idx = path.indexOf("/!"); if (idx > 0) { return URLDecoder.decode(path.substring(0, idx), StandardCharsets.UTF_8); } }

For the URL above, this correctly extracts /path/to/camel-launcher-4.220.jar, causing getCamelCommand() to return ["java", "-jar",
"/path/to/camel-launcher-4.22.0.jar"].

Magic number offsets in the existing jar:file: and file: branches were also replaced with named-length constants for readability.

Verification

Replicated the failure locally by running java -jar camel-launcher-4.22.0.jar run route.java --background with camel removed from PATH. Confirmed the identical
Cannot run program "camel" error. Applied the fix and verified getLauncherJarPath() returns the correct outer JAR path from the jar:nested: URL.

@Croway Croway left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracking this down, the analysis of the jar:nested: scheme is correct and the fix works for the loader we ship. I checked the built camel-launcher jar: the manifest carries Main-Class: org.springframework.boot.loader.launch.JarLauncher and Spring-Boot-Version: 4.1.1, and the NestedLocation parser in that loader still uses /! as the outer/inner separator. So this is not a Spring Boot 3.x specific format, it is the 3.2+ loader that Spring Boot 4 also uses. Could you reword the comment to "Spring Boot 3.2+ / 4.x loader" so nobody reads it as legacy handling?

A few things I'd like to see before merging:

1. The same bug is still in the launcher's main class

CamelLauncher.detectJarPath() in dsl/camel-jbang/camel-launcher is a verbatim copy of the old jar:file: / file: parsing. Under the current loader it returns null, so the camel.launcher.jar system property is never set and every caller ends up in the LauncherHelper fallback this PR patches. Since camel-launcher already depends on camel-jbang-core, the simplest fix is to drop detectJarPath() and call LauncherHelper.getLauncherJarPath() from main, so there is exactly one implementation of this parsing.

2. Add a unit test for the parser

The parsing is buried in a method that reads its own class's code source, so it cannot be tested directly. Extracting a package-private parseJarPath(String url) and adding a test in camel-jbang-core covering the jar:nested:, jar:file: and file: forms gives a regression check for the loader URL scheme, which is exactly what broke silently when the loader moved from jar:file: to jar:nested:.

3. Minor, optional

  • URLDecoder.decode also turns + into a space, so a directory such as camel+tools breaks. Spring's own NestedLocation decodes only percent escapes. Pre-existing in the other branches, but the new branch copies it.
  • Spring's parser strips the leading slash on Windows (/C:/x.jarC:/x.jar); ours does not. Also pre-existing.
  • A one-line comment explaining why indexOf("/!") (first match) is used rather than lastIndexOf like Spring does would help: on the jar:-wrapped URL a BOOT-INF/classes/!/ suffix would confuse lastIndexOf.

Claude Code on behalf of Croway

@Croway

Croway commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Follow-up after a deeper pass with a probe run inside the built camel-launcher-4.23.0-SNAPSHOT.jar (Spring Boot loader 4.1.1). A few of the points in my review turn out to be more serious than I framed them, plus two new ones.

Point 1 is a functional bug, not just duplication. Inside the fat jar the main class's code source is jar:nested:/.../camel-launcher.jar/!BOOT-INF/classes/!/, so CamelLauncher.detectJarPath() returns null and camel.launcher.jar is never set. InstallDetector.locate() then returns UNKNOWN, which means camel self-update exits with "unable to determine how the Camel CLI was installed" and UpdateChecker never announces new releases. The self-update feature introduced in 4.22.0 stays broken after this PR unless the main class is fixed too.

Related: the description's diagnosis is slightly off. camel.launcher=true is set by CamelLauncher.main, so isRunningFromLauncher() was already true; the fallback to camel happened because getLauncherJarPath() returned null inside getCamelCommand().

The Windows path is a regression, not a nit. The new branch returns /C:/Users/me/.../camel-launcher.jar and getCamelCommand() hands that verbatim to java -jar, which the native launcher rejects with Error: Unable to access jarfile /C:/.... Before this PR, Windows users who also had JBang fell back to a working camel.cmd; after it, run --background, infra run --background and every TUI launch fail for them. Decoding through the URI API fixes both this and the + issue in one go, e.g.

Path.of(URI.create("file:" + path.substring(0, idx))).toString()

and the same should be applied to the jar:file: / file: branches. BasePackageScanResolver in camel-support documents this exact URLDecoder pitfall and uses new URI(...).getPath() for that reason.

JVM options are dropped on spawn. Now that the java -jar <launcher> branch is reachable for the first time, background and TUI children are started as bare <java.home>/bin/java -jar camel-launcher.jar ... with no -X/-D forwarding. The wrapper scripts (camel.sh, camel.bat) apply JAVA_OPTS to the foreground process, and it is a documented contract in the 4.22 upgrade guide, so a user with proxy or truststore settings in JAVA_OPTS gets a foreground run that works and a background run that cannot download dependencies. Consider spawning through the wrapper script when present, or forwarding JAVA_OPTS / the relevant parent system properties.

Silent fallback masks failures. When camel.launcher=true but the jar path cannot be resolved, getCamelCommand() falls through to whichever camel is first on PATH, possibly a different version or an old JBang shim. That is how this regression went unnoticed, and the next loader URL-shape change would be masked the same way. Logging a warning that includes the raw code-source URL (or failing) in that case would be a cheap safeguard.

Minor: isRunningFromLauncher() matches contains("camel-launcher") against the whole absolute path, and this PR makes that branch reachable for any Spring Boot 3.2+/4.x app that embeds camel-jbang-core. Matching on the file name would avoid false positives such as ~/projects/camel-launcher-poc/target/myapp.jar.

A table test on an extracted parseJarPath(String) covering jar:nested: (both BOOT-INF/classes and BOOT-INF/lib forms), jar:file:, file:, %20, + and /C:/ inputs would have caught the decoding and Windows issues.


Claude Code on behalf of Croway

@jvrubel

jvrubel commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the comments and the analysis, Fede. So the plan is:

  1. Drop CamelLauncher.detectJarPath(), call LauncherHelper.getLauncherJarPath() instead
  2. Use Path.of(URI.create(...)) to normalize Windows paths in all three URL branches
  3. Tighten isRunningFromLauncher() to check filename only
  4. Extract parseJarPath(String) as package-private + add unit tests
  5. Forward JVM args in getCamelCommand()
  6. Log warning with raw URL on fallback

+Fixed the PR description
Sounds good?

@Croway

Croway commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Thanks for the comments and the analysis, Fede. So the plan is:

  1. Drop CamelLauncher.detectJarPath(), call LauncherHelper.getLauncherJarPath() instead
  2. Use Path.of(URI.create(...)) to normalize Windows paths in all three URL branches
  3. Tighten isRunningFromLauncher() to check filename only
  4. Extract parseJarPath(String) as package-private + add unit tests
  5. Forward JVM args in getCamelCommand()
  6. Log warning with raw URL on fallback

+Fixed the PR description Sounds good?

yes, sounds reasonable

@jvrubel
jvrubel force-pushed the launcher-fix-nested-url branch from 9ae2693 to 813857d Compare September 2, 2026 17:29
@jvrubel

jvrubel commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the points in the following way:

  1. CamelLauncher.detectJarPath() dropped — CamelLauncher.java now calls LauncherHelper.getLauncherJarPath() directly and uses the shared property constants, eliminating the
    duplicated parsing logic that was missing jar:nested: support.

  2. Windows path regression fixed — All three URL branches in LauncherHelper.parseJarPath() now use Paths.get(URI.create(...)) instead of URLDecoder.decode. This correctly
    converts /C:/Users/... to C:\Users\... on Windows, and also avoids the +→space bug from URLDecoder.

  3. isRunningFromLauncher() false positive fixed — Now checks Path.of(jarPath).getFileName().toString().startsWith("camel-launcher") instead of
    jarPath.contains("camel-launcher").

  4. Unit tests added — LauncherHelperTest covers all three URL forms plus edge cases (percent-encoded spaces, Windows drive letters, unknown schemes, missing /! boundary).

  5. JVM args forwarded on spawn — getCamelCommand() now inserts ManagementFactory.getRuntimeMXBean().getInputArguments() between java and -jar, so child processes inherit proxy
    settings, truststores, etc.

  6. Warning on null fallback — When the launcher is detected but the JAR path can't be resolved, getCamelCommand() now logs a warning including the raw code-source URL before
    falling back to camel.

@jvrubel
jvrubel requested a review from Croway September 2, 2026 17:32

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this, @jvrubel — the core fix is correct and genuinely valuable. The jar:nested: fallback bug is real, the URI-based decoding is a solid improvement over the manual URLDecoder+substring approach, the filename-based isRunningFromLauncher() check is more precise, and de-duplicating the parsing into a shared LauncherHelper.parseJarPath() (dropping the copy in CamelLauncher) is a nice cleanup.

A few things need addressing before this can go in.

🔴 Blocking

1. The three jar:nested: unit tests use malformed URLs and will fail. parseJarPath() correctly searches for /! — the real Spring Boot boundary between the outer jar and the nested entry (jar:nested:/path/myjar.jar/!BOOT-INF/lib/mylib.jar!/, per Spring's docs, and matching your own PR description). But the test inputs use !/ instead of /! after the outer jar, so indexOf("/!") returns -1, parseJarPath returns null, and the assertions fail. See the inline suggestions on the three lines. (CI hasn't run on this PR yet, so this wasn't caught automatically.)

2. Unrelated generated file core/camel-core-model/src/generated/resources/META-INF/services/org/apache/camel/model.properties reintroduces removed model entries. It re-adds csimple, serviceCall, *ServiceDiscovery, *ServiceFilter, defaultLoadBalancer, etc. — none of which are on current main. csimple was removed on 2026-08-28 (9e8d8e4fe979 Removal of csimple language). This is a stale-branch regeneration artifact: the branch predates that removal. Please rebase on current main and drop this file from the changeset so the PR touches only the launcher files.

🟡 Concern

3. getCamelCommand() now forwards all JVM input arguments via ManagementFactory.getRuntimeMXBean().getInputArguments(). This is beyond the stated nested-URL fix, is untested, and is risky — it blindly forwards -agentlib:jdwp=… (debug port), -javaagent, and -Dcom.sun.management.jmxremote.port=… to the child, which can cause port conflicts. Consider splitting this into its own PR, or filtering to just the -D props you intend to propagate.

🟢 Minor

  • The commit message [LAUNCHER] Fix nested jar url handling doesn't follow the project's CAMEL-XXXX: <description> convention and references no JIRA ticket. If none exists, one should be created.

This is a rules-and-conventions review and does not replace CodeRabbit/Sourcery or SonarCloud.

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

@Croway

Croway commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Two more things on top of @davsclaus' review, both in LauncherHelperTest:

1. parsesNestedJarUrlOnWindows will fail on Linux/macOS CI even after the /! fix. Paths.get(URI) only strips the leading slash on Windows; on a Unix host file:/C:/Users/... resolves to /C:/Users/user/camel-launcher-4.23.0.jar, so doesNotStartWith("/C:") fails (verified on macOS). Guard the strict assertion with @EnabledOnOs(OS.WINDOWS) and keep a platform-neutral check (e.g. endsWith("camel-launcher-4.23.0.jar")) for the other OSes.

2. The Javadoc on parseJarPath and the inline comment in the jar:nested: branch still show the wrong form (jar:nested:/outer.jar!/...); the suggestion commits only fix the test strings. Should be jar:nested:/outer.jar/!BOOT-INF/lib/inner.jar!/.

Minor: Path.of and Paths.get are mixed in the same class; pick one.

Claude Code on behalf of Croway

@jvrubel
jvrubel force-pushed the launcher-fix-nested-url branch from 813857d to a3ed554 Compare September 3, 2026 10:10
@jvrubel

jvrubel commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for the reviews @Croway and @davsclaus. I have modified the PR to address your points, validated the tests (bar Windows) and cleaned up the config files.

@jvrubel
jvrubel requested a review from davsclaus September 3, 2026 10:15
@github-actions github-actions Bot added the dsl label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

✅ Generated files are up to date

An earlier CI run reported uncommitted generated changes; the latest run no longer does.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • dsl/camel-jbang/camel-jbang-core
  • dsl/camel-jbang/camel-launcher

🔬 Scalpel shadow comparison — Scalpel: 8 tested, 7 compile-only — current: 6 all tested

Maveniverse Scalpel detected 15 affected modules (current approach: 6).

⚠️ Modules only in Scalpel (9)
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-launcher
  • coverage

Skip-tests mode would test 8 modules (2 direct + 6 downstream), skip tests for 7 (generated code, meta-modules)

Modules Scalpel would test (8)
  • camel-jbang-core
  • camel-jbang-mcp
  • camel-jbang-plugin-mcp
  • camel-jbang-plugin-route-parser
  • camel-jbang-plugin-tui
  • camel-jbang-plugin-validate
  • camel-launcher
  • camel-launcher-container
Modules with tests skipped (7)
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • coverage

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

⚠️ Some tests are disabled on GitHub Actions (@DisabledIfSystemProperty(named = "ci.env.name")) and require manual verification:

  • dsl/camel-jbang/camel-jbang-core: 1 test(s) disabled on GitHub Actions

💡 Manual integration tests recommended:

You modified dsl/camel-jbang/camel-jbang-core, dsl/camel-jbang/camel-launcher. The related integration tests in dsl/camel-jbang/camel-jbang-it are excluded from CI. Consider running them manually:

mvn verify -f dsl/camel-jbang/camel-jbang-it -Djbang-it-test
All tested modules (15 modules)
  • Camel :: Coverage
  • Camel :: JBang :: Core
  • Camel :: JBang :: Integration tests
  • Camel :: JBang :: MCP
  • Camel :: JBang :: Main
  • Camel :: JBang :: Plugin :: Edit
  • Camel :: JBang :: Plugin :: Generate
  • Camel :: JBang :: Plugin :: Kubernetes
  • Camel :: JBang :: Plugin :: MCP
  • Camel :: JBang :: Plugin :: Route Parser
  • Camel :: JBang :: Plugin :: TUI
  • Camel :: JBang :: Plugin :: Testing
  • Camel :: JBang :: Plugin :: Validate
  • Camel :: Launcher
  • Camel :: Launcher :: Container

⚙️ View full build and test results

@Croway
Croway merged commit 8ffbfa7 into apache:main Sep 3, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants