Findings and a phased plan for consuming deps.edn so projects can require real Clojure libraries. Jolt already reads EDN, jars ship .clj/.cljc source, and a real lib (medley) loads and runs. The gaps are a single-rooted loader (needs a classpath + .cljc) and dependency resolution. Recommends reusing 'clojure -Spath' to start, then native git-dep resolution; jpm stays the Janet build tool alongside this. See doc/tools-deps.md.
6.9 KiB
Loading Clojure libraries via deps.edn
Research notes on letting Jolt consume deps.edn so a project can pull real
Clojure libraries and (require ...) them. This documents what works today, what
it would take, and a recommended path. Nothing here is implemented yet.
Goal
Given a deps.edn like
{:paths ["src"]
:deps {medley/medley {:mvn/version "1.0.0"}
some/gitlib {:git/url "https://..." :git/sha "..."}}}
run jolt in that directory and have (require '[medley.core :as m]) find and
load the library's source from the resolved dependency, the same way the stdlib
is loaded today.
What works today
- Jolt reads EDN.
(read-string (slurp "deps.edn"))parses a deps.edn into a Jolt map — no extra parser needed. - Library source ships in the jars. Maven Clojure jars contain the
.clj/.cljcsource at namespace-matching paths (e.g.medley/core.cljc,msgpack/core.clj), not just compiled.classfiles. So we never need a JVM to run the code — only to fetch/resolve it, and even that is optional (below). - Jolt can run real library source. Loading
medley/core.cljcstraight from its jar works:(medley.core/abs -5)→5,(medley.core/find-first odd? …)→5. Some functions hit features Jolt doesn't fully support yet — coverage is per-function, not all-or-nothing. - The real resolver is on the box.
clojure,clj, andmvnare installed, with~/.m2and~/.gitlibspopulated.clojure -Spathalready prints the fully-resolved, transitive classpath (dirs + jars).
What's missing
The loader is single-rooted. evaluator.janet/ns->path hardcodes:
(string "src/jolt/" (dots->slashes (dashes->underscores ns)) ".clj")
and maybe-require-ns loads exactly that one path if it exists. To load deps we
need:
- A classpath — a list of source roots searched in order, not one fixed
prefix. Roots =
:pathsfrom deps.edn + each resolved dependency's source. .cljcsupport — tryfoo/bar.cljcas well asfoo/bar.clj(most libs ship.cljcor.clj; the loader only tries.cljtoday).ns-form handling on load. Stdlib files have nonsform, somaybe-require-nssets the current ns manually before loading. Library files do have(ns ...). Both already work in practice (thensform re-asserts the namespace), but the loader should not assume "no ns form."
Resolving dependencies — three options
The hard part is turning coordinates into local source roots. Maven resolution (transitive deps, version conflict resolution, POM parsing) is real work; git deps are comparatively easy.
A. Shell out to the Clojure CLI (recommended first cut)
Run clojure -Spath (optionally -Sdeps/aliases) in the project dir, capture
the :-separated classpath, then:
- directory entries → add directly as source roots;
- jar entries → extract
*.clj/*.cljcinto a cache dir (.jolt/classpath/<sha>/) withunzip/jar(both present; Janet has no built-in zip) and add the cache dir as a root.
Pros: reuses the canonical resolver, so transitive deps, exclusions, aliases, and version conflict resolution are all correct and match what JVM Clojure sees. Tiny amount of code.
Cons: requires the Clojure CLI (hence a JVM) at resolve time. Runtime stays
JVM-free. We'd cache the result so resolution only reruns when deps.edn
changes.
B. Jolt-native resolver
Parse deps.edn ourselves and resolve:
:git/url+:git/sha→git clone/checkout into a cache (this is roughly what jpm already does for Janet git deps — see "jpm" below);:mvn/version→ download the POM + jar from Maven Central over HTTP, parse the POM for transitive deps, resolve versions.
Pros: no JVM dependency at all; self-contained.
Cons: reimplementing Maven resolution (POM transitive graph, :exclusions,
nearest-wins version selection) is the bulk of tools.deps and easy to get subtly
wrong. Large effort.
C. Hybrid
Native path for :paths and :git/* deps (cheap, no JVM); shell to clojure -Spath only when :mvn/* deps are present. Gives a JVM-free experience for
git-only / local projects and correct Maven resolution when needed.
Where jpm fits
jpm builds the Jolt binary and manages Janet packages; it has no Maven/Clojure notion, so deps.edn support sits beside jpm rather than inside it. Two useful touch points:
- jpm already fetches and caches git repositories for Janet deps — the same
machinery (or
~/.gitlibs) can back option B/C's git-dep handling, so we don't write a git cache from scratch. - A project-level
jpmrule (inproject.janet) could run resolution as a build step and write a classpath file, for projects that want deps resolved at build time rather than first run.
But the primary integration is at the Jolt runtime/CLI, not jpm: see below.
Proposed shape
- Classpath in the context. Add a
:classpath(ordered list of roots) to the ctx env.ns->pathbecomes "search each root forfoo/bar.cljthenfoo/bar.cljc", withsrc/jolt/always first so the stdlib wins. - Resolution step. On startup (or via
jolt deps), ifdeps.ednexists, resolve it to roots (option A to start) and set:classpath. Cache keyed on a hash ofdeps.ednso it's a no-op when unchanged. - Config knobs.
JOLT_CLASSPATHenv /--classpathflag to set roots directly (bypassing resolution), mirroring howJOLT_MUTABLEworks.
Limitations (set expectations)
- JVM-only libraries don't run. Anything depending on Java interop, host
classes, or
clojure.corefeatures Jolt lacks will fail to load or fail at a call. Target audience is pure-clj/cljclibraries. - Coverage is per-function. As the medley probe showed, a namespace can load and have most functions work while a few hit unimplemented core behavior.
- No AOT/
.classexecution — ever. We only consume source from the classpath; compiled classes in jars are ignored. - Macro/protocol/reader-conditional support is whatever the Jolt interpreter
already provides (reader conditionals
#?are supported, which is why.cljcloads).
Recommended plan (phased)
- Loader classpath. Generalize
ns->path/maybe-require-nsto search an ordered root list and try.clj+.cljc. AddJOLT_CLASSPATH/--classpath. No resolution yet — point it at a directory of source by hand and load a lib. (Unblocks everything; independently testable.) - deps.edn → classpath via
clojure -Spath(option A). Resolve, extract jar source to a cache, set the classpath.jolt depsto resolve/print; auto-resolve on startup whendeps.ednis present. - Native git deps (toward option C). Resolve
:git/*(and:local/root) without the JVM, falling back to the CLI only for:mvn/*. - Conformance pass. Pull a handful of popular pure-
cljclibs, see what loads/runs, and use the failures to drive interpreter gaps — same loop as the clojure-test-suite battery.