diff --git a/jolt-core/jolt/deps.clj b/jolt-core/jolt/deps.clj index 1474b26..e4c2384 100644 --- a/jolt-core/jolt/deps.clj +++ b/jolt-core/jolt/deps.clj @@ -78,33 +78,37 @@ (map #(abspath root %) paths))) (defn- resolve-deps - "Breadth-first walk of a deps map; returns an ordered, de-duplicated list of - source-root directories. `base-dir` resolves :local/root and is replaced by a - dep's own root as the walk descends." + "Breadth-first walk of a deps map; returns {:roots [...] :natives [...]} — the + ordered, de-duplicated source-root directories and the collected :jolt/native + declarations from every dep's deps.edn. `base-dir` resolves :local/root and is + replaced by a dep's own root as the walk descends." [deps base-dir] (loop [queue (mapv (fn [[c s]] [c s base-dir]) (seq deps)) seen #{} - roots []] + roots [] + natives []] (if (empty? queue) - (distinct roots) + {:roots (distinct roots) :natives natives} (let [[coord spec bd] (first queue) queue (subvec (vec queue) 1)] (if (contains? seen coord) - (recur queue seen roots) + (recur queue seen roots natives) (let [root (coord-root coord spec bd)] (if (nil? root) - (recur queue (conj seen coord) roots) + (recur queue (conj seen coord) roots natives) (let [edn (read-edn (str root "/deps.edn")) child (mapv (fn [[c s]] [c s root]) (seq (:deps edn)))] (recur (into queue child) (conj seen coord) - (into roots (dep-source-roots root))))))))))) + (into roots (dep-source-roots root)) + (into natives (:jolt/native edn))))))))))) ;; --- public ----------------------------------------------------------------- (defn resolve-project "Resolve `project-dir`'s deps.edn with the selected alias keywords. Returns - {:roots [...] :main-opts [...] :tasks {...}}; :main-opts is the last selected - alias's, else nil." + {:roots [...] :main-opts [...] :tasks {...} :natives [...]}; :main-opts is the + last selected alias's, else nil; :natives are the project's + deps' :jolt/native + shared-library declarations." ([project-dir] (resolve-project project-dir [])) ([project-dir alias-kws] (let [edn (read-edn (str project-dir "/deps.edn")) @@ -116,10 +120,11 @@ project-paths (concat (or (:paths edn) ["src"]) extra-paths) project-roots (map #(abspath project-dir %) project-paths) all-deps (merge (:deps edn) extra-deps) - dep-roots (resolve-deps all-deps project-dir)] + {dep-roots :roots dep-natives :natives} (resolve-deps all-deps project-dir)] {:roots (vec (distinct (concat project-roots dep-roots))) :main-opts main-opts - :tasks (:tasks edn)}))) + :tasks (:tasks edn) + :natives (vec (distinct (concat (:jolt/native edn) dep-natives)))}))) (defn has-deps-edn? [project-dir] (file-exists? (str project-dir "/deps.edn"))) diff --git a/jolt-core/jolt/main.clj b/jolt-core/jolt/main.clj index fb46c93..6cfe3dc 100644 --- a/jolt-core/jolt/main.clj +++ b/jolt-core/jolt/main.clj @@ -8,10 +8,34 @@ (defn- project-dir [] (or (jolt.host/getenv "JOLT_PWD") ".")) +;; Load a library's declared native shared objects (deps.edn :jolt/native) before +;; its Clojure is required, so its foreign-fn bindings resolve. Each entry is a +;; map: {:name "sqlite3" :darwin ["libsqlite3.0.dylib" ...] :linux ["libsqlite3.so.0" ...]} +;; with optional :optional (missing is fine — a feature-gated dep) and :process +;; (use the running process's symbols, e.g. libc sockets — no external file). +(defn- load-natives! [natives] + (when (seq natives) + (let [os (str/lower-case (or (System/getProperty "os.name") "")) + plat (cond (str/includes? os "mac") :darwin + (str/includes? os "win") :windows + :else :linux)] + (doseq [spec natives] + (if (:process spec) + (jolt.ffi/load-library) + (let [c (get spec plat) + cands (if (string? c) [c] (vec c)) + hit (some #(when (jolt.ffi/loaded? %) %) cands)] + (when (and (nil? hit) (not (:optional spec))) + (throw (ex-info (str "required native library " + (or (:name spec) (first cands) "?") + " not found — tried " (pr-str cands) " for " (name plat)) + {:native spec}))))))))) + ;; Apply a resolved project's roots on top of the current (jolt-core) roots so app -;; namespaces resolve while jolt.* stays loadable. -(defn- apply-roots! [roots] - (jolt.host/set-source-roots! (vec (distinct (concat roots (jolt.host/source-roots)))))) +;; namespaces resolve while jolt.* stays loadable, then load its native deps. +(defn- apply-project! [{:keys [roots natives]}] + (jolt.host/set-source-roots! (vec (distinct (concat roots (jolt.host/source-roots))))) + (load-natives! natives)) (defn- run-ns "Require ns-name and invoke its -main with the string app args." @@ -36,27 +60,25 @@ ;; run [-m NS args… | FILE] (defn- cmd-run [more] - (let [{:keys [roots]} (deps/resolve-project (project-dir))] - (apply-roots! roots) - (cond - (= "-m" (first more)) (run-ns (second more) (drop 2 more)) - (seq more) (do (load-file (first more)) nil) - :else (throw (ex-info "run needs -m NS or a FILE" {}))))) + (apply-project! (deps/resolve-project (project-dir))) + (cond + (= "-m" (first more)) (run-ns (second more) (drop 2 more)) + (seq more) (do (load-file (first more)) nil) + :else (throw (ex-info "run needs -m NS or a FILE" {})))) ;; -M:alias… — resolve with the aliases, run their :main-opts (defn- cmd-M [arg more] (let [aliases (parse-aliases arg) - {:keys [roots main-opts]} (deps/resolve-project (project-dir) aliases)] - (apply-roots! roots) + {:keys [main-opts] :as resolved} (deps/resolve-project (project-dir) aliases)] + (apply-project! resolved) (if main-opts (apply-main-opts main-opts more) (throw (ex-info (str "alias(es) " (pr-str aliases) " have no :main-opts") {}))))) ;; -A:alias… — add the aliases' paths/deps, then run the remaining argv as a command (defn- cmd-A [arg more] - (let [aliases (parse-aliases arg) - {:keys [roots]} (deps/resolve-project (project-dir) aliases)] - (apply-roots! roots) + (let [aliases (parse-aliases arg)] + (apply-project! (deps/resolve-project (project-dir) aliases)) (when (seq more) (run-ns (second more) (drop 2 more))))) (defn- cmd-path [] @@ -75,12 +97,12 @@ ;; A deps.edn :tasks entry: a string is a shell command; a map is {:main-opts …}. (defn- run-task [name more] - (let [{:keys [roots tasks]} (deps/resolve-project (project-dir)) + (let [{:keys [tasks] :as resolved} (deps/resolve-project (project-dir)) task (get tasks (symbol name))] (cond (nil? task) (throw (ex-info (str "unknown command or task: " name) {:name name})) (string? task) (jolt.host/sh task) - (map? task) (do (apply-roots! roots) (apply-main-opts (:main-opts task) more)) + (map? task) (do (apply-project! resolved) (apply-main-opts (:main-opts task) more)) :else (throw (ex-info (str "bad task " name) {}))))) (defn- usage []