deps.edn resolution + a file loader + a project-aware CLI
joltc grew from a single -e expression into a real project runner. require now loads a namespace's .clj/.cljc from the source roots transitively (load-once), so a multi-file project works; the corpus/unit gates load compile-eval.ss but not the loader, so their alias-only require is unchanged. jolt.deps resolves a deps.edn into ordered source roots — git + local deps only (no Maven), breadth-first so a top-level pin wins, with aliases (:extra-paths/ :extra-deps/:main-opts) and tasks. Git deps clone into a sha-immutable cache ($JOLT_GITLIBS, else ~/.jolt/gitlibs) by shelling out to git via a new jolt.host/sh primitive. jolt.main dispatches run -m / -M:alias / -A / repl / path / a deps.edn task. The launcher passes the user's cwd as JOLT_PWD (the project dir) since it cd's to the repo root for the runtime's relative loads.
This commit is contained in:
parent
85cec65937
commit
7e2704642b
5 changed files with 451 additions and 29 deletions
19
bin/joltc
19
bin/joltc
|
|
@ -1,14 +1,19 @@
|
|||
#!/bin/sh
|
||||
# joltc (jolt-9phg, inc9b) — the pure-Chez jolt runtime. NO Janet.
|
||||
# joltc — the pure-Chez jolt runtime. NO Janet.
|
||||
#
|
||||
# Runs a Clojure expression through the zero-Janet spine on Chez, using the
|
||||
# checked-in bootstrap seed (host/chez/seed/). With only Chez installed and no
|
||||
# Janet anywhere, this compiles and evaluates jolt code end to end.
|
||||
# Compiles and evaluates jolt (Clojure) on Chez using the checked-in bootstrap
|
||||
# seed (host/chez/seed/). With only Chez installed it runs jolt end to end:
|
||||
#
|
||||
# bin/joltc -e "(+ 1 2)"
|
||||
# joltc -e "(+ 1 2)" evaluate an expression
|
||||
# joltc run -m app.core [args] resolve deps.edn, run a namespace's -main
|
||||
# joltc -M:alias [args] run an alias's :main-opts
|
||||
# joltc repl | path | <task> REPL, print roots, or a deps.edn task
|
||||
#
|
||||
# (bin/jolt-chez is the older Janet-hosted -e path still used as the corpus oracle;
|
||||
# joltc is the fully self-hosted runtime.)
|
||||
# The launcher cd's to the jolt repo root so the runtime's relative loads work;
|
||||
# the user's original cwd (the project dir, where deps.edn lives) is passed in
|
||||
# JOLT_PWD.
|
||||
root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
JOLT_PWD="${JOLT_PWD:-$PWD}"
|
||||
export JOLT_PWD
|
||||
cd "$root" || exit 1
|
||||
exec chez --script host/chez/cli.ss "$@"
|
||||
|
|
|
|||
|
|
@ -1,24 +1,16 @@
|
|||
;; cli.ss (jolt-9phg, Phase 3 inc9b) — the pure-Chez jolt runtime. NO Janet.
|
||||
;; cli.ss (jolt-9phg / jolt-90sp) — the pure-Chez jolt runtime. NO Janet.
|
||||
;;
|
||||
;; This is the zero-Janet runtime counterpart to bootstrap.ss (the zero-Janet
|
||||
;; build). It loads the checked-in seed (host/chez/seed/{prelude,image}.ss — the
|
||||
;; bootstrap compiler) and the zero-Janet spine, reads a Clojure expression, and
|
||||
;; compiles+evals it ON CHEZ (read -> analyze -> IR -> emit -> eval). With the seed
|
||||
;; checked in, a clone of the repo runs jolt with only Chez installed — no Janet at
|
||||
;; build or run time.
|
||||
;; Loads the checked-in seed (host/chez/seed/{prelude,image}.ss — the bootstrap
|
||||
;; compiler) and the zero-Janet spine, then either evaluates a -e expression or
|
||||
;; dispatches a CLI command (run/-M/repl/path/task) through jolt.main. The loader
|
||||
;; (loader.ss) turns `require` into real file loading off the source roots, so a
|
||||
;; multi-file project with deps.edn dependencies runs end to end.
|
||||
;;
|
||||
;; Run from the repo root:
|
||||
;; chez --script host/chez/cli.ss -e "EXPR"
|
||||
;; Run from the repo root (bin/joltc cd's there); the project dir is JOLT_PWD.
|
||||
(import (chezscheme))
|
||||
|
||||
(define cli-args (cdr (command-line))) ; drop the script name
|
||||
(unless (and (= (length cli-args) 2) (string=? (car cli-args) "-e"))
|
||||
(display "usage: cli.ss -e EXPR\n")
|
||||
(exit 2))
|
||||
(define cli-expr (cadr cli-args))
|
||||
|
||||
;; Assemble the zero-Janet spine from the checked-in seed, exactly as
|
||||
;; driver/program-zero-janet does — but with no Janet generating the program.
|
||||
(load "host/chez/rt.ss")
|
||||
(set-chez-ns! "clojure.core")
|
||||
(load "host/chez/seed/prelude.ss")
|
||||
|
|
@ -27,11 +19,23 @@
|
|||
(load "host/chez/host-contract.ss")
|
||||
(load "host/chez/seed/image.ss")
|
||||
(load "host/chez/compile-eval.ss")
|
||||
(load "host/chez/loader.ss")
|
||||
|
||||
;; Compile + eval on Chez; print the result (blank for nil, like the spine). Wrap
|
||||
;; in (do ...) so a multi-form -e string evaluates every form and returns the last,
|
||||
;; matching Clojure's -e (jolt-compile-eval reads a single form).
|
||||
(let ((result (jolt-final-str
|
||||
(jolt-compile-eval (string-append "(do " cli-expr ")") "user"))))
|
||||
(unless (string=? result "")
|
||||
(display result) (newline)))
|
||||
;; jolt.main + jolt.deps live under jolt-core; keep them (and src/jolt) on the
|
||||
;; roots so the CLI's own namespaces — and any jolt.* an app pulls in — resolve.
|
||||
;; A project's resolved deps roots are prepended to these by jolt.main.
|
||||
(set-source-roots! (list "jolt-core" "src/jolt"))
|
||||
|
||||
(cond
|
||||
;; -e EXPR — evaluate one expression and print it (blank for nil). Wrapped in
|
||||
;; (do …) so a multi-form string evaluates every form and returns the last.
|
||||
((and (= (length cli-args) 2) (string=? (car cli-args) "-e"))
|
||||
(let ((result (jolt-final-str
|
||||
(jolt-compile-eval (string-append "(do " (cadr cli-args) ")") "user"))))
|
||||
(unless (string=? result "")
|
||||
(display result) (newline))))
|
||||
;; otherwise dispatch the argv through jolt.main/-main
|
||||
(else
|
||||
(load-namespace "jolt.main")
|
||||
(let ((mainv (var-deref "jolt.main" "-main")))
|
||||
(apply jolt-invoke mainv cli-args))))
|
||||
|
|
|
|||
182
host/chez/loader.ss
Normal file
182
host/chez/loader.ss
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
;; loader.ss (jolt-90sp) — file-based namespace loading + a shell primitive.
|
||||
;;
|
||||
;; The corpus/CLI spine compiles one program at a time; namespaces declared in
|
||||
;; that program see each other because a top-level (do …) unrolls. A real project
|
||||
;; spans many FILES, so `require` must locate a namespace's source on the search
|
||||
;; roots and load it — transitively, once each. This is the piece the Phase-3
|
||||
;; "cross-ns load is deferred" note left open (ns.ss).
|
||||
;;
|
||||
;; Loaded by cli.ss AFTER compile-eval.ss (it calls jolt-compile-eval-form). The
|
||||
;; gates load compile-eval.ss but NOT this file, so the corpus/unit/sci runners
|
||||
;; keep their alias-only `require` and are unaffected.
|
||||
|
||||
;; --- search roots -----------------------------------------------------------
|
||||
;; An ordered list of directory strings. `require` searches them left to right.
|
||||
;; The CLI seeds this with the project's resolved deps roots (jolt.deps) plus the
|
||||
;; jolt-core roots so jolt.main/jolt.deps themselves load.
|
||||
(define source-roots '("."))
|
||||
(define (set-source-roots! roots) (set! source-roots roots))
|
||||
(define (get-source-roots) source-roots)
|
||||
|
||||
;; --- namespace -> file path -------------------------------------------------
|
||||
;; "app.commonmark-test" -> "app/commonmark_test": split on '.', munge '-'->'_'
|
||||
;; per segment, join with '/'. Matches Clojure's ns->file munging.
|
||||
(define (ns-seg-munge seg)
|
||||
(list->string (map (lambda (c) (if (char=? c #\-) #\_ c)) (string->list seg))))
|
||||
(define (ns-name->rel name)
|
||||
(let loop ((cs (string->list name)) (seg '()) (segs '()))
|
||||
(cond
|
||||
((null? cs)
|
||||
(let ((all (reverse (cons (list->string (reverse seg)) segs))))
|
||||
(let join ((xs all) (acc ""))
|
||||
(cond ((null? xs) acc)
|
||||
((string=? acc "") (join (cdr xs) (ns-seg-munge (car xs))))
|
||||
(else (join (cdr xs) (string-append acc "/" (ns-seg-munge (car xs)))))))))
|
||||
((char=? (car cs) #\.)
|
||||
(loop (cdr cs) '() (cons (list->string (reverse seg)) segs)))
|
||||
(else (loop (cdr cs) (cons (car cs) seg) segs)))))
|
||||
|
||||
(define (find-ns-file name)
|
||||
(let ((rel (ns-name->rel name)))
|
||||
(let loop ((roots source-roots))
|
||||
(if (null? roots) #f
|
||||
(let ((clj (string-append (car roots) "/" rel ".clj"))
|
||||
(cljc (string-append (car roots) "/" rel ".cljc")))
|
||||
(cond ((file-exists? clj) clj)
|
||||
((file-exists? cljc) cljc)
|
||||
(else (loop (cdr roots)))))))))
|
||||
|
||||
;; --- the loaded set ---------------------------------------------------------
|
||||
;; Seeded with every namespace that already has vars at load time — the baked
|
||||
;; prelude/image (clojure.core, clojure.string, jolt.analyzer, …). A `require` of
|
||||
;; one of those then no-ops instead of hunting for a (nonexistent) source file.
|
||||
(define loaded-ns (make-hashtable string-hash string=?))
|
||||
(vector-for-each (lambda (c) (hashtable-set! loaded-ns (var-cell-ns c) #t))
|
||||
(hashtable-values var-table))
|
||||
|
||||
;; Read every form from a file and compile+eval it in turn. The first form is
|
||||
;; normally (ns …), which expands to (in-ns …) and switches the current ns, so
|
||||
;; later forms compile in that namespace — (chez-current-ns) is re-read each step.
|
||||
(define (load-jolt-file path)
|
||||
(let loop ((src (read-file-string path)))
|
||||
(let ((pn (jolt-parse-next src)))
|
||||
(unless (jolt-nil? pn)
|
||||
(jolt-compile-eval-form (jolt-nth pn 0) (chez-current-ns))
|
||||
(loop (jolt-nth pn 1))))))
|
||||
|
||||
;; load-namespace: load `name`'s source once. Marked loaded BEFORE eval so a
|
||||
;; dependency cycle terminates (Clojure's behavior). The caller's current ns is
|
||||
;; restored afterward, since loading the file switched it.
|
||||
(define (load-namespace name)
|
||||
(unless (hashtable-ref loaded-ns name #f)
|
||||
(hashtable-set! loaded-ns name #t)
|
||||
(let ((file (find-ns-file name)))
|
||||
(if (not file)
|
||||
(begin
|
||||
(hashtable-delete! loaded-ns name)
|
||||
(error #f (string-append "Could not locate " (ns-name->rel name)
|
||||
".clj (or .cljc) on the source roots") name))
|
||||
(let ((saved (chez-current-ns)))
|
||||
(load-jolt-file file)
|
||||
(set-chez-ns! saved)
|
||||
(def-var! "clojure.core" "*ns*" (intern-ns! saved)))))))
|
||||
|
||||
;; load-file: load an explicit path (a `run FILE`), in the current ns.
|
||||
(define (jolt-load-file path)
|
||||
(load-jolt-file path)
|
||||
jolt-nil)
|
||||
|
||||
;; The target ns name of a require/use spec ([ns …] / (ns …) / bare ns).
|
||||
(define (spec-target-name spec)
|
||||
(let ((items (cond ((pvec? spec) (seq->list spec))
|
||||
((or (cseq? spec) (empty-list-t? spec)) (seq->list spec))
|
||||
((symbol-t? spec) (list spec))
|
||||
(else '()))))
|
||||
(and (pair? items) (symbol-t? (car items)) (symbol-t-name (car items)))))
|
||||
|
||||
;; --- require/use that LOAD ---------------------------------------------------
|
||||
;; Override the alias-only versions from natives-str.ss. Load each spec's target
|
||||
;; (no-op if baked/already loaded), THEN register its :as/:refer under the caller
|
||||
;; ns (chez-register-spec! reads the current ns, restored by load-namespace).
|
||||
(define (loader-require . specs)
|
||||
(for-each
|
||||
(lambda (s)
|
||||
(let ((target (spec-target-name s)))
|
||||
(when target (load-namespace target)))
|
||||
(chez-register-spec! (chez-current-ns) s))
|
||||
specs)
|
||||
jolt-nil)
|
||||
(def-var! "clojure.core" "require" loader-require)
|
||||
|
||||
(define (loader-use . specs)
|
||||
(for-each
|
||||
(lambda (spec)
|
||||
(let ((target (spec-target-name spec)))
|
||||
(when target (load-namespace target)))
|
||||
(chez-register-spec! (chez-current-ns) spec)
|
||||
(let* ((items (cond ((pvec? spec) (seq->list spec))
|
||||
((or (cseq? spec) (empty-list-t? spec)) (seq->list spec))
|
||||
((symbol-t? spec) (list spec))
|
||||
(else '())))
|
||||
(target (and (pair? items) (symbol-t? (car items)) (symbol-t-name (car items))))
|
||||
(filtered (let scan ((xs (if (pair? items) (cdr items) '())))
|
||||
(cond ((null? xs) #f)
|
||||
((and (keyword? (car xs))
|
||||
(member (keyword-t-name (car xs)) '("only" "refer"))) #t)
|
||||
(else (scan (cdr xs)))))))
|
||||
(when (and target (not filtered))
|
||||
(chez-register-refer-all! (chez-current-ns) target))))
|
||||
specs)
|
||||
jolt-nil)
|
||||
(def-var! "clojure.core" "use" loader-use)
|
||||
|
||||
(def-var! "clojure.core" "load-file" jolt-load-file)
|
||||
;; load: each arg is a "/"-rooted resource path like "/app/extra"; load the file
|
||||
;; for it relative to the search roots (strip the leading slash, try .clj/.cljc).
|
||||
(define (jolt-load . paths)
|
||||
(for-each
|
||||
(lambda (p)
|
||||
(let* ((rel (if (and (> (string-length p) 0) (char=? (string-ref p 0) #\/))
|
||||
(substring p 1 (string-length p)) p)))
|
||||
(let loop ((roots source-roots))
|
||||
(if (null? roots)
|
||||
(error #f "Could not locate resource on source roots" p)
|
||||
(let ((clj (string-append (car roots) "/" rel ".clj"))
|
||||
(cljc (string-append (car roots) "/" rel ".cljc")))
|
||||
(cond ((file-exists? clj) (load-jolt-file clj))
|
||||
((file-exists? cljc) (load-jolt-file cljc))
|
||||
(else (loop (cdr roots)))))))))
|
||||
paths)
|
||||
jolt-nil)
|
||||
(def-var! "clojure.core" "load" jolt-load)
|
||||
|
||||
;; --- shell primitive (jolt.host/sh, sh-out) ---------------------------------
|
||||
;; `sh` runs `sh -c CMD`, inheriting stdout/stderr (so git progress shows), and
|
||||
;; returns the exit code. `sh-out` captures stdout to a string (exit ignored) for
|
||||
;; commands whose output we parse (git rev-parse). Used by jolt.deps for git.
|
||||
(define (jolt-sh cmd) (system cmd))
|
||||
(def-var! "jolt.host" "sh" jolt-sh)
|
||||
|
||||
(define (jolt-sh-out cmd)
|
||||
(call-with-values
|
||||
(lambda () (open-process-ports (string-append "exec sh -c " (sh-quote cmd))
|
||||
(buffer-mode block) (native-transcoder)))
|
||||
(lambda (stdin stdout stderr pid)
|
||||
(close-port stdin)
|
||||
(let ((out (get-string-all stdout)))
|
||||
(close-port stdout) (close-port stderr)
|
||||
(if (eof-object? out) "" out)))))
|
||||
(define (sh-quote s) ; single-quote for the outer sh -c
|
||||
(string-append "'"
|
||||
(apply string-append
|
||||
(map (lambda (c) (if (char=? c #\') "'\\''" (string c))) (string->list s)))
|
||||
"'"))
|
||||
(def-var! "jolt.host" "sh-out" jolt-sh-out)
|
||||
|
||||
;; Expose source-root control + ns loading to Clojure (jolt.main / jolt.deps).
|
||||
(def-var! "jolt.host" "set-source-roots!"
|
||||
(lambda (roots) (set-source-roots! (seq->list roots)) jolt-nil))
|
||||
(def-var! "jolt.host" "source-roots" (lambda () (list->cseq source-roots)))
|
||||
(def-var! "jolt.host" "load-namespace" (lambda (n) (load-namespace n) jolt-nil))
|
||||
(def-var! "jolt.host" "file-exists?" (lambda (p) (if (file-exists? p) #t #f)))
|
||||
(def-var! "jolt.host" "getenv" (lambda (n) (let ((v (getenv n))) (if v v jolt-nil))))
|
||||
125
jolt-core/jolt/deps.clj
Normal file
125
jolt-core/jolt/deps.clj
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
(ns jolt.deps
|
||||
"Resolve a deps.edn into an ordered list of source roots — git + local deps
|
||||
only, no Maven. A reduced tools.deps: :paths, :deps (`:git/url`+`:git/sha` /
|
||||
`:local/root`), :aliases (:extra-paths / :extra-deps / :main-opts), :tasks.
|
||||
|
||||
The deps walk is breadth-first so a top-level coordinate registers before any
|
||||
transitive one (a top-level pin wins). Git deps clone into a sha-immutable
|
||||
cache ($JOLT_GITLIBS, else ~/.jolt/gitlibs) shared across projects. Resolution
|
||||
shells out to `git` through jolt.host/sh; nothing here touches the JVM."
|
||||
(:require [clojure.edn :as edn]
|
||||
[clojure.string :as str]))
|
||||
|
||||
;; --- small host seams -------------------------------------------------------
|
||||
(defn- getenv [n] (jolt.host/getenv n))
|
||||
(defn- file-exists? [p] (jolt.host/file-exists? p))
|
||||
(defn- sh [cmd] (jolt.host/sh cmd)) ; exit code, inherits stdout/stderr
|
||||
(defn- sh-out [cmd] (jolt.host/sh-out cmd)) ; captured stdout
|
||||
(defn- warn [& xs] (println (str "[jolt.deps] " (apply str xs))))
|
||||
|
||||
(defn- read-edn [path]
|
||||
(when (file-exists? path)
|
||||
(try (edn/read-string (slurp path))
|
||||
(catch :default e (warn "could not read " path ": " (ex-message e)) nil))))
|
||||
|
||||
(defn- abspath [dir p]
|
||||
(if (str/starts-with? p "/") p (str dir "/" p)))
|
||||
|
||||
;; --- git cache --------------------------------------------------------------
|
||||
(defn- gitlibs-dir []
|
||||
(or (getenv "JOLT_GITLIBS")
|
||||
(str (or (getenv "HOME") ".") "/.jolt/gitlibs")))
|
||||
|
||||
(defn- alnum? [c]
|
||||
(let [n (int c)]
|
||||
(or (and (>= n 48) (<= n 57)) ; 0-9
|
||||
(and (>= n 65) (<= n 90)) ; A-Z
|
||||
(and (>= n 97) (<= n 122))))) ; a-z
|
||||
(defn- sanitize [s]
|
||||
(str/join (map (fn [c] (if (or (alnum? c) (= c \.) (= c \-)) c \_)) (seq s))))
|
||||
|
||||
(defn- ensure-git
|
||||
"Clone url at sha into the cache (once); return the checkout dir."
|
||||
[url sha]
|
||||
(let [dir (str (gitlibs-dir) "/" (sanitize url) "/" sha)]
|
||||
(if (file-exists? dir)
|
||||
dir
|
||||
(do
|
||||
(warn "fetching " url " @ " (subs sha 0 (min 12 (count sha))))
|
||||
(sh (str "mkdir -p " (pr-str dir)))
|
||||
(when-not (zero? (sh (str "git clone --quiet " (pr-str url) " " (pr-str dir))))
|
||||
(throw (ex-info (str "git clone failed: " url) {:url url})))
|
||||
(when-not (zero? (sh (str "git -C " (pr-str dir) " checkout --quiet " (pr-str sha))))
|
||||
(throw (ex-info (str "git checkout failed: " sha " in " url) {:url url :sha sha})))
|
||||
;; submodules are pinned in the checkout; pull them if the dep uses any.
|
||||
(sh (str "git -C " (pr-str dir) " submodule update --init --recursive --quiet"))
|
||||
dir))))
|
||||
|
||||
;; --- coordinate -> root dir -------------------------------------------------
|
||||
(defn- coord-root
|
||||
"The on-disk root directory for one dependency coordinate, or nil to skip."
|
||||
[coord spec base-dir]
|
||||
(cond
|
||||
(:local/root spec) (abspath base-dir (:local/root spec))
|
||||
(and (:git/url spec) (:git/sha spec))
|
||||
(let [checkout (ensure-git (:git/url spec) (:git/sha spec))]
|
||||
(if-let [root (:deps/root spec)] (str checkout "/" root) checkout))
|
||||
(:jolt/module spec)
|
||||
(do (warn "skipping janet dependency " coord " (:jolt/module is obsolete on Chez)") nil)
|
||||
:else
|
||||
(do (warn "skipping unsupported coordinate " coord " " (pr-str spec)) nil)))
|
||||
|
||||
(defn- dep-source-roots
|
||||
"Source roots a resolved dep contributes: its deps.edn :paths (default [\"src\"])
|
||||
resolved under its root dir."
|
||||
[root]
|
||||
(let [edn (read-edn (str root "/deps.edn"))
|
||||
paths (or (:paths edn) ["src"])]
|
||||
(map #(abspath root %) paths)))
|
||||
|
||||
(defn- resolve-deps
|
||||
"Breadth-first walk of a deps map; returns an ordered, de-duplicated list of
|
||||
source-root directories. `base-dir` resolves :local/root and is replaced by a
|
||||
dep's own root as the walk descends."
|
||||
[deps base-dir]
|
||||
(loop [queue (mapv (fn [[c s]] [c s base-dir]) (seq deps))
|
||||
seen #{}
|
||||
roots []]
|
||||
(if (empty? queue)
|
||||
(distinct roots)
|
||||
(let [[coord spec bd] (first queue)
|
||||
queue (subvec (vec queue) 1)]
|
||||
(if (contains? seen coord)
|
||||
(recur queue seen roots)
|
||||
(let [root (coord-root coord spec bd)]
|
||||
(if (nil? root)
|
||||
(recur queue (conj seen coord) roots)
|
||||
(let [edn (read-edn (str root "/deps.edn"))
|
||||
child (mapv (fn [[c s]] [c s root]) (seq (:deps edn)))]
|
||||
(recur (into queue child)
|
||||
(conj seen coord)
|
||||
(into roots (dep-source-roots root)))))))))))
|
||||
|
||||
;; --- public -----------------------------------------------------------------
|
||||
(defn resolve-project
|
||||
"Resolve `project-dir`'s deps.edn with the selected alias keywords. Returns
|
||||
{:roots [...] :main-opts [...] :tasks {...}}; :main-opts is the last selected
|
||||
alias's, else nil."
|
||||
([project-dir] (resolve-project project-dir []))
|
||||
([project-dir alias-kws]
|
||||
(let [edn (read-edn (str project-dir "/deps.edn"))
|
||||
aliases (:aliases edn)
|
||||
selected (keep #(get aliases %) alias-kws)
|
||||
extra-paths (mapcat :extra-paths selected)
|
||||
extra-deps (apply merge (map :extra-deps selected))
|
||||
main-opts (some :main-opts (reverse selected))
|
||||
project-paths (concat (or (:paths edn) ["src"]) extra-paths)
|
||||
project-roots (map #(abspath project-dir %) project-paths)
|
||||
all-deps (merge (:deps edn) extra-deps)
|
||||
dep-roots (resolve-deps all-deps project-dir)]
|
||||
{:roots (vec (distinct (concat project-roots dep-roots)))
|
||||
:main-opts main-opts
|
||||
:tasks (:tasks edn)})))
|
||||
|
||||
(defn has-deps-edn? [project-dir]
|
||||
(file-exists? (str project-dir "/deps.edn")))
|
||||
106
jolt-core/jolt/main.clj
Normal file
106
jolt-core/jolt/main.clj
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
(ns jolt.main
|
||||
"The jolt CLI dispatch: resolve a project's deps.edn, set the source roots, and
|
||||
run a namespace's -main, a file, a deps.edn task, or a REPL. Driven by cli.ss,
|
||||
which hands it the raw argv; the project directory is JOLT_PWD (the user's cwd
|
||||
before the launcher cd'd to the jolt repo)."
|
||||
(:require [jolt.deps :as deps]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(defn- project-dir [] (or (jolt.host/getenv "JOLT_PWD") "."))
|
||||
|
||||
;; Apply a resolved project's roots on top of the current (jolt-core) roots so app
|
||||
;; namespaces resolve while jolt.* stays loadable.
|
||||
(defn- apply-roots! [roots]
|
||||
(jolt.host/set-source-roots! (vec (distinct (concat roots (jolt.host/source-roots))))))
|
||||
|
||||
(defn- run-ns
|
||||
"Require ns-name and invoke its -main with the string app args."
|
||||
[ns-name app-args]
|
||||
(require (symbol ns-name))
|
||||
(if-let [mainv (ns-resolve (symbol ns-name) (symbol "-main"))]
|
||||
(apply (deref mainv) app-args)
|
||||
(throw (ex-info (str "namespace " ns-name " has no -main") {:ns ns-name}))))
|
||||
|
||||
;; main-opts is a vector like ["-m" "app.core"] (optionally trailing args). Apply
|
||||
;; it with the user-supplied extra args appended.
|
||||
(defn- apply-main-opts [main-opts extra-args]
|
||||
(cond
|
||||
(and (seq main-opts) (= "-m" (first main-opts)))
|
||||
(run-ns (second main-opts) (concat (drop 2 main-opts) extra-args))
|
||||
:else
|
||||
(throw (ex-info (str "unsupported :main-opts " (pr-str main-opts)) {}))))
|
||||
|
||||
(defn- parse-aliases [s] ; "-M:a:b" / ":a:b" -> [:a :b]
|
||||
(let [s (if (str/starts-with? s "-") (subs s 2) s)]
|
||||
(->> (str/split s #":") (remove str/blank?) (map keyword) vec)))
|
||||
|
||||
;; run [-m NS args… | FILE]
|
||||
(defn- cmd-run [more]
|
||||
(let [{:keys [roots]} (deps/resolve-project (project-dir))]
|
||||
(apply-roots! roots)
|
||||
(cond
|
||||
(= "-m" (first more)) (run-ns (second more) (drop 2 more))
|
||||
(seq more) (do (load-file (first more)) nil)
|
||||
:else (throw (ex-info "run needs -m NS or a FILE" {})))))
|
||||
|
||||
;; -M:alias… — resolve with the aliases, run their :main-opts
|
||||
(defn- cmd-M [arg more]
|
||||
(let [aliases (parse-aliases arg)
|
||||
{:keys [roots main-opts]} (deps/resolve-project (project-dir) aliases)]
|
||||
(apply-roots! roots)
|
||||
(if main-opts
|
||||
(apply-main-opts main-opts more)
|
||||
(throw (ex-info (str "alias(es) " (pr-str aliases) " have no :main-opts") {})))))
|
||||
|
||||
;; -A:alias… — add the aliases' paths/deps, then run the remaining argv as a command
|
||||
(defn- cmd-A [arg more]
|
||||
(let [aliases (parse-aliases arg)
|
||||
{:keys [roots]} (deps/resolve-project (project-dir) aliases)]
|
||||
(apply-roots! roots)
|
||||
(when (seq more) (run-ns (second more) (drop 2 more)))))
|
||||
|
||||
(defn- cmd-path []
|
||||
(let [{:keys [roots]} (deps/resolve-project (project-dir))]
|
||||
(println (str/join ":" roots))))
|
||||
|
||||
(defn- repl []
|
||||
(println ";; jolt repl — ^D to exit")
|
||||
(loop []
|
||||
(print "user=> ") (flush)
|
||||
(let [line (read-line)]
|
||||
(when line
|
||||
(try (println (pr-str (load-string line)))
|
||||
(catch :default e (println "error:" (ex-message e))))
|
||||
(recur)))))
|
||||
|
||||
;; A deps.edn :tasks entry: a string is a shell command; a map is {:main-opts …}.
|
||||
(defn- run-task [name more]
|
||||
(let [{:keys [roots tasks]} (deps/resolve-project (project-dir))
|
||||
task (get tasks (symbol name))]
|
||||
(cond
|
||||
(nil? task) (throw (ex-info (str "unknown command or task: " name) {:name name}))
|
||||
(string? task) (jolt.host/sh task)
|
||||
(map? task) (do (apply-roots! roots) (apply-main-opts (:main-opts task) more))
|
||||
:else (throw (ex-info (str "bad task " name) {})))))
|
||||
|
||||
(defn- usage []
|
||||
(println "usage: jolt <command> [args]")
|
||||
(println " run -m NS [args] resolve deps.edn, load NS, call its -main")
|
||||
(println " run FILE load a Clojure file")
|
||||
(println " -M:alias [args] run the alias's :main-opts")
|
||||
(println " -A:alias [args] add the alias's paths/deps")
|
||||
(println " repl start a line REPL")
|
||||
(println " path print the resolved source roots")
|
||||
(println " <task> run a deps.edn :tasks entry"))
|
||||
|
||||
(defn -main [& args]
|
||||
(let [[cmd & more] args]
|
||||
(cond
|
||||
(nil? cmd) (usage)
|
||||
(= cmd "run") (cmd-run more)
|
||||
(= cmd "repl") (repl)
|
||||
(= cmd "path") (cmd-path)
|
||||
(str/starts-with? cmd "-M") (cmd-M cmd more)
|
||||
(str/starts-with? cmd "-A") (cmd-A cmd more)
|
||||
(= cmd "-m") (cmd-run (cons "-m" more))
|
||||
:else (run-task cmd more))))
|
||||
Loading…
Add table
Add a link
Reference in a new issue