From 30a12f39ff09eb229449e9d05592c74270850dd5 Mon Sep 17 00:00:00 2001 From: Dmitri Sotnikov Date: Tue, 16 Jun 2026 02:30:28 +0000 Subject: [PATCH] Fold jolt-deps into the jolt binary (#133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Co-authored-by: Yogthos --- CLAUDE.md | 2 +- README.md | 10 +- docs/building-and-deps.md | 69 +++++++------ docs/tools-deps.md | 28 ++++-- project.janet | 5 +- src/jolt/deps_cli.janet | 105 +++----------------- src/jolt/main.janet | 96 +++++++++++++++++- test/integration/deps-merged-cli-test.janet | 98 ++++++++++++++++++ 8 files changed, 274 insertions(+), 139 deletions(-) create mode 100644 test/integration/deps-merged-cli-test.janet diff --git a/CLAUDE.md b/CLAUDE.md index 40942e7..18db596 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,7 @@ bd close # 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/.janet # one spec file janet test/integration/conformance-test.janet # 3-mode conformance (interpret/compile/self-host) diff --git a/README.md b/README.md index c74f50a..e9de902 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/building-and-deps.md b/docs/building-and-deps.md index c3023eb..4bece89 100644 --- a/docs/building-and-deps.md +++ b/docs/building-and-deps.md @@ -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 ``` diff --git a/docs/tools-deps.md b/docs/tools-deps.md index 04a062b..12999a2 100644 --- a/docs/tools-deps.md +++ b/docs/tools-deps.md @@ -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 `.clj` then `.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 ` once, then re-checks. Without it the - resolve fails with the install hint. + `jolt` runs `jpm install ` 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 diff --git a/project.janet b/project.janet index d6d9ddd..f843185 100644 --- a/project.janet +++ b/project.janet @@ -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") diff --git a/src/jolt/deps_cli.janet b/src/jolt/deps_cli.janet index 31feac8..3d0de25 100644 --- a/src/jolt/deps_cli.janet +++ b/src/jolt/deps_cli.janet @@ -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))) diff --git a/src/jolt/main.janet b/src/jolt/main.janet index 536f66d..e326cbc 100644 --- a/src/jolt/main.janet +++ b/src/jolt/main.janet @@ -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)))) diff --git a/test/integration/deps-merged-cli-test.janet b/test/integration/deps-merged-cli-test.janet new file mode 100644 index 0000000..e5acfa2 --- /dev/null +++ b/test/integration/deps-merged-cli-test.janet @@ -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"))