Skip to content

Commit 7ffe588

Browse files
leifericfclaude
andcommitted
fix(launcher): reuse system Java 21+ and stop JRE extract failing on WSL
Two related JRE-bootstrap fixes in the launcher: 1. Detect a usable system Java before downloading. The launcher used to pull a ~200MB Adoptium JRE to ~/.noumenon/jre/ on first run even when the user already had Java 21+ installed. It now checks $JAVA_HOME and `java` on PATH; if either points at a Java 21+ runtime, the launcher uses it. The minimum is 21 because Noumenon's uberjar is compiled targeting 21 -- older JVMs fail with UnsupportedClassVersionError at class-load time. The bundled JRE remains the fallback for users on older Java or none at all. 2. Stage the downloaded JRE under ~/.noumenon/ instead of /tmp. On WSL, /tmp is tmpfs while ~ is ext4. java.nio.file.Files/move can rename a single file across filesystems but throws FileSystemException on non-empty directories, so the move into ~/.noumenon/jre/ blew up on the JRE's `legal/` / `bin/` / `lib/` subdirs ("Error: /tmp/noum-jre-…/jdk-21…/legal"). Staging under ~/.noumenon/jre-staging-…/ keeps the move intra-filesystem; a copy-tree fallback covers any other cross-fs configuration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 73729dd commit 7ffe588

2 files changed

Lines changed: 94 additions & 10 deletions

File tree

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
### Fixes
6+
7+
- **`noum` reuses an existing system Java when one is available** — the launcher used to unconditionally download a ~200MB JRE to `~/.noumenon/jre/` on first run, even when the user already had Java 21+ installed. It now checks `$JAVA_HOME` and `java` on `PATH` first; if either points at a Java 21+ runtime (the minimum the uberjar targets — older JVMs fail with `UnsupportedClassVersionError` at class-load time), the launcher uses it and skips the download. The bundled JRE remains the fallback for users on Java 17 or older, or with no Java at all.
8+
- **`noum` JRE bootstrap no longer fails on WSL with a cross-filesystem extraction error** — the launcher staged the downloaded JRE in `/tmp` (which on WSL is `tmpfs`) before moving it into `~/.noumenon/jre/` (which lives on ext4). `java.nio.file.Files/move` can rename single files across filesystems but throws `FileSystemException` on non-empty directories, so the move blew up on the JRE's `legal/`/`bin/`/`lib/` subdirs with `Error: /tmp/noum-jre-…/jdk-21…/legal`. Staging now happens under `~/.noumenon/jre-staging-…/` so the move is always intra-filesystem; a copy-tree fallback covers any other cross-fs configuration.
9+
510
## 0.12.0
611

712
### BREAKING CHANGES

launcher/src/noum/jre.clj

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
[noum.paths :as paths]
99
[noum.tui.core :as tui]
1010
[noum.tui.spinner :as spinner])
11-
(:import [java.security MessageDigest]))
11+
(:import [java.io IOException]
12+
[java.security MessageDigest]))
1213

1314
(def ^:private jre-version "21")
1415

@@ -78,16 +79,63 @@
7879
(do ((:stop s) "Warning: checksum verification failed")
7980
(tui/eprintln (str " " (.getMessage e) ". Continuing without verification."))))))))
8081

82+
(def ^:private min-major-version
83+
"Minimum Java major version Noumenon's uberjar runs on. Bump when deps.edn
84+
targets a newer LTS — this is the only system-Java acceptance gate."
85+
21)
86+
8187
(defn installed?
82-
"Check if a JRE is available at the expected location."
88+
"Check if the bundled JRE is available at the expected location."
8389
[]
8490
(let [java-bin (str (fs/path paths/jre-dir "bin" "java"))]
8591
(fs/exists? java-bin)))
8692

