Skip to content

fix: keep the Gradle classpath order in packaged launchers - #729

Merged
kdroidFilter merged 2 commits into
nucleus-2.6from
fix/packaged-classpath-order
Sep 25, 2026
Merged

kdroidFilter merged 2 commits into
nucleus-2.6from
fix/packaged-classpath-order

Conversation

@kdroidFilter

Copy link
Copy Markdown
Collaborator

Summary

  • Plugin — classpath order in packaged apps. jpackage has no classpath option: it builds app.classpath from the --input directory, sorted by name. When two JARs define the same classes, a packaged app loaded whichever sorted first, while ./gradlew run follows Gradle's runtime-classpath order. Every Jewel app was affected: the IntelliJ icon libraries pull kotlinx-coroutines-core-jvm-1.10.2-intellij-2, which sorted before 1.11.0 and made the packaged app fail with NoSuchMethodError: Job.cancel$default.
    • New LauncherClasspathOrder rewrites the launcher .cfg classpath in classpath order (main JAR first; unknown entries keep their relative place; jvm.cfg and line endings untouched). It runs right after jpackage, before macOS signing and before the Linux pathing-jar collapse, both of which preserve order.
    • The sandboxed strip task outputs a directory, which loses the order, so it now records .classpath-order; AbstractJPackageTask.classpathOrderFile consumes it.
  • Examples — JDK 25 runtime. jewel-demo, scheduler-demo and system-info-demo compile to class file 69 but were packaged with the Gradle JVM's runtime (21 here), so the distributable died with UnsupportedClassVersionError. They now resolve a JDK 25 toolchain, as jewel-tabs-demo already does.

Test plan

  • LauncherClasspathOrderTest (5 cases); full :plugin:test — 438 tests, 0 failures; :plugin:ktlintCheck
  • jewel-tabs-demo createDistributable, before → after: the .cfg lists the fork before 1.11.0 → 1.11.0 before the fork
  • Packaged exe run with -Xlog:class+load: Job, JobSupport, BuildersKt load from the fork before the fix, from 1.11.0 after it (zero classes loaded from the fork)
  • Crash repro: a packaged app calling Job.cancel() with the fork on its classpath hit NoSuchMethodError before the fix and runs clean after it
  • jewel-demo, scheduler-demo, system-info-demo distributables ship a 25.0.3 runtime and stay up when launched (scheduler-demo did not start before)
  • macOS / Linux packaging (not run; the rewrite happens before signing and before the pathing-jar collapse)

Known, not addressed here

  • system-info-demo logs IllegalAccessError in lets-plot (SkiaContext2d calling the private Matrix33(float[]) constructor) — the same under ./gradlew run, so it's a lets-plot/skiko version mismatch, not packaging.
  • One createRuntimeImage failure (jlink AbstractMethodError), seen once with three jlink runs in parallel in the same build; did not reproduce when rerun.

jpackage has no classpath option: it lists every file of --input, sorted by
name, after the main jar. When two JARs define the same classes, the packaged
app therefore loaded whichever sorted first, while ./gradlew run follows the
runtime-classpath order. Jewel apps hit it: the IntelliJ icon libraries pull
kotlinx-coroutines-core-jvm-1.10.2-intellij-2, which sorted before 1.11.0 and
failed at run time with NoSuchMethodError (Job.cancel$default).

The launcher .cfg classpath is now rewritten in classpath order right after
jpackage, before macOS signing and the Linux pathing-jar collapse. The
sandboxed strip task, whose output directory loses the order, records it in
.classpath-order for the package task.
jewel-demo, scheduler-demo and system-info-demo compile to class file 69 but
were packaged with the Gradle JVM's runtime, so the distributable died with
UnsupportedClassVersionError. Resolve a 25 toolchain as jewel-tabs-demo does.
@kdroidFilter
kdroidFilter merged commit 567b518 into nucleus-2.6 Sep 25, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant