diff --git a/.gitmodules b/.gitmodules index 23e23e23..bfe304ad 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,3 +17,6 @@ [submodule "pods"] path = pods url = https://github.com/babashka/pods +[submodule "deps.clj"] + path = deps.clj + url = https://github.com/borkdude/deps.clj diff --git a/deps.clj b/deps.clj new file mode 160000 index 00000000..1418e6d7 --- /dev/null +++ b/deps.clj @@ -0,0 +1 @@ +Subproject commit 1418e6d704df5b3331a080a2cff240fdb60c82bf diff --git a/deps.edn b/deps.edn index c4de84d4..ee800051 100644 --- a/deps.edn +++ b/deps.edn @@ -6,6 +6,7 @@ "sci/src" "babashka.curl/src" "pods/src" "babashka.nrepl/src" "depstar/src" "process/src" + "deps.clj/src" "deps.clj/resources" "resources" "sci/resources"], :deps {org.clojure/clojure {:mvn/version "1.10.2-alpha2"}, org.clojure/tools.reader {:mvn/version "1.3.3"}, diff --git a/project.clj b/project.clj index c2554e04..f93d6804 100644 --- a/project.clj +++ b/project.clj @@ -8,7 +8,8 @@ :license {:name "Eclipse Public License 1.0" :url "http://opensource.org/licenses/eclipse-1.0.php"} :source-paths ["src" "sci/src" "babashka.curl/src" "pods/src" - "babashka.nrepl/src" "depstar/src" "process/src"] + "babashka.nrepl/src" "depstar/src" "process/src" + "deps.clj/src" "deps.clj/resources"] ;; for debugging Reflector.java code: ;; :java-source-paths ["sci/reflector/src-java"] :java-source-paths ["src-java"] diff --git a/src/babashka/impl/deps.clj b/src/babashka/impl/deps.clj new file mode 100644 index 00000000..a45387d6 --- /dev/null +++ b/src/babashka/impl/deps.clj @@ -0,0 +1,120 @@ +(ns babashka.impl.deps + (:require [babashka.impl.classpath :as cp] + [borkdude.deps :as deps] + [clojure.string :as str] + [sci.core :as sci] + [babashka.process :as p])) + +(def dns (sci/create-ns 'dns nil)) + +;;;; merge deps.edn files + +(defn- merge-or-replace + "If maps, merge, otherwise replace" + [& vals] + (when (some identity vals) + (reduce (fn [ret val] + (if (and (map? ret) (map? val)) + (merge ret val) + (or val ret))) + nil vals))) + +(defn merge-deps + "Merge multiple deps edn maps from left to right into a single deps edn map." + [deps-edn-maps] + (apply merge-with merge-or-replace (remove nil? deps-edn-maps))) + +(defn- merge-defaults [deps defaults] + (let [overriden (select-keys deps (keys defaults)) + overriden-deps (keys overriden) + defaults (select-keys defaults overriden-deps)] + (merge deps defaults))) + +(defn merge-default-deps [deps-map defaults] + (let [paths (into [[:deps]] + (map (fn [alias] + [:aliases alias]) + (keys (:aliases deps-map))))] + (reduce + (fn [acc path] + (update-in acc path merge-defaults defaults)) + deps-map + paths))) + +#_(merge-default-deps '{:deps {medley/medley nil} + :aliases {:foo {medley/medley nil}}} + '{medley/medley {:mvn/version "1.3.0"}}) + +;;;; end merge edn files + +;; We are optimizing for the 1-file script with deps scenario where people can +;; call this function to include e.g. {:deps {medley/medley +;; {:mvn/version "1.3.3"}}}. Optionally they can include aliases, to modify the +;; classpath. +(defn add-deps + "Takes deps edn map and optionally a map with :aliases (seq of + keywords) which will used to calculate classpath. The classpath is + then used to resolve dependencies in babashka." + ([deps-map] (add-deps deps-map nil)) + ([deps-map {:keys [:aliases]}] + (let [args ["-Spath" "-Sdeps" (str deps-map)] + args (cond-> args + aliases (conj (str "-A:" (str/join ":" aliases)))) + cp (with-out-str (apply deps/-main args))] + (cp/add-classpath cp)))) + +(defn clojure + "Starts clojure similar to CLI. Use `rlwrap bb` for `clj`-like invocation. + Invokes java with babashka.process/process for `-M`, `-X` and `-A` + and returns the associated record. Default options passed to + babashka.process/process are: + + {:in :inherit + :out :inherit + :err :inherit + :shutdown p/destroy-tree} + + which can be overriden with opts. + + Returns `nil` and prints to *out* for --help, -Spath, -Sdescribe and + -Stree. + + Examples: + + (-> (clojure '[-M -e (+ 1 2 3)] {:out :string}) deref :out) returns + \"6\n\". + + (-> @(clojure) :exit) starts a clojure REPL, waits for it + to finish and returns the exit code from the process." + ([] (clojure [])) + ([args] (clojure args nil)) + ([args opts] + (let [opts (merge {:in :inherit + :out :inherit + :err :inherit + :shutdown p/destroy-tree} + opts)] + (binding [*in* @sci/in + *out* @sci/out + *err* @sci/err + deps/*process-fn* (fn + ([cmd] (p/process cmd opts)) + ([cmd _] (p/process cmd opts))) + deps/*exit-fn* (fn + ([_]) + ([_exit-code msg] + (throw (Exception. msg))))] + (apply deps/-main (map str args)))))) + +;; (-> (clojure ["-Sdeps" edn "-M:foo"] {:out :inherit}) p/check) + +;; TODO: +;; (uberjar {:out "final.jar" :main 'foo.bar}) +;; (uberscript {:out "final.clj" :main 'foo.bar}) + +(def deps-namespace + {'add-deps (sci/copy-var add-deps dns) + 'clojure (sci/copy-var clojure dns) + 'merge-deps (sci/copy-var merge-deps dns) + ;; undocumented + 'merge-defaults (sci/copy-var merge-default-deps dns)}) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index 0c4896ce..8bc1ce9e 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -18,6 +18,7 @@ [babashka.impl.curl :refer [curl-namespace]] [babashka.impl.data :as data] [babashka.impl.datafy :refer [datafy-namespace]] + [babashka.impl.deps :as deps :refer [deps-namespace]] [babashka.impl.error-handler :refer [error-handler]] [babashka.impl.features :as features] [babashka.impl.pods :as pods] @@ -30,6 +31,7 @@ [babashka.impl.test :as t] [babashka.impl.tools.cli :refer [tools-cli-namespace]] [babashka.nrepl.server :as nrepl-server] + [babashka.process :as process] [babashka.wait :as wait] [clojure.edn :as edn] [clojure.java.io :as io] @@ -109,6 +111,8 @@ (let [opt (first options)] (case opt ("--") (assoc opts-map :command-line-args (next options)) + ("clojure") (assoc opts-map :clojure true + :opts (rest options)) ("--version") {:version true} ("--help" "-h" "-?") {:help? true} ("--verbose")(recur (next options) @@ -345,7 +349,8 @@ If neither -e, -f, or --socket-repl are specified, then the first argument that io clojure.java.io json cheshire.core curl babashka.curl - bencode bencode.core} + bencode bencode.core + deps babashka.deps} features/xml? (assoc 'xml 'clojure.data.xml) features/yaml? (assoc 'yaml 'clj-yaml.core) features/jdbc? (assoc 'jdbc 'next.jdbc) @@ -391,7 +396,8 @@ If neither -e, -f, or --socket-repl are specified, then the first argument that 'clojure.datafy datafy-namespace 'clojure.core.protocols protocols-namespace 'clojure.core.server clojure-core-server - 'babashka.process process-namespace} + 'babashka.process process-namespace + 'babashka.deps deps-namespace} features/xml? (assoc 'clojure.data.xml @(resolve 'babashka.impl.xml/xml-namespace)) features/yaml? (assoc 'clj-yaml.core @(resolve 'babashka.impl.yaml/yaml-namespace) 'flatland.ordered.map @(resolve 'babashka.impl.ordered/ordered-map-ns)) @@ -476,8 +482,12 @@ If neither -e, -f, or --socket-repl are specified, then the first argument that :repl :socket-repl :nrepl :verbose? :classpath :main :uberscript :describe? - :jar :uberjar] :as _opts} + :jar :uberjar :clojure] :as opts} (parse-opts args) + _ (when clojure + (if-let [proc (deps/clojure (:opts opts))] + (-> @proc :exit (System/exit)) + (System/exit 0))) _ (when verbose? (vreset! common/verbose? true)) _ (do ;; set properties (when main (System/setProperty "babashka.main" main)) diff --git a/test/babashka/deps_test.clj b/test/babashka/deps_test.clj new file mode 100644 index 00000000..c7c5d448 --- /dev/null +++ b/test/babashka/deps_test.clj @@ -0,0 +1,59 @@ +(ns babashka.deps-test + (:require + [babashka.test-utils :as test-utils] + [clojure.edn :as edn] + [clojure.test :as test :refer [deftest is testing]])) + +(defn bb [& args] + (edn/read-string + {:readers *data-readers* + :eof nil} + (apply test-utils/bb nil (map str args)))) + +(deftest dependency-test (is (= #{:a :c :b} (bb " +(require '[babashka.deps :as deps]) + +(deps/add-deps '{:deps {com.stuartsierra/dependency {:mvn/version \"1.0.0\"}}}) + +(require '[com.stuartsierra.dependency :as dep]) + +(def g1 (-> (dep/graph) + (dep/depend :b :a) + (dep/depend :c :b) + (dep/depend :c :a) + (dep/depend :d :c))) + +(dep/transitive-dependencies g1 :d) +")))) + +(deftest clojure-test + (testing "-Stree prints to *out*" + (is (true? (bb " +(require '[babashka.deps :as deps]) +(require '[clojure.string :as str]) +(str/includes? + (with-out-str (babashka.deps/clojure [\"-Stree\"])) + \"org.clojure/clojure\") +")))) + (testing "-P does not exit babashka script" + (is (true? (bb " +(require '[babashka.deps :as deps]) +(require '[clojure.string :as str]) +(babashka.deps/clojure [\"-P\"]) +true +")))) + (is (= "6\n" (bb " +(require '[babashka.deps :as deps]) +(require '[babashka.process :as p]) + +(-> (babashka.deps/clojure [\"-M\" \"-e\" \"(+ 1 2 3)\"] {:out :string}) + (p/check) + :out) +"))) + (when-not test-utils/native? + (is (thrown-with-msg? Exception #"Option changed" (bb " +(require '[babashka.deps :as deps]) +(babashka.deps/clojure [\"-Sresolve-tags\"]) +")))) + (is (true? (bb " +(= 5 (:exit @(babashka.deps/clojure [] {:in \"(System/exit 5)\" :out :string})))"))))