deps.edn :jolt/native — declare a library's native shared libraries
An FFI library declares the system shared objects it binds in its deps.edn (:jolt/native), with per-platform candidate sonames, :optional for feature-gated deps, and :process for libraries that use the running process's own symbols (libc sockets). jolt.deps collects them transitively; jolt loads them before the library's namespaces are required, so foreign-fn bindings resolve — and a missing required lib fails early with a clear message instead of a cryptic symbol error. Replaces hardcoded soname-probing inside library .clj files.
This commit is contained in:
parent
b7bd144321
commit
21fbc50014
2 changed files with 55 additions and 28 deletions
|
|
@ -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")))
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue