Fold jolt-deps into the jolt binary

Dependency resolution now lives in the `jolt` CLI itself instead of a separate
jolt-deps executable. `jolt` resolves a deps.edn into JOLT_PATH/JOLT_APP_PATHS
in-process and dispatches the deps subcommands:

  jolt -M:alias [args]   run the alias :main-opts
  jolt -A:alias CMD      run CMD with the alias paths
  jolt run FILE          resolve, then run FILE
  jolt path | tasks | task NAME

A deps.edn in the working dir is auto-resolved for the runnable commands
(repl/-m/-e/nrepl-server/FILE), so e.g. `jolt -M:nrepl` (or plain
`jolt nrepl-server`) starts an nREPL with the project and its deps loaded.

The runtime core stays deps-agnostic — it only reads JOLT_PATH. The resolver
(deps.janet) is reached only from the CLI entry and loads jpm lazily, so a run
with no deps.edn never touches it and an app baked from its own jolt/api entry
never links it. resolve-deps-argv only resolves on an explicit deps command or
when a deps.edn is present; help/version never do.

jolt-deps stays as a thin deprecation shim that forwards to `jolt`, so existing
scripts keep working. Docs (README, CLAUDE.md, building-and-deps, tools-deps)
and the help text updated.
This commit is contained in:
Yogthos 2026-06-15 22:17:35 -04:00
parent 048de0200d
commit 0df9493b49
8 changed files with 274 additions and 139 deletions

View file

@ -54,7 +54,7 @@ bd close <id> # Complete work
## Build & Test
```bash
jpm build # build/jolt + build/jolt-deps (ctx baked at build time)
jpm build # build/jolt (one binary; ctx baked at build time)
jpm test # FULL gate — recursive over test/ (spec, unit, integration, bench)
janet test/spec/<f>.janet # one spec file
janet test/integration/conformance-test.janet # 3-mode conformance (interpret/compile/self-host)

View file

@ -10,13 +10,14 @@ A Clojure implementation on top of [Janet](https://janet-lang.org). Jolt reads C
git clone https://github.com/jolt-lang/jolt.git
cd jolt
git submodule update --init # pulls vendor/sci and vendor/clojure-test-suite
jpm build # builds build/jolt and build/jolt-deps
jpm build # builds build/jolt (one binary)
```
Requires `jpm` and a recent Janet (CI-tested against 1.41). See
[docs/building-and-deps.md](docs/building-and-deps.md) for build details, the
`jpm clean` caveat, how namespaces are resolved (`JOLT_PATH`), and pulling
Clojure libraries from a `deps.edn` with the `jolt-deps` tool.
Clojure libraries from a `deps.edn` (resolved by `jolt` itself — `jolt -M:…`,
`jolt run`, `jolt path`).
## Run
@ -164,6 +165,11 @@ jolt nrepl-server 12345 # choose a port
jolt nrepl-server 0.0.0.0:12345 # choose host and port (alias: nrepl)
```
In a project with a `deps.edn`, `jolt nrepl-server` auto-resolves it, so the
server starts with the project and its dependencies on the path — connect your
editor, then `(require 'your.app)`. (Equivalently, add `:aliases {:nrepl
{:main-opts ["nrepl-server"]}}` and run `jolt -M:nrepl`.)
Supported ops: `clone`, `describe`, `eval`, `load-file`, `close`, `ls-sessions`,
`interrupt` (acknowledged; an in-flight eval can't actually be interrupted), and
`eldoc`. `eval` streams `out`, reports the current `ns`, evaluates each form in

View file

@ -11,16 +11,22 @@ git submodule update --init # vendor/sci (used by the SCI bootstrap tests)
jpm build
```
This produces two executables under `build/`:
This produces `build/jolt` — one binary that is both the runtime (REPL,
file/expr runner, nREPL server) and the dependency front-end (`deps.edn`
resolution, see below). The whole `.clj` standard library
(`clojure.string`/`set`/`walk`/`edn`/`zip`, `jolt.http`/`interop`/`shell`/
`nrepl`) is baked in at build time, so it loads from any directory — the artifact
is self-contained. (`clojure.core` is built into the runtime in Janet and
auto-referred, so it's always available.)
- **`jolt`** — the runtime: REPL, file/expr runner, nREPL server. The whole `.clj`
standard library (`clojure.string`/`set`/`walk`/`edn`/`zip`, `jolt.http`/
`interop`/`shell`/`nrepl`) is baked into this binary at build time, so it loads
from any directory — the build artifact is self-contained. (`clojure.core` is
built into the runtime in Janet and auto-referred, so it's always available.)
- **`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.
The runtime **core** stays deps-agnostic: it only reads source roots from
`JOLT_PATH`. Dependency resolution lives in a separate CLI front-end module
(`src/jolt/deps.janet`) that the `jolt` entry point calls *before* running your
code, and that lazily loads `jpm` (for git fetch + cache) only when it actually
resolves. So a run with no `deps.edn` never touches the resolver, and an app
baked from its own entry — which imports `jolt/api`, not the CLI — never links
it at all. (`build/` also contains a `jolt-deps` shim that just forwards to
`jolt` so old scripts keep working; prefer calling `jolt` directly.)
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`,
@ -54,23 +60,24 @@ 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`.
`jolt` reads a `deps.edn` in the current directory, fetches its dependencies,
and puts the resolved source directories on `JOLT_PATH` for the run. A `deps.edn`
in the working dir is **auto-resolved** for the runnable commands (`repl`, `-m`,
`-e`, `nrepl-server`, a `FILE`); the explicit subcommands below also work
anywhere:
```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 -A:dev path # include the :dev alias's extra paths/deps
jolt-deps -M:test [args] # run the :test alias's :main-opts through jolt
jolt-deps tasks # list :tasks from deps.edn
jolt-deps task NAME [args] # run a task
jolt -M:test [args] # run the :test alias's :main-opts (the usual entry)
jolt -A:dev repl # run a command with the :dev alias's extra paths/deps
jolt run FILE [args] # resolve, then run FILE
jolt path # print the resolved roots (':'-joined)
jolt tasks # list :tasks from deps.edn
jolt task NAME [args] # run a task
```
`jolt-deps` launches the `jolt` binary it finds on `PATH` (override with
`$JOLT_BIN`).
So, for example, to start an nREPL server that loads a project and its deps,
add `:aliases {:nrepl {:main-opts ["nrepl-server"]}}` to `deps.edn` and run
`jolt -M:nrepl` (or just `jolt nrepl-server`, which auto-resolves the `deps.edn`).
Example `deps.edn`:
@ -82,7 +89,7 @@ Example `deps.edn`:
```
```bash
jolt-deps run -m myapp.main
jolt run -m myapp.main
```
### What's supported
@ -102,8 +109,8 @@ jolt-deps run -m myapp.main
with the project winning.
- **tasks** — `:tasks {clean "rm -rf target" test {:doc "run the suite"
:main-opts ["-e" "(run-tests)"]}}`. A string task is a shell command; a map
task runs jolt with its `:main-opts`. `jolt-deps tasks` lists, `jolt-deps
task NAME` runs.
task runs jolt with its `:main-opts`. `jolt tasks` lists, `jolt task NAME`
runs.
Conflicts resolve the tools.deps way: resolution is breadth-first, so a
top-level coordinate always beats a transitive one for the same lib, and
@ -124,14 +131,14 @@ hash of the project `deps.edn` + the user `deps.edn` + the selected aliases.
### Bundling into one file
`jolt uberscript OUT.clj -m NS` (or `jolt-deps uberscript …`, which resolves deps
first) bundles `NS` and every namespace it requires — your code plus its
dependencies — into a single `.clj` in dependency order, ending with a call to
`NS/-main`. The result runs on a plain `jolt` with no `JOLT_PATH`, no deps
fetched, and no jpm:
`jolt uberscript OUT.clj -m NS` bundles `NS` and every namespace it requires —
your code plus its dependencies — into a single `.clj` in dependency order,
ending with a call to `NS/-main`. Run it from a project dir and the `deps.edn`
is resolved first, so dependency namespaces are on the path to bundle. The
result runs on a plain `jolt` with no `JOLT_PATH`, no deps fetched, and no jpm:
```bash
jolt-deps uberscript app.clj -m myapp.main
jolt uberscript app.clj -m myapp.main
jolt app.clj arg1 arg2
```

View file

@ -11,8 +11,13 @@ Scope, decided up front:
"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
way `jpm` sits beside `janet`. The `jolt` runtime knows nothing about deps.edn.
- **deps-agnostic runtime core** — resolution is a CLI front-end concern, not a
runtime one. The `jolt` *runtime* knows nothing about deps.edn; it only reads
source roots from `JOLT_PATH`. The `jolt` *CLI* resolves a deps.edn into that
env var before running, in a module (`deps.janet`) that loads `jpm` lazily.
(This was a separate `jolt-deps` binary originally; it was folded into `jolt`
for a single-binary UX — the code boundary stayed, only the executable merged.
A back-compat `jolt-deps` shim still ships and forwards to `jolt`.)
## How jpm handles dependencies
@ -70,9 +75,15 @@ The loader (`evaluator.janet/find-ns-file`) resolves a namespace by searching th
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.
The `jolt` CLI (`src/jolt/main.janet`, `resolve-deps-argv`) ties it together: on
a deps subcommand — or any runnable command in a directory that has a `deps.edn`
— it resolves the roots, sets `JOLT_PATH`/`JOLT_APP_PATHS`, and de-sugars the
argv into a plain runtime command (`-M:alias` → the alias `:main-opts`, `run
FILE` → `FILE`, …) that the normal dispatch then runs. `main.janet` imports
`deps.janet`, so the resolver ships in the `jolt` binary; but `deps.janet` loads
`jpm` lazily, and the runtime modules (`api`/`backend`/RT) never import it, so an
app baked from its own `jolt/api` entry doesn't link it. The runtime's only
dependency interface remains that one 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
@ -111,8 +122,7 @@ a regex feature like Unicode property classes (`\p{…}`).
## Janet dependencies: `:jpm/module`
A jolt project can depend on janet libraries. jpm owns their installation;
`deps.edn` declares the requirement and `jolt-deps` verifies it at resolve
time:
`deps.edn` declares the requirement and `jolt` verifies it at resolve time:
```clojure
:deps {janet/spork-http {:jpm/module "spork/http"
@ -121,8 +131,8 @@ time:
- `:jpm/module` — the janet module path that must be importable.
- `:jpm/install` (optional) — the jpm package to install when it isn't;
`jolt-deps` runs `jpm install <name>` once, then re-checks. Without it the
resolve fails with the install hint.
`jolt` runs `jpm install <name>` once, then re-checks. Without it the resolve
fails with the install hint.
A `:jpm/module` dep contributes no source roots. At runtime the `janet.*`
interop bridge autoloads the module on first reference

View file

@ -9,8 +9,9 @@
:name "jolt"
: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.
# Deprecated shim kept for back-compat: deps.edn resolution is built into `jolt`
# now (the CLI front-end resolves into JOLT_PATH in-process; the runtime core
# stays deps-agnostic). Forwards to `jolt`; prefer `jolt -M:…` / `jolt path`.
(declare-executable
:name "jolt-deps"
:entry "src/jolt/deps_cli.janet")

View file

@ -1,45 +1,16 @@
# jolt-deps — a separate tool that resolves a deps.edn into Jolt source roots.
# jolt-deps — deprecated shim. Dependency resolution is now built into `jolt`
# itself (the runtime stays deps-agnostic; the CLI front-end resolves deps.edn
# into JOLT_PATH in-process). This binary forwards everything to `jolt` so
# existing scripts keep working — prefer calling `jolt` directly:
#
# 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 -M:nrepl -> jolt -M:nrepl
# jolt-deps path -> jolt path
# jolt-deps run FILE -> jolt run FILE
#
# jolt-deps [-A:a:b] path print the resolved roots (':'-joined), e.g.
# JOLT_PATH=$(jolt-deps path) jolt file.clj
# jolt-deps [-A:a:b] run FILE [args]
# resolve, then `jolt FILE args` with JOLT_PATH set
# jolt-deps [-A:a:b] repl resolve, then a jolt REPL with JOLT_PATH set
# jolt-deps [-A:a:b] -e EXPR resolve, then `jolt -e EXPR ...`
# jolt-deps -M:a[:b] [args] resolve with the aliases, then run jolt with
# the last alias's :main-opts ++ args
# jolt-deps uberscript OUT -m NS resolve, then bundle NS + deps into one .clj
# jolt-deps tasks list :tasks (merged user+project deps.edn)
# jolt-deps task NAME [args] run a task: a string task is a shell command
# (args appended); a {:main-opts [...]} task
# runs jolt with those args ++ extra args
#
# -A:dev:test selects aliases (tools.deps style): their :extra-paths and
# :extra-deps join the resolution. A user-level deps.edn ($JOLT_CONFIG, else
# $XDG_CONFIG_HOME/jolt, else ~/.jolt) merges under the project's.
# The jolt binary is found via $JOLT_BIN, else `jolt` on PATH.
# The jolt binary is found via $JOLT_BIN, else the `jolt` sitting next to this
# shim (built as a pair), else `jolt` on PATH.
(import ./deps :as deps)
(defn- parse-alias-flag
"\"-A:dev:test\" -> [:dev :test] (also accepts -M:...)."
[arg]
(map keyword (filter |(not= "" $) (string/split ":" (string/slice arg 2)))))
(defn- roots [aliases]
(if (os/stat "deps.edn") (deps/resolve-deps-cached "deps.edn" nil aliases) @[]))
(defn- jolt-bin
"The jolt executable: $JOLT_BIN, else the `jolt` sitting NEXT TO this
jolt-deps binary (the pair is built together — running a checkout's
build/jolt-deps by path must not pick up some other jolt, or fail when
none is on PATH), else `jolt` from PATH."
[]
(defn- jolt-bin []
(or (os/getenv "JOLT_BIN")
(let [self (or (first (dyn :args)) (dyn :executable))
slashes (when self (string/find-all "/" self))
@ -49,56 +20,8 @@
(when (and sibling (os/stat sibling)) sibling))
"jolt"))
(defn- exec-jolt [aliases 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 aliases) ":"))
(def existing (os/getenv "JOLT_PATH"))
(os/setenv "JOLT_PATH" (if (and existing (> (length existing) 0)) (string rs ":" existing) rs))
# The project's OWN source roots (jolt-87e): the runtime scopes whole-program
# inference to namespaces under these, inferring deps per-ns at load instead of
# re-inferring every transitive dep in one fixpoint. Absent => the runtime
# treats all namespaces as app (whole-program over everything, as before).
(when (os/stat "deps.edn")
(os/setenv "JOLT_APP_PATHS" (string/join (deps/project-source-roots "deps.edn" aliases) ":")))
(os/execute [(jolt-bin) ;extra-args] :p))
(defn- usage []
(print "usage: jolt-deps [-A:alias[:alias]] [path | run FILE [args] | repl | -e EXPR [args]]")
(print " jolt-deps -M:alias[:alias] [args] (runs the alias :main-opts)")
(print " jolt-deps uberscript OUT -m NS"))
(defn main [&]
(var argv (tuple/slice (or (dyn :args) @[]) 1))
(var aliases nil)
# leading -A:... selects aliases for whatever command follows
(while (string/has-prefix? "-A" (or (get argv 0) ""))
(set aliases (array/concat (or aliases @[]) (parse-alias-flag (get argv 0))))
(set argv (tuple/slice argv 1)))
(def cmd (get argv 0))
(cond
(or (nil? cmd) (= cmd "help") (= cmd "-h") (= cmd "--help")) (usage)
(string/has-prefix? "-M" cmd)
(let [als (array/concat (or aliases @[]) (parse-alias-flag cmd))
mo (or (deps/alias-main-opts "deps.edn" als)
(do (eprint "jolt-deps: no :main-opts in aliases " (string/format "%j" (map string als)))
(os/exit 1)))]
(os/exit (exec-jolt als [;mo ;(tuple/slice argv 1)])))
(= cmd "path") (print (string/join (roots aliases) ":"))
(= cmd "tasks")
(each row (deps/tasks "deps.edn")
(print (row 0) (if (row 1) (string "\t" (row 1)) "")))
(= cmd "task")
(let [name (get argv 1)
spec (when name (deps/task-spec "deps.edn" name))]
(cond
(nil? name) (do (eprint "jolt-deps: task needs a name") (os/exit 1))
(nil? spec) (do (eprint "jolt-deps: no such task: " name) (os/exit 1))
(= :shell (spec :type))
(os/exit (os/execute ["sh" "-c" (string/join [(spec :cmd) ;(tuple/slice argv 2)] " ")] :p))
(os/exit (exec-jolt aliases [;(spec :argv) ;(tuple/slice argv 2)]))))
(= cmd "run") (os/exit (exec-jolt aliases (tuple/slice argv 1)))
(= cmd "repl") (os/exit (exec-jolt aliases []))
(= cmd "-e") (os/exit (exec-jolt aliases argv))
(= cmd "uberscript") (os/exit (exec-jolt aliases argv))
(do (eprint "jolt-deps: unknown command " cmd) (usage) (os/exit 1))))
(def argv (tuple/slice (or (dyn :args) @[]) 1))
(eprint "jolt-deps is deprecated — dependency resolution is built into `jolt` now "
"(e.g. `jolt -M:nrepl`, `jolt path`, `jolt run FILE`).")
(os/exit (os/execute [(jolt-bin) ;argv] :p)))

View file

@ -11,6 +11,7 @@
(use ./config)
(use ./reader)
(import ./core :as jcore)
(import ./deps :as deps)
(def jolt-version "0.1.0")
@ -622,6 +623,14 @@
(print " uberscript OUT -m NS Bundle NS + its required namespaces into one .clj")
(print " --version, version Print the Jolt version")
(print " -h, --help, help Show this help\n")
(print "Dependencies (deps.edn, git + :local deps — resolved into JOLT_PATH):")
(print " -M:a[:b] [args] Run the alias(es) :main-opts ++ args")
(print " -A:a[:b] CMD [args] Run CMD (repl/-m/nrepl-server/…) with the alias paths")
(print " run FILE [args] Run FILE with deps.edn resolved")
(print " path Print the resolved source roots (':'-joined)")
(print " tasks List :tasks from deps.edn")
(print " task NAME [args] Run a :tasks entry (shell string or :main-opts)")
(print " A deps.edn in the working dir is auto-resolved for repl/-m/-e/nrepl-server/FILE.\n")
(print "Running a program (a file, -m/-M) direct-links by default; type inference")
(print "and specialization are opt-in via JOLT_OPTIMIZE (the cost is paid once, then")
(print "cached). The repl, -e, and nrepl-server stay open so you can redefine vars.")
@ -632,7 +641,7 @@
(print " JOLT_DIRECT_LINK=1 force direct-linking + optimization on (e.g. for -e)")
(print " JOLT_WHOLE_PROGRAM=1 force the whole-program pass on")
(print " JOLT_INTERPRET=1 run the tree-walking interpreter\n")
(print "Dependencies (deps.edn) are handled by the separate jolt-deps tool."))
(print " JOLT_PATH=dir1:dir2 extra source roots (set automatically by deps resolution)"))
(def- help-flags {"-h" true "--help" true "help" true "-?" true})
(def- version-flags {"--version" true "version" true})
@ -641,13 +650,94 @@
(def- file-flags {"-f" true "--file" true})
(def- main-flags {"-m" true "--main" true})
# --- deps.edn integration (the old jolt-deps tool, folded in) ----------------
# The runtime stays deps-agnostic: it reads source roots from JOLT_PATH (applied
# in `main`, below). This layer resolves a deps.edn into those roots IN-PROCESS,
# so a single `jolt` binary both resolves dependencies and runs code. ./deps
# loads jpm (git fetch + cache) lazily — only at an actual resolve — so a run
# with no deps.edn never pulls it in, and an app baked from its own entry (which
# imports jolt/api, not this CLI) never links the resolver at all.
(defn- parse-alias-flag
``"-A:dev:test" / "-M:dev" -> [:dev :test].``
[arg]
(map keyword (filter |(not= "" $) (string/split ":" (string/slice arg 2)))))
(defn- deps-roots [aliases]
(if (os/stat "deps.edn") (deps/resolve-deps-cached "deps.edn" nil aliases) @[]))
(defn- set-deps-env! [aliases]
# Prepend the resolved roots to any existing JOLT_PATH; `main` (below) applies
# them to the ctx source-paths. JOLT_APP_PATHS scopes whole-program inference
# to the project's own namespaces — deps load-infer per-ns instead (jolt-87e).
(def rs (string/join (deps-roots aliases) ":"))
(def existing (os/getenv "JOLT_PATH"))
(os/setenv "JOLT_PATH" (if (and existing (> (length existing) 0)) (string rs ":" existing) rs))
(when (os/stat "deps.edn")
(os/setenv "JOLT_APP_PATHS" (string/join (deps/project-source-roots "deps.edn" aliases) ":"))))
(defn- resolve-deps-argv
``Resolve deps.edn (when relevant) and de-sugar the deps subcommands into a
plain runtime argv that `main` then dispatches on. The pure deps queries
(path/tasks/task) print and exit. A runnable command resolves a deps.edn in
cwd when one is present — so `jolt repl` / `-m` / `nrepl-server` pick up the
project and its dependencies — and is a no-op (resolver untouched) with none.``
[argv]
# leading -A:alias flags apply to whatever command follows
(var aliases nil)
(var rest argv)
(while (string/has-prefix? "-A" (or (get rest 0) ""))
(set aliases (array/concat (or aliases @[]) (parse-alias-flag (get rest 0))))
(set rest (array/slice rest 1)))
(def cmd (get rest 0))
(cond
# pure deps queries — produce output and exit, never start the runtime
(= cmd "path")
(do (print (string/join (deps-roots aliases) ":")) (os/exit 0))
(= cmd "tasks")
(do (each row (deps/tasks "deps.edn")
(print (row 0) (if (row 1) (string "\t" (row 1)) "")))
(os/exit 0))
(= cmd "task")
(let [name (get rest 1)
spec (when name (deps/task-spec "deps.edn" name))]
(cond
(nil? name) (do (eprint "jolt: task needs a name") (os/exit 1))
(nil? spec) (do (eprint "jolt: no such task: " name) (os/exit 1))
(= :shell (spec :type))
(os/exit (os/execute ["sh" "-c" (string/join [(spec :cmd) ;(array/slice rest 2)] " ")] :p))
(do (set-deps-env! aliases)
(array/concat @[] (spec :argv) (array/slice rest 2)))))
# -M:aliases — resolve and run the alias's :main-opts ++ extra args
(and cmd (string/has-prefix? "-M" cmd))
(let [als (array/concat (or aliases @[]) (parse-alias-flag cmd))
mo (deps/alias-main-opts "deps.edn" als)]
(if mo
(do (set-deps-env! als) (array/concat @[] mo (array/slice rest 1)))
(do (eprint "jolt: no :main-opts in alias(es) " (string/format "%j" (map string als)))
(os/exit 1))))
# explicit `run FILE` — resolve, then run the file
(= cmd "run")
(do (set-deps-env! aliases) (array/slice rest 1))
# any runnable command: resolve a deps.edn if present (or if -A forced it);
# help/version never resolve, and with no deps.edn + no -A the resolver and
# jpm are never touched.
(and (not (help-flags cmd)) (not (version-flags cmd))
(or aliases (os/stat "deps.edn")))
(do (set-deps-env! aliases) rest)
# nothing deps-related — argv unchanged
true rest))
(defn main [&]
(def args (or (dyn :args) @[])) # @["jolt" arg1 arg2 ...]
(def argv (if (> (length args) 1) (array/slice args 1) @[]))
# Resolve deps.edn + de-sugar deps subcommands (jolt-deps folded in): sets
# JOLT_PATH/JOLT_APP_PATHS in our env (read just below) and rewrites argv to a
# plain runtime command. A no-deps invocation passes through untouched.
(def argv (resolve-deps-argv (if (> (length args) 1) (array/slice args 1) @[])))
(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.
# resolve-deps-argv (above) sets it from 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))))

View file

@ -0,0 +1,98 @@
# jolt-deps folded into jolt: the single `jolt` binary resolves a deps.edn into
# JOLT_PATH in-process and dispatches the deps subcommands (path/-M/run/tasks/
# task), and auto-resolves a deps.edn for runnable commands (repl/-m/-e/FILE).
# Drives the BUILT binary (baked overlay -> cwd-independent) from a fixture
# project dir so deps.edn in cwd is picked up. :local/root deps only — no
# network. Skips cleanly if build/jolt is absent (source-only run).
(def repo-root (os/cwd))
(def jolt (string repo-root "/build/jolt"))
(if-not (os/stat jolt)
(do (print "deps-merged-cli: SKIP (no build/jolt — run from source)") (os/exit 0)))
(def base (string (or (os/getenv "TMPDIR") "/tmp") "/jolt-merged-cli-" (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]
(var acc nil)
(each seg (filter |(not= "" $) (string/split "/" p))
(set acc (if (nil? acc) (string "/" seg) (string acc "/" seg)))
(unless (os/stat acc) (os/mkdir acc))))
# project depends on a local lib; an alias with :main-opts; an :extra-paths
# alias; a :tasks map (shell task)
(each d ["proj/src/app" "lib/src/mylib"] (mkdirs (string base "/" d)))
(spit (string base "/proj/deps.edn")
`{:paths ["src"]
:deps {my/lib {:local/root "../lib"}}
:aliases {:run {:main-opts ["-m" "app.core"]}
:dev {:extra-paths ["dev"]}}
:tasks {greet "echo hello-task"}}`)
(spit (string base "/lib/deps.edn") `{:paths ["src"]}`)
(spit (string base "/lib/src/mylib/core.clj") "(ns mylib.core)\n(defn answer [] 42)\n")
(spit (string base "/proj/src/app/core.clj")
"(ns app.core (:require [mylib.core :as m]))\n(defn -main [& args] (println \"MAIN\" (m/answer) (count args)))\n")
(spit (string base "/proj/script.clj")
"(require '[mylib.core :as m])\n(println \"SCRIPT\" (m/answer))\n")
# Run the built jolt from a given dir (cwd matters: deps.edn is read from cwd).
# Direct spawn (no shell) so arbitrary -e exprs need no quoting. Returns out+err.
(defn- run-in [dir & args]
(def prev (os/cwd))
(os/cd dir)
(def p (os/spawn [jolt ;args] :p {:out :pipe :err :pipe}))
(def out (:read (p :out) :all))
(def err (:read (p :err) :all))
(os/proc-wait p)
(os/cd prev)
(string (or out "") (or err "")))
(var fails 0)
(defn check [label got pred]
(if (pred got) (print " ok " label)
(do (++ fails) (printf " FAIL %s: got %q" label got))))
(defn- has [sub] (fn [s] (truthy? (string/find sub s))))
(def proj (string base "/proj"))
# `path` prints the resolved roots: project's own src + the local dep's src
(check "path includes project src" (run-in proj "path") (has "/proj/src"))
(check "path includes local dep src" (run-in proj "path") (has "/lib/src"))
# `-M:run` runs the alias :main-opts (-m app.core), requiring the local dep
(check "-M:run runs -main through resolved deps" (run-in proj "-M:run" "x" "y") (has "MAIN 42 2"))
# `run FILE` resolves deps then runs the file
(check "run FILE resolves deps" (run-in proj "run" "script.clj") (has "SCRIPT 42"))
# `tasks` lists the :tasks entries; `task NAME` runs a shell task
(check "tasks lists greet" (run-in proj "tasks") (has "greet"))
(check "task runs shell command" (run-in proj "task" "greet") (has "hello-task"))
# auto-resolve: a plain -e in a deps.edn dir can require the local dep
(check "-e auto-resolves deps.edn in cwd"
(run-in proj "-e" "(require '[mylib.core :as m]) (println (m/answer))")
(has "42"))
# -A:alias forces resolution (with that alias) for a runnable command
(check "-A:dev with -e resolves + runs"
(run-in proj "-A:dev" "-e" "(println (+ 1 2))")
(has "3"))
# no deps.edn: plain run is unaffected (resolver/jpm never touched)
(mkdirs (string base "/plain"))
(check "-e in a no-deps dir works unchanged"
(run-in (string base "/plain") "-e" "(println (* 6 7))")
(has "42"))
# help/version never trigger resolution
(check "version works in a deps dir" (run-in proj "--version") (has "jolt v"))
(rmrf base)
(if (> fails 0)
(do (printf "deps-merged-cli: %d FAILED" fails) (os/exit 1))
(print "deps-merged-cli: all cases passed"))