|
1 | 1 | (ns lambdaisland.launchpad.env |
2 | | - "Make environment variables modifiable from within Java, and use that to watch a |
3 | | - .env file for changes, and hot reload them. |
4 | | -
|
5 | | - This is *very* dirty, it uses reflection to get at various private bits of |
6 | | - Java, it relies on implementation details of OpenJDK, and it requires breaking |
7 | | - module isolation (the process has to start with |
8 | | - `--add-opens=java.base/java.lang=ALL-UNNAMED` |
9 | | - `--add-opens=java.base/java.util=ALL-UNNAMED`). We also rely on jnr-posix to |
10 | | - get to the underlying setenv system call, for good measure. |
11 | | -
|
12 | | - But hey it works!" |
13 | | - (:require [lambdaisland.dotenv :as dotenv] |
14 | | - [lambdaisland.classpath.watch-deps :as watch-deps]) |
15 | | - (:import (java.nio.file Path Files LinkOption))) |
16 | | - |
17 | | -(set! *warn-on-reflection* true) |
18 | | - |
19 | | -(defn accessible-field ^java.lang.reflect.Field [^Class klz field] |
20 | | - (doto (.getDeclaredField klz field) |
21 | | - (.setAccessible true))) |
22 | | - |
23 | | -(defn get-static [field] |
24 | | - (let [klz (Class/forName (namespace field))] |
25 | | - (.get (accessible-field klz |
26 | | - (name field)) klz))) |
27 | | - |
28 | | -(defn get-field [^Object instance field] |
29 | | - (.get (accessible-field (.getClass instance) (str field)) instance)) |
30 | | - |
31 | | -(defn set-field! [klz field obj val] |
32 | | - (.set (accessible-field klz field) obj val)) |
33 | | - |
34 | | -(defn set-static! [klz field val] |
35 | | - (set-field! klz field klz val)) |
36 | | - |
37 | | -(def ^java.util.Map theEnvironment |
38 | | - (get-static 'java.lang.ProcessEnvironment/theEnvironment)) |
39 | | - |
40 | | -(def ^java.lang.ProcessEnvironment$StringEnvironment theUnmodifiableEnvironment |
41 | | - (get-field (get-static 'java.lang.ProcessEnvironment/theUnmodifiableEnvironment) 'm)) |
42 | | - |
43 | | -(def ^jnr.posix.POSIX posix (jnr.posix.POSIXFactory/getPOSIX)) |
44 | | - |
45 | | -(defn new-value [^String str] |
46 | | - (assert (= -1 (.indexOf str "\u0000"))) |
47 | | - (let [^java.lang.reflect.Constructor init |
48 | | - (first (.getDeclaredConstructors java.lang.ProcessEnvironment$Value))] |
49 | | - (.setAccessible init true) |
50 | | - (.newInstance init (into-array Object ["XXX" (.getBytes "XXX")])))) |
51 | | - |
52 | | -(defn new-variable [^String str] |
53 | | - (assert (and (= -1 (.indexOf str "=")) |
54 | | - (= -1 (.indexOf str "\u0000")))) |
55 | | - (let [^java.lang.reflect.Constructor init |
56 | | - (first (.getDeclaredConstructors java.lang.ProcessEnvironment$Variable))] |
57 | | - (.setAccessible init true) |
58 | | - (.newInstance init (into-array Object ["XXX" (.getBytes "XXX")])))) |
59 | | - |
60 | | -(defn setenv |
61 | | - ([env] |
62 | | - (run! (fn [[k v]] (setenv k v)) env)) |
63 | | - ([^String var ^String val] |
64 | | - ;; This one is used by ProcessBuilder |
65 | | - (.put theEnvironment (new-variable var) (new-value val)) |
66 | | - ;; This one is used by System/getenv |
67 | | - (.put theUnmodifiableEnvironment var val) |
68 | | - ;; Also change the actual OS environment for the process |
69 | | - (.setenv posix var val 1))) |
| 2 | + (:require [lambdaisland.dotenv :as dotenv]) |
| 3 | + (:import (java.nio.file Files LinkOption Path))) |
70 | 4 |
|
71 | 5 | (defn exists? |
72 | 6 | "Does the given path exist." |
73 | 7 | [path] |
74 | | - (Files/exists (watch-deps/path path) (into-array LinkOption []))) |
| 8 | + (Files/exists path (into-array LinkOption []))) |
| 9 | + |
| 10 | +(defn ->path |
| 11 | + [^String path] |
| 12 | + (Path/of path (into-array String []))) |
| 13 | + |
| 14 | +(defn canonical-path |
| 15 | + [^Path path] |
| 16 | + (.toRealPath path (into-array LinkOption []))) |
| 17 | + |
| 18 | +(def ^{:doc "The list of dotenv paths to watch. |
75 | 19 |
|
76 | | -(defn dotenv-watch-handler [paths] |
77 | | - (let [paths (map #(Path/of % (into-array String [])) paths)] |
78 | | - (fn [_] |
79 | | - (setenv |
80 | | - (apply merge |
81 | | - (map #(when (exists? %) |
82 | | - (dotenv/parse-dotenv (Files/readString %))) |
83 | | - paths)))))) |
| 20 | +Order counts as we use merge semantics when reading them in."} |
| 21 | + watch-paths [".env" ".env.local"]) |
84 | 22 |
|
85 | | -(defn watch-handlers [] |
86 | | - (let [h (dotenv-watch-handler [".env" ".env.local"])] |
87 | | - {".env" h |
88 | | - ".env.local" h})) |
| 23 | +(defn parse-dotenv |
| 24 | + [^Path path] |
| 25 | + (when (.exists (.toFile path)) |
| 26 | + (-> (Files/readString path) |
| 27 | + (dotenv/parse-dotenv)))) |
0 commit comments