Merge pull request #2 from jolt-lang/deps-research
deps.edn support: load Clojure git deps via a separate jolt-deps tool
This commit is contained in:
commit
f5205e8c0b
12 changed files with 527 additions and 20 deletions
13
README.md
13
README.md
|
|
@ -10,16 +10,13 @@ A Clojure interpreter running on [Janet](https://janet-lang.org). Jolt reads Clo
|
||||||
git clone https://github.com/jolt-lang/jolt.git
|
git clone https://github.com/jolt-lang/jolt.git
|
||||||
cd jolt
|
cd jolt
|
||||||
git submodule update --init # pulls vendor/sci
|
git submodule update --init # pulls vendor/sci
|
||||||
jpm build # compiles build/jolt
|
jpm build # builds build/jolt and build/jolt-deps
|
||||||
```
|
```
|
||||||
|
|
||||||
Requires `jpm` and a recent Janet — developed and CI-tested against **1.41**. The
|
Requires `jpm` and a recent Janet (CI-tested against 1.41). See
|
||||||
futures and core.async layers rely on Janet's threaded `ev/` channels (`ev/thread`,
|
[doc/building-and-deps.md](doc/building-and-deps.md) for build details, the
|
||||||
`ev/thread-chan`), so an older Janet may not run the full suite.
|
`jpm clean` caveat, how namespaces are resolved (`JOLT_PATH`), and pulling
|
||||||
|
Clojure libraries from a `deps.edn` with the `jolt-deps` tool.
|
||||||
`jpm build` doesn't always detect source changes — run `jpm clean && jpm build`
|
|
||||||
after editing `src/` to be sure `build/jolt` is current. The test suite (`jpm
|
|
||||||
test`) runs against the source directly, so it never goes stale.
|
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
|
|
|
||||||
97
doc/building-and-deps.md
Normal file
97
doc/building-and-deps.md
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
# Building and dependencies
|
||||||
|
|
||||||
|
How to build Jolt from source and how to pull Clojure libraries into a project.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/jolt-lang/jolt.git
|
||||||
|
cd jolt
|
||||||
|
git submodule update --init # vendor/sci (used by the SCI bootstrap tests)
|
||||||
|
jpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces two executables under `build/`:
|
||||||
|
|
||||||
|
- **`jolt`** — the runtime: REPL, file/expr runner, nREPL server.
|
||||||
|
- **`jolt-deps`** — a separate tool that resolves a `deps.edn` (see below). It
|
||||||
|
sits beside the runtime the way `jpm` sits beside `janet`; the runtime itself
|
||||||
|
knows nothing about deps.edn.
|
||||||
|
|
||||||
|
Needs `jpm` and a recent Janet — developed and CI-tested against **1.41**. The
|
||||||
|
futures and core.async layers use Janet's threaded `ev/` channels (`ev/thread`,
|
||||||
|
`ev/thread-chan`), so older Janets may not run the full suite.
|
||||||
|
|
||||||
|
`jpm build` doesn't always notice source changes; run `jpm clean && jpm build`
|
||||||
|
after editing `src/` to be sure the binaries are current. `jpm test` runs against
|
||||||
|
the source directly, so it never goes stale.
|
||||||
|
|
||||||
|
## How namespaces are found
|
||||||
|
|
||||||
|
`(require ...)` resolves a namespace to a file by searching an ordered list of
|
||||||
|
source roots — the stdlib first, then any extra roots — trying `<ns>.clj` then
|
||||||
|
`<ns>.cljc` (dots become directories, dashes become underscores). Extra roots
|
||||||
|
come from:
|
||||||
|
|
||||||
|
- `JOLT_PATH` — a colon-separated list of directories (like a classpath), applied
|
||||||
|
at runtime;
|
||||||
|
- the `:paths` option to `init` when embedding Jolt as a library.
|
||||||
|
|
||||||
|
So you can point Jolt at a directory of Clojure source with no deps machinery at
|
||||||
|
all:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
JOLT_PATH=/path/to/lib/src build/jolt myfile.clj
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies via deps.edn
|
||||||
|
|
||||||
|
`jolt-deps` reads a `deps.edn` in the current directory, fetches its
|
||||||
|
dependencies, and runs `jolt` with the resolved source directories on
|
||||||
|
`JOLT_PATH`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jolt-deps path # print the resolved roots (':'-joined)
|
||||||
|
jolt-deps run FILE [args] # resolve, then run `jolt FILE …`
|
||||||
|
jolt-deps repl # resolve, then start a REPL
|
||||||
|
jolt-deps -e EXPR [args] # resolve, then evaluate EXPR
|
||||||
|
```
|
||||||
|
|
||||||
|
`jolt-deps` launches the `jolt` binary it finds on `PATH` (override with
|
||||||
|
`$JOLT_BIN`).
|
||||||
|
|
||||||
|
Example `deps.edn`:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
{:paths ["src"]
|
||||||
|
:deps {weavejester/medley {:git/url "https://github.com/weavejester/medley"
|
||||||
|
:git/tag "1.0.0"}
|
||||||
|
my/helpers {:local/root "../helpers"}}}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jolt-deps run -m myapp.main
|
||||||
|
```
|
||||||
|
|
||||||
|
### What's supported
|
||||||
|
|
||||||
|
- **git deps** — `{:git/url … :git/tag …}` or `{:git/url … :git/sha …}` (use a
|
||||||
|
full SHA; `git fetch` can't resolve a short one). Transitive deps from each
|
||||||
|
dependency's own `deps.edn` are resolved too.
|
||||||
|
- **local deps** — `{:local/root "../path"}`.
|
||||||
|
- The project's own `:paths` (default `["src"]`) are included.
|
||||||
|
|
||||||
|
Resolution reuses jpm's git fetch and cache (a dependency is cloned once into
|
||||||
|
`jpm_tree/.cache` and reused). Resolved roots are cached on a hash of `deps.edn`,
|
||||||
|
so an unchanged `deps.edn` doesn't re-fetch.
|
||||||
|
|
||||||
|
### What's not
|
||||||
|
|
||||||
|
- **No Maven.** `:mvn/version` deps are ignored — git and local only.
|
||||||
|
- **Pure `clj`/`cljc` only.** A library that needs the JVM (Java interop, host
|
||||||
|
classes) or a `clojure.core` feature Jolt doesn't implement will fail to load
|
||||||
|
or fail at a call. Coverage is per-function: a namespace can load with most
|
||||||
|
functions working and a few not.
|
||||||
|
|
||||||
|
See [`tools-deps.md`](tools-deps.md) for the design rationale and the road to
|
||||||
|
compiling used dependency namespaces into the build.
|
||||||
140
doc/tools-deps.md
Normal file
140
doc/tools-deps.md
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
# 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/`~/.m2` resolution.
|
||||||
|
- **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 `require` to 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-bundle`** normalizes 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 shallow`** clones 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 hood
|
||||||
|
`download-git-bundle` does `git init` + `remote add origin` + fetch + reset to
|
||||||
|
the tag/sha (plus submodules). Pure git — **no build step**.
|
||||||
|
- **`bundle-install`** is the part we *don't* want: after downloading it does
|
||||||
|
`(require-jpm "./project.janet")` and runs build/install rules. A Clojure lib
|
||||||
|
has no `project.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
|
||||||
|
|
||||||
|
```janet
|
||||||
|
(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
|
||||||
|
|
||||||
|
1. **Resolve.** Read `deps.edn`, take the `:deps` whose specs are git
|
||||||
|
(`:git/url` + `:git/sha`/`:git/tag`). For each, `resolve-bundle` +
|
||||||
|
`download-bundle` into the project's `jpm_tree/.cache`. Read each cloned
|
||||||
|
dep's own `deps.edn` and recurse for transitive git deps. (`:local/root`
|
||||||
|
deps are even simpler — just a path, no fetch.)
|
||||||
|
2. **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.
|
||||||
|
3. **Teach the loader the roots.** `evaluator.janet/ns->path` currently hardcodes
|
||||||
|
`src/jolt/<ns>.clj`. Generalize `maybe-require-ns` to, after the stdlib path,
|
||||||
|
search each dep root for `<ns>.clj` then `<ns>.cljc`. `src/jolt/` stays first
|
||||||
|
so the stdlib always wins. (Two small changes: a root list in the ctx, and
|
||||||
|
trying `.cljc`.)
|
||||||
|
4. **Dev vs build.**
|
||||||
|
- *Dev:* `jolt-deps` resolves `deps.edn` (cached on a hash of it, so it's a
|
||||||
|
no-op when unchanged) and runs jolt with the roots on `JOLT_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.nrepl` source is today — load them at
|
||||||
|
build time so the shipped binary needs neither the deps nor jpm.
|
||||||
|
|
||||||
|
## 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`/`cljc` only. JVM interop, host classes, and unimplemented
|
||||||
|
`clojure.core` corners fail — expected, not a goal.
|
||||||
|
- Per-function coverage: a namespace can load with most functions working and a
|
||||||
|
few not.
|
||||||
|
- Source only; compiled `.class` files (if any in a git dep) are ignored.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
1. **Loader roots.** *(done)* `maybe-require-ns` now searches an ordered root
|
||||||
|
list (`ctx.env :source-paths`, stdlib first) trying `.clj` then `.cljc`;
|
||||||
|
`init` adds roots from the `:paths` opt and `JOLT_PATH` (colon-separated).
|
||||||
|
Verified loading a real lib (medley) from an added root. See
|
||||||
|
`test/integration/deps-loader-test.janet`.
|
||||||
|
2. **Resolve git deps via jpm.** *(done)* `src/jolt/deps.janet` reads `deps.edn`,
|
||||||
|
resolves `:git/*` + `:local/root` through `jpm/pm` into `jpm_tree/.cache`,
|
||||||
|
recurses for transitive deps, and returns the roots (cached on a deps.edn
|
||||||
|
hash). The separate `jolt-deps` tool (`src/jolt/deps_cli.janet`,
|
||||||
|
`declare-executable`) exposes `path`/`run`/`repl`/`-e`. See
|
||||||
|
`test/integration/deps-resolve-test.janet`.
|
||||||
|
3. **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)*
|
||||||
|
4. **Conformance.** Pull a few popular pure-`cljc` git libs, see what loads/runs,
|
||||||
|
and drive interpreter gaps from the failures — same loop as the
|
||||||
|
clojure-test-suite battery.
|
||||||
|
|
@ -8,3 +8,9 @@
|
||||||
(declare-executable
|
(declare-executable
|
||||||
:name "jolt"
|
:name "jolt"
|
||||||
:entry "src/jolt/main.janet")
|
:entry "src/jolt/main.janet")
|
||||||
|
|
||||||
|
# Separate tool (like jpm beside janet): resolves deps.edn into Jolt source
|
||||||
|
# roots. The jolt runtime stays deps-agnostic — it just reads JOLT_PATH.
|
||||||
|
(declare-executable
|
||||||
|
:name "jolt-deps"
|
||||||
|
:entry "src/jolt/deps_cli.janet")
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,17 @@
|
||||||
opts may contain:
|
opts may contain:
|
||||||
:namespaces — map of {ns-name → {sym → value, ...}, ...}
|
:namespaces — map of {ns-name → {sym → value, ...}, ...}
|
||||||
:mutable? — use Janet mutable data structures instead of persistent
|
:mutable? — use Janet mutable data structures instead of persistent
|
||||||
:compile? — enable compilation of Clojure forms to Janet"
|
:compile? — enable compilation of Clojure forms to Janet
|
||||||
|
:paths — extra source roots to search for namespaces (after the stdlib)"
|
||||||
[&opt opts]
|
[&opt opts]
|
||||||
(default opts {})
|
(default opts {})
|
||||||
(let [ctx (make-ctx opts)]
|
(let [ctx (make-ctx opts)]
|
||||||
|
# Extra source roots: opts :paths, then JOLT_PATH (colon-separated). These are
|
||||||
|
# searched after the stdlib so (require ...) finds deps.edn-resolved libs.
|
||||||
|
(let [roots (get (ctx :env) :source-paths)]
|
||||||
|
(each p (get opts :paths []) (array/push roots p))
|
||||||
|
(when-let [jp (os/getenv "JOLT_PATH")]
|
||||||
|
(each p (string/split ":" jp) (when (> (length p) 0) (array/push roots p)))))
|
||||||
# Collection representation (persistent vs mutable) is selected at BUILD time
|
# Collection representation (persistent vs mutable) is selected at BUILD time
|
||||||
# via JOLT_MUTABLE (see config.janet); init-core! registers vec/vector/conj/
|
# via JOLT_MUTABLE (see config.janet); init-core! registers vec/vector/conj/
|
||||||
# etc. that produce the mode-appropriate values, so nothing extra to load.
|
# etc. that produce the mode-appropriate values, so nothing extra to load.
|
||||||
|
|
|
||||||
88
src/jolt/deps.janet
Normal file
88
src/jolt/deps.janet
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# deps.edn resolution for Jolt.
|
||||||
|
#
|
||||||
|
# Resolve git and :local/root dependencies from a deps.edn into a list of source
|
||||||
|
# directories, which the loader then searches (see evaluator/find-ns-file). We
|
||||||
|
# reuse jpm's git fetch + cache (jpm/pm) rather than shipping a package manager.
|
||||||
|
# Maven (:mvn/version) deps are ignored — git only, pure clj/cljc only.
|
||||||
|
#
|
||||||
|
# jpm is loaded lazily (require, not import) so it's needed only at resolve time
|
||||||
|
# (dev/build), never embedded in the shipped binary.
|
||||||
|
|
||||||
|
# A typical deps.edn is also valid Janet data, so we read it with Janet's parser.
|
||||||
|
# (EDN-only forms — #{} sets, tagged literals, namespaced maps — aren't handled;
|
||||||
|
# deps.edn rarely uses them in :deps/:paths.)
|
||||||
|
(defn read-edn [path]
|
||||||
|
(when (os/stat path)
|
||||||
|
(try (parse (slurp path)) ([_] nil))))
|
||||||
|
|
||||||
|
(defn- jpm-fn [mod sym]
|
||||||
|
(get-in (require mod) [sym :value]))
|
||||||
|
|
||||||
|
(defn- ensure-jpm-config [tree]
|
||||||
|
((jpm-fn "jpm/config" 'load-default))
|
||||||
|
(setdyn :modpath tree)
|
||||||
|
(setdyn :gitpath (dyn :gitpath "git")))
|
||||||
|
|
||||||
|
(defn- clone-git [spec]
|
||||||
|
# spec is a deps.edn dep value: {:git/url ... :git/sha/:git/tag ...}
|
||||||
|
(def resolve-bundle (jpm-fn "jpm/pm" 'resolve-bundle))
|
||||||
|
(def download-bundle (jpm-fn "jpm/pm" 'download-bundle))
|
||||||
|
(def b (resolve-bundle {:url (get spec :git/url)
|
||||||
|
:sha (get spec :git/sha)
|
||||||
|
:tag (get spec :git/tag)
|
||||||
|
:shallow false}))
|
||||||
|
(download-bundle (b :url) (b :type) (b :tag) (b :shallow)))
|
||||||
|
|
||||||
|
(defn- src-roots
|
||||||
|
"Source dirs of a project/dep at `dir`: its deps.edn :paths joined to dir
|
||||||
|
(default [\"src\"])."
|
||||||
|
[dir edn]
|
||||||
|
(map |(string dir "/" $) (or (and edn (get edn :paths)) ["src"])))
|
||||||
|
|
||||||
|
(defn resolve-deps
|
||||||
|
"Resolve the git/:local deps of `deps-edn-path` into an ordered, de-duplicated
|
||||||
|
array of source dirs (the project's own :paths first, then each dependency's,
|
||||||
|
transitively). `tree` is where jpm's clone cache lives (default ./jpm_tree)."
|
||||||
|
[deps-edn-path &opt tree]
|
||||||
|
(default tree (string (os/cwd) "/jpm_tree"))
|
||||||
|
(os/mkdir tree)
|
||||||
|
(ensure-jpm-config tree)
|
||||||
|
(def roots @[])
|
||||||
|
(def seen @{})
|
||||||
|
(defn add-root [r] (unless (index-of r roots) (array/push roots r)))
|
||||||
|
(defn process [edn base-dir own-paths?]
|
||||||
|
(when (dictionary? edn)
|
||||||
|
(when own-paths? (each r (src-roots base-dir edn) (add-root r)))
|
||||||
|
(eachp [lib spec] (or (get edn :deps) {})
|
||||||
|
(def k (string lib))
|
||||||
|
(unless (get seen k)
|
||||||
|
(put seen k true)
|
||||||
|
(def dir
|
||||||
|
(cond
|
||||||
|
(and (dictionary? spec) (get spec :git/url)) (clone-git spec)
|
||||||
|
(and (dictionary? spec) (get spec :local/root))
|
||||||
|
(let [lr (get spec :local/root)]
|
||||||
|
(if (string/has-prefix? "/" lr) lr (string base-dir "/" lr)))
|
||||||
|
nil)) # :mvn/* and anything else: skip
|
||||||
|
(when dir
|
||||||
|
(def dep-edn (read-edn (string dir "/deps.edn")))
|
||||||
|
(each r (src-roots dir dep-edn) (add-root r))
|
||||||
|
(process dep-edn dir false))))))
|
||||||
|
(process (read-edn deps-edn-path) (os/cwd) true)
|
||||||
|
roots)
|
||||||
|
|
||||||
|
(defn resolve-deps-cached
|
||||||
|
"Like resolve-deps, but caches the resolved roots in the tree keyed on a hash
|
||||||
|
of the deps.edn, so an unchanged deps.edn resolves without re-fetching."
|
||||||
|
[deps-edn-path &opt tree]
|
||||||
|
(default tree (string (os/cwd) "/jpm_tree"))
|
||||||
|
(when (os/stat deps-edn-path)
|
||||||
|
(os/mkdir tree)
|
||||||
|
(def cache-file (string tree "/.jolt-deps-roots.jdn"))
|
||||||
|
(def h (hash (slurp deps-edn-path)))
|
||||||
|
(def cached (when (os/stat cache-file) (try (parse (slurp cache-file)) ([_] nil))))
|
||||||
|
(if (and cached (= h (get cached :hash)))
|
||||||
|
(get cached :roots)
|
||||||
|
(let [roots (resolve-deps deps-edn-path tree)]
|
||||||
|
(spit cache-file (string/format "%j" {:hash h :roots roots}))
|
||||||
|
roots))))
|
||||||
41
src/jolt/deps_cli.janet
Normal file
41
src/jolt/deps_cli.janet
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# jolt-deps — a separate tool that resolves a deps.edn into Jolt source roots.
|
||||||
|
#
|
||||||
|
# Mirrors how jpm is a tool beside the janet runtime: the jolt runtime knows
|
||||||
|
# nothing about deps.edn — it just searches the roots in JOLT_PATH (see
|
||||||
|
# api/init). This tool does the resolution (git + :local deps, via jpm's fetch
|
||||||
|
# cache) and either prints the roots or launches jolt with JOLT_PATH set.
|
||||||
|
#
|
||||||
|
# jolt-deps path print the resolved roots (':'-joined), e.g. for
|
||||||
|
# JOLT_PATH=$(jolt-deps path) jolt file.clj
|
||||||
|
# jolt-deps run FILE [args] resolve, then run `jolt FILE args` 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 EXPR ...` with JOLT_PATH set
|
||||||
|
#
|
||||||
|
# The jolt binary is found via $JOLT_BIN, else `jolt` on PATH.
|
||||||
|
|
||||||
|
(import ./deps :as deps)
|
||||||
|
|
||||||
|
(defn- roots []
|
||||||
|
(if (os/stat "deps.edn") (deps/resolve-deps-cached "deps.edn") @[]))
|
||||||
|
|
||||||
|
(defn- exec-jolt [extra-args]
|
||||||
|
# Set JOLT_PATH in our own env and let the child inherit it (os/execute's env
|
||||||
|
# arg isn't honored here; inheriting is reliable).
|
||||||
|
(def rs (string/join (roots) ":"))
|
||||||
|
(def existing (os/getenv "JOLT_PATH"))
|
||||||
|
(os/setenv "JOLT_PATH" (if (and existing (> (length existing) 0)) (string rs ":" existing) rs))
|
||||||
|
(os/execute [(os/getenv "JOLT_BIN" "jolt") ;extra-args] :p))
|
||||||
|
|
||||||
|
(defn- usage []
|
||||||
|
(print "usage: jolt-deps [path | run FILE [args] | repl | -e EXPR [args]]"))
|
||||||
|
|
||||||
|
(defn main [&]
|
||||||
|
(def argv (tuple/slice (or (dyn :args) @[]) 1))
|
||||||
|
(def cmd (get argv 0))
|
||||||
|
(cond
|
||||||
|
(or (nil? cmd) (= cmd "help") (= cmd "-h") (= cmd "--help")) (usage)
|
||||||
|
(= cmd "path") (print (string/join (roots) ":"))
|
||||||
|
(= cmd "run") (os/exit (exec-jolt (tuple/slice argv 1)))
|
||||||
|
(= cmd "repl") (os/exit (exec-jolt []))
|
||||||
|
(= cmd "-e") (os/exit (exec-jolt argv))
|
||||||
|
(do (eprint "jolt-deps: unknown command " cmd) (usage) (os/exit 1))))
|
||||||
|
|
@ -185,12 +185,24 @@
|
||||||
[sym-s]
|
[sym-s]
|
||||||
(if (sym-s :ns) (string (sym-s :ns) "/" (sym-s :name)) (sym-s :name)))
|
(if (sym-s :ns) (string (sym-s :ns) "/" (sym-s :name)) (sym-s :name)))
|
||||||
|
|
||||||
(defn- ns->path
|
(defn- ns->relpath
|
||||||
"Map a namespace name to its Jolt stdlib source path (dots->dirs, dashes->_)."
|
"Namespace name to its file-relative path (dots->dirs, dashes->_), no extension."
|
||||||
[ns-name]
|
[ns-name]
|
||||||
(string "src/jolt/"
|
(string/replace-all "." "/" (string/replace-all "-" "_" ns-name)))
|
||||||
(string/replace-all "." "/" (string/replace-all "-" "_" ns-name))
|
|
||||||
".clj"))
|
(defn- find-ns-file
|
||||||
|
"Search the context's source roots (stdlib first, then deps.edn dirs) for the
|
||||||
|
namespace's source, trying .clj then .cljc. Returns the path or nil."
|
||||||
|
[ctx ns-name]
|
||||||
|
(let [rel (ns->relpath ns-name)
|
||||||
|
roots (or (get (ctx :env) :source-paths) @["src/jolt"])]
|
||||||
|
(var found nil)
|
||||||
|
(each root roots
|
||||||
|
(each ext [".clj" ".cljc"]
|
||||||
|
(when (nil? found)
|
||||||
|
(let [p (string root "/" rel ext)]
|
||||||
|
(when (os/stat p) (set found p))))))
|
||||||
|
found))
|
||||||
|
|
||||||
(defn- load-ns-file
|
(defn- load-ns-file
|
||||||
"Parse and evaluate every form in a .clj file in the given context."
|
"Parse and evaluate every form in a .clj file in the given context."
|
||||||
|
|
@ -202,17 +214,18 @@
|
||||||
(when (not (nil? f)) (eval-form ctx @{} f))))
|
(when (not (nil? f)) (eval-form ctx @{} f))))
|
||||||
|
|
||||||
(defn- maybe-require-ns
|
(defn- maybe-require-ns
|
||||||
"If namespace ns-name isn't populated yet and a stdlib source file exists,
|
"If namespace ns-name isn't populated yet and source for it exists on the
|
||||||
load it. Restores the current namespace afterwards (the file's `ns` form
|
context's source roots, load it. Restores the current namespace afterwards (a
|
||||||
|
library's own `ns` form, or our manual switch for ns-form-less stdlib files,
|
||||||
changes it). No-op for already-loaded namespaces."
|
changes it). No-op for already-loaded namespaces."
|
||||||
[ctx ns-name]
|
[ctx ns-name]
|
||||||
(let [ns (ctx-find-ns ctx ns-name)]
|
(let [ns (ctx-find-ns ctx ns-name)]
|
||||||
(when (and (= 0 (length (ns :mappings))) (not= ns-name "clojure.core"))
|
(when (and (= 0 (length (ns :mappings))) (not= ns-name "clojure.core"))
|
||||||
(let [path (ns->path ns-name)]
|
(let [path (find-ns-file ctx ns-name)]
|
||||||
(when (os/stat path)
|
(when path
|
||||||
(let [saved (ctx-current-ns ctx)]
|
(let [saved (ctx-current-ns ctx)]
|
||||||
# Jolt stdlib files have no `ns` form; switch into the target ns so
|
# Stdlib files have no `ns` form, so switch into the target ns first
|
||||||
# their defs intern there, then restore.
|
# (their defs intern there); a library's own `ns` form overrides this.
|
||||||
(ctx-set-current-ns ctx ns-name)
|
(ctx-set-current-ns ctx ns-name)
|
||||||
(load-ns-file ctx path)
|
(load-ns-file ctx path)
|
||||||
(ctx-set-current-ns ctx saved)))))))
|
(ctx-set-current-ns ctx saved)))))))
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,12 @@
|
||||||
(def args (or (dyn :args) @[])) # @["jolt" arg1 arg2 ...]
|
(def args (or (dyn :args) @[])) # @["jolt" arg1 arg2 ...]
|
||||||
(def argv (if (> (length args) 1) (array/slice args 1) @[]))
|
(def argv (if (> (length args) 1) (array/slice args 1) @[]))
|
||||||
(ctx-set-current-ns ctx "user")
|
(ctx-set-current-ns ctx "user")
|
||||||
|
# JOLT_PATH must be applied at runtime: this `ctx` is built into the image at
|
||||||
|
# build time, so its source-paths can't capture the runtime environment.
|
||||||
|
# `jolt-deps` sets JOLT_PATH to the resolved deps.edn source roots.
|
||||||
|
(when-let [jp (os/getenv "JOLT_PATH")]
|
||||||
|
(each p (string/split ":" jp)
|
||||||
|
(when (> (length p) 0) (array/push (get (ctx :env) :source-paths) p))))
|
||||||
(cond
|
(cond
|
||||||
(empty? argv) (run-repl)
|
(empty? argv) (run-repl)
|
||||||
(or (= (argv 0) "-h") (= (argv 0) "--help")) (print-help)
|
(or (= (argv 0) "-h") (= (argv 0) "--help")) (print-help)
|
||||||
|
|
|
||||||
|
|
@ -366,6 +366,9 @@
|
||||||
:class->opts @{}
|
:class->opts @{}
|
||||||
:current-ns "user"
|
:current-ns "user"
|
||||||
:compile? compile?
|
:compile? compile?
|
||||||
|
# Ordered roots searched (after the stdlib) to resolve a namespace
|
||||||
|
# to a .clj/.cljc file. deps.edn resolution appends dep src dirs.
|
||||||
|
:source-paths @["src/jolt"]
|
||||||
:compiled-cache @{}
|
:compiled-cache @{}
|
||||||
:type-registry @{}
|
:type-registry @{}
|
||||||
:data-readers (let [dr @{}]
|
:data-readers (let [dr @{}]
|
||||||
|
|
|
||||||
48
test/integration/deps-loader-test.janet
Normal file
48
test/integration/deps-loader-test.janet
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# The loader resolves namespaces against an ordered list of source roots (the
|
||||||
|
# stdlib first, then deps.edn-resolved dirs), trying .clj then .cljc. This is the
|
||||||
|
# foundation for loading Clojure libraries via deps.edn — here we point a root at
|
||||||
|
# a hand-written "library" and require it.
|
||||||
|
|
||||||
|
(use ../../src/jolt/api)
|
||||||
|
(use ../../src/jolt/types)
|
||||||
|
|
||||||
|
(def tmp (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-deps-test-" (os/time)))
|
||||||
|
(os/mkdir tmp)
|
||||||
|
(os/mkdir (string tmp "/mylib"))
|
||||||
|
(os/mkdir (string tmp "/other"))
|
||||||
|
|
||||||
|
# a .cljc library with its own ns form and a require of a sibling ns
|
||||||
|
(spit (string tmp "/mylib/core.cljc")
|
||||||
|
"(ns mylib.core (:require [other.util :as u]))\n(defn double [x] (* 2 x))\n(defn doubled-inc [x] (u/inc1 (double x)))\n")
|
||||||
|
# a .clj sibling, with a dash in the name (-> underscore in the path)
|
||||||
|
(os/mkdir (string tmp "/other"))
|
||||||
|
(spit (string tmp "/other/util.clj") "(ns other.util)\n(defn inc1 [x] (+ x 1))\n")
|
||||||
|
|
||||||
|
(def ctx (init {:paths [tmp]}))
|
||||||
|
(ctx-set-current-ns ctx "user")
|
||||||
|
|
||||||
|
(var fails 0)
|
||||||
|
(defn check [label expr expected]
|
||||||
|
(let [r (protect (eval-string ctx expr))
|
||||||
|
got (if (r 0) (normalize-pvecs (r 1)) (string "ERR " (r 1)))]
|
||||||
|
(if (= got expected)
|
||||||
|
(print " ok " label)
|
||||||
|
(do (++ fails) (printf " FAIL %s: want %q, got %q" label expected got)))))
|
||||||
|
|
||||||
|
# require a .cljc lib from the added root and call it
|
||||||
|
(check "require .cljc lib" "(do (require (quote [mylib.core :as m])) (m/double 21))" 42)
|
||||||
|
# transitive require (mylib.core requires other.util) resolved from the root too
|
||||||
|
(check "transitive .clj require" "(mylib.core/doubled-inc 10)" 21)
|
||||||
|
# the stdlib still resolves from its default root
|
||||||
|
(check "stdlib still loads" "(do (require (quote [jolt.interop :as j])) (j/janet-type 1))" :number)
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
(defn rmrf [p]
|
||||||
|
(if (= :directory (os/stat p :mode))
|
||||||
|
(do (each e (os/dir p) (rmrf (string p "/" e))) (os/rmdir p))
|
||||||
|
(os/rm p)))
|
||||||
|
(rmrf tmp)
|
||||||
|
|
||||||
|
(if (> fails 0)
|
||||||
|
(error (string "deps-loader-test: " fails " failing check(s)"))
|
||||||
|
(print "\nAll deps-loader tests passed!"))
|
||||||
61
test/integration/deps-resolve-test.janet
Normal file
61
test/integration/deps-resolve-test.janet
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# deps.edn resolution into source roots, then loading a library through them.
|
||||||
|
# Uses :local/root deps only so it needs no network (the git path is the same
|
||||||
|
# code, exercised manually against real repos).
|
||||||
|
|
||||||
|
(use ../../src/jolt/api)
|
||||||
|
(use ../../src/jolt/types)
|
||||||
|
(import ../../src/jolt/deps :as deps)
|
||||||
|
|
||||||
|
(def base (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-deps-resolve-" (os/time)))
|
||||||
|
(defn rmrf [p]
|
||||||
|
(when (os/stat p)
|
||||||
|
(if (= :directory (os/stat p :mode))
|
||||||
|
(do (each e (os/dir p) (rmrf (string p "/" e))) (os/rmdir p))
|
||||||
|
(os/rm p))))
|
||||||
|
(rmrf base)
|
||||||
|
|
||||||
|
(defn mkdirs [p]
|
||||||
|
(def abs (string/has-prefix? "/" p))
|
||||||
|
(var acc nil)
|
||||||
|
(each seg (filter |(not= "" $) (string/split "/" p))
|
||||||
|
(set acc (cond (nil? acc) (if abs (string "/" seg) seg) (string acc "/" seg)))
|
||||||
|
(unless (os/stat acc) (os/mkdir acc))))
|
||||||
|
|
||||||
|
# project -> depends on lib-a (local), which depends on lib-b (local, transitive)
|
||||||
|
(each d ["proj/src" "a/src/liba" "b/src/libb"] (mkdirs (string base "/" d)))
|
||||||
|
(spit (string base "/proj/deps.edn") `{:paths ["src"] :deps {my/a {:local/root "../a"}}}`)
|
||||||
|
(spit (string base "/a/deps.edn") `{:paths ["src"] :deps {my/b {:local/root "../b"}}}`)
|
||||||
|
(spit (string base "/b/deps.edn") `{:paths ["src"]}`)
|
||||||
|
(spit (string base "/a/src/liba/core.clj") "(ns liba.core (:require [libb.core :as b]))\n(defn val [] (b/n))\n")
|
||||||
|
(spit (string base "/b/src/libb/core.clj") "(ns libb.core)\n(defn n [] 99)\n")
|
||||||
|
|
||||||
|
(var fails 0)
|
||||||
|
(defn check [label got expected]
|
||||||
|
(if (= got expected) (print " ok " label)
|
||||||
|
(do (++ fails) (printf " FAIL %s: want %q, got %q" label expected got))))
|
||||||
|
|
||||||
|
(os/cd (string base "/proj"))
|
||||||
|
(def roots (deps/resolve-deps "deps.edn" (string base "/proj/jpm_tree")))
|
||||||
|
# roots: proj/src, a/src (transitive: b/src) — at least the two dep srcs present
|
||||||
|
(check "resolves transitive local dep roots"
|
||||||
|
(truthy? (and (some |(string/has-suffix? "/a/src" $) roots)
|
||||||
|
(some |(string/has-suffix? "/b/src" $) roots)))
|
||||||
|
true)
|
||||||
|
|
||||||
|
# load through the resolved roots: liba.core requires libb.core transitively
|
||||||
|
(def ctx (init {:paths roots}))
|
||||||
|
(ctx-set-current-ns ctx "user")
|
||||||
|
(let [r (protect (eval-string ctx "(do (require (quote [liba.core :as a])) (a/val))"))]
|
||||||
|
(check "require local lib + transitive dep" (if (r 0) (r 1) (string "ERR " (r 1))) 99))
|
||||||
|
|
||||||
|
# cached resolution returns the same roots without re-walking
|
||||||
|
(check "cached resolve matches"
|
||||||
|
(deep= roots (deps/resolve-deps-cached "deps.edn" (string base "/proj/jpm_tree")))
|
||||||
|
true)
|
||||||
|
|
||||||
|
(os/cd "/")
|
||||||
|
(rmrf base)
|
||||||
|
|
||||||
|
(if (> fails 0)
|
||||||
|
(error (string "deps-resolve-test: " fails " failing check(s)"))
|
||||||
|
(print "\nAll deps-resolve tests passed!"))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue