Git clones now default to a shared, sha-immutable cache —
$JOLT_GITLIBS, else <config-dir>/gitlibs — instead of a per-project
./jpm_tree, the tools.gitlibs ~/.gitlibs model. Passing tree
explicitly still works (tests do). The resolved-roots cache moves
out of the clone tree to the project-local .cpcache/jolt-deps.jdn,
since roots depend on the project while clones don't.
deps.edn grows :tasks, the honest subset of babashka's: a string
task is a shell command, a map task is {:main-opts [...] :doc}.
jolt-deps tasks lists them (merged user+project), jolt-deps task
NAME runs one. Bare-expression tasks are out of scope: the reader
hands back parsed data and round-tripping to source is fragile.
Also fixes load-config skipping the symbol-key normalization when
only one config file existed — :tasks/:deps keys stayed raw reader
symbols (which embed positions and never compare equal), so lookups
missed. Regression rows in deps-tasks-test; docs updated for the
whole tools.deps surface (aliases, -A/-M, user config, conflicts,
gitlibs cache, tasks).
5.6 KiB
deps.edn support — design notes
How Jolt loads pure-Clojure libraries from a deps.edn, and why it's built the
way it is. For how to use it, see building-and-deps.md.
Scope, decided up front:
- git + local deps only — no Maven/
~/.m2resolution. - pure
clj/cljc— anything needing the JVM won't load or run; expected. - no classpath abstraction —
requirejust needs to find a dep's namespaces; "the classpath" is an ordered list of source directories. - piggyback on jpm — reuse jpm's git fetch + cache; don't write a package manager.
- separate tool — resolution lives in
jolt-deps, beside the runtime, the wayjpmsits besidejanet. Thejoltruntime knows nothing about deps.edn.
How jpm handles dependencies
jpm's package code (jpm/pm.janet) splits into a fetch half and a build half,
and we use only the first:
resolve-bundlenormalizes a dep spec to{:url :tag :type :shallow}, accepting:url/:repo+:tag/:sha/:commit/:ref. A deps.edn{:git/url … :git/sha …}maps straight onto it.download-bundle url :git tag shallowclones into a content-addressed cache (<modpath>/.cache/git_<tag>_<sanitized-url>) and returns the path —git init+remote add+ fetch + reset, plus submodules. No build step.bundle-installis the half we skip: it then runsproject.janetbuild rules, which a Clojure lib doesn't have. It's cleanly separable from the clone.
So jpm gives us git resolution and a cache for free; calling download-bundle
needs jpm/config/load-default first (it sets gitpath and the cache dyns).
How it works
src/jolt/deps.janet reads deps.edn (Janet parses it directly — EDN and Janet
syntax overlap for the :deps/:paths subset), then walks :deps:
:git/url(+:git/shaor:git/tag) →resolve-bundle+download-bundleintojpm_tree/.cache;:local/root→ the path as-is;:mvn/*and anything else → ignored.
Each resolved dependency contributes its own :paths (default ["src"]) as
source roots; the walk is breadth-first so every top-level coordinate
registers before any transitive one — a top-level pin always wins, matching
tools.deps, and a coordinate conflict warns on stderr naming both. The result
is a de-duplicated, ordered list of directories. resolve-deps-cached memoizes
that list in the project-local .cpcache/jolt-deps.jdn, keyed on a hash of the
project deps.edn + the user-level deps.edn + the selected aliases. jpm is
loaded lazily (require, not import) so it's pulled in only when resolving —
never embedded in a built binary.
Three tools.deps features are mirrored in reduced form. Aliases: :aliases
entries supply :extra-paths/:extra-deps (accumulate across the aliases
selected with -A:a:b) and :main-opts (last-wins, run with -M:alias).
User config: a deps.edn under $JOLT_CONFIG (else
$XDG_CONFIG_HOME/jolt, else ~/.jolt) merges beneath the project file,
per key, project wins. Tasks: the honest subset of babashka's — a string
task is a shell command, a map task is {:main-opts […] :doc "…"}; bare
Clojure expressions aren't supported because the reader hands back parsed
data, and round-tripping it to source isn't worth the fragility.
Clones default to a global sha-immutable cache ($JOLT_GITLIBS, else
<config-dir>/gitlibs) shared across projects, the tools.gitlibs
~/.gitlibs model; per-project trees remain available by passing tree
explicitly.
The loader (evaluator.janet/find-ns-file) resolves a namespace by searching the
context's :source-paths in order (the stdlib src/jolt first), trying <ns>.clj
then <ns>.cljc. Extra roots come from JOLT_PATH or init's :paths option.
jolt-deps (src/jolt/deps_cli.janet, its own declare-executable) ties it
together: it resolves the roots and runs the jolt binary with them on
JOLT_PATH. The runtime's only dependency interface is that env var.
jolt uberscript bundles a namespace and everything it requires into one
standalone .clj. It requires the entry namespace and uses the order in which
the loader finishes loading files — a dependency finishes before the file that
required it, so the order is topological — then concatenates that source. The
baked-in stdlib is excluded (it's part of the runtime, not bundled).
Gotcha worth remembering: the jolt CLI's context is built into its image at
build time, so JOLT_PATH is applied 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. Coverage is per-function: a namespace can load with most functions working and a few not. - Source only; compiled
.classfiles in a git dep are ignored. - git
:git/shamust be a full SHA (git fetchcan't resolve a short one).
Conformance
test/integration/deps-conformance-test.janet resolves a few real pure-cljc
git libraries and reports whether their namespaces load and a sample call works.
It's network-gated behind JOLT_CONFORMANCE=1 so CI stays offline. Use it to
check a library against the current interpreter, and to drive fixes for whatever
gap a failure points at (the same loop as the clojure-test-suite battery). A
library fails when it relies on something Jolt doesn't provide — JVM interop, or
a regex feature like Unicode property classes (\p{…}).
Not yet
- Compiling deps into a binary image.
uberscriptalready produces a standalone.clj; baking a project's dependencies directly into a custom executable image is a heavier variant that isn't implemented.