|
8 | 8 | [noum.paths :as paths] |
9 | 9 | [noum.tui.core :as tui] |
10 | 10 | [noum.tui.spinner :as spinner]) |
11 | | - (:import [java.security MessageDigest])) |
| 11 | + (:import [java.io IOException] |
| 12 | + [java.security MessageDigest])) |
12 | 13 |
|
13 | 14 | (def ^:private jre-version "21") |
14 | 15 |
|
|
78 | 79 | (do ((:stop s) "Warning: checksum verification failed") |
79 | 80 | (tui/eprintln (str " " (.getMessage e) ". Continuing without verification.")))))))) |
80 | 81 |
|
| 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 | + |
81 | 87 | (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." |
83 | 89 | [] |
84 | 90 | (let [java-bin (str (fs/path paths/jre-dir "bin" "java"))] |
85 | 91 | (fs/exists? java-bin))) |
86 | 92 |
|
| 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 | + |
87 | 133 | (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." |
89 | 136 | [] |
90 | | - (when (installed?) paths/jre-dir)) |
| 137 | + (or (when (installed?) paths/jre-dir) |
| 138 | + (system-jre-home))) |
91 | 139 |
|
92 | 140 | (defn- find-jre-root |
93 | 141 | "After extracting, find the actual JRE root (may be nested in a directory). |
|
101 | 149 | (if (fs/exists? home) home inner)) |
102 | 150 | (str extract-dir)))) |
103 | 151 |
|
| 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 | + |
104 | 165 | (defn download! |
105 | 166 | "Download and install JRE. Returns the JRE directory path." |
106 | 167 | [] |
|
109 | 170 | url (adoptium-url os arch) |
110 | 171 | s (spinner/start (str "Downloading JRE " jre-version " for " os "/" arch "...")) |
111 | 172 | 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-"})) |
113 | 180 | ext (if (= os "windows") ".zip" ".tar.gz") |
114 | 181 | archive (str (fs/path tmp-dir (str "jre" ext)))] |
115 | 182 | (try |
|
130 | 197 | (doseq [f (fs/list-dir root)] |
131 | 198 | (let [target (str (fs/path paths/jre-dir (fs/file-name f)))] |
132 | 199 | (when-not (str/ends-with? (str f) ext) |
133 | | - (fs/move f target {:replace-existing true}))))) |
| 200 | + (relocate! f target))))) |
134 | 201 | ((:stop s2) "JRE installed.")) |
135 | 202 | paths/jre-dir |
136 | 203 | (catch Exception e |
|
142 | 209 | (fs/delete-tree tmp-dir))))) |
143 | 210 |
|
144 | 211 | (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." |
146 | 217 | [] |
147 | | - (if (installed?) |
| 218 | + (cond |
| 219 | + (installed?) |
148 | 220 | 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