93+
(defn- parse-major-version
94+
"Parse the major Java version from `java -version` output. Returns int or nil.
95+
Handles both modern (\"21.0.4\") and legacy (\"1.8.0_392\") strings — for
96+
the latter the relevant major is the second segment (8)."
97+
[version-text]
98+
(when-let [[_ a b] (some->> version-text (re-find #"version \"(\d+)(?:\.(\d+))?"))]
99+
(try
100+
(let [a-int (Integer/parseInt a)]
101+
(if (and (= 1 a-int) b) (Integer/parseInt b) a-int))
102+
(catch Exception _ nil))))
103+
104+
(defn- system-java-version
105+
"Run `java-bin -version` and return its major version, or nil on error.
106+
`java -version` writes to stderr; some JVMs split between out and err."
107+
[java-bin]
108+
(try
109+
(let [{:keys [out err]} (proc/shell {:out :string :err :string :continue true}
110+
java-bin "-version")]
111+
(parse-major-version (str err out)))
112+
(catch Exception _ nil)))
113+
114+
(defn- system-jre-home
115+
"Return the home of a usable system JRE (Java `min-major-version`+), or nil.
116+
Checks $JAVA_HOME, then `java` on PATH."
117+
[]
118+
(let [candidates (concat
119+
(when-let [jh (System/getenv "JAVA_HOME")]
120+
[{:home jh :bin (str (fs/path jh "bin" "java"))}])
121+
(when-let [path-java (some-> (fs/which "java") str)]
122+
;; java's home is <prefix>/bin/java → <prefix>. Resolve
123+
;; the symlink so `~/.local/bin/java`-style shims point
124+
;; at the real install root.
125+
[{:home (str (fs/parent (fs/parent (fs/canonicalize path-java))))
126+
:bin path-java}]))]
127+
(some (fn [{:keys [home bin]}]
128+
(when (fs/exists? bin)
129+
(when-let [v (system-java-version bin)]
130+
(when (>= v min-major-version) home))))
131+
candidates)))
132+
87133
(defn java-home
88-
"Return the JRE home directory."
134+
"Return a usable JRE home: bundled if installed, otherwise a Java
135+
`min-major-version`+ runtime on the system. Nil if neither is available."
89136
[]
90-
(when (installed?) paths/jre-dir))
137+
(or (when (installed?) paths/jre-dir)
138+
(system-jre-home)))
91139

92140
(defn- find-jre-root
93141
"After extracting, find the actual JRE root (may be nested in a directory).
@@ -101,6 +149,19 @@
101149
(if (fs/exists? home) home inner))
102150
(str extract-dir))))
103151

152+
(defn- relocate!
153+
"Move src → target, falling back to recursive copy + delete if the
154+
underlying Files/move can't rename (typically a cross-filesystem move
155+
of a non-empty directory). WSL is the common case: /tmp lives on
156+
tmpfs while $HOME lives on ext4, so Files/move on the JRE's
157+
subdirectories throws a FileSystemException."
158+
[src target]
159+
(try
160+
(fs/move src target {:replace-existing true})
161+
(catch IOException _
162+
(fs/copy-tree src target {:replace-existing true})
163+
(fs/delete-tree src))))
164+
104165
(defn download!
105166
"Download and install JRE. Returns the JRE directory path."
106167
[]
@@ -109,7 +170,13 @@
109170
url (adoptium-url os arch)
110171
s (spinner/start (str "Downloading JRE " jre-version " for " os "/" arch "..."))
111172
s2-atom (atom nil)
112-
tmp-dir (str (fs/create-temp-dir {:prefix "noum-jre-"}))
173+
;; Stage under ~/.noumenon/ rather than the system temp dir so the
174+
;; move into paths/jre-dir is always intra-filesystem. On WSL the
175+
;; system /tmp is tmpfs and ~ is ext4, so a /tmp staging dir would
176+
;; force a cross-fs move and break on the JRE's non-empty
177+
;; subdirectories (`legal/`, `bin/`, `lib/`, ...).
178+
_ (fs/create-dirs paths/noum-dir)
179+
tmp-dir (str (fs/create-temp-dir {:path paths/noum-dir :prefix "jre-staging-"}))
113180
ext (if (= os "windows") ".zip" ".tar.gz")
114181
archive (str (fs/path tmp-dir (str "jre" ext)))]
115182
(try
@@ -130,7 +197,7 @@
130197
(doseq [f (fs/list-dir root)]
131198
(let [target (str (fs/path paths/jre-dir (fs/file-name f)))]
132199
(when-not (str/ends-with? (str f) ext)
133-
(fs/move f target {:replace-existing true})))))
200+
(relocate! f target)))))
134201
((:stop s2) "JRE installed."))
135202
paths/jre-dir
136203
(catch Exception e
@@ -142,9 +209,21 @@
142209
(fs/delete-tree tmp-dir)))))
143210

144211
(defn ensure!
145-
"Ensure JRE is installed. Download if not. Returns JRE directory."
212+
"Return a usable JRE home. Tries (1) the bundled JRE under ~/.noumenon/,
213+
(2) a system Java that satisfies `min-major-version`, (3) a fresh
214+
download as a last resort. Noumenon's uberjar targets Java
215+
`min-major-version`, so anything older is rejected here rather than
216+
blowing up later with UnsupportedClassVersionError."
146217
[]
147-
(if (installed?)
218+
(cond
219+
(installed?)
148220
paths/jre-dir
149-
(do (tui/eprintln "First run: downloading JRE (~200MB) to ~/.noumenon/")
150-
(download!))))
221+
222+
:else
223+
(if-let [sys-home (system-jre-home)]
224+
(do (tui/eprintln (str "Using system Java at " sys-home
225+
" (skipping " jre-version "+ bundled download)."))
226+
sys-home)
227+
(do (tui/eprintln (str "No Java " min-major-version "+ found. "
228+
"First run: downloading JRE (~200MB) to ~/.noumenon/"))
229+
(download!)))))

0 commit comments

Comments
 (0)