A separate jolt-deps executable (mirroring jpm beside janet) resolves a deps.edn into source roots and runs jolt with them on JOLT_PATH; the runtime stays deps-agnostic. - src/jolt/deps.janet: read deps.edn (Janet parses it directly), resolve :git/* and :local/root via jpm/pm's download-bundle (git fetch + cache, no build), recurse for transitive deps, return de-duped roots. jpm loaded lazily so it's never embedded. Roots cached on a deps.edn hash. - src/jolt/deps_cli.janet + project.janet: the jolt-deps tool — path/run/repl/-e. - main: apply JOLT_PATH at runtime (the CLI ctx is frozen into the image at build, so init's env read wouldn't see it). Verified end to end against a local dep and a real git dep (medley). git only, pure clj/cljc. Test: deps-resolve-test (local deps, no network).
6.9 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:
jolt-depsresolvesdeps.edn(cached on a hash of it, so it's a no-op when unchanged) and runs jolt with the roots onJOLT_PATH;(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:
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.
A separate tool, like jpm beside janet
The jolt runtime knows nothing about deps.edn — exactly as the janet binary
knows nothing about jpm. Resolution lives in a separate jolt-deps tool (its own
declare-executable), and the runtime's only interface is the source roots in
JOLT_PATH:
jolt-deps path # print the resolved roots, ':'-joined
jolt-deps run FILE [args] # resolve, then run `jolt FILE …` with JOLT_PATH set
jolt-deps repl # resolve, then start a jolt REPL with JOLT_PATH set
jolt-deps -e EXPR [args] # resolve, then `jolt -e …`
It reuses jpm/pm (resolve-bundle + download-bundle, after
jpm/config/load-default) for the git fetch/cache rather than shelling git —
less code, shares jpm's cache. jpm is loaded lazily (require, not import), so
it's pulled in only when resolving (dev time), never embedded in a binary. The
internal-API risk is acceptable since it's dev-time only.
One gotcha worth recording: jolt's context is built into its image at build
time, so JOLT_PATH is read at runtime in main (not in init, whose env read
would be frozen at build).
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. (done)
src/jolt/deps.janetreadsdeps.edn, resolves:git/*+:local/rootthroughjpm/pmintojpm_tree/.cache, recurses for transitive deps, and returns the roots (cached on a deps.edn hash). The separatejolt-depstool (src/jolt/deps_cli.janet,declare-executable) exposespath/run/repl/-e. Seetest/integration/deps-resolve-test.janet. - Build-time compile-in. Fold the used dep namespaces into the image at
build (as with embedded
jolt.nrepl), so a built artifact needs neither the deps nor jpm. (not started) - 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.