From a74be0ad1a98104fbeb1384f2991ba0d0c3d5233 Mon Sep 17 00:00:00 2001 From: Michiel Borkent Date: Thu, 12 Dec 2019 23:07:35 +0100 Subject: [PATCH] [#146] support --classpath / -cp and --main / -m (#150) --- .circleci/script/release | 6 +- .gitignore | 3 + README.md | 115 ++++++--- sci | 2 +- script/test | 13 +- src-bash/bbk | 219 ++++++++++++++++++ src/babashka/impl/classpath.clj | 62 +++++ src/babashka/main.clj | 62 +++-- .../src_for_classpath_test/env/env_ns.clj | 4 + .../babashka/src_for_classpath_test/foo.jar | Bin 0 -> 492 bytes .../src_for_classpath_test/my/impl.cljc | 5 + .../src_for_classpath_test/my/main.clj | 5 + .../src_for_classpath_test/my_script.bb | 4 + test/babashka/classpath_test.clj | 27 +++ test/babashka/main_test.clj | 3 +- 15 files changed, 470 insertions(+), 60 deletions(-) create mode 100755 src-bash/bbk create mode 100644 src/babashka/impl/classpath.clj create mode 100644 test-resources/babashka/src_for_classpath_test/env/env_ns.clj create mode 100644 test-resources/babashka/src_for_classpath_test/foo.jar create mode 100644 test-resources/babashka/src_for_classpath_test/my/impl.cljc create mode 100644 test-resources/babashka/src_for_classpath_test/my/main.clj create mode 100644 test-resources/babashka/src_for_classpath_test/my_script.bb create mode 100644 test/babashka/classpath_test.clj diff --git a/.circleci/script/release b/.circleci/script/release index fa085248..87d81d3a 100755 --- a/.circleci/script/release +++ b/.circleci/script/release @@ -3,14 +3,16 @@ rm -rf /tmp/release mkdir -p /tmp/release cp bb /tmp/release +cp src-bash/bbk /tmp/release + VERSION=$(cat resources/BABASHKA_VERSION) cd /tmp/release ## release binary as zip archive -zip "babashka-$VERSION-$BABASHKA_PLATFORM-amd64.zip" bb +zip "babashka-$VERSION-$BABASHKA_PLATFORM-amd64.zip" bb bbk ## cleanup -rm bb +rm bb bbk diff --git a/.gitignore b/.gitignore index 2a556140..a96a9d65 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ pom.xml.asc /bb .clj-kondo/.cache !java/src/babashka/impl/LockFix.class +!test-resources/babashka/src_for_classpath_test/foo.jar +.cpcache +deps.edn diff --git a/README.md b/README.md index 995b0eb6..da953657 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,83 @@ $ cat script.clj ("hello" "1" "2" "3") ``` +## Preloads + +The environment variable `BABASHKA_PRELOADS` allows to define code that will be +available in all subsequent usages of babashka. + +``` shellsession +BABASHKA_PRELOADS='(defn foo [x] (+ x 2))' +BABASHKA_PRELOADS=$BABASHKA_PRELOADS' (defn bar [x] (* x 2))' +export BABASHKA_PRELOADS +``` + +Note that you can concatenate multiple expressions. Now you can use these functions in babashka: + +``` shellsession +$ bb '(-> (foo *in*) bar)' <<< 1 +6 +``` + +You can also preload an entire file using `load-file`: + +``` shellsession +export BABASHKA_PRELOADS='(load-file "my_awesome_prelude.clj")' +``` + +Note that `*in*` is not available in preloads. + +## Classpath + +Babashka accepts a `--classpath` option that will be used to search for +namespaces and load them: + +``` clojure +$ cat src/my/namespace.clj +(ns my.namespace) +(defn -main [& _args] + (println "Hello from my namespace!")) + +$ bb --classpath src --main my.namespace +Hello from my namespace! +``` + +Note that you can use the `clojure` tool to produce classpaths and download dependencies: + +``` shellsession +$ cat deps.edn +{:deps + {my_gist_script + {:git/url "https://gist.github.com/borkdude/263b150607f3ce03630e114611a4ef42" + :sha "cfc761d06dfb30bb77166b45d439fe8fe54a31b8"}}} + + +$ CLASSPATH=$(clojure -Spath) +$ bb --classpath "$CLASSPATH" --main my-gist-script +Hello from gist script! +``` + +The `bbk` shell script is a thin wrapper around the `clojure` tool, so you can +use Babashka projects in a similar way: + +``` shellsession +$ bbk -m my-gist-script +Hello from gist script! +``` + +The script will call `bb` with the `--classpath` argument as a result of calling +`clojure`. + +If there is no `--classpath` argument, the `BABASHKA_CLASSPATH` environment +variable will be used if set: + +``` shellsession +$ export BABASHKA_CLASSPATH=$(clojure -Spath) +$ export BABASHKA_PRELOADS="(require '[my-gist-script])" +$ bb "(my-gist-script/-main)" +Hello from gist script! +``` + ## Parsing command line arguments Babashka ships with `clojure.tools.cli`: @@ -348,32 +425,6 @@ $ ./bb example.clj babashka doesn't support in-ns yet! ``` -## Preloads - -The environment variable `BABASHKA_PRELOADS` allows to define code that will be -available in all subsequent usages of babashka. - -``` shellsession -BABASHKA_PRELOADS='(defn foo [x] (+ x 2))' -BABASHKA_PRELOADS=$BABASHKA_PRELOADS' (defn bar [x] (* x 2))' -export BABASHKA_PRELOADS -``` - -Note that you can concatenate multiple expressions. Now you can use these functions in babashka: - -``` shellsession -$ bb '(-> (foo *in*) bar)' <<< 1 -6 -``` - -You can also preload an entire file using `load-file`: - -``` shellsession -export BABASHKA_PRELOADS='(load-file "my_awesome_prelude.clj")' -``` - -Note that `*in*` is not available in preloads. - ## Socket REPL Start the socket REPL like this: @@ -448,16 +499,8 @@ Differences with Clojure: - A subset of Java classes are supported. -- Only the `clojure.core`, `clojure.set` and `clojure.string` namespaces are - available from Clojure. - -- There is no classpath and no support for loading code from Maven/Clojars - dependencies. However, you can use `load-file` to load external code from - disk. - -- `require` does not load files; it only provides a way to create different - aliases for included namespaces, which makes it easier to make scripts - portable between the JVM and babashka. +- Only the `clojure.core`, `clojure.set`, `clojure.string` and `clojure.walk` + namespaces are available from Clojure. - Interpretation comes with overhead. Therefore tight loops are likely slower than in Clojure on the JVM. diff --git a/sci b/sci index 60778bfa..968734ac 160000 --- a/sci +++ b/sci @@ -1 +1 @@ -Subproject commit 60778bfaabf4fbed635d9490bac88b86fdfb0e4d +Subproject commit 968734ac60a2410f0b716df3829a5287f263e8f0 diff --git a/script/test b/script/test index e631756a..58f31d5f 100755 --- a/script/test +++ b/script/test @@ -1,6 +1,15 @@ #!/usr/bin/env bash set -eo pipefail -export BABASHKA_PRELOADS='(defn __bb__foo [] "foo") (defn __bb__bar [] "bar")' +BABASHKA_PRELOADS="" +BABASHKA_CLASSPATH="" +lein test "$@" -lein test +BABASHKA_PRELOADS='(defn __bb__foo [] "foo") (defn __bb__bar [] "bar")' +BABASHKA_PRELOADS_TEST=true +lein test :only babashka.main-test/preloads-test + +BABASHKA_PRELOADS="(require '[env-ns])" +BABASHKA_CLASSPATH_TEST=true +BABASHKA_CLASSPATH="test-resources/babashka/src_for_classpath_test/env" +lein test :only babashka.classpath-test/classpath-env-test diff --git a/src-bash/bbk b/src-bash/bbk new file mode 100755 index 00000000..38b6c36e --- /dev/null +++ b/src-bash/bbk @@ -0,0 +1,219 @@ +#!/usr/bin/env bash + +set -e + +function join { local d=$1; shift; echo -n "$1"; shift; printf "%s" "${@/#/$d}"; } + +# Extract opts +print_classpath=false +describe=false +verbose=false +force=false +repro=false +tree=false +pom=false +resolve_tags=false +help=false +resolve_aliases=() +classpath_aliases=() +main_aliases=() +all_aliases=() +while [ $# -gt 0 ] +do + case "$1" in + -J*) + shift + ;; + -R*) + resolve_aliases+=("${1:2}") + shift + ;; + -C*) + classpath_aliases+=("${1:2}") + shift + ;; + -O*) + shift + ;; + -M*) + main_aliases+=("${1:2}") + shift + ;; + -A*) + all_aliases+=("${1:2}") + shift + ;; + -Sdeps) + shift + deps_data="${1}" + shift + ;; + -Scp) + shift + force_cp="${1}" + shift + ;; + -Spath) + print_classpath=true + shift + ;; + -Sverbose) + verbose=true + shift + ;; + -Sdescribe) + describe=true + shift + ;; + -Sforce) + force=true + shift + ;; + -Srepro) + repro=true + shift + ;; + -Stree) + tree=true + shift + ;; + -Spom) + pom=true + shift + ;; + -Sresolve-tags) + resolve_tags=true + shift + ;; + -S*) + echo "Invalid option: $1" + exit 1 + ;; + -h|--help|"-?") + if [[ ${#main_aliases[@]} -gt 0 ]] || [[ ${#all_aliases[@]} -gt 0 ]]; then + break + else + help=true + shift + fi + ;; + *) + break + ;; + esac +done + +# Find clojure executable +set +e +CLOJURE_CMD=$(type -p clojure) +set -e +if [[ ! -n "$CLOJURE_CMD" ]]; then + >&2 echo "Couldn't find 'clojure'." + >&2 echo "You can launch Babashka directly using 'bb'." + >&2 echo "To use 'bbk', please ensure 'clojure' is installed and on" + >&2 echo "your path. See https://clojure.org/guides/getting_started" + exit 1 +fi + +if "$help"; then + cat <<-END +Usage: bbk [dep-opt*] [bb-opt*] [arg*] + +The bbk script is a runner for Babashka which ultimately constructs and +invokes a command-line of the form: + +bb --classpath classpath [bb-opt*] [*args] + + The dep-opts are used to build the classpath using the clojure tool: + -Ralias... Concatenated resolve-deps aliases, ex: -R:bench:1.9 + -Calias... Concatenated make-classpath aliases, ex: -C:dev + -Malias... Concatenated main option aliases, ex: -M:test + -Aalias... Concatenated aliases of any kind, ex: -A:dev:mem + -Sdeps EDN Deps data to use as the final deps file + -Spath Compute classpath and echo to stdout only + -Scp CP Do NOT compute or cache classpath, use this one instead + -Srepro Ignore the ~/.clojure/deps.edn config file + -Sforce Force recomputation of the classpath (don't use the cache) + -Spom Generate (or update existing) pom.xml with deps and paths + -Stree Print dependency tree + -Sresolve-tags Resolve git coordinate tags to shas and update deps.edn + -Sverbose Print important path info to console + -Sdescribe Print environment and command parsing info as data + + Additionally, for compatibility with clojure, -Jopt and -Oalias... dep-opts + are accepted but ignored. + +Babashka options: +END + bb -h | tail -n +9 + exit 0 +fi + +# Execute resolve-tags command +if "$resolve_tags"; then + "$CLOJURE_CMD" -Sresolve-tags + exit +fi + +clojure_args=() +if [[ -n "$deps_data" ]]; then + clojure_args+=("-Sdeps" "$deps_data") +fi +if [[ ${#resolve_aliases[@]} -gt 0 ]]; then + clojure_args+=("-R$(join '' ${resolve_aliases[@]})") +fi +if [[ ${#classpath_aliases[@]} -gt 0 ]]; then + clojure_args+=("-C$(join '' ${classpath_aliases[@]})") +fi +if [[ ${#main_aliases[@]} -gt 0 ]]; then + clojure_args+=("-M$(join '' ${main_aliases[@]})") +fi +if [[ ${#all_aliases[@]} -gt 0 ]]; then + clojure_args+=("-A$(join '' ${all_aliases[@]})") +fi +if "$repro"; then + clojure_args+=("-Srepro") +fi +if "$force"; then + clojure_args+=("-Sforce") +fi + +if "$pom"; then + if "$verbose"; then + clojure_args+=("-Sverbose") + fi + "$CLOJURE_CMD" "${clojure_args[@]}" -Spom +elif "$describe"; then + if "$verbose"; then + clojure_args+=("-Sverbose") + fi + "$CLOJURE_CMD" "${clojure_args[@]}" -Sdescribe +elif "$tree"; then + if "$verbose"; then + clojure_args+=("-Sverbose") + fi + "$CLOJURE_CMD" "${clojure_args[@]}" -Stree +else + set -f + if [[ -n "$force_cp" ]]; then + cp="$force_cp" + else + if "$verbose"; then + "$CLOJURE_CMD" "${clojure_args[@]}" -Sverbose -e nil + fi + cp=`"$CLOJURE_CMD" "${clojure_args[@]}" -Spath` + fi + if "$print_classpath"; then + echo $cp + else + if [[ ${#main_aliases[@]} -gt 0 ]] || [[ ${#all_aliases[@]} -gt 0 ]]; then + # Attempt to extract the main cache filename by parsing the output of -Sverbose + cp_file=`"$CLOJURE_CMD" "${clojure_args[@]}" -Sverbose -Spath | grep cp_file | cut -d = -f 2 | sed 's/^ *//g'` + main_file="${cp_file%.cp}.main" + fi + if [[ -e "$main_file" ]]; then + main_cache_opts=($(cat "$main_file")) + fi + exec bb --classpath "$cp" "${main_cache_opts[@]}" "$@" + fi +fi diff --git a/src/babashka/impl/classpath.clj b/src/babashka/impl/classpath.clj new file mode 100644 index 00000000..1b16611f --- /dev/null +++ b/src/babashka/impl/classpath.clj @@ -0,0 +1,62 @@ +(ns babashka.impl.classpath + {:no-doc true} + (:require [clojure.java.io :as io] + [clojure.string :as str]) + (:import [java.util.jar JarFile JarFile$JarFileEntry])) + +(set! *warn-on-reflection* true) + +(defprotocol IResourceResolver + (getResource [this path])) + +(deftype DirectoryResolver [path] + IResourceResolver + (getResource [this resource-path] + (let [f (io/file path resource-path)] + (when (.exists f) + (slurp f))))) + +(defn path-from-jar + [^java.io.File jar-file path] + (with-open [jar (JarFile. jar-file)] + (let [entries (enumeration-seq (.entries jar)) + entry (some (fn [^JarFile$JarFileEntry x] + (let [nm (.getName x)] + (when (and (not (.isDirectory x)) (= path nm)) + (slurp (.getInputStream jar x))))) entries)] + entry))) + +(deftype JarFileResolver [path] + IResourceResolver + (getResource [this resource-path] + (path-from-jar path resource-path))) + +(defn part->entry [part] + (if (str/ends-with? part ".jar") + (JarFileResolver. (io/file part)) + (DirectoryResolver. (io/file part)))) + +(deftype Loader [entries] + IResourceResolver + (getResource [this resource-path] + (some #(getResource % resource-path) entries))) + +(defn loader [^String classpath] + (let [parts (.split classpath (System/getProperty "path.separator")) + entries (map part->entry parts)] + (Loader. entries))) + +(defn source-for-namespace [loader namespace] + (let [ns-str (name namespace) + ^String ns-str (munge ns-str) + path (.replace ns-str "." (System/getProperty "file.separator")) + paths (map #(str path %) [".bb" ".clj" ".cljc"])] + (some #(getResource loader %) paths))) + +;;;; Scratch + +(comment + (def l (loader "src:/Users/borkdude/.m2/repository/cheshire/cheshire/5.9.0/cheshire-5.9.0.jar")) + (source-for-namespace l 'babashka.impl.cheshire) + (source-for-namespace l 'cheshire.core) + ) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index 73ae4579..392436b4 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -13,6 +13,7 @@ [babashka.impl.socket-repl :as socket-repl] [babashka.impl.tools.cli :refer [tools-cli-namespace]] [babashka.impl.utils :refer [eval-string]] + [babashka.impl.classpath :as cp] [babashka.wait :as wait] [clojure.edn :as edn] [clojure.java.io :as io] @@ -81,7 +82,15 @@ (let [options (rest options)] (recur (rest options) (assoc opts-map :expression (first options)))) - (if (some opts-map [:file :socket-repl :expression]) + ("--classpath", "-cp") + (let [options (rest options)] + (recur (rest options) + (assoc opts-map :classpath (first options)))) + ("--main", "-m") + (let [options (rest options)] + (recur (rest options) + (assoc opts-map :main (first options)))) + (if (some opts-map [:file :socket-repl :expression :main]) (assoc opts-map :command-line-args options) (if (and (not= \( (first (str/trim opt))) @@ -111,7 +120,10 @@ (defn print-version [] (println (str "babashka v"(str/trim (slurp (io/resource "BABASHKA_VERSION")))))) -(def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [--verbose] [ --stream ] ( -e | -f | --repl | --socket-repl [:] )") +(def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose] + [ ( --classpath | -cp ) ] [ ( --main | -m ) ] + ( -e | -f | --repl | --socket-repl [:] ) + [ arg* ]") (defn print-usage [] (println usage-string)) @@ -123,20 +135,21 @@ (println) (println "Options:") (println " - --help, -h or -?: print this help text. - --version: print the current version of babashka. - - -i: bind *in* to a lazy seq of lines from stdin. - -I: bind *in* to a lazy seq of EDN values from stdin. - -o: write lines to stdout. - -O: write EDN values to stdout. - --verbose: print entire stacktrace in case of exception. - --stream: stream over lines or EDN values from stdin. Combined with -i or -I *in* becomes a single value per iteration. - -e, --eval : evaluate an expression - -f, --file : evaluate a file - --repl: start REPL - --socket-repl: start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666). - --time: print execution time before exiting. + --help, -h or -? Print this help text. + --version Print the current version of babashka. + -i Bind *in* to a lazy seq of lines from stdin. + -I Bind *in* to a lazy seq of EDN values from stdin. + -o Write lines to stdout. + -O Write EDN values to stdout. + --verbose Print entire stacktrace in case of exception. + --stream Stream over lines or EDN values from stdin. Combined with -i or -I *in* becomes a single value per iteration. + -e, --eval Evaluate an expression. + -f, --file Evaluate a file. + -cp, --classpath Classpath to use. + -m, --main Call the -main function from namespace with args. + --repl Start REPL + --socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666). + --time Print execution time before exiting. If neither -e, -f, or --socket-repl are specified, then the first argument that is not parsed as a option is treated as a file if it exists, or as an expression otherwise. Everything after that is bound to *command-line-args*.")) @@ -193,7 +206,8 @@ Everything after that is bound to *command-line-args*.")) :help? :file :command-line-args :expression :stream? :time? :repl :socket-repl - :verbose?] :as _opts} + :verbose? :classpath + :main] :as _opts} (parse-opts args) read-next (fn [*in*] (if (pipe-signal-received?) @@ -208,6 +222,13 @@ Everything after that is bound to *command-line-args*.")) :else (edn/read *in*)))))) env (atom {}) + classpath (or classpath + (System/getenv "BABASHKA_CLASSPATH")) + loader (when classpath + (cp/loader classpath)) + load-fn (when classpath + (fn [{:keys [:namespace]}] + (cp/source-for-namespace loader namespace))) ctx {:aliases '{tools.cli 'clojure.tools.cli edn clojure.edn wait babashka.wait @@ -273,11 +294,16 @@ Everything after that is bound to *command-line-args*.")) File java.io.File String java.lang.String System java.lang.System - Thread java.lang.Thread}} + Thread java.lang.Thread} + :load-fn load-fn} ctx (update ctx :bindings assoc 'eval #(eval* ctx %) 'load-file #(load-file* ctx %)) ctx (addons/future ctx) _preloads (some-> (System/getenv "BABASHKA_PRELOADS") (str/trim) (eval-string ctx)) + expression (if main + (format "(ns user (:require [%1$s])) (apply %1$s/-main *command-line-args*)" + main) + expression) exit-code (or #_(binding [*out* *err*] diff --git a/test-resources/babashka/src_for_classpath_test/env/env_ns.clj b/test-resources/babashka/src_for_classpath_test/env/env_ns.clj new file mode 100644 index 00000000..bc600eec --- /dev/null +++ b/test-resources/babashka/src_for_classpath_test/env/env_ns.clj @@ -0,0 +1,4 @@ +(ns env-ns) + +(defn foo [] + "env!") diff --git a/test-resources/babashka/src_for_classpath_test/foo.jar b/test-resources/babashka/src_for_classpath_test/foo.jar new file mode 100644 index 0000000000000000000000000000000000000000..870949e7c0e6cdac776bb633be69a6f56b20763c GIT binary patch literal 492 zcmWIWW@Zs#;Nak3Fj&&<&wvCt8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1g>>wOP*EktjPXU^s$H$>;Qe|y^rmI#)B}z0oS8ZUC$1bP0P>M zOU}u