55 [rules-clojure.java.classpath :as cp]
66 [rules-clojure.namespace.parse :as parse]
77 [rules-clojure.fs :as fs]
8- [rules-clojure.util :refer [with-context-classloader]])
9- (:import [java.util.concurrent CompletableFuture]
10- [java.security MessageDigest]))
8+ [rules-clojure.util :refer [shasum]])
9+ (:import [java.util.concurrent CompletableFuture]))
1110
1211(set! *warn-on-reflection* true )
1312
1716; ; all users to upgrade so try to do that sparingly as well.
1817
1918(defn debug [& args]
20- #_ (println (locking true (apply print-str args))))
19+ #_ (println (locking true (apply print-str args))))
20+
21+ (defn deref!
22+ " throw `ex` if *f does not complete within timeout"
23+ [*f timeout ex]
24+ {:pre [(future? *f)
25+ (integer? timeout)
26+ (instance? Throwable ex)]}
27+ (when (= ::timeout (deref *f timeout ::timeout ))
28+ (throw ex)))
2129
2230(defn src-resource-name [ns ]
2331 (.substring ^String (#'clojure.core/root-resource ns ) 1 ))
2432
25- (defn get-class-loader-path
26- " returns a tuple of [classloader classpath]"
27- []
28- (let [thread-cl (-> (Thread/currentThread ) (.getContextClassLoader ))]
29- [thread-cl (cp/classpath thread-cl)]))
30-
3133(defn src-resource
3234 " given a namespace symbol, return a tuple of [filename URL] where the
3335 backing .clj is located, or nil if it couldn't be found"
3436 [ns ]
3537 {:pre [(symbol? ns )]}
36- (let [[cl _] (get-class-loader-path )]
37- (->> [" .clj" " .cljc" ]
38- (map (fn [ext]
39- (let [src-path (str (src-resource-name ns ) ext)
40- src-resource (io/resource src-path cl)]
41- (when src-resource
42- [src-path src-resource]))))
43- (filter identity)
38+ (->> [" .clj" " .cljc" ]
39+ (map (fn [ext]
40+ (let [src-path (str (src-resource-name ns ) ext)
41+ src-resource (io/resource src-path)]
42+ (when src-resource
43+ [src-path src-resource]))))
44+ (filter identity)
4445 ; ; ((fn [srcs]
4546 ; ; (when (> (count srcs) 1)
4647 ; ; (println "WARNING multiple copies of" ns "found:" srcs))
4748 ; ; srcs))
48- (first ) )))
49+ (first )))
4950
5051(defn loaded? [ns ]
5152 {:pre [(symbol? ns )]}
5455; ; block i.e. `(ns foo (:require bar))`. We use futures that require
5556; ; namespaces, so the best way to know if a namespace is _done_
5657; ; loading is to either deref the future, or check inside _your own_
57- ; ; namespace future. if loaded? returns true anywhere else, it could
58- ; ; be in the process of loading, which will lead to weird errors like
59- ; ; `Unable to resolve symbol: foo`, when you know it should be there.
58+ ; ; namespace future. if loaded? returns true anywhere else, the
59+ ; ; namespace loading could be after the ns block, but before the end
60+ ; ; of the file, which will lead to weird errors like `Unable to
61+ ; ; resolve symbol: foo`, when you know it should be there.
6062
6163 (contains? (loaded-libs ) ns ))
6264
@@ -93,18 +95,10 @@ be found"
9395 (str/replace #"\. " " /" )
9496 (str " __init.class" )))
9597
96- (defn ns->class-name
97- " given a namespace symbol, return the name of the class that will load it"
98- [ns ]
99- (-> ns
100- (munge )
101- (str " __init" )))
102-
10398(defn compiled?
10499 " truthy if the namespace has AOT .class files on the classpath"
105100 [ns ]
106- (let [[cl _cp] (get-class-loader-path )]
107- (.getResource ^ClassLoader cl (ns->resource-name ns ))))
101+ (io/resource (ns->resource-name ns )))
108102
109103; ; root directory for all compiles. Each compile will be a subdir of
110104; ; this
@@ -113,14 +107,6 @@ be found"
113107(-> (Runtime/getRuntime ) (.addShutdownHook (Thread. ^Runnable (fn []
114108 (fs/rm-rf temp-dir)))))
115109
116- (defn shasum [^bytes bs]
117- {:pre [(seq bs)]}
118- (let [digest (MessageDigest/getInstance " SHA-1" )
119- hexer (java.util.HexFormat/of )]
120- (-> bs
121- (#(.digest digest %))
122- (#(.formatHex hexer %)))))
123-
124110(defn ns-sha
125111 " return the hash of the ns file contents"
126112 [ns ]
@@ -208,34 +194,32 @@ be found"
208194
209195(defn ns-deps- [ns ]
210196 (assert (src-resource ns ) (print-str " couldn't find resource for" ns ))
211-
212197 (-> ns
213198 src-resource
214199 second
215200 reader
216201 parse/read-ns-decl
217- parse/deps-from-ns-decl))
202+ parse/deps-from-ns-decl
203+ (disj ns )))
218204
219205(def ns-deps (memoize ns-deps-))
220206
221- (defn ensure-dir-classpath [dir]
222- (let [[cl classpath] (get-class-loader-path )]
223- (when (not (some (partial = dir) classpath))
224- (.addURL cl (.toURL (fs/path->file dir))))))
225-
226207(defn compile- [ns ]
227208 (let [sha (ns-sha ns )
228209 classes-dir (->cache-dir sha)]
229- (when-not (compiled? ns )
230- (when (loaded? ns )
231- (debug " WARNING:" ns " already loaded before compilation!"
232- :compiled? (compiled? ns )
233- :loaded? (loaded? ns )
234- :bound-require? (bound? #'clojure.core/require)
235- :sha sha))
236- (assert (not (loaded? ns )) (print-str ns :compiled? (compiled? ns ) :loaded? (loaded? ns ) :sha sha))
237- (binding [*compile-path* (str classes-dir)]
238- (compile ns )))))
210+ (if (compiled? ns )
211+ (require ns )
212+ (do
213+ (when (loaded? ns )
214+ (debug " WARNING:" ns " already loaded before compilation!"
215+ :compiled? (compiled? ns )
216+ :loaded? (loaded? ns )
217+ :bound-require? (bound? #'clojure.core/require)
218+ :sha sha))
219+ (assert (not (loaded? ns )) (print-str ns :compiled? (compiled? ns ) :loaded? (loaded? ns ) :sha sha))
220+ (binding [*compile-path* (str classes-dir)]
221+ (compile ns )
222+ (assert (seq (fs/ls-r classes-dir)) (print-str " compile-: no .class files found in" classes-dir)))))))
239223
240224; ; map of ns symbol to future.
241225(def ns-futures (atom {}))
@@ -264,19 +248,44 @@ be found"
264248; ; once. Compilation will happen once, repeat sends to the same ns do
265249; ; nothing
266250
251+ (defn context-classloader-conveyor-fn [f]
252+ ; ; context classloaders are not conveyed by default in futures, but we set it in rules-clojure.jar/compile!
253+ (let [cl (.getContextClassLoader (Thread/currentThread ))]
254+ (fn
255+ ([]
256+ (.setContextClassLoader (Thread/currentThread ) cl)
257+ (f )))))
258+
259+ (defn binding-conveyor-fn [f]
260+ ; ; don't use clojure.core/binding-conveyor-fn, because that usees
261+ ; ; clone/reset ThreadBindings, rather than push/pop. We use push and
262+ ; ; pop because clone shares TBoxes, which store the threadId they
263+ ; ; came from, which breaks clojure.lang.Compiler when an in-progress
264+ ; ; compile attempts to Var/set! from a new thread
265+ (let [bindings (clojure.lang.Var/getThreadBindings )]
266+ (fn
267+ ([]
268+ (try
269+ (clojure.lang.Var/pushThreadBindings bindings)
270+ (f )
271+ (finally
272+ (clojure.lang.Var/popThreadBindings )))))))
273+
267274(defn ns-send
268275 [ns f]
269276 {:pre [(symbol? ns )]
270277 :post [(future? %)]}
271- (let [cl (.getContextClassLoader (Thread/currentThread ))]
278+
279+ (let [f (context-classloader-conveyor-fn
280+ (binding-conveyor-fn f))]
272281 (-> ns-futures
273282 (swap! update ns (fn [**f]
274283 (or **f
275- (delay (future ( with-context-classloader cl
276- (try
277- (f )
278- (catch Throwable t
279- (throw (ex-info (print-str " in ns-send" ns :parallel? *parallel*) {} t) )))))))))
284+ (delay (future
285+ (try
286+ (f )
287+ (catch Throwable t
288+ (throw (ex-info (print-str " in ns-send" ns :parallel? *parallel*) {} t)))))))))
280289 (get ns )
281290 (deref ))))
282291
@@ -300,8 +309,9 @@ be found"
300309 (deref )))
301310
302311(defn track-dep!
303- " track that ns a depends on ns b. Returns truthy if there's no cycle,
304- falsey if there is"
312+ " track that ns a depends on ns b. Updates the graph and returns truthy
313+ if there's no cycle. Does not update the graph and returns false if
314+ there is"
305315 [a b]
306316 {:pre [(symbol? a)
307317 (symbol? b)]}
@@ -315,15 +325,12 @@ be found"
315325 (debug " WARNING cycle:" a " ->" b)
316326 false ))))
317327
318- (def ^:dynamic *parallel* true )
319-
320328(defn pcompile
321329 " From namespace `parent`, compile `ns`, in parallel"
322330 [parent ns ]
323331 {:pre [(or (symbol? parent) (nil? parent))
324332 (symbol? ns )]
325333 :post [(future? %)]}
326-
327334 (let [parallel? (and *parallel* (not (contains? no-parallel ns )))
328335 send (if parallel? ns-send ns-send-sync)]
329336 (if (or (not parent) (track-dep! parent ns ))
@@ -337,12 +344,12 @@ be found"
337344 (let [cycle? (not (track-dep! ns d))
338345 *f (pcompile ns d)]
339346 (when (not cycle?)
340- ; ; don't deref the compiles that cause cycles
341- [d *f]))))
347+ ; ; don't deref the compile that
348+ ; ; cause cycles, the other
349+ ; ; thread will take care of it
350+ *f))))
342351 (filter identity)
343- (mapv (fn [[d *f]]
344- (when (= :timeout (deref *f 30000 :timeout ))
345- (throw (ex-info (print-str " timeout in" ns " waiting for" d) {:dep d}))))))
352+ (mapv deref))
346353 (if compile?
347354 (compile- ns )
348355 (require ns ))
@@ -352,7 +359,8 @@ be found"
352359 (CompletableFuture/completedFuture true )))))
353360
354361(defn pcopy [dest-dir ns ]
355- @(pcompile nil ns )
362+ (deref! (pcompile nil ns ) 120000
363+ (ex-info (print-str " pcopy timeout waiting for" ns ) {:dest-dir dest-dir :ns ns }))
356364 (assert (loaded? ns ) (print-str ns " not loaded" ))
357365 (let [sha (ns-sha ns )
358366 cache-dir (get-cache-dir sha)]
@@ -361,23 +369,28 @@ be found"
361369 :compiled? (compiled? ns )
362370 :dest dest-dir
363371 :actual-classpath (cp/classpath )))
372+ (assert (seq (fs/ls-r cache-dir)) (print-str " pcopy: no .class files found in" cache-dir))
364373 (copy-classes (fs/->path cache-dir) (fs/->path dest-dir))))
365374
366375(defn prequire
367376 " given a seq of namespaces, ensure all are compiled (and loaded) in parallel. Blocking."
368377 [nses]
369378 {:pre [(every? symbol? nses)]}
370379 (let [ns-block? (not (contains? (loaded-libs ) (symbol (str *ns*))))
371- ns-sym (symbol (str *ns*))]
380+ ns-sym (symbol (str *ns*))
381+ ; ; don't hang on self-requires. Silly bug, but it happens in
382+ ; ; real life, and regular compiles don't explode
383+ nses (disj (set nses) ns-sym)]
372384 (when-not ns-block?
373385 (debug ns-sym " loading" nses " at toplevel" ))
374- (debug " spy-require" ns-sym nses)
375386 (->> nses
376387 (mapv (partial pcompile ns-sym))
377- ; ; (mapv (fn [*f]
378- ; ; (when (= :timeout (deref *f 30000 :timeout))
379- ; ; (throw (ex-info (print-str "in" ns-sym "timeout waiting for" nses) {})))))
380- (mapv deref))))
388+ (mapv (fn [*f]
389+ ; ; if we hang using 1-arity deref here, bazel doesn't
390+ ; ; return logging and it's very hard to debug. Set
391+ ; ; this high enough that it never accidentally
392+ ; ; triggers
393+ (deref! *f 120000 (ex-info (print-str " prequire timeout in" ns-sym " waiting for" nses) {})))))))
381394
382395; ; `require` calls load-libs->load-libs->load-one
383396; ; `load` calls clojure.lang.RT/load, so there's no common place to hook into both
@@ -526,3 +539,5 @@ be found"
526539 (pcompile nil n))
527540 (doseq [n aot-nses]
528541 (pcopy (str classes-dir " /" ) n))))))
542+
543+ (comment " text here to force rebuilding" )
0 commit comments