The loader resolved a namespace against one hardcoded root (src/jolt, .clj only). Now it searches an ordered list (ctx env :source-paths, stdlib first) and tries .clj then .cljc. init adds roots from the :paths opt and JOLT_PATH. This is the foundation for loading deps.edn libraries; verified loading medley from an added root. No change to stdlib loading (3913 suite baseline held).
6.1 KiB
Loading Clojure libraries via deps.edn (git deps, on jpm)
Research notes for letting a Jolt project pull pure-Clojure libraries through a
deps.edn and (require ...) them. Scope is deliberately narrow:
- git deps only — no Maven/
~/.m2resolution. - pure
clj/cljc— anything needing the JVM (Java interop, host classes) won't load or run, and that's expected. - no classpath machinery — we just need
requireto find a dep's namespaces at dev time (REPL) so they can be compiled into the image at build time. - piggyback on jpm — reuse jpm's existing git fetch + cache; don't write a package manager.
Nothing here is implemented yet. Everything below was verified against the installed jpm and a real lib (medley).
How jpm handles dependencies today
jpm's package code lives in /opt/homebrew/lib/janet/jpm/pm.janet. The relevant
pieces:
resolve-bundlenormalizes a dep spec to{:url :tag :type :shallow}. It accepts a table (:url/:repo, and:tag/:sha/:commit/:ref) or a"url::type::tag"string. So a deps.edn{:git/url … :git/sha …}maps onto it directly.download-bundle url :git tag shallowclones into a cache dir and returns the path. The cache is(find-cache)=<modpath>/.cache, and the per-dep directory is a deterministic id:git_<tag>_<sanitized-url>. Under the hooddownload-git-bundledoesgit init+remote add origin+ fetch + reset to the tag/sha (plus submodules). Pure git — no build step.bundle-installis the part we don't want: after downloading it does(require-jpm "./project.janet")and runs build/install rules. A Clojure lib has noproject.janet, so this would fail. The clone/cache half (download-bundle) is cleanly separable from this build half.
So jpm already gives us git resolution + a content-addressed cache for free; we just skip its build phase.
Verified
(import jpm/config :as cfg)
(import jpm/pm :as pm)
(cfg/load-default) ; sets gitpath, cache defaults, etc.
(setdyn :modpath "<project>/jpm_tree") ; where the cache lives
(def s (pm/resolve-bundle {:url "https://github.com/weavejester/medley"
:tag "1.0.0" :shallow true}))
(pm/download-bundle (s :url) (s :type) (s :tag) (s :shallow))
;; => "<modpath>/.cache/git_1.0.0_https___github.com_weavejester_medley"
;; with source at <that>/src/medley/core.cljc
cfg/load-default is required — calling download-bundle without jpm's config
dyns set fails in its shell helper.
And the source loads and runs in Jolt: (medley.core/abs -5) → 5,
(medley.core/find-first odd? [2 4 5]) → 5 (coverage is per-function; a few
hit interpreter gaps, which is fine).
The shape
- Resolve. Read
deps.edn, take the:depswhose specs are git (:git/url+:git/sha/:git/tag). For each,resolve-bundle+download-bundleinto the project'sjpm_tree/.cache. Read each cloned dep's owndeps.ednand recurse for transitive git deps. (:local/rootdeps are even simpler — just a path, no fetch.) - Collect source roots. A dep's source dirs are its deps.edn
:paths(default["src"]), so a root is<clone-dir>/<path>. The result is just an ordered list of directories — not a classpath abstraction, a list of roots. - Teach the loader the roots.
evaluator.janet/ns->pathcurrently hardcodessrc/jolt/<ns>.clj. Generalizemaybe-require-nsto, after the stdlib path, search each dep root for<ns>.cljthen<ns>.cljc.src/jolt/stays first so the stdlib always wins. (Two small changes: a root list in the ctx, and trying.cljc.) - Dev vs build.
- Dev: on REPL/CLI start, if
deps.ednis present, resolve once (cache keyed on a hash of deps.edn so it's a no-op when unchanged) and register the roots.(require '[medley.core])then just works. - Build: the dep namespaces a project actually uses get compiled into the
image the same way the embedded
jolt.nreplsource is today — load them at build time so the shipped binary needs neither the deps nor jpm.
- Dev: on REPL/CLI start, if
Why this fits "no classpath"
We never construct a Java-style classpath or deal with jar extraction, ordering
semantics, or version conflict resolution. Resolution is a tree walk over git
deps; "the classpath" is just the list of src dirs of the clones. The loader
already does path-based namespace lookup — we widen it from one root to a few.
Integration with jpm
jpm stays the build tool for the Jolt binary; this lives beside it and calls
into jpm/pm for the git/cache work (import the module at dev/build time — jpm
is always present then; the shipped binary doesn't need it). Decision: reuse
jpm/pm (resolve-bundle + download-bundle, after jpm/config/load-default)
rather than shelling git ourselves — less code, and it shares jpm's cache. The
internal-API risk is acceptable since it's only used at dev/build time.
Limitations
- Pure
clj/cljconly. JVM interop, host classes, and unimplementedclojure.corecorners fail — expected, not a goal. - Per-function coverage: a namespace can load with most functions working and a few not.
- Source only; compiled
.classfiles (if any in a git dep) are ignored.
Plan
- Loader roots. (done)
maybe-require-nsnow searches an ordered root list (ctx.env :source-paths, stdlib first) trying.cljthen.cljc;initadds roots from the:pathsopt andJOLT_PATH(colon-separated). Verified loading a real lib (medley) from an added root. Seetest/integration/deps-loader-test.janet. - Resolve git deps via jpm. Read
deps.edn, resolve:git/*(+:local/root) throughjpm/pmintojpm_tree/.cache, recurse for transitive git deps, register the roots.jolt depsto resolve/print; auto on startup whendeps.ednexists. - Build-time compile-in. Fold the used dep namespaces into the image at
build (as with embedded
jolt.nrepl). - Conformance. Pull a few popular pure-
cljcgit libs, see what loads/runs, and drive interpreter gaps from the failures — same loop as the clojure-test-suite battery.