├── cljfmt.edn ├── test-resources ├── local-tool │ ├── deps.edn │ ├── bb.edn │ └── src │ │ └── bbin │ │ └── foo.clj ├── hello.jar ├── hello2.jar ├── git-wrapper.bat ├── hello-no-main-class.jar ├── id_ed25519_bbin_test_lib_private.pub ├── id_ed25519_bbin_test_lib_private └── git-wrapper ├── .gitattributes ├── bbin.bat ├── docs ├── faq.md ├── auto-completion.md ├── design.md ├── packaging.md └── installation.md ├── src └── babashka │ └── bbin │ ├── protocols.clj │ ├── meta.clj │ ├── specs.clj │ ├── scripts │ ├── local_dir.clj │ ├── git_dir.clj │ ├── http_file.clj │ ├── local_file.clj │ ├── local_jar.clj │ ├── http_jar.clj │ ├── maven_jar.clj │ └── common.clj │ ├── dirs.clj │ ├── cli.clj │ ├── git.clj │ ├── scripts.clj │ ├── deps.clj │ ├── migrate.clj │ └── util.clj ├── .github ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── deps.edn ├── LICENSE ├── dev └── babashka │ └── bbin │ ├── dev.clj │ └── gen_script.clj ├── test └── babashka │ └── bbin │ ├── scripts │ ├── http_file_test.clj │ ├── http_jar_test.clj │ ├── maven_jar_test.clj │ ├── local_jar_test.clj │ ├── local_dir_test.clj │ ├── git_dir_test.clj │ └── local_file_test.clj │ ├── cli_test.clj │ ├── scripts_test.clj │ ├── test_util.clj │ ├── util_test.clj │ └── migrate_test.clj ├── templates ├── docs │ └── installation.template.md └── README.template.md ├── bb.edn ├── README.md └── CHANGELOG.md /cljfmt.edn: -------------------------------------------------------------------------------- 1 | {:sort-ns-references? true} 2 | -------------------------------------------------------------------------------- /test-resources/local-tool/deps.edn: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test-resources/local-tool/bb.edn: -------------------------------------------------------------------------------- 1 | {:bbin/bin {foo {}}} 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | test-resources/id_ed25519_bbin_test_lib_private eol=lf 2 | -------------------------------------------------------------------------------- /bbin.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set ARGS=%* 3 | set SCRIPT=%~dp0bbin 4 | bb -f %SCRIPT% %ARGS% 5 | -------------------------------------------------------------------------------- /test-resources/hello.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babashka/bbin/HEAD/test-resources/hello.jar -------------------------------------------------------------------------------- /test-resources/hello2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babashka/bbin/HEAD/test-resources/hello2.jar -------------------------------------------------------------------------------- /test-resources/git-wrapper.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set ARGS=%* 3 | set SCRIPT=%~dp0git-wrapper 4 | bb -f %SCRIPT% %ARGS% 5 | -------------------------------------------------------------------------------- /test-resources/hello-no-main-class.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/babashka/bbin/HEAD/test-resources/hello-no-main-class.jar -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | See the [Packaging](packaging.md) page for information about how to package your repository for use with bbin. -------------------------------------------------------------------------------- /test-resources/id_ed25519_bbin_test_lib_private.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDbmyEyKAcazRktb6jgtBqABFjh9tT89f/CnEjtA8H46 contact@radsmith.com 2 | -------------------------------------------------------------------------------- /src/babashka/bbin/protocols.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.protocols) 2 | 3 | (defprotocol Script 4 | (install [script]) 5 | (upgrade [script]) 6 | (uninstall [script])) 7 | -------------------------------------------------------------------------------- /test-resources/local-tool/src/bbin/foo.clj: -------------------------------------------------------------------------------- 1 | (ns bbin.foo) 2 | 3 | (defn k 4 | "just `keys`" 5 | [m] 6 | (prn (keys m))) 7 | 8 | (defn v 9 | "just `vals`" 10 | [m] 11 | (prn (vals m))) 12 | -------------------------------------------------------------------------------- /src/babashka/bbin/meta.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.meta) 2 | 3 | ;; This is a fallback if the var hasn't already been set earlier by a build 4 | ;; script, such as when bbin is called as a library instead of a script. 5 | (defonce min-bb-version :version-not-set) 6 | (defonce version :version-not-set) 7 | -------------------------------------------------------------------------------- /src/babashka/bbin/specs.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.specs 2 | (:require [clojure.spec.alpha :as s])) 3 | 4 | (s/def ::ns-default symbol?) 5 | (s/def ::main-opts (s/coll-of string?)) 6 | (s/def ::script-config (s/keys :opt-un [::main-opts ::ns-default])) 7 | (s/def :bbin/bin (s/map-of symbol? ::script-config)) 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Please answer the following questions and leave the below in as part of your PR. 2 | 3 | - [ ] This PR corresponds to an [issue with a clear problem statement](https://github.com/babashka/babashka/blob/master/doc/dev.md#start-with-an-issue-before-writing-code). 4 | - ADD LINK TO ISSUE HERE 5 | -------------------------------------------------------------------------------- /docs/auto-completion.md: -------------------------------------------------------------------------------- 1 | # Auto-Completion 2 | 3 | ### ZSH 4 | 5 | Add this to `~/.zshrc`: 6 | 7 | ```shell 8 | function _bbin() { _arguments "1: :($(bbin commands))" } 9 | compdef _bbin bbin 10 | ``` 11 | 12 | ### BASH 13 | 14 | Add this to `~/.bashrc`: 15 | 16 | ```shell 17 | complete -W "$(bbin commands)" bbin 18 | ``` 19 | -------------------------------------------------------------------------------- /src/babashka/bbin/scripts/local_dir.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.local-dir 2 | (:require [babashka.bbin.protocols :as p] 3 | [babashka.bbin.scripts.common :as common])) 4 | 5 | (defrecord LocalDir [cli-opts summary] 6 | p/Script 7 | (install [_] 8 | (common/install-deps-git-or-local cli-opts summary)) 9 | 10 | (upgrade [_] 11 | (throw (ex-info "Not implemented" {}))) 12 | 13 | (uninstall [_] 14 | (common/delete-files cli-opts))) 15 | -------------------------------------------------------------------------------- /test-resources/id_ed25519_bbin_test_lib_private: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACA25shMigHGs0ZLW+o4LQagARY4fbU/PX/wpxI7QPB+OgAAAJiPXqIWj16i 4 | FgAAAAtzc2gtZWQyNTUxOQAAACA25shMigHGs0ZLW+o4LQagARY4fbU/PX/wpxI7QPB+Og 5 | AAAEAJAjE5MtVw6AQpbhqiCujtTwrrn6T34y2meiVwEekyrzbmyEyKAcazRktb6jgtBqAB 6 | Fjh9tT89f/CnEjtA8H46AAAAFGNvbnRhY3RAcmFkc21pdGguY29tAQ== 7 | -----END OPENSSH PRIVATE KEY----- 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .calva/output-window/ 2 | .classpath 3 | .clj-kondo/* 4 | !.clj-kondo/config.edn 5 | .cpcache 6 | .eastwood 7 | .factorypath 8 | .hg/ 9 | .hgignore 10 | .java-version 11 | .lein-* 12 | .lsp/.cache 13 | .lsp/sqlite.db 14 | .nrepl-history 15 | .nrepl-port 16 | .project 17 | .rebel_readline_history 18 | .settings 19 | .socket-repl-port 20 | .sw* 21 | .vscode 22 | *.class 23 | *.jar 24 | !test-resources/hello.jar 25 | *.swp 26 | *~ 27 | /checkouts 28 | /classes 29 | /target 30 | -------------------------------------------------------------------------------- /src/babashka/bbin/scripts/git_dir.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.git-dir 2 | (:require [babashka.bbin.protocols :as p] 3 | [babashka.bbin.scripts.common :as common])) 4 | 5 | (defrecord GitDir [cli-opts summary coords] 6 | p/Script 7 | (install [_] 8 | (common/install-deps-git-or-local cli-opts summary)) 9 | 10 | (upgrade [_] 11 | (throw (ex-info "Not implemented" {:cli-opts cli-opts 12 | :summary summary 13 | :coords coords}))) 14 | 15 | (uninstall [_] 16 | (common/delete-files cli-opts))) 17 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {babashka/fs {:mvn/version "0.3.17"} 3 | babashka/process {:mvn/version "0.4.14"} 4 | com.taoensso/timbre {:mvn/version "5.2.1"} 5 | expound/expound {:mvn/version "0.9.0"} 6 | fipp/fipp {:mvn/version "0.6.26"} 7 | org.babashka/cli {:mvn/version "0.6.43"} 8 | org.babashka/http-client {:mvn/version "0.1.8"} 9 | org.babashka/json {:mvn/version "0.1.1"} 10 | org.clojure/tools.gitlibs {:mvn/version "2.4.181"} 11 | selmer/selmer {:mvn/version "1.12.55"} 12 | version-clj/version-clj {:mvn/version "2.0.2"}} 13 | :aliases {:neil {:project {:name babashka/bbin 14 | :version "0.2.5"}}}} 15 | -------------------------------------------------------------------------------- /test-resources/git-wrapper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (require '[babashka.fs :as fs] 4 | '[babashka.process :as p]) 5 | 6 | (def key-path 7 | (str (fs/file (fs/parent *file*) "id_ed25519_bbin_test_lib_private"))) 8 | 9 | (def allow-list 10 | #{"git@bitbucket.org:radsmith/bbin-test-lib-private.git"}) 11 | 12 | (def git-config-opts 13 | (str "core.sshCommand=ssh -o StrictHostKeyChecking=no -i '" key-path "'")) 14 | 15 | (if (and (#{"clone" "fetch"} (first *command-line-args*)) 16 | (some allow-list *command-line-args*)) 17 | (do 18 | (when-not (fs/windows?) 19 | (fs/set-posix-file-permissions key-path "rw-------")) 20 | (apply p/exec (concat ["git" (first *command-line-args*) "-c" git-config-opts] 21 | (rest *command-line-args*)))) 22 | (apply p/exec (concat ["git"] *command-line-args*))) 23 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Design Docs 2 | 3 | ## Goals 4 | 5 | - Provide a CLI that's easy to remember and type 6 | - Allow project authors to provide default script names 7 | - When possible, follow existing patterns from `tools.deps` for managing dependencies 8 | 9 | 10 | ## Basic Flows 11 | ### `install` 12 | - Determine the type of install (http, maven, etc) 13 | - Build the script from the corresponding template 14 | - Selmer template rendering of a bash script (or a babashka script on Windows) 15 | - Script also includes commented metadata for use by `ls` 16 | - Write script out to bbin's bin directory (~/.local/bin) 17 | - On Windows, a batch file is also written to provide a command-line executable 18 | 19 | ### `uninstall` 20 | - Delete the script created by install 21 | - On Windows, also delete the batch file 22 | 23 | ### `ls` 24 | - Find all files in the bin directory that contain metadata 25 | - pprint a map of script -> metadata 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Radford Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/babashka/bbin/scripts/http_file.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.http-file 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.protocols :as p] 4 | [babashka.bbin.scripts.common :as common] 5 | [babashka.fs :as fs])) 6 | 7 | (defrecord HttpFile [cli-opts coords] 8 | p/Script 9 | (install [_] 10 | (let [http-url (:script/lib cli-opts) 11 | script-deps {:bbin/url http-url} 12 | header {:coords script-deps} 13 | script-name (or (:as cli-opts) (common/http-url->script-name http-url)) 14 | script-contents (-> (slurp (:bbin/url script-deps)) 15 | (common/insert-script-header header)) 16 | script-file (fs/canonicalize (fs/file (dirs/bin-dir cli-opts) script-name) 17 | {:nofollow-links true})] 18 | (common/install-script script-name header script-file script-contents cli-opts))) 19 | 20 | (upgrade [_] 21 | (let [cli-opts' (merge (select-keys cli-opts [:edn]) 22 | {:script/lib (:bbin/url coords)})] 23 | (p/install (map->HttpFile {:cli-opts cli-opts' 24 | :coords coords})))) 25 | 26 | (uninstall [_] 27 | (common/delete-files cli-opts))) 28 | -------------------------------------------------------------------------------- /docs/packaging.md: -------------------------------------------------------------------------------- 1 | # Packaging for bbin 2 | 3 | `bbin` can install scripts and projects from various sources. This guide explains how to package your repo for seamless installation with bbin. 4 | 5 | ## Installation Sources 6 | 7 | bbin supports installing packages from: 8 | 9 | - Git repositories 10 | - HTTP URLs 11 | - Maven artifacts 12 | - Local filesystem 13 | 14 | There is no central registry for `bbin` - it leverages existing infrastructure like GitHub and Maven repositories. 15 | 16 | ## Configuration 17 | 18 | To properly package your repo for bbin, include a configuration in your `bb.edn` file that specifies the binaries to install: 19 | 20 | ```clojure 21 | {:bbin/bin {cmd-name {:main-opts ["-m" "cmd.core"]}}} 22 | ``` 23 | 24 | This configuration defines: 25 | - The binary name (`neil` in this example) 26 | - How to execute it (with the main namespace to run being `cmd.core`) 27 | 28 | ## Installation Methods 29 | 30 | - **For projects with `deps.edn`**: Install using `--git/url`, `--mvn/version`, or `--local/root` options 31 | - GitHub projects can use the shorthand syntax: `io.github.user/repo` 32 | 33 | - **For standalone `.clj` scripts**: Host on GitHub and install from the raw HTTP URL 34 | - Scripts can be installed from any local path or HTTP URL ending in `.clj` 35 | -------------------------------------------------------------------------------- /dev/babashka/bbin/dev.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.dev 2 | (:require [babashka.process :refer [sh]] 3 | [clojure.core.async :refer [script-name file-path)) 15 | script-contents (-> (slurp file-path) 16 | (common/insert-script-header header)) 17 | script-file (fs/canonicalize (fs/file (dirs/bin-dir cli-opts) script-name) 18 | {:nofollow-links true})] 19 | (common/install-script script-name header script-file script-contents cli-opts))) 20 | 21 | (upgrade [_] 22 | (let [cli-opts' (merge (select-keys cli-opts [:edn]) 23 | {:script/lib (str/replace (:bbin/url coords) #"^file://" "")})] 24 | (p/install (map->LocalFile {:cli-opts cli-opts' 25 | :coords coords})))) 26 | 27 | (uninstall [_] 28 | (common/delete-files cli-opts))) 29 | -------------------------------------------------------------------------------- /test/babashka/bbin/scripts/http_file_test.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.http-file-test 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.test-util :as tu] 4 | [babashka.fs :as fs] 5 | [clojure.test :refer [deftest is testing use-fixtures]])) 6 | 7 | (use-fixtures :once 8 | (tu/bbin-dirs-fixture) 9 | (tu/http-server-fixture)) 10 | 11 | (def hello-script-url 12 | (format "http://localhost:%d/hello.clj" tu/http-port)) 13 | 14 | (deftest install-from-url-clj-test 15 | (testing "install https://*.clj" 16 | (tu/reset-test-dir) 17 | (dirs/ensure-bbin-dirs {}) 18 | (let [cli-opts {:script/lib hello-script-url} 19 | script-file (fs/file tu/http-public-dir "hello.clj") 20 | _ (spit script-file "#!/usr/bin/env bb\n(println \"Hello world\")") 21 | out (tu/run-install cli-opts)] 22 | (is (= {:coords {:bbin/url hello-script-url}} out)) 23 | (is (fs/exists? (fs/file (dirs/bin-dir nil) "hello"))) 24 | (is (= "Hello world" (tu/run-bin-script :hello))) 25 | (is (= {'hello {:coords {:bbin/url hello-script-url}}} (tu/run-ls)))))) 26 | 27 | (deftest upgrade-http-file-test 28 | (testing "upgrade (http file)" 29 | (tu/reset-test-dir) 30 | (dirs/ensure-bbin-dirs {}) 31 | (let [script-file (fs/file tu/http-public-dir "hello.clj")] 32 | (spit script-file "#!/usr/bin/env bb\n(println \"Hello world\")") 33 | (tu/run-install {:script/lib hello-script-url}) 34 | (is (= "Hello world" (tu/run-bin-script :hello))) 35 | (spit script-file "#!/usr/bin/env bb\n(println \"Upgraded\")") 36 | (tu/run-upgrade {:script/lib "hello"}) 37 | (is (= "Upgraded" (tu/run-bin-script :hello)))))) 38 | -------------------------------------------------------------------------------- /test/babashka/bbin/scripts/http_jar_test.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.http-jar-test 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.test-util :as tu] 4 | [babashka.fs :as fs] 5 | [clojure.test :refer [deftest is testing use-fixtures]])) 6 | 7 | (use-fixtures :once 8 | (tu/bbin-dirs-fixture) 9 | (tu/http-server-fixture)) 10 | 11 | (def hello-jar-url 12 | (format "http://localhost:%d/hello.jar" tu/http-port)) 13 | 14 | (deftest install-from-url-jar-test 15 | (testing "install https://*.jar" 16 | (tu/reset-test-dir) 17 | (dirs/ensure-bbin-dirs {}) 18 | (let [cli-opts {:script/lib hello-jar-url} 19 | _ (fs/copy (fs/file "test-resources" "hello.jar") 20 | (fs/file tu/http-public-dir "hello.jar")) 21 | out (tu/run-install cli-opts)] 22 | (is (= {:coords {:bbin/url hello-jar-url}} out)) 23 | (is (= "Hello JAR" (tu/run-bin-script :hello))))) 24 | (testing "install https://*.jar (reinstall)" 25 | (let [cli-opts {:script/lib hello-jar-url} 26 | out (tu/run-install cli-opts)] 27 | (is (= {:coords {:bbin/url hello-jar-url}} out)) 28 | (is (= "Hello JAR" (tu/run-bin-script :hello))) 29 | (is (= {'hello {:coords {:bbin/url hello-jar-url}}} (tu/run-ls)))))) 30 | 31 | (deftest upgrade-http-jar-test 32 | (testing "upgrade (http jar)" 33 | (tu/reset-test-dir) 34 | (dirs/ensure-bbin-dirs {}) 35 | (fs/copy (fs/file "test-resources" "hello.jar") 36 | (fs/file tu/http-public-dir "hello.jar")) 37 | (tu/run-install {:script/lib hello-jar-url}) 38 | (is (= "Hello JAR" (tu/run-bin-script :hello))) 39 | (fs/copy (fs/file "test-resources" "hello2.jar") 40 | (fs/file tu/http-public-dir "hello.jar") 41 | {:replace-existing true}) 42 | (tu/run-upgrade {:script/lib "hello"}) 43 | (is (= "Hello JAR 2" (tu/run-bin-script :hello))))) 44 | -------------------------------------------------------------------------------- /test/babashka/bbin/scripts/maven_jar_test.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.maven-jar-test 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.test-util :as tu] 4 | [babashka.fs :as fs] 5 | [clojure.string :as str] 6 | [clojure.test :refer [deftest is testing use-fixtures]])) 7 | 8 | (use-fixtures :once (tu/bbin-dirs-fixture)) 9 | 10 | (def maven-lib 11 | {:lib 'org.babashka/http-server 12 | :coords {:mvn/version "0.1.11"}}) 13 | 14 | (def help-text 15 | "Serves static assets using web server.") 16 | 17 | (deftest install-from-mvn-version-test 18 | (testing "install */* --mvn/version *" 19 | (tu/reset-test-dir) 20 | (dirs/ensure-bbin-dirs {}) 21 | (let [cli-opts {:script/lib (str (:lib maven-lib)) 22 | :mvn/version (-> maven-lib :coords :mvn/version)} 23 | out (tu/run-install cli-opts)] 24 | (is (= maven-lib out)) 25 | (is (fs/exists? (fs/file (dirs/bin-dir nil) (name (:lib maven-lib))))) 26 | (is (str/starts-with? (tu/run-bin-script (:lib maven-lib) "--help") 27 | help-text)) 28 | (is (= `{~'http-server ~maven-lib} (tu/run-ls)))))) 29 | 30 | (def upgraded-lib 31 | (assoc-in maven-lib [:coords :mvn/version] "0.1.14")) 32 | 33 | (deftest upgrade-maven-jar-test 34 | (testing "upgrade (maven jar)" 35 | (tu/reset-test-dir) 36 | (dirs/ensure-bbin-dirs {}) 37 | (let [out (tu/run-install {:script/lib (str (:lib maven-lib)) 38 | :mvn/version (-> maven-lib :coords :mvn/version)}) 39 | out2 (tu/run-upgrade {:script/lib "http-server"})] 40 | (is (= maven-lib out)) 41 | (is (= upgraded-lib out2)) 42 | (is (fs/exists? (fs/file (dirs/bin-dir nil) (name (:lib upgraded-lib))))) 43 | (is (str/starts-with? (tu/run-bin-script (:lib upgraded-lib) "--help") 44 | help-text)) 45 | (is (= `{~'http-server ~upgraded-lib} (tu/run-ls)))))) 46 | -------------------------------------------------------------------------------- /test/babashka/bbin/scripts/local_jar_test.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.local-jar-test 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.test-util :as tu] 4 | [babashka.fs :as fs] 5 | [clojure.test :refer [deftest is testing use-fixtures]]) 6 | (:import (clojure.lang ExceptionInfo))) 7 | 8 | (use-fixtures :once (tu/bbin-dirs-fixture)) 9 | 10 | (deftest install-from-local-root-jar-test 11 | (testing "install ./*.jar" 12 | (tu/reset-test-dir) 13 | (dirs/ensure-bbin-dirs {}) 14 | (let [script-jar (str "test-resources" fs/file-separator "hello.jar") 15 | cli-opts {:script/lib script-jar} 16 | out (tu/run-install cli-opts)] 17 | (is (= {:coords {:bbin/url (str "file://" (fs/canonicalize script-jar {:nofollow-links true}))}} 18 | out)) 19 | (is (= "Hello JAR" (tu/run-bin-script :hello))))) 20 | (testing "install ./*.jar (no main class)" 21 | (tu/reset-test-dir) 22 | (dirs/ensure-bbin-dirs {}) 23 | (let [script-jar (str "test-resources" fs/file-separator "hello-no-main-class.jar") 24 | cli-opts {:script/lib script-jar}] 25 | (is (thrown-with-msg? ExceptionInfo #"jar has no Main-Class" (tu/run-install cli-opts))) 26 | (is (not (fs/exists? (fs/file (dirs/bin-dir nil) "hello-no-main-class")))) 27 | (is (not (fs/exists? (fs/file (dirs/jars-dir nil) "hello-no-main-class.jar"))))))) 28 | 29 | (deftest upgrade-local-jar-test 30 | (testing "upgrade (local jar)" 31 | (tu/reset-test-dir) 32 | (dirs/ensure-bbin-dirs {}) 33 | (let [script-jar (str "test-resources" fs/file-separator "hello.jar") 34 | upgraded-jar (str "test-resources" fs/file-separator "hello2.jar") 35 | tmp-jar (fs/file tu/test-dir "tmp" "hello.jar")] 36 | (fs/create-dirs (fs/file tu/test-dir "tmp")) 37 | (testing "before upgrade" 38 | (fs/copy script-jar tmp-jar) 39 | (tu/run-install {:script/lib (str tmp-jar)}) 40 | (is (= "Hello JAR" (tu/run-bin-script :hello)))) 41 | (testing "after upgrade" 42 | (fs/copy upgraded-jar tmp-jar {:replace-existing true}) 43 | (tu/run-upgrade {:script/lib "hello"}) 44 | (is (= "Hello JAR 2" (tu/run-bin-script :hello))))))) 45 | -------------------------------------------------------------------------------- /test/babashka/bbin/cli_test.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.cli-test 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.meta :as meta] 4 | [babashka.bbin.test-util :refer [bbin bbin-dirs-fixture]] 5 | [babashka.bbin.util :as util] 6 | [clojure.string :as str] 7 | [clojure.test :refer [deftest is testing use-fixtures]])) 8 | 9 | (use-fixtures :once (bbin-dirs-fixture)) 10 | 11 | (deftest bin-test 12 | (testing "bin" 13 | (let [out (bbin ["bin"] :out :string)] 14 | (is (= (str (dirs/bin-dir nil)) out))))) 15 | 16 | (deftest help-test 17 | (doseq [args [["help"] ["install"] ["uninstall"]]] 18 | (let [out (bbin args :out :string) 19 | [version _ usage] (str/split-lines out)] 20 | (is (= version (str "Version: " meta/version))) 21 | (is (= usage "Usage: bbin "))))) 22 | 23 | (deftest version-test 24 | (let [out (bbin ["--version"] :out :string)] 25 | (is (str/starts-with? out (str "bbin " meta/version))))) 26 | 27 | (def expected-commands 28 | (cond-> 29 | #{"commands" 30 | "help" 31 | "install" 32 | "uninstall" 33 | "migrate" 34 | "version" 35 | "ls" 36 | "bin"} 37 | (util/upgrade-enabled?) (conj "upgrade"))) 38 | 39 | (deftest commands-test 40 | (let [out (bbin ["commands"] :out :string) 41 | commands (set (str/split out #" "))] 42 | (is (= expected-commands commands)))) 43 | 44 | (deftest install-test 45 | (let [calls (atom []) 46 | args ["install" "io.github.rads/watch"]] 47 | (bbin args 48 | :out :string 49 | :install-fn #(swap! calls conj %)) 50 | (is (= [{:script/lib "io.github.rads/watch"}] 51 | @calls)))) 52 | 53 | (deftest uninstall-test 54 | (let [calls (atom []) 55 | args ["uninstall" "watch"]] 56 | (bbin args 57 | :out :string 58 | :uninstall-fn #(swap! calls conj %)) 59 | (is (= [{:script/lib "watch"}] 60 | @calls)))) 61 | 62 | (deftest migrate-test 63 | (let [calls (atom []) 64 | args ["migrate"]] 65 | (bbin args 66 | :out :string 67 | :migrate-fn #(swap! calls conj %)) 68 | (is (= [{}] @calls)))) 69 | 70 | (comment 71 | (clojure.test/run-tests)) 72 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | bb-run-test-linux: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | bb-version: [1.3.190, latest] 10 | steps: 11 | - name: Check out repository code 12 | uses: actions/checkout@v3 13 | - name: Install clojure tools 14 | uses: DeLaGuardo/setup-clojure@9.4 15 | with: 16 | bb: ${{ matrix.bb-version }} 17 | - run: bb run test 18 | 19 | bb-run-test-macos: 20 | runs-on: macos-latest 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | bb-version: [1.3.190, latest] 25 | steps: 26 | - name: Check out repository code 27 | uses: actions/checkout@v3 28 | - name: Install clojure tools 29 | uses: DeLaGuardo/setup-clojure@9.4 30 | with: 31 | bb: ${{ matrix.bb-version }} 32 | - run: bb run test 33 | 34 | bb-run-test-windows: 35 | runs-on: windows-latest 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | bb-version: [latest] 40 | steps: 41 | - name: Check out repository code 42 | uses: actions/checkout@v3 43 | - name: Install clojure tools 44 | uses: DeLaGuardo/setup-clojure@9.4 45 | with: 46 | bb: ${{ matrix.bb-version }} 47 | - name: bb test 48 | shell: bash 49 | run: | 50 | curl -sLO https://github.com/babashka/babashka-dev-builds/releases/download/v1.3.191-SNAPSHOT/babashka-1.3.191-SNAPSHOT-windows-amd64.zip 51 | unzip babashka-1.3.191-SNAPSHOT-windows-amd64.zip 52 | # overwrite installed bb with custom 53 | cp bb.exe "$(which bb)" 54 | - name: bb test 55 | run: | 56 | bb run test 57 | 58 | bb-run-lint: 59 | runs-on: ubuntu-latest 60 | strategy: 61 | fail-fast: false 62 | matrix: 63 | bb-version: [1.3.190, latest] 64 | steps: 65 | - name: Check out repository code 66 | uses: actions/checkout@v3 67 | - name: Install clojure tools 68 | uses: DeLaGuardo/setup-clojure@9.4 69 | with: 70 | bb: ${{ matrix.bb-version }} 71 | clj-kondo: latest 72 | - run: bb run lint 73 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | - [Homebrew (Linux and macOS)](#homebrew-linux-and-macos) 4 | - [Scoop (Windows)](#scoop-windows) 5 | - [Manual (Linux and macOS)](#manual-linux-and-macos) 6 | - [Manual (Windows)](#manual-windows) 7 | 8 | ## Homebrew (Linux and macOS) 9 | 10 | **1. Install via `brew`:** 11 | ```shell 12 | brew install babashka/brew/bbin 13 | ``` 14 | 15 | **2. Add `~/.local/bin` to `PATH`:** 16 | ```shell 17 | echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.$(basename $SHELL)rc && exec $SHELL 18 | ``` 19 | 20 | ## Scoop (Windows) 21 | 22 | **1. Install `bbin` CLI:** 23 | ```shell 24 | scoop bucket add scoop-clojure https://github.com/littleli/scoop-clojure 25 | scoop install bbin 26 | ``` 27 | 28 | The Scoop package will automatically update your `Path` with `%HOMEDRIVE%%HOMEPATH%\.local\bin`, but you will have to restart your terminal for this to take effect. 29 | 30 | ## Manual (Linux and macOS) 31 | 32 | **1. Install `bbin` CLI:** 33 | ```shell 34 | mkdir -p ~/.local/bin && curl -o- -L https://raw.githubusercontent.com/babashka/bbin/v0.2.4/bbin > ~/.local/bin/bbin && chmod +x ~/.local/bin/bbin 35 | ``` 36 | 37 | **2. Add `~/.local/bin` to `PATH`:** 38 | ```shell 39 | echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.$(basename $SHELL)rc && exec $SHELL 40 | ``` 41 | 42 | ## Manual (Windows) 43 | 44 | **1. Open Windows Powershell and run the following commands to install the `bbin` CLI (including `.bat` wrapper):** 45 | ```powershell 46 | New-Item -ItemType Directory -Force -Path $Env:HOMEDRIVE$Env:HOMEPATH\.local\bin 47 | Invoke-WebRequest -Uri https://raw.githubusercontent.com/babashka/bbin/v0.2.4/bbin -OutFile $Env:HOMEDRIVE$Env:HOMEPATH\.local\bin\bbin 48 | Invoke-WebRequest -Uri https://raw.githubusercontent.com/babashka/bbin/v0.2.4/bbin.bat -OutFile $Env:HOMEDRIVE$Env:HOMEPATH\.local\bin\bbin.bat 49 | ``` 50 | 51 | **2. Add `%HOMEDRIVE%%HOMEPATH%\.local\bin` to `Path` environment variable** 52 | 53 | 1. Search for `View advanced system settings` in the Start Menu 54 | 2. Click on the `Environment Variables...` button 55 | 3. Double-click on the `Path` variable to edit 56 | 4. When the edit dialog opens, click on `New` 57 | 5. Paste `%HOMEDRIVE%%HOMEPATH%\.local\bin` into the text field 58 | 6. Click `OK` on all remaining dialogs to save the changes 59 | -------------------------------------------------------------------------------- /test/babashka/bbin/scripts_test.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts-test 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.scripts :as scripts] 4 | [babashka.bbin.scripts.common :as common] 5 | [babashka.bbin.test-util :as tu] 6 | [babashka.fs :as fs] 7 | [clojure.string :as str] 8 | [clojure.test :refer [deftest is testing use-fixtures]])) 9 | 10 | (use-fixtures :once (tu/bbin-dirs-fixture)) 11 | 12 | (def bbin-test-lib 13 | '{:lib io.github.rads/bbin-test-lib, 14 | :coords {:git/url "https://github.com/rads/bbin-test-lib.git", 15 | :git/tag "v0.0.1", 16 | :git/sha "9140acfc12d8e1567fc6164a50d486de09433919"}}) 17 | 18 | (def test-script 19 | (common/insert-script-header "#!/usr/bin/env bb" bbin-test-lib)) 20 | 21 | (deftest load-scripts-test 22 | (let [cli-opts {}] 23 | (tu/reset-test-dir) 24 | (dirs/ensure-bbin-dirs cli-opts) 25 | (is (= {} (scripts/load-scripts (dirs/bin-dir nil)))) 26 | (spit (fs/file (dirs/bin-dir nil) "test-script") test-script) 27 | (is (= {'test-script bbin-test-lib} (scripts/load-scripts (dirs/bin-dir nil)))))) 28 | 29 | (deftest local-lib-path-test 30 | (let [script-deps {'io.github.example/foo {:git/sha "abc123"}} 31 | expected-suffix ["libs" "io.github.example" "foo" "abc123"] 32 | home-gitlibs (str (apply fs/path (fs/home) ".gitlibs" expected-suffix)) 33 | custom-gitlibs (str (apply fs/path "/custom/gitlibs/" expected-suffix))] 34 | (testing "uses ~/.gitlibs when GITLIBS is empty" 35 | (is (= home-gitlibs 36 | (str (common/local-lib-path script-deps ""))))) 37 | (testing "uses custom path when GITLIBS is set" 38 | (is (= custom-gitlibs 39 | (str (common/local-lib-path script-deps "/custom/gitlibs"))))))) 40 | 41 | (deftest uninstall-test 42 | (testing "uninstall foo" 43 | (tu/reset-test-dir) 44 | (dirs/ensure-bbin-dirs {}) 45 | (let [script-file (fs/file (dirs/bin-dir nil) "foo")] 46 | (spit script-file "#!/usr/bin/env bb") 47 | (let [cli-opts {:script/lib "foo"} 48 | out (str/trim (with-out-str (scripts/uninstall cli-opts)))] 49 | (is (= (str "Removing " script-file) out)) 50 | (is (not (fs/exists? script-file))))))) 51 | -------------------------------------------------------------------------------- /templates/docs/installation.template.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | - [Homebrew (Linux and macOS)](#homebrew-linux-and-macos) 4 | - [Scoop (Windows)](#scoop-windows) 5 | - [Manual (Linux and macOS)](#manual-linux-and-macos) 6 | - [Manual (Windows)](#manual-windows) 7 | 8 | ## Homebrew (Linux and macOS) 9 | 10 | **1. Install via `brew`:** 11 | ```shell 12 | brew install babashka/brew/bbin 13 | ``` 14 | 15 | **2. Add `~/.local/bin` to `PATH`:** 16 | ```shell 17 | echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.$(basename $SHELL)rc && exec $SHELL 18 | ``` 19 | 20 | ## Scoop (Windows) 21 | 22 | **1. Install `bbin` CLI:** 23 | ```shell 24 | scoop bucket add scoop-clojure https://github.com/littleli/scoop-clojure 25 | scoop install bbin 26 | ``` 27 | 28 | The Scoop package will automatically update your `Path` with `%HOMEDRIVE%%HOMEPATH%\.local\bin`, but you will have to restart your terminal for this to take effect. 29 | 30 | ## Manual (Linux and macOS) 31 | 32 | **1. Install `bbin` CLI:** 33 | ```shell 34 | mkdir -p ~/.local/bin && curl -o- -L https://raw.githubusercontent.com/babashka/bbin/v{{version}}/bbin > ~/.local/bin/bbin && chmod +x ~/.local/bin/bbin 35 | ``` 36 | 37 | **2. Add `~/.local/bin` to `PATH`:** 38 | ```shell 39 | echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.$(basename $SHELL)rc && exec $SHELL 40 | ``` 41 | 42 | ## Manual (Windows) 43 | 44 | **1. Open Windows Powershell and run the following commands to install the `bbin` CLI (including `.bat` wrapper):** 45 | ```powershell 46 | New-Item -ItemType Directory -Force -Path $Env:HOMEDRIVE$Env:HOMEPATH\.local\bin 47 | Invoke-WebRequest -Uri https://raw.githubusercontent.com/babashka/bbin/v{{version}}/bbin -OutFile $Env:HOMEDRIVE$Env:HOMEPATH\.local\bin\bbin 48 | Invoke-WebRequest -Uri https://raw.githubusercontent.com/babashka/bbin/v{{version}}/bbin.bat -OutFile $Env:HOMEDRIVE$Env:HOMEPATH\.local\bin\bbin.bat 49 | ``` 50 | 51 | **2. Add `%HOMEDRIVE%%HOMEPATH%\.local\bin` to `Path` environment variable** 52 | 53 | 1. Search for `View advanced system settings` in the Start Menu 54 | 2. Click on the `Environment Variables...` button 55 | 3. Double-click on the `Path` variable to edit 56 | 4. When the edit dialog opens, click on `New` 57 | 5. Paste `%HOMEDRIVE%%HOMEPATH%\.local\bin` into the text field 58 | 6. Click `OK` on all remaining dialogs to save the changes 59 | -------------------------------------------------------------------------------- /dev/babashka/bbin/gen_script.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.gen-script 2 | (:require [babashka.fs :as fs] 3 | [clojure.edn :as edn] 4 | [clojure.string :as str] 5 | [clojure.tools.namespace.dependency :as ns-dep] 6 | [clojure.tools.namespace.file :as ns-file] 7 | [clojure.tools.namespace.track :as ns-track] 8 | [fipp.edn :as fipp])) 9 | 10 | (def bbin-deps (some-> (slurp "deps.edn") edn/read-string :deps)) 11 | 12 | (def version 13 | (-> (slurp "deps.edn") edn/read-string 14 | :aliases :neil :project :version)) 15 | 16 | (def prelude-template 17 | (str/triml " 18 | #!/usr/bin/env bb 19 | 20 | ; :bbin/start 21 | ; 22 | ; {:coords {:bbin/url \"https://raw.githubusercontent.com/babashka/bbin/%s/bbin\"}} 23 | ; 24 | ; :bbin/end 25 | 26 | (babashka.deps/add-deps 27 | '{:deps %s}) 28 | ")) 29 | 30 | (def min-bb-version 31 | (some-> (slurp "bb.edn") edn/read-string :min-bb-version)) 32 | 33 | (def meta-template 34 | `[(~'ns ~'babashka.bbin.meta) 35 | (~'def ~'min-bb-version 36 | "This def was generated by the bbin build script." 37 | ~min-bb-version) 38 | (~'def ~'version 39 | "This def was generated by the bbin build script." 40 | ~version)]) 41 | 42 | (def meta-str 43 | (str/join "\n" (map pr-str meta-template))) 44 | 45 | (def prelude-str 46 | (let [lines (-> (with-out-str (fipp/pprint bbin-deps {:width 80})) str/split-lines)] 47 | (format prelude-template 48 | (if (str/ends-with? version "-SNAPSHOT") "main" (str "v" version)) 49 | (str/join "\n" (cons (first lines) (map #(str " " %) (rest lines))))))) 50 | 51 | (defn sorted-namespaces [path] 52 | (->> (file-seq (fs/file path)) 53 | (filter ns-file/clojure-file?) 54 | (ns-file/add-files {}) 55 | ::ns-track/deps 56 | ns-dep/topo-sort)) 57 | 58 | (defn ns-sym->path [ns-sym] 59 | (let [parts (str/split (str/replace ns-sym "-" "_") #"\.") 60 | path-vec (concat ["src"] 61 | (butlast parts) 62 | [(str (last parts) ".clj")])] 63 | (apply fs/path path-vec))) 64 | 65 | (def all-scripts 66 | (concat 67 | [prelude-str 68 | meta-str] 69 | (->> (sorted-namespaces "src") 70 | (filter #(str/starts-with? % "babashka.bbin")) 71 | (map #(slurp (str (ns-sym->path %))))))) 72 | 73 | (defn gen-script [] 74 | (spit "bbin" (str/join "\n" all-scripts))) 75 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:min-bb-version "0.9.162" 2 | :paths ["src" "test" "test-resources" "dev"] 3 | :pods {org.babashka/fswatcher {:version "0.0.3"}} 4 | :deps {org.babashka/spec.alpha {:git/url "https://github.com/babashka/spec.alpha" 5 | :git/sha "951b49b8c173244e66443b8188e3ff928a0a71e7"} 6 | local/deps {:local/root "."}} 7 | :bbin/bin {bbin {:main-opts ["-f" "bbin"]}} 8 | :tasks {bbin {:requires ([babashka.bbin.cli :as bbin]) 9 | :task (apply bbin/-main *command-line-args*)} 10 | dev {:doc "Starts watcher to auto-build bbin script" 11 | :requires ([babashka.bbin.dev :as dev]) 12 | :task (dev/dev)} 13 | ci {:doc "Run all CI tasks locally" 14 | :requires ([taoensso.timbre :as log]) 15 | :task (do 16 | (log/info "bb run lint") 17 | (run 'lint) 18 | (log/info "bb run test") 19 | (run 'test))} 20 | lint (shell "clj-kondo --lint .") 21 | test {:depends [gen-script] 22 | :extra-deps {io.github.cognitect-labs/test-runner 23 | {:git/tag "v0.5.1" :git/sha "dfb30dd"} 24 | ring/ring-core {:mvn/version "1.12.1"}} 25 | :task cognitect.test-runner/-main} 26 | gen-script {:doc "Build the bbin script" 27 | :extra-deps {org.clojure/tools.namespace 28 | {:mvn/version "1.5.0"}} 29 | :requires ([babashka.bbin.gen-script :as gen-script]) 30 | :task (gen-script/gen-script)} 31 | render-templates {:requires ([clojure.string :as str] 32 | [clojure.edn :as edn] 33 | [selmer.parser :as p] 34 | [selmer.util :refer [without-escaping]]) 35 | :task (let [version (some-> (slurp "deps.edn") edn/read-string 36 | :aliases :neil :project :version)] 37 | (without-escaping 38 | (->> (p/render (slurp "templates/docs/installation.template.md") 39 | {:version version}) 40 | (spit "docs/installation.md"))) 41 | (without-escaping 42 | (->> (p/render (slurp "templates/README.template.md") 43 | {:version version}) 44 | (spit "README.md"))))}}} 45 | -------------------------------------------------------------------------------- /src/babashka/bbin/dirs.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.dirs 2 | (:require [babashka.fs :as fs] 3 | [clojure.string :as str])) 4 | 5 | (defn user-home [] 6 | (System/getProperty "user.home")) 7 | 8 | (defn print-legacy-path-warning [] 9 | (binding [*out* *err*] 10 | (println (str/triml " 11 | WARNING: In bbin 0.2.0, we now use the XDG Base Directory Specification by 12 | WARNING: default. This means the ~/.babashka/bbin/bin path is deprecated in 13 | WARNING: favor of ~/.local/bin. 14 | WARNING: 15 | WARNING: To remove this message, run `bbin migrate` for further instructions. 16 | WARNING: (We won't make any changes without asking you first.) 17 | ")))) 18 | 19 | (def ^:dynamic *legacy-bin-dir* nil) 20 | 21 | (defn- legacy-override-dir [] 22 | (some-> (or (System/getenv "BABASHKA_BBIN_DIR") 23 | (some-> (System/getenv "XDG_DATA_HOME") (fs/file ".babashka" "bbin"))) 24 | (fs/canonicalize {:nofollow-links true}))) 25 | 26 | (defn legacy-bin-dir-base [] 27 | (if-let [override (legacy-override-dir)] 28 | (fs/file override "bin") 29 | (fs/file (user-home) ".babashka" "bbin" "bin"))) 30 | 31 | (defn legacy-bin-dir [] 32 | (or *legacy-bin-dir* (legacy-bin-dir-base))) 33 | 34 | (defn using-legacy-paths? [] 35 | (fs/exists? (legacy-bin-dir))) 36 | 37 | (def ^:dynamic *legacy-jars-dir* nil) 38 | 39 | (defn legacy-jars-dir-base [] 40 | (if-let [override (legacy-override-dir)] 41 | (fs/file override "jars") 42 | (fs/file (user-home) ".babashka" "bbin" "jars"))) 43 | 44 | (defn legacy-jars-dir [] 45 | (or *legacy-jars-dir* (legacy-jars-dir-base))) 46 | 47 | (defn check-legacy-paths [] 48 | (when (using-legacy-paths?) 49 | (print-legacy-path-warning))) 50 | 51 | (def ^:dynamic *xdg-bin-dir* nil) 52 | 53 | (defn xdg-bin-dir [_] 54 | (or *xdg-bin-dir* 55 | (if-let [override (System/getenv "BABASHKA_BBIN_BIN_DIR")] 56 | (fs/file (fs/expand-home override)) 57 | (fs/file (user-home) ".local" "bin")))) 58 | 59 | (defn bin-dir [opts] 60 | (if (using-legacy-paths?) 61 | (legacy-bin-dir) 62 | (xdg-bin-dir opts))) 63 | 64 | (def ^:dynamic *xdg-jars-dir* nil) 65 | 66 | (defn xdg-jars-dir [_] 67 | (or *xdg-jars-dir* 68 | (if-let [override (System/getenv "BABASHKA_BBIN_JARS_DIR")] 69 | (fs/file (fs/expand-home override)) 70 | (fs/file (fs/xdg-cache-home) "babashka" "bbin" "jars")))) 71 | 72 | (defn jars-dir [opts] 73 | (if (using-legacy-paths?) 74 | (legacy-jars-dir) 75 | (xdg-jars-dir opts))) 76 | 77 | (defn ensure-bbin-dirs [cli-opts] 78 | (fs/create-dirs (bin-dir cli-opts))) 79 | 80 | (defn ensure-xdg-dirs [cli-opts] 81 | (fs/create-dirs (xdg-bin-dir cli-opts)) 82 | (fs/create-dirs (xdg-jars-dir cli-opts))) 83 | -------------------------------------------------------------------------------- /src/babashka/bbin/scripts/local_jar.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.local-jar 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.protocols :as p] 4 | [babashka.bbin.scripts.common :as common] 5 | [babashka.bbin.util :as util] 6 | [babashka.fs :as fs] 7 | [clojure.string :as str] 8 | [selmer.parser :as selmer] 9 | [selmer.util :as selmer-util])) 10 | 11 | (def ^:private local-jar-template-str 12 | (str/trim " 13 | #!/usr/bin/env bb 14 | 15 | ; :bbin/start 16 | ; 17 | {{script/meta}} 18 | ; 19 | ; :bbin/end 20 | 21 | (require '[babashka.classpath :refer [add-classpath]]) 22 | 23 | (def script-jar {{script/jar|pr-str}}) 24 | 25 | (add-classpath script-jar) 26 | 27 | (require '[{{script/main-ns}}]) 28 | (apply {{script/main-ns}}/-main *command-line-args*) 29 | ")) 30 | 31 | (defrecord LocalJar [cli-opts coords] 32 | p/Script 33 | (install [_] 34 | (fs/create-dirs (dirs/jars-dir cli-opts)) 35 | (let [file-path (str (fs/canonicalize (:script/lib cli-opts) {:nofollow-links true})) 36 | main-ns (common/jar->main-ns file-path) 37 | script-deps {:bbin/url (str "file://" file-path)} 38 | header {:coords script-deps} 39 | script-name (or (:as cli-opts) (common/file-path->script-name file-path)) 40 | cached-jar-path (fs/file (dirs/jars-dir cli-opts) (str script-name ".jar")) 41 | script-edn-out (with-out-str 42 | (binding [*print-namespace-maps* false] 43 | (util/pprint header))) 44 | template-opts {:script/meta (->> script-edn-out 45 | str/split-lines 46 | (map #(str common/comment-char " " %)) 47 | (str/join "\n")) 48 | :script/main-ns main-ns 49 | :script/jar cached-jar-path} 50 | script-contents (selmer-util/without-escaping 51 | (selmer/render local-jar-template-str template-opts)) 52 | script-file (fs/canonicalize (fs/file (dirs/bin-dir cli-opts) script-name) 53 | {:nofollow-links true})] 54 | (fs/copy file-path cached-jar-path {:replace-existing true}) 55 | (common/install-script script-name header script-file script-contents cli-opts))) 56 | 57 | (upgrade [_] 58 | (let [cli-opts' (merge (select-keys cli-opts [:edn]) 59 | {:script/lib (str/replace (:bbin/url coords) #"^file://" "")})] 60 | (p/install (map->LocalJar {:cli-opts cli-opts' 61 | :coords coords})))) 62 | 63 | (uninstall [_] 64 | (common/delete-files cli-opts))) 65 | -------------------------------------------------------------------------------- /src/babashka/bbin/cli.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.cli 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.migrate :as migrate] 4 | [babashka.bbin.scripts :as scripts] 5 | [babashka.bbin.util :as util] 6 | [babashka.cli :as cli] 7 | [clojure.string :as str])) 8 | 9 | (declare print-commands) 10 | 11 | (defn- run [command-fn parsed & {:keys [disable-legacy-paths-check]}] 12 | (let [cli-opts (:opts parsed)] 13 | (when-not disable-legacy-paths-check 14 | (dirs/check-legacy-paths)) 15 | (if (and (:version cli-opts) (not (:help cli-opts))) 16 | (util/print-version) 17 | (command-fn cli-opts)))) 18 | 19 | (defn- add-global-aliases [commands] 20 | (map #(assoc-in % [:aliases :h] :help) commands)) 21 | 22 | (defn- base-commands 23 | [& {:as opts}] 24 | (->> [{:cmds ["commands"] 25 | :fn #(run print-commands %)} 26 | 27 | {:cmds ["help"] 28 | :fn #(run util/print-help %)} 29 | 30 | {:cmds ["install"] 31 | :fn #(run (:install-fn opts) %) 32 | :args->opts [:script/lib] 33 | :aliases {:T :tool}} 34 | 35 | {:cmds ["migrate" "auto"] 36 | :fn #(run (partial (:migrate-fn opts) :auto) % 37 | :disable-legacy-paths-check true)} 38 | 39 | {:cmds ["migrate"] 40 | :fn #(run (:migrate-fn opts) % 41 | :disable-legacy-paths-check true)} 42 | 43 | (when (util/upgrade-enabled?) 44 | {:cmds ["upgrade"] 45 | :fn #(run (:upgrade-fn opts) %) 46 | :args->opts [:script/lib]}) 47 | 48 | {:cmds ["uninstall"] 49 | :fn #(run (:uninstall-fn opts) %) 50 | :args->opts [:script/lib]} 51 | 52 | {:cmds ["ls"] 53 | :fn #(run (:ls-fn opts) %)} 54 | 55 | {:cmds ["bin"] 56 | :fn #(run (:bin-fn opts) %)} 57 | 58 | {:cmds ["version"] 59 | :fn #(run util/print-version %)} 60 | 61 | {:cmds [] 62 | :fn #(run util/print-help %)}] 63 | (remove nil?))) 64 | 65 | (defn- full-commands [& {:as run-opts}] 66 | (add-global-aliases (base-commands run-opts))) 67 | 68 | (defn- print-commands [_] 69 | (println (str/join " " (keep #(first (:cmds %)) (full-commands))))) 70 | 71 | (def default-run-opts 72 | {:install-fn scripts/install 73 | :upgrade-fn scripts/upgrade 74 | :uninstall-fn scripts/uninstall 75 | :ls-fn scripts/ls 76 | :bin-fn scripts/bin 77 | :migrate-fn migrate/migrate}) 78 | 79 | (defn bbin [main-args & {:as run-opts}] 80 | (let [run-opts' (merge default-run-opts run-opts)] 81 | (util/set-logging-config! (cli/parse-opts main-args)) 82 | (cli/dispatch (full-commands run-opts') main-args {}))) 83 | 84 | (defn -main [& args] 85 | (bbin args)) 86 | 87 | (when (= *file* (System/getProperty "babashka.file")) 88 | (util/check-min-bb-version) 89 | (apply -main *command-line-args*)) 90 | -------------------------------------------------------------------------------- /src/babashka/bbin/scripts/http_jar.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.http-jar 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.protocols :as p] 4 | [babashka.bbin.scripts.common :as common] 5 | [babashka.bbin.util :as util] 6 | [babashka.fs :as fs] 7 | [babashka.http-client :as http] 8 | [clojure.java.io :as io] 9 | [clojure.string :as str] 10 | [selmer.parser :as selmer] 11 | [selmer.util :as selmer-util])) 12 | 13 | (def ^:private local-jar-template-str 14 | (str/trim " 15 | #!/usr/bin/env bb 16 | 17 | ; :bbin/start 18 | ; 19 | {{script/meta}} 20 | ; 21 | ; :bbin/end 22 | 23 | (require '[babashka.classpath :refer [add-classpath]]) 24 | 25 | (def script-jar {{script/jar|pr-str}}) 26 | 27 | (add-classpath script-jar) 28 | 29 | (require '[{{script/main-ns}}]) 30 | (apply {{script/main-ns}}/-main *command-line-args*) 31 | ")) 32 | 33 | (defrecord HttpJar [cli-opts coords] 34 | p/Script 35 | (install [_] 36 | (fs/create-dirs (dirs/jars-dir cli-opts)) 37 | (let [http-url (:script/lib cli-opts) 38 | script-deps {:bbin/url http-url} 39 | header {:coords script-deps} 40 | script-name (or (:as cli-opts) (common/http-url->script-name http-url)) 41 | tmp-jar-path (doto (fs/file (fs/temp-dir) (str script-name ".jar")) 42 | (fs/delete-on-exit)) 43 | _ (io/copy (:body (http/get http-url {:as :bytes})) tmp-jar-path) 44 | main-ns (common/jar->main-ns tmp-jar-path) 45 | cached-jar-path (fs/file (dirs/jars-dir cli-opts) (str script-name ".jar")) 46 | _ (fs/move tmp-jar-path cached-jar-path {:replace-existing true}) 47 | script-edn-out (with-out-str 48 | (binding [*print-namespace-maps* false] 49 | (util/pprint header))) 50 | template-opts {:script/meta (->> script-edn-out 51 | str/split-lines 52 | (map #(str common/comment-char " " %)) 53 | (str/join "\n")) 54 | :script/main-ns main-ns 55 | :script/jar cached-jar-path} 56 | script-contents (selmer-util/without-escaping 57 | (selmer/render local-jar-template-str template-opts)) 58 | script-file (fs/canonicalize (fs/file (dirs/bin-dir cli-opts) script-name) 59 | {:nofollow-links true})] 60 | (common/install-script script-name header script-file script-contents cli-opts))) 61 | 62 | (upgrade [_] 63 | (let [cli-opts' (merge (select-keys cli-opts [:edn]) 64 | {:script/lib (:bbin/url coords)})] 65 | (p/install (map->HttpJar {:cli-opts cli-opts' 66 | :coords coords})))) 67 | 68 | (uninstall [_] 69 | (common/delete-files cli-opts))) 70 | -------------------------------------------------------------------------------- /test/babashka/bbin/scripts/local_dir_test.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.local-dir-test 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.test-util :as tu] 4 | [babashka.fs :as fs] 5 | [clojure.string :as str] 6 | [clojure.test :refer [deftest is testing use-fixtures]]) 7 | (:import (clojure.lang ExceptionInfo))) 8 | 9 | (use-fixtures :once (tu/bbin-dirs-fixture)) 10 | 11 | (deftest install-from-lib-local-root-dir-test 12 | (testing "install */* --local/root *" 13 | (tu/reset-test-dir) 14 | (dirs/ensure-bbin-dirs {}) 15 | (let [local-root (str (fs/file tu/test-dir "foo"))] 16 | (fs/create-dir local-root) 17 | (spit (fs/file local-root "bb.edn") (pr-str {})) 18 | (spit (fs/file local-root "deps.edn") (pr-str {})) 19 | (let [cli-opts {:script/lib "babashka/foo" 20 | :local/root local-root} 21 | out (tu/run-install cli-opts)] 22 | (is (= {:lib 'babashka/foo 23 | :coords {:local/root local-root}} 24 | out)) 25 | (is (fs/exists? (fs/file (dirs/bin-dir nil) "foo"))))))) 26 | 27 | (deftest invalid-bin-config-test 28 | (testing "install */* --local/root * (invalid bin config)" 29 | (tu/reset-test-dir) 30 | (dirs/ensure-bbin-dirs {}) 31 | (let [local-root (str (fs/file tu/test-dir "foo")) 32 | invalid-config 123] 33 | (fs/create-dir local-root) 34 | (spit (fs/file local-root "bb.edn") (pr-str {:bbin/bin invalid-config})) 35 | (spit (fs/file local-root "deps.edn") (pr-str {})) 36 | (let [cli-opts {:script/lib "babashka/foo" 37 | :local/root local-root}] 38 | (is (thrown-with-msg? ExceptionInfo #"123 - failed: map\? spec: :bbin/bin" 39 | (tu/run-install cli-opts))))))) 40 | 41 | (deftest install-from-no-lib-local-root-dir-test 42 | (testing "install ./" 43 | (tu/reset-test-dir) 44 | (dirs/ensure-bbin-dirs {}) 45 | (let [local-root (str (fs/file tu/test-dir "foo"))] 46 | (fs/create-dir local-root) 47 | (spit (fs/file local-root "bb.edn") 48 | (pr-str {:bbin/bin {'foo {:main-opts ["-m" "babashka/foo"]}}})) 49 | (spit (fs/file local-root "deps.edn") (pr-str {})) 50 | (let [cli-opts {:script/lib local-root} 51 | script-url (str "file://" local-root) 52 | out (tu/run-install cli-opts)] 53 | (is (= {:coords {:bbin/url script-url}} out)) 54 | (is (fs/exists? (fs/file (dirs/bin-dir nil) "foo"))) 55 | (is (= {'foo {:coords {:bbin/url script-url}}} (tu/run-ls))))))) 56 | 57 | (deftest install-tool-from-local-root-test 58 | (testing "install ./ --tool" 59 | (tu/reset-test-dir) 60 | (dirs/ensure-bbin-dirs {}) 61 | (let [opts {:script/lib "bbin/foo" 62 | :local/root (str "test-resources" fs/file-separator "local-tool") 63 | :as "footool" 64 | :tool true} 65 | full-path (str (fs/canonicalize (:local/root opts) {:nofollow-links true})) 66 | _ (tu/run-install opts)] 67 | (is (fs/exists? (fs/file (dirs/bin-dir nil) "footool"))) 68 | (let [usage-out (tu/run-bin-script "footool")] 69 | (is (every? #(str/includes? usage-out %) ["`keys`" "`vals`"]))) 70 | (is (str/includes? (tu/run-bin-script "footool" "k" ":a" "1") "(:a)")) 71 | (is (str/includes? (tu/run-bin-script "footool" "v" ":a" "1") "(1)")) 72 | (is (= {'footool {:coords {:local/root full-path} 73 | :lib 'bbin/foo}} 74 | (tu/run-ls)))))) 75 | -------------------------------------------------------------------------------- /src/babashka/bbin/git.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.git 2 | (:require [babashka.fs :as fs] 3 | [babashka.process :refer [sh]] 4 | [clojure.string :as str])) 5 | 6 | (defn- ensure-git-dir [client git-url] 7 | (binding [*err* (java.io.StringWriter.)] 8 | (let [path ((:ensure-git-dir client) git-url)] 9 | ((:git-fetch client) (fs/file path)) 10 | path))) 11 | 12 | (defn default-branch [client git-url] 13 | (let [lib-dir (ensure-git-dir client git-url) 14 | remote-info (sh "git remote show origin" {:dir lib-dir 15 | :extra-env {"LC_ALL" "C"}}) 16 | [[_ branch]] (->> (:out remote-info) 17 | str/split-lines 18 | (some #(re-seq #"HEAD branch: (\w+)" %)))] 19 | branch)) 20 | 21 | (defn latest-git-sha [client git-url] 22 | (let [lib-dir (ensure-git-dir client git-url) 23 | branch (default-branch client git-url) 24 | log-result (sh ["git" "log" "-n" "1" branch "--pretty=format:%H"] 25 | {:dir lib-dir})] 26 | (str/trim-newline (:out log-result)))) 27 | 28 | (defn find-git-tag [client git-url tag] 29 | (let [lib-dir (ensure-git-dir client git-url) 30 | log-result (sh ["git" "log" "-n" "1" tag "--pretty=format:%H"] 31 | {:dir lib-dir}) 32 | sha (str/trim-newline (:out log-result))] 33 | {:name (str tag) 34 | :commit {:sha sha}})) 35 | 36 | (defn latest-git-tag [client git-url] 37 | (let [lib-dir (ensure-git-dir client git-url) 38 | describe-result (sh "git describe --tags --abbrev=0" {:dir lib-dir}) 39 | tag (str/trim-newline (:out describe-result))] 40 | (when-not (str/blank? tag) 41 | (find-git-tag client git-url tag)))) 42 | 43 | (def providers 44 | {#"^(com|io)\.github\." :github 45 | #"^(com|io)\.gitlab\." :gitlab 46 | #"^(org|io)\.bitbucket\." :bitbucket 47 | #"^(com|io)\.beanstalkapp\." :beanstalk 48 | #"^ht\.sr\." :sourcehut}) 49 | 50 | (defn- clean-lib-str [lib] 51 | (->> (reduce #(str/replace %1 %2 "") lib (keys providers)) 52 | symbol)) 53 | 54 | (defn git-http-url [lib] 55 | (let [provider (some #(when (re-seq (key %) (str lib)) %) providers) 56 | s (clean-lib-str (str lib))] 57 | (case (val provider) 58 | :github (str "https://github.com/" s ".git") 59 | :gitlab (str "https://gitlab.com/" s ".git") 60 | :bitbucket (let [[u] (str/split (str s) #"/")] 61 | (str "https://" u "@bitbucket.org/" s ".git")) 62 | :beanstalk (let [[u] (str/split (str s) #"/")] 63 | (str "https://" u ".git.beanstalkapp.com/" (name lib) ".git")) 64 | :sourcehut (str "https://git.sr.ht/~" s)))) 65 | 66 | (defn git-ssh-url [lib] 67 | (let [provider (some #(when (re-seq (key %) (str lib)) %) providers) 68 | s (clean-lib-str (str lib))] 69 | (case (val provider) 70 | :github (str "git@github.com:" s ".git") 71 | :gitlab (str "git@gitlab.com:" s ".git") 72 | :bitbucket (str "git@bitbucket.org:" s ".git") 73 | :beanstalk (let [[u] (str/split (str s) #"/")] 74 | (str "git@" u ".git.beanstalkapp.com:/" s ".git")) 75 | :sourcehut (str "git@git.sr.ht:~" s)))) 76 | 77 | (defn git-repo-url [client lib] 78 | (try 79 | (let [url (git-http-url lib)] 80 | (ensure-git-dir client url) 81 | url) 82 | (catch Exception e 83 | (if (re-seq #"^Unable to clone " (ex-message e)) 84 | (let [url (git-ssh-url lib)] 85 | (ensure-git-dir client url) 86 | url) 87 | (throw e))))) 88 | -------------------------------------------------------------------------------- /test/babashka/bbin/test_util.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.test-util 2 | (:require [babashka.bbin.cli :as bbin] 3 | [babashka.bbin.dirs :as dirs] 4 | [babashka.bbin.scripts :as scripts] 5 | [babashka.bbin.util :as util] 6 | [babashka.fs :as fs] 7 | [babashka.process :as p] 8 | [clojure.edn :as edn] 9 | [clojure.java.io :as io] 10 | [clojure.string :as str] 11 | [clojure.test :as test] 12 | [org.httpkit.server :as http] 13 | [ring.util.mime-type :as mime-type]) 14 | (:import (java.net ServerSocket))) 15 | 16 | (defmethod test/report :begin-test-var [m] 17 | (println "===" (-> m :var meta :name)) 18 | (println)) 19 | 20 | (defn reset-test-dir [] 21 | (let [path (str (fs/file (fs/temp-dir) "bbin-test"))] 22 | (fs/delete-tree path) 23 | (fs/create-dirs path) 24 | (fs/create-dirs (fs/file path "public")) 25 | (doto path (fs/delete-on-exit)))) 26 | 27 | (defn- random-available-port [] 28 | (with-open [socket (ServerSocket. 0)] 29 | (.getLocalPort socket))) 30 | 31 | (def test-dir (reset-test-dir)) 32 | (def http-public-dir (fs/file test-dir "public")) 33 | (def http-port (random-available-port)) 34 | 35 | (defn- relativize [original] 36 | (fs/file test-dir (fs/relativize (dirs/user-home) original))) 37 | 38 | (defn bbin-dirs-fixture [] 39 | (fn [f] 40 | (binding [dirs/*legacy-bin-dir* (relativize (dirs/legacy-bin-dir)) 41 | dirs/*legacy-jars-dir* (relativize (dirs/legacy-jars-dir)) 42 | dirs/*xdg-bin-dir* (relativize (dirs/xdg-bin-dir nil)) 43 | dirs/*xdg-jars-dir* (relativize (dirs/xdg-jars-dir nil))] 44 | (f)))) 45 | 46 | (def git-wrapper-path 47 | (-> (if (fs/windows?) 48 | "test-resources/git-wrapper.bat" 49 | "test-resources/git-wrapper") 50 | fs/file 51 | fs/canonicalize 52 | str)) 53 | 54 | (defn- set-gitlibs-command [] 55 | (System/setProperty "clojure.gitlibs.command" git-wrapper-path)) 56 | 57 | (defn bbin-private-keys-fixture [] 58 | (fn [f] 59 | (set-gitlibs-command) 60 | (f))) 61 | 62 | (defn bbin [main-args & {:as opts}] 63 | (let [out (str/trim (with-out-str (bbin/bbin main-args opts)))] 64 | (if (#{:edn} (:out opts)) 65 | (edn/read-string out) 66 | out))) 67 | 68 | (defn run-install [cli-opts] 69 | (some-> (with-out-str (scripts/install (assoc cli-opts :edn true))) 70 | edn/read-string)) 71 | 72 | (defn run-upgrade [cli-opts] 73 | (some-> (with-out-str (scripts/upgrade (assoc cli-opts :edn true))) 74 | edn/read-string)) 75 | 76 | (defn run-ls [] 77 | (some-> (with-out-str (scripts/ls {:edn true})) 78 | edn/read-string)) 79 | 80 | (defn exec-cmd-line [script-name] 81 | (concat (when util/windows? ["cmd" "/c"]) 82 | [(str (fs/canonicalize (fs/file (dirs/bin-dir nil) (name script-name)) {:nofollow-links true}))])) 83 | 84 | (defn run-bin-script [script-name & script-args] 85 | (let [args (concat (exec-cmd-line script-name) script-args) 86 | {:keys [out]} (p/sh args {:err :inherit})] 87 | (str/trim out))) 88 | 89 | (defn- static-file-handler [{:keys [uri] :as _req} public-dir] 90 | (let [file (io/file public-dir (subs uri 1))] 91 | (if (fs/exists? file) 92 | {:status 200 93 | :headers {"Content-Type" (mime-type/ext-mime-type (str file))} 94 | :body file} 95 | {:status 404 96 | :body "Not Found"}))) 97 | 98 | (defn- start-http-server [] 99 | (http/run-server #(static-file-handler % http-public-dir) 100 | {:port http-port})) 101 | 102 | (defn http-server-fixture [] 103 | (fn [f] 104 | (let [server (start-http-server)] 105 | (try 106 | (f) 107 | (finally 108 | (server)))))) 109 | -------------------------------------------------------------------------------- /test/babashka/bbin/scripts/git_dir_test.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.git-dir-test 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.test-util :as tu] 4 | [babashka.fs :as fs] 5 | [clojure.test :refer [deftest is testing use-fixtures]])) 6 | 7 | (use-fixtures :once 8 | (tu/bbin-dirs-fixture) 9 | (tu/bbin-private-keys-fixture)) 10 | 11 | (def bbin-test-lib 12 | '{:lib io.github.rads/bbin-test-lib, 13 | :coords {:git/url "https://github.com/rads/bbin-test-lib.git", 14 | :git/tag "v0.0.1", 15 | :git/sha "9140acfc12d8e1567fc6164a50d486de09433919"}}) 16 | 17 | (def bbin-test-lib-no-tag 18 | '{:lib io.github.rads/bbin-test-lib-no-tag, 19 | :coords {:git/url "https://github.com/rads/bbin-test-lib-no-tag.git", 20 | :git/sha "cefb15e3320dd4c599e8be62f7a01a00b07e2e72"}}) 21 | 22 | (def bbin-test-lib-private 23 | '{:lib io.bitbucket.radsmith/bbin-test-lib-private, 24 | :coords {:git/url "git@bitbucket.org:radsmith/bbin-test-lib-private.git" 25 | :git/tag "v0.0.1", 26 | :git/sha "9140acfc12d8e1567fc6164a50d486de09433919"}}) 27 | 28 | (deftest install-from-qualified-lib-name-public-test 29 | (testing "install */* (public Git repo)" 30 | (tu/reset-test-dir) 31 | (dirs/ensure-bbin-dirs {}) 32 | (let [cli-opts {:script/lib "io.github.rads/bbin-test-lib"} 33 | out (tu/run-install cli-opts) 34 | bin-file (fs/file (dirs/bin-dir nil) "hello")] 35 | (is (= bbin-test-lib out)) 36 | (is (fs/exists? bin-file)) 37 | (is (= "Hello world!" (tu/run-bin-script 'hello)))))) 38 | 39 | (deftest install-from-qualified-lib-name-no-tag-test 40 | (testing "install */* (public Git repo, no tags)" 41 | (tu/reset-test-dir) 42 | (dirs/ensure-bbin-dirs {}) 43 | (let [cli-opts {:script/lib "io.github.rads/bbin-test-lib-no-tag"} 44 | out (tu/run-install cli-opts) 45 | bin-file (fs/file (dirs/bin-dir nil) "hello")] 46 | (is (= bbin-test-lib-no-tag out)) 47 | (is (fs/exists? bin-file)) 48 | (is (= "Hello world!" (tu/run-bin-script 'hello)))))) 49 | 50 | (deftest install-from-qualified-lib-name-private-test 51 | (testing "install */* (private Git repo)" 52 | (tu/reset-test-dir) 53 | (dirs/ensure-bbin-dirs {}) 54 | (let [cli-opts {:script/lib "io.bitbucket.radsmith/bbin-test-lib-private"} 55 | out (tu/run-install cli-opts) 56 | bin-file (fs/file (dirs/bin-dir nil) "hello")] 57 | (is (= bbin-test-lib-private out)) 58 | (is (fs/exists? bin-file)) 59 | (is (= "Hello world!" (tu/run-bin-script 'hello)))))) 60 | 61 | (def git-http-url-lib 62 | '{:lib org.babashka.bbin/script-1039504783-https-github-com-rads-bbin-test-lib-git 63 | :coords {:git/url "https://github.com/rads/bbin-test-lib.git" 64 | :git/sha "cefb15e3320dd4c599e8be62f7a01a00b07e2e72"}}) 65 | 66 | (deftest install-from-git-http-url-test 67 | (testing "install https://*.git" 68 | (tu/reset-test-dir) 69 | (dirs/ensure-bbin-dirs {}) 70 | (let [cli-opts {:script/lib (get-in git-http-url-lib [:coords :git/url])} 71 | out (tu/run-install cli-opts) 72 | bin-file (fs/file (dirs/bin-dir nil) "hello")] 73 | (is (= git-http-url-lib out)) 74 | (is (fs/exists? bin-file)) 75 | (is (= "Hello world!" (tu/run-bin-script 'hello)))))) 76 | 77 | (def git-ssh-url-lib 78 | '{:lib org.babashka.bbin/script-1166637990-git-bitbucket-org-radsmith-bbin-test-lib-private-git 79 | :coords {:git/url "git@bitbucket.org:radsmith/bbin-test-lib-private.git" 80 | :git/sha "cefb15e3320dd4c599e8be62f7a01a00b07e2e72"}}) 81 | 82 | (deftest install-from-git-ssh-url-test 83 | (testing "install git@*:*.git" 84 | (tu/reset-test-dir) 85 | (dirs/ensure-bbin-dirs {}) 86 | (let [cli-opts {:script/lib (get-in git-ssh-url-lib [:coords :git/url])} 87 | out (tu/run-install cli-opts) 88 | bin-file (fs/file (dirs/bin-dir nil) "hello")] 89 | (is (= git-ssh-url-lib out)) 90 | (is (fs/exists? bin-file)) 91 | (is (= "Hello world!" (tu/run-bin-script 'hello)))))) 92 | -------------------------------------------------------------------------------- /test/babashka/bbin/scripts/local_file_test.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.local-file-test 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.test-util :as tu] 4 | [babashka.fs :as fs] 5 | [clojure.test :refer [deftest is testing use-fixtures]])) 6 | 7 | (use-fixtures :once (tu/bbin-dirs-fixture)) 8 | 9 | (deftest install-from-local-root-clj-test 10 | (testing "install ./*.clj (with shebang)" 11 | (tu/reset-test-dir) 12 | (dirs/ensure-bbin-dirs {}) 13 | (let [script-file (doto (fs/file tu/test-dir "hello.clj") 14 | (spit "#!/usr/bin/env bb\n(println \"Hello world\")")) 15 | script-url (str "file://" script-file) 16 | cli-opts {:script/lib (str script-file)} 17 | out (tu/run-install cli-opts)] 18 | (is (= {:coords {:bbin/url (str "file://" script-file)}} out)) 19 | (is (= "Hello world" (tu/run-bin-script :hello))) 20 | (is (= {'hello {:coords {:bbin/url script-url}}} (tu/run-ls))))) 21 | (testing "install ./*.clj (without shebang)" 22 | (tu/reset-test-dir) 23 | (dirs/ensure-bbin-dirs {}) 24 | (let [script-file (doto (fs/file tu/test-dir "hello.clj") 25 | (spit "(println \"Hello world\")")) 26 | script-url (str "file://" script-file) 27 | cli-opts {:script/lib (str script-file)} 28 | out (tu/run-install cli-opts)] 29 | (is (= {:coords {:bbin/url script-url}} out)) 30 | (is (= "Hello world" (tu/run-bin-script :hello))) 31 | (is (= {'hello {:coords {:bbin/url script-url}}} (tu/run-ls)))))) 32 | 33 | (deftest install-from-local-root-bb-test 34 | (testing "install ./*.bb (with shebang)" 35 | (tu/reset-test-dir) 36 | (dirs/ensure-bbin-dirs {}) 37 | (let [script-file (doto (fs/file tu/test-dir "hello.bb") 38 | (spit "#!/usr/bin/env bb\n(println \"Hello world\")")) 39 | script-url (str "file://" script-file) 40 | cli-opts {:script/lib (str script-file)} 41 | out (tu/run-install cli-opts)] 42 | (is (= {:coords {:bbin/url (str "file://" script-file)}} out)) 43 | (is (= "Hello world" (tu/run-bin-script :hello))) 44 | (is (= {'hello {:coords {:bbin/url script-url}}} (tu/run-ls))))) 45 | (testing "install ./*.bb (without shebang)" 46 | (tu/reset-test-dir) 47 | (dirs/ensure-bbin-dirs {}) 48 | (let [script-file (doto (fs/file tu/test-dir "hello.bb") 49 | (spit "(println \"Hello world\")")) 50 | script-url (str "file://" script-file) 51 | cli-opts {:script/lib (str script-file)} 52 | out (tu/run-install cli-opts)] 53 | (is (= {:coords {:bbin/url (str "file://" script-file)}} out)) 54 | (is (= "Hello world" (tu/run-bin-script :hello))) 55 | (is (= {'hello {:coords {:bbin/url script-url}}} (tu/run-ls)))))) 56 | 57 | (deftest install-from-local-root-no-extension-test 58 | (testing "install ./* (no extension, with shebang)" 59 | (tu/reset-test-dir) 60 | (dirs/ensure-bbin-dirs {}) 61 | (let [script-file (doto (fs/file tu/test-dir "hello") 62 | (spit "#!/usr/bin/env bb\n(println \"Hello world\")")) 63 | script-url (str "file://" script-file) 64 | cli-opts {:script/lib (str script-file)} 65 | out (tu/run-install cli-opts)] 66 | (is (= {:coords {:bbin/url (str "file://" script-file)}} out)) 67 | (is (= "Hello world" (tu/run-bin-script :hello))) 68 | (is (= {'hello {:coords {:bbin/url script-url}}} (tu/run-ls))))) 69 | (testing "install ./* (no extension, without shebang)" 70 | (tu/reset-test-dir) 71 | (dirs/ensure-bbin-dirs {}) 72 | (let [script-file (doto (fs/file tu/test-dir "hello") 73 | (spit "(println \"Hello world\")")) 74 | script-url (str "file://" script-file) 75 | cli-opts {:script/lib (str script-file)} 76 | out (tu/run-install cli-opts)] 77 | (is (= {:coords {:bbin/url (str "file://" script-file)}} out)) 78 | (is (= "Hello world" (tu/run-bin-script :hello))) 79 | (is (= {'hello {:coords {:bbin/url script-url}}} (tu/run-ls)))))) 80 | 81 | (deftest upgrade-local-file-test 82 | (testing "upgrade (local file)" 83 | (tu/reset-test-dir) 84 | (dirs/ensure-bbin-dirs {}) 85 | (let [script-file (fs/file tu/test-dir "hello.clj") 86 | script-url (str "file://" script-file)] 87 | (spit script-file "#!/usr/bin/env bb\n(println \"Hello world\")") 88 | (tu/run-install {:script/lib (str script-file)}) 89 | (is (= "Hello world" (tu/run-bin-script :hello))) 90 | (spit script-file "#!/usr/bin/env bb\n(println \"Upgraded\")") 91 | (let [out (tu/run-upgrade {:script/lib "hello"})] 92 | (is (= {:coords {:bbin/url (str "file://" script-file)}} out)) 93 | (is (= "Upgraded" (tu/run-bin-script :hello))) 94 | (is (= {'hello {:coords {:bbin/url script-url}}} (tu/run-ls))))))) 95 | -------------------------------------------------------------------------------- /test/babashka/bbin/util_test.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.util-test 2 | (:require [babashka.bbin.util :as util] 3 | [clojure.string :as str] 4 | [clojure.test :refer [deftest is testing are]])) 5 | 6 | (deftest truncate-test 7 | (are [input opts expected] (= expected (util/truncate input opts)) 8 | "123456" {:truncate-to 6} "123456" 9 | "123456" {:truncate-to 5} "12..." 10 | 11 | "123456" {:truncate-to 5 :omission "longer than 5"} "longer than 5" 12 | 13 | "12345" {:truncate-to 3 :omission "…" :omission-position :center} "1…5")) 14 | 15 | (deftest print-table-test 16 | (let [split-table (fn [table] 17 | (let [[header div & data :as lines] (str/split-lines table)] 18 | (if (re-find (re-pattern (str \u2500)) (str div)) 19 | [header div data] 20 | [nil nil lines]))) 21 | print-table (fn 22 | ([rows] 23 | (with-out-str (util/print-table rows))) 24 | ([ks-or-rows rows-or-opts] 25 | (with-out-str (util/print-table ks-or-rows rows-or-opts))) 26 | ([ks rows opts] 27 | (with-out-str (util/print-table ks rows opts)))) 28 | header-matches (fn [re table] 29 | (let [[header & _r] (split-table table)] 30 | (is (re-find re (str header)) 31 | (str "expected header to match " (pr-str re))))) 32 | contains-row-matching (fn [re table] 33 | (let [[_header _div rows] (split-table table)] 34 | (is (some #(re-find re %) rows) 35 | (str "expected " (pr-str rows) " to contain a row matching " (pr-str re)))))] 36 | (testing ":no-color skips escape characters" 37 | (is (re-find #"^a\n─\n1\r?\n$" 38 | (print-table [{:a 1}] {:no-color true})))) 39 | (testing "header from rows or keys" 40 | (header-matches #"a.+b" (print-table [{:a "12" :b "34"}])) 41 | (header-matches #"b.+a" (print-table '(:b :a) [{:a "12" :b "34"}])) 42 | (header-matches #"A" (print-table [{"A" 1}])) 43 | (header-matches #"A" (print-table '("A") [{"A" 1}])) 44 | (is (re-find #"^12 34\r?\n$" (print-table [{:a "12" :b "34"}] {:skip-header true})) 45 | "prints only rows when :skip-header")) 46 | (testing "naming columns" 47 | (header-matches #"A.+B" (print-table {:a "A" :b "B"} 48 | [{:a "12" :b "34"}]))) 49 | (testing "skipping empty columns" 50 | (is (empty? (print-table [{:a nil :b ""}]))) 51 | (header-matches #"(?> payload 64 | :docs 65 | (map :v)))) 66 | 67 | (defn- latest-stable-mvn-version [qlib] 68 | (first-stable-version (mvn-versions qlib {:limit 100}))) 69 | 70 | (defn- get-clojars-artifact [qlib] 71 | (let [url (format "https://clojars.org/api/artifacts/%s" 72 | qlib)] 73 | (json/read-str (:body (http/get url))))) 74 | 75 | (defn- clojars-versions [qlib {:keys [limit] :or {limit 10}}] 76 | (let [body (get-clojars-artifact qlib)] 77 | (->> body 78 | :recent_versions 79 | (map :version) 80 | (take limit)))) 81 | 82 | (defn- latest-stable-clojars-version 83 | [qlib] 84 | (first-stable-version (clojars-versions qlib {:limit 100}))) 85 | 86 | (defrecord MavenJar [cli-opts lib] 87 | p/Script 88 | (install [_] 89 | (let [script-deps {(edn/read-string (:script/lib cli-opts)) 90 | (select-keys cli-opts [:mvn/version])} 91 | header {:lib (key (first script-deps)) 92 | :coords (val (first script-deps))} 93 | _ (deps/add-deps {:deps script-deps}) 94 | script-root (fs/canonicalize (or (:local/root cli-opts) (common/local-lib-path script-deps)) {:nofollow-links true}) 95 | script-name (or (:as cli-opts) (second (str/split (:script/lib cli-opts) #"/"))) 96 | script-config (common/default-script-config cli-opts) 97 | script-edn-out (with-out-str 98 | (binding [*print-namespace-maps* false] 99 | (util/pprint header))) 100 | main-opts (or (some-> (:main-opts cli-opts) edn/read-string) 101 | (:main-opts script-config)) 102 | template-opts {:script/meta (->> script-edn-out 103 | str/split-lines 104 | (map #(str common/comment-char " " %)) 105 | (str/join "\n")) 106 | :script/root script-root 107 | :script/lib (pr-str (key (first script-deps))) 108 | :script/coords (binding [*print-namespace-maps* false] (pr-str (val (first script-deps)))) 109 | :script/main-opts (common/process-main-opts main-opts script-root)} 110 | template-out (selmer-util/without-escaping 111 | (selmer/render maven-template-str template-opts)) 112 | script-file (fs/canonicalize (fs/file (dirs/bin-dir cli-opts) script-name) {:nofollow-links true})] 113 | (common/install-script script-name header script-file template-out cli-opts))) 114 | 115 | (upgrade [_] 116 | (let [latest-version (or (latest-stable-clojars-version lib) 117 | (latest-stable-mvn-version lib)) 118 | cli-opts' (merge (select-keys cli-opts [:edn]) 119 | {:script/lib (str lib) 120 | :mvn/version latest-version})] 121 | (p/install (map->MavenJar {:cli-opts cli-opts' 122 | :lib lib})))) 123 | 124 | (uninstall [_] 125 | (common/delete-files cli-opts))) 126 | -------------------------------------------------------------------------------- /src/babashka/bbin/scripts.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts 2 | (:require 3 | [babashka.bbin.deps :as bbin-deps] 4 | [babashka.bbin.dirs :as dirs] 5 | [babashka.bbin.protocols :as p] 6 | [babashka.bbin.scripts.common :as common] 7 | [babashka.bbin.scripts.git-dir :refer [map->GitDir]] 8 | [babashka.bbin.scripts.http-file :refer [map->HttpFile]] 9 | [babashka.bbin.scripts.http-jar :refer [map->HttpJar]] 10 | [babashka.bbin.scripts.local-dir :refer [map->LocalDir]] 11 | [babashka.bbin.scripts.local-file :refer [map->LocalFile]] 12 | [babashka.bbin.scripts.local-jar :refer [map->LocalJar]] 13 | [babashka.bbin.scripts.maven-jar :refer [map->MavenJar]] 14 | [babashka.bbin.util :as util] 15 | [babashka.fs :as fs] 16 | [clojure.edn :as edn] 17 | [clojure.java.io :as io] 18 | [clojure.string :as str] 19 | [selmer.filters :as filters])) 20 | 21 | ;; selmer filter for clojure escaping for e.g. files 22 | (filters/add-filter! :pr-str (comp pr-str str)) 23 | 24 | (defn parse-script [s] 25 | (let [lines (str/split-lines s) 26 | prefix (if (str/ends-with? (first lines) "bb") ";" "#")] 27 | (->> lines 28 | (drop-while #(not (re-seq (re-pattern (str "^" prefix " *:bbin/start")) %))) 29 | next 30 | (take-while #(not (re-seq (re-pattern (str "^" prefix " *:bbin/end")) %))) 31 | (map #(str/replace % (re-pattern (str "^" prefix " *")) "")) 32 | (str/join "\n") 33 | edn/read-string))) 34 | 35 | (defn- read-header [filename] 36 | (or (with-open [input-stream (io/input-stream filename)] 37 | (let [buffer (byte-array (* 1024 5)) 38 | n (.read input-stream buffer)] 39 | (when (nat-int? n) 40 | (String. buffer 0 n)))) 41 | "")) 42 | 43 | (defn load-scripts [dir] 44 | (->> (file-seq dir) 45 | (filter #(.isFile %)) 46 | (map (fn [x] [(symbol (str (fs/relativize dir x))) 47 | (-> (read-header x) (parse-script ))])) 48 | (filter second) 49 | (into {}))) 50 | 51 | (defn ls [cli-opts] 52 | (let [scripts (load-scripts (dirs/bin-dir cli-opts))] 53 | (if (:edn cli-opts) 54 | (util/pprint scripts cli-opts) 55 | (do 56 | (println) 57 | (util/print-scripts (util/printable-scripts scripts) cli-opts) 58 | (println))))) 59 | 60 | (defn bin [cli-opts] 61 | (println (str (dirs/bin-dir cli-opts)))) 62 | 63 | (defn- throw-invalid-script [summary cli-opts] 64 | (let [{:keys [procurer artifact]} summary] 65 | (throw (ex-info "Invalid script coordinates.\nIf you're trying to install from the filesystem, make sure the path actually exists." 66 | {:script/lib (:script/lib cli-opts) 67 | :procurer procurer 68 | :artifact artifact})))) 69 | 70 | (defn- new-script [cli-opts] 71 | (let [summary (bbin-deps/summary cli-opts) 72 | {:keys [procurer artifact]} summary] 73 | (case [procurer artifact] 74 | [:git :dir] (map->GitDir {:cli-opts cli-opts :summary summary}) 75 | [:http :file] (map->HttpFile {:cli-opts cli-opts}) 76 | [:http :jar] (map->HttpJar {:cli-opts cli-opts}) 77 | [:local :dir] (map->LocalDir {:cli-opts cli-opts :summary summary}) 78 | [:local :file] (map->LocalFile {:cli-opts cli-opts}) 79 | [:local :jar] (map->LocalJar {:cli-opts cli-opts}) 80 | [:maven :jar] (map->MavenJar {:cli-opts cli-opts}) 81 | (throw-invalid-script summary cli-opts)))) 82 | 83 | (defn install [cli-opts] 84 | (if-not (:script/lib cli-opts) 85 | (util/print-help) 86 | (do 87 | (dirs/ensure-bbin-dirs cli-opts) 88 | (when-not (util/edn? cli-opts) 89 | (println) 90 | (println (util/bold "Starting install..." cli-opts))) 91 | (let [cli-opts' (util/canonicalized-cli-opts cli-opts) 92 | script (new-script cli-opts')] 93 | (p/install script))))) 94 | 95 | (defn- default-script [cli-opts] 96 | (reify 97 | p/Script 98 | (install [_]) 99 | (upgrade [_] 100 | (throw (ex-info "Not implemented" {}))) 101 | (uninstall [_] 102 | (common/delete-files cli-opts)))) 103 | 104 | (defn- load-script [cli-opts] 105 | (let [script-name (:script/lib cli-opts) 106 | script-file (fs/file (fs/canonicalize (fs/file (dirs/bin-dir cli-opts) script-name) {:nofollow-links true})) 107 | parsed (parse-script (read-header script-file))] 108 | (cond 109 | (-> parsed :coords :bbin/url) 110 | (let [summary (bbin-deps/summary {:script/lib (-> parsed :coords :bbin/url)}) 111 | {:keys [procurer artifact]} summary] 112 | (case [procurer artifact] 113 | [:git :dir] (map->GitDir {:cli-opts cli-opts :summary summary :coords (:coords parsed)}) 114 | [:http :file] (map->HttpFile {:cli-opts cli-opts :coords (:coords parsed)}) 115 | [:http :jar] (map->HttpJar {:cli-opts cli-opts :coords (:coords parsed)}) 116 | [:local :dir] (map->LocalDir {:cli-opts cli-opts :summary summary}) 117 | [:local :file] (map->LocalFile {:cli-opts cli-opts :coords (:coords parsed)}) 118 | [:local :jar] (map->LocalJar {:cli-opts cli-opts :coords (:coords parsed)}) 119 | (throw-invalid-script summary cli-opts))) 120 | 121 | (-> parsed :coords :mvn/version) 122 | (map->MavenJar {:cli-opts cli-opts :lib (:lib parsed)}) 123 | 124 | (-> parsed :coords :git/tag) 125 | (let [summary (bbin-deps/summary {:script/lib (:lib parsed) 126 | :git/tag (-> parsed :coords :git/tag)})] 127 | (map->GitDir {:cli-opts cli-opts :summary summary :coords (:coords parsed)})) 128 | 129 | (-> parsed :coords :git/sha) 130 | (let [summary (bbin-deps/summary {:script/lib (:lib parsed) 131 | :git/sha (-> parsed :coords :git/sha)})] 132 | (map->GitDir {:cli-opts cli-opts :summary summary :coords (:coords parsed)})) 133 | 134 | :else (default-script cli-opts)))) 135 | 136 | (defn upgrade [cli-opts] 137 | (if-not (:script/lib cli-opts) 138 | (util/print-help) 139 | (do 140 | (dirs/ensure-bbin-dirs cli-opts) 141 | (let [script (load-script cli-opts)] 142 | (p/upgrade script))))) 143 | 144 | (defn uninstall [cli-opts] 145 | (if-not (:script/lib cli-opts) 146 | (util/print-help) 147 | (do 148 | (dirs/ensure-bbin-dirs cli-opts) 149 | (let [script-name (:script/lib cli-opts) 150 | script-file (fs/canonicalize (fs/file (dirs/bin-dir cli-opts) script-name) {:nofollow-links true})] 151 | (when (fs/delete-if-exists script-file) 152 | (when util/windows? (fs/delete-if-exists (fs/file (str script-file common/windows-wrapper-extension)))) 153 | (fs/delete-if-exists (fs/file (dirs/jars-dir cli-opts) (str script-name ".jar"))) 154 | (println "Removing" (str script-file))))))) 155 | -------------------------------------------------------------------------------- /src/babashka/bbin/deps.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.deps 2 | (:require [babashka.bbin.git :as git] 3 | [babashka.fs :as fs] 4 | [clojure.edn :as edn] 5 | [clojure.set :as set] 6 | [clojure.string :as str] 7 | [clojure.tools.gitlibs.impl :as gitlibs-impl])) 8 | 9 | (def lib-opts->template-deps-fn 10 | "A map to define valid CLI options. 11 | 12 | - Each key is a sequence of valid combinations of CLI opts. 13 | - Each value is a function which returns a tools.deps lib map." 14 | {[#{:local/root}] 15 | (fn [_ lib-sym lib-opts] 16 | {lib-sym (select-keys lib-opts [:local/root])}) 17 | 18 | [#{} #{:git/url}] 19 | (fn [client lib-sym lib-opts] 20 | (let [url (or (:git/url lib-opts) (git/git-repo-url client lib-sym)) 21 | tag (git/latest-git-tag client url)] 22 | (if tag 23 | {lib-sym {:git/url url 24 | :git/tag (:name tag) 25 | :git/sha (-> tag :commit :sha)}} 26 | (let [sha (git/latest-git-sha client url)] 27 | {lib-sym {:git/url url 28 | :git/sha sha}})))) 29 | 30 | [#{:git/tag} #{:git/url :git/tag}] 31 | (fn [client lib-sym lib-opts] 32 | (let [url (or (:git/url lib-opts) (git/git-repo-url client lib-sym)) 33 | tag (:git/tag lib-opts) 34 | {:keys [commit]} (git/find-git-tag client url tag)] 35 | {lib-sym {:git/url url 36 | :git/tag tag 37 | :git/sha (:sha commit)}})) 38 | 39 | [#{:git/sha} #{:git/url :git/sha}] 40 | (fn [client lib-sym lib-opts] 41 | (let [url (or (:git/url lib-opts) (git/git-repo-url client lib-sym)) 42 | sha (:git/sha lib-opts)] 43 | {lib-sym {:git/url url 44 | :git/sha sha}})) 45 | 46 | [#{:latest-sha} #{:git/url :latest-sha}] 47 | (fn [client lib-sym lib-opts] 48 | (let [url (or (:git/url lib-opts) (git/git-repo-url client lib-sym)) 49 | sha (git/latest-git-sha client url)] 50 | {lib-sym {:git/url url 51 | :git/sha sha}})) 52 | 53 | [#{:git/url :git/tag :git/sha}] 54 | (fn [_ lib-sym lib-opts] 55 | {lib-sym (select-keys lib-opts [:git/url :git/tag :git/sha])})}) 56 | 57 | (def valid-lib-opts 58 | "The set of all valid combinations of CLI opts." 59 | (into #{} cat (keys lib-opts->template-deps-fn))) 60 | 61 | (defn- cli-opts->lib-opts 62 | "Returns parsed lib opts from raw CLI opts." 63 | [cli-opts] 64 | (-> cli-opts 65 | (set/rename-keys {:sha :git/sha}) 66 | (select-keys (into #{} cat valid-lib-opts)))) 67 | 68 | (defn- find-template-deps-fn 69 | "Returns a template-deps-fn given lib-opts parsed from raw CLI opts." 70 | [lib-opts] 71 | (some (fn [[k v]] (and (contains? (set k) (set (keys lib-opts))) v)) 72 | lib-opts->template-deps-fn)) 73 | 74 | (defn- invalid-lib-opts-error [provided-lib-opts] 75 | (ex-info "Provided invalid combination of CLI options" 76 | {:provided-opts (set (keys provided-lib-opts)) 77 | :valid-combinations valid-lib-opts})) 78 | 79 | (def ^:private default-deps-info-client 80 | {:ensure-git-dir gitlibs-impl/ensure-git-dir 81 | :git-fetch gitlibs-impl/git-fetch}) 82 | 83 | (defn infer 84 | "Returns a tools.deps lib map for the given CLI opts." 85 | ([cli-opts] (infer nil cli-opts)) 86 | ([client cli-opts] 87 | (let [client (merge default-deps-info-client client) 88 | lib-opts (cli-opts->lib-opts cli-opts) 89 | lib-sym (edn/read-string (:lib cli-opts)) 90 | template-deps-fn (find-template-deps-fn lib-opts)] 91 | (if-not template-deps-fn 92 | (throw (invalid-lib-opts-error lib-opts)) 93 | (template-deps-fn client lib-sym lib-opts))))) 94 | 95 | (def ^:private symbol-regex 96 | (re-pattern 97 | (str "(?i)^" 98 | "(?:((?:[a-z0-9-]+\\.)*[a-z0-9-]+)/)?" 99 | "((?:[a-z0-9-]+\\.)*[a-z0-9-]+)" 100 | "$"))) 101 | 102 | (defn- lib-str? [x] 103 | (boolean (and (string? x) (re-seq symbol-regex x)))) 104 | 105 | (defn- local-script-path? [x] 106 | (boolean (and (string? x) (or (fs/exists? x) 107 | (fs/exists? (str/replace x #"^file://" "")))))) 108 | 109 | (defn- http-url? [x] 110 | (boolean (and (string? x) (re-seq #"^https?://" x)))) 111 | 112 | (defn- git-ssh-url? [x] 113 | (boolean (and (string? x) (re-seq #"^.+@.+:.+\.git$" x)))) 114 | 115 | (defn- git-http-url? [x] 116 | (boolean (and (string? x) (re-seq #"^https?://.+\.git$" x)))) 117 | 118 | (defn git-repo-url? [s] 119 | (or (git-http-url? s) (git-ssh-url? s))) 120 | 121 | (def ^:private deps-types 122 | [{:lib lib-str? 123 | :coords #{:local/root} 124 | :procurer :local} 125 | 126 | {:lib lib-str? 127 | :coords #{:mvn/version} 128 | :procurer :maven} 129 | 130 | {:lib local-script-path? 131 | :coords #{:bbin/url} 132 | :procurer :local} 133 | 134 | {:lib #(or (git-http-url? %) (git-ssh-url? %)) 135 | :coords #{:bbin/url} 136 | :procurer :git} 137 | 138 | {:lib http-url? 139 | :coords #{:bbin/url} 140 | :procurer :http} 141 | 142 | {:lib lib-str? 143 | :coords #{:git/sha :git/url :git/tag} 144 | :procurer :git} 145 | 146 | {:lib local-script-path? 147 | :coords #{} 148 | :procurer :local} 149 | 150 | {:lib #(or (git-http-url? %) (git-ssh-url? %) (lib-str? %)) 151 | :coords #{} 152 | :procurer :git} 153 | 154 | {:lib http-url? 155 | :coords #{} 156 | :procurer :http}]) 157 | 158 | (defn- deps-type-match? [cli-opts deps-type] 159 | (and ((:lib deps-type) (:script/lib cli-opts)) 160 | (or (empty? (:coords deps-type)) 161 | (seq (set/intersection (:coords deps-type) (set (keys cli-opts))))) 162 | deps-type)) 163 | 164 | (defn- match-deps-type [cli-opts] 165 | (or (some #(deps-type-match? cli-opts %) deps-types) 166 | {:procurer :unknown-procurer})) 167 | 168 | (defn directory? [x] 169 | (fs/directory? (str/replace x #"^file://" ""))) 170 | 171 | (defn regular-file? [x] 172 | (fs/regular-file? (str/replace x #"^file://" ""))) 173 | 174 | (defn- match-artifact [cli-opts procurer] 175 | (cond 176 | (or (#{:maven} procurer) 177 | (and (#{:local} procurer) 178 | (or (and (:script/lib cli-opts) (re-seq #"\.jar$" (:script/lib cli-opts))) 179 | (and (:local/root cli-opts) (re-seq #"\.jar$" (:local/root cli-opts))))) 180 | (and (#{:http} procurer) (re-seq #"\.jar$" (:script/lib cli-opts)))) 181 | :jar 182 | 183 | (or (#{:git} procurer) 184 | (and (#{:local} procurer) 185 | (or (and (:script/lib cli-opts) 186 | (directory? (:script/lib cli-opts))) 187 | (and (:local/root cli-opts) (directory? (:local/root cli-opts))))) 188 | (and (#{:http} procurer) (re-seq #"\.git$" (:script/lib cli-opts)))) 189 | :dir 190 | 191 | (or (and (#{:local} procurer) 192 | (:script/lib cli-opts) 193 | (regular-file? (:script/lib cli-opts))) 194 | (and (#{:http} procurer) (re-seq #"\.(cljc?|bb)$" (:script/lib cli-opts)))) 195 | :file 196 | 197 | :else :unknown-artifact)) 198 | 199 | (defn summary [cli-opts] 200 | (let [{:keys [procurer]} (match-deps-type cli-opts) 201 | artifact (match-artifact cli-opts procurer)] 202 | {:procurer procurer 203 | :artifact artifact})) 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bbin 2 | 3 | **Install any Babashka script or project with one command.** 4 | 5 | ``` 6 | $ bbin install io.github.babashka/neil 7 | {:lib io.github.babashka/neil, 8 | :coords 9 | {:git/url "https://github.com/babashka/neil", 10 | :git/tag "v0.1.45", 11 | :git/sha "0474d4cb5cfb0207265a4508a0e82ae7a293ab61"}} 12 | 13 | $ neil --version 14 | neil 0.1.45 15 | 16 | $ bbin install https://gist.githubusercontent.com/rads/da8ecbce63fe305f3520637810ff9506/raw/25e47ce2fb5f9a7f9d12a20423e801b64c20e787/portal.clj 17 | {:coords {:bbin/url "https://gist.githubusercontent.com/rads/da8ecbce63fe305f3520637810ff9506/raw/25e47ce2fb5f9a7f9d12a20423e801b64c20e787/portal.clj"}} 18 | 19 | # Open a Portal window with all installed scripts 20 | $ portal <(bbin ls) 21 | ``` 22 | 23 | 📦 See the [**Scripts and Projects**](https://github.com/babashka/bbin/wiki/Scripts-and-Projects) wiki page for a list of CLI tools from the community. This list is just a starting point — any existing Babashka script or project can be installed out-of-the-box! 24 | 25 | 📚 See the [**Usage**](#usage) and [**CLI**](#cli) docs for more examples of what `bbin` can do. 26 | 27 | ## Table of Contents 28 | 29 | - [Installation](#installation) 30 | - [Usage](#usage) 31 | - [Docs](#docs) 32 | - [CLI](#cli) 33 | - [Contributing](#contributing) 34 | - [License](#license) 35 | 36 | ## Installation 37 | 38 | ### Homebrew (Linux and macOS) 39 | 40 | **1. Install via `brew`:** 41 | ```shell 42 | brew install babashka/brew/bbin 43 | ``` 44 | 45 | **2. Add `~/.local/bin` to `PATH`:** 46 | ```shell 47 | echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.$(basename $SHELL)rc && exec $SHELL 48 | ``` 49 | 50 | ### Scoop (Windows) 51 | 52 | **1. Install `bbin` CLI:** 53 | ```shell 54 | scoop bucket add scoop-clojure https://github.com/littleli/scoop-clojure 55 | scoop install bbin 56 | ``` 57 | 58 | The Scoop package will automatically update your `Path` with `%HOMEDRIVE%%HOMEPATH%\.local\bin`, but you will have to restart your terminal for this to take effect. 59 | 60 | ### Manual (Linux, macOS, and Windows) 61 | 62 | [Click here for manual installation instructions.](docs/installation.md#manual-linux-and-macos) 63 | 64 | ## Usage 65 | 66 | ``` 67 | # Install a script from a qualified lib name 68 | $ bbin install io.github.babashka/neil 69 | $ bbin install io.github.rads/watch --latest-sha 70 | $ bbin install org.babashka/http-server --mvn/version 0.1.11 71 | 72 | # Install an auto-generated CLI from a namespace of functions 73 | $ bbin install io.github.borkdude/quickblog --tool --ns-default quickblog.api 74 | 75 | # Install a script from a URL 76 | $ bbin install https://gist.githubusercontent.com/rads/da8ecbce63fe305f3520637810ff9506/raw/25e47ce2fb5f9a7f9d12a20423e801b64c20e787/portal.clj 77 | $ bbin install https://github.com/babashka/http-server/releases/download/v0.1.11/http-server.jar 78 | 79 | # Install a script from a Git repo 80 | $ bbin install https://gist.github.com/1d7670142f8117fa78d7db40a9d6ee80.git 81 | $ bbin install git@gist.github.com:1d7670142f8117fa78d7db40a9d6ee80.git 82 | 83 | # Install a script from a local file 84 | $ bbin install foo.clj 85 | $ bbin install http-server.jar 86 | 87 | # Install a script from a local root (with no lib name) 88 | $ git clone https://github.com/babashka/bbin.git ~/src/bbin 89 | $ bbin install ~/src/bbin --as bbin-dev 90 | 91 | # Install a script from a local root (with lib name) 92 | $ bbin install io.github.babashka/bbin --local/root ~/src/bbin --as bbin-dev 93 | 94 | # Remove a script 95 | $ bbin uninstall watch 96 | 97 | # Show installed scripts 98 | $ bbin ls 99 | 100 | # Show the bin path 101 | $ bbin bin 102 | ``` 103 | 104 | ## Docs 105 | 106 | - [CLI Docs](#cli) 107 | - [Packaging](docs/packaging.md) 108 | - [Design Docs](docs/design.md) 109 | - [Community Scripts and Projects](https://github.com/babashka/bbin/wiki/Scripts-and-Projects) 110 | - [Auto-Completion](docs/auto-completion.md) 111 | 112 | ## CLI 113 | 114 | - [`bbin install [script]`](#bbin-install-script) 115 | - [`bbin uninstall [script]`](#bbin-uninstall-script) 116 | - [`bbin ls`](#bbin-ls) 117 | - [`bbin bin`](#bbin-bin) 118 | - [`bbin version`](#bbin-version) 119 | - [`bbin help`](#bbin-help) 120 | 121 | --- 122 | 123 | ### `bbin install [script]` 124 | 125 | **Install a script** 126 | 127 | - By default, scripts will be installed to `~/.local/bin` 128 | - If `$BABASHKA_BBIN_BIN_DIR` is set, then use `$BABASHKA_BBIN_BIN_DIR` (explicit override) 129 | - Each bin script is a self-contained shell script that fetches deps and invokes `bb` with the correct arguments. 130 | - The bin scripts can be configured using the CLI options or the `:bbin/bin` key in `bb.edn` 131 | - [See the Packaging page for additional info on setting up your code to work with bbin](docs/packaging.md) 132 | 133 | **Example `bb.edn` Config:** 134 | 135 | ```clojure 136 | {:bbin/bin {neil {:main-opts ["-f" "neil"]}}} 137 | ``` 138 | 139 | **Supported Options:** 140 | 141 | *Note:* `bbin` will throw an error if conflicting options are provided, such as using both `--git/sha` and `--mvn/version` at the same time. 142 | 143 | If no `--git/tag` or `--git/sha` is provided, the latest tag from the Git repo will be used. If no tags exist, the latest SHA will be used. 144 | 145 | - `--as` 146 | - The name of the script to be saved in the `bbin bin` path 147 | - `--git/sha` 148 | - The SHA for a Git repo 149 | - `--git/tag` 150 | - The tag for a Git repo 151 | - `--git/url` 152 | - The URL for a Git repo 153 | - `--latest-sha` 154 | - If provided, find the latest SHA from the Git repo 155 | - `--local/root` 156 | - The path of a local directory containing a `deps.edn` file 157 | - `--main-opts` 158 | - The provided options (EDN format) will be passed to the `bb` command-line when the installed script is run 159 | - By default, `--main-opts` will be set to `["-m" ...]`, inferring the main function from the lib name 160 | - For example, if you provide a lib name like `io.github.rads/watch`, `bbin` will infer `rads.watch/-main` 161 | - Project authors can provide a default in the `:bbin/bin` key in `bb.edn` 162 | - `--mvn/version` 163 | - The version of a Maven dependency 164 | - `--ns-default` 165 | - The namespace to use to find functions (tool mode only) 166 | - Project authors can provide a default in the `:bbin/bin` key in `bb.edn` 167 | - `--tool` 168 | - If this option is provided, the script will be installed using **tool mode** 169 | - When enabled, the installed script acts as an entry point for functions in a namespace, similar to `clj -T` 170 | - If no function is provided, the installed script will infer a help message based on the function docstrings 171 | --- 172 | 173 | ### `bbin uninstall [script]` 174 | 175 | **Remove a script** 176 | 177 | --- 178 | 179 | ### `bbin ls` 180 | 181 | **List installed scripts** 182 | 183 | --- 184 | 185 | ### `bbin bin` 186 | 187 | **Display bbin bin folder** 188 | 189 | - The default folder is `~/.local/bin` 190 | 191 | --- 192 | 193 | ### `bbin version` 194 | 195 | **Display bbin version** 196 | 197 | --- 198 | 199 | ### `bbin help` 200 | 201 | **Display bbin help** 202 | 203 | --- 204 | 205 | ### `bbin migrate` 206 | 207 | **Migrate from bbin v0.1.x** 208 | 209 | --- 210 | 211 | ## Dev 212 | 213 | -To install a development version of bbin, first install bbin stable, then install bbin with bbin. 214 | 215 | ``` 216 | $ bbin install . --as bbin-dev --main-opts '["-m" babashka.bbin.cli/-main]' 217 | ``` 218 | 219 | You can now run your development copy of bbin with `bbin-dev`. 220 | Rebuilding is not required for `bbin-dev` installed this way, changes in Clojure source code is reflected instantly. 221 | 222 | ## Contributing 223 | 224 | If you'd like to contribute to `bbin`, you're welcome to create [issues for ideas, feature requests, and bug reports](https://github.com/babashka/bbin/issues). 225 | 226 | ## License 227 | 228 | `bbin` is released under the [MIT License](LICENSE). 229 | -------------------------------------------------------------------------------- /templates/README.template.md: -------------------------------------------------------------------------------- 1 | # bbin 2 | 3 | **Install any Babashka script or project with one command.** 4 | 5 | ``` 6 | $ bbin install io.github.babashka/neil 7 | {:lib io.github.babashka/neil, 8 | :coords 9 | {:git/url "https://github.com/babashka/neil", 10 | :git/tag "v0.1.45", 11 | :git/sha "0474d4cb5cfb0207265a4508a0e82ae7a293ab61"}} 12 | 13 | $ neil --version 14 | neil 0.1.45 15 | 16 | $ bbin install https://gist.githubusercontent.com/rads/da8ecbce63fe305f3520637810ff9506/raw/25e47ce2fb5f9a7f9d12a20423e801b64c20e787/portal.clj 17 | {:coords {:bbin/url "https://gist.githubusercontent.com/rads/da8ecbce63fe305f3520637810ff9506/raw/25e47ce2fb5f9a7f9d12a20423e801b64c20e787/portal.clj"}} 18 | 19 | # Open a Portal window with all installed scripts 20 | $ portal <(bbin ls) 21 | ``` 22 | 23 | 📦 See the [**Scripts and Projects**](https://github.com/babashka/bbin/wiki/Scripts-and-Projects) wiki page for a list of CLI tools from the community. This list is just a starting point — any existing Babashka script or project can be installed out-of-the-box! 24 | 25 | 📚 See the [**Usage**](#usage) and [**CLI**](#cli) docs for more examples of what `bbin` can do. 26 | 27 | ## Table of Contents 28 | 29 | - [Installation](#installation) 30 | - [Usage](#usage) 31 | - [Docs](#docs) 32 | - [CLI](#cli) 33 | - [Contributing](#contributing) 34 | - [License](#license) 35 | 36 | ## Installation 37 | 38 | ### Homebrew (Linux and macOS) 39 | 40 | **1. Install via `brew`:** 41 | ```shell 42 | brew install babashka/brew/bbin 43 | ``` 44 | 45 | **2. Add `~/.local/bin` to `PATH`:** 46 | ```shell 47 | echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.$(basename $SHELL)rc && exec $SHELL 48 | ``` 49 | 50 | ### Scoop (Windows) 51 | 52 | **1. Install `bbin` CLI:** 53 | ```shell 54 | scoop bucket add scoop-clojure https://github.com/littleli/scoop-clojure 55 | scoop install bbin 56 | ``` 57 | 58 | The Scoop package will automatically update your `Path` with `%HOMEDRIVE%%HOMEPATH%\.local\bin`, but you will have to restart your terminal for this to take effect. 59 | 60 | ### Manual (Linux, macOS, and Windows) 61 | 62 | [Click here for manual installation instructions.](docs/installation.md#manual-linux-and-macos) 63 | 64 | ## Usage 65 | 66 | ``` 67 | # Install a script from a qualified lib name 68 | $ bbin install io.github.babashka/neil 69 | $ bbin install io.github.rads/watch --latest-sha 70 | $ bbin install org.babashka/http-server --mvn/version 0.1.11 71 | 72 | # Install an auto-generated CLI from a namespace of functions 73 | $ bbin install io.github.borkdude/quickblog --tool --ns-default quickblog.api 74 | 75 | # Install a script from a URL 76 | $ bbin install https://gist.githubusercontent.com/rads/da8ecbce63fe305f3520637810ff9506/raw/25e47ce2fb5f9a7f9d12a20423e801b64c20e787/portal.clj 77 | $ bbin install https://github.com/babashka/http-server/releases/download/v0.1.11/http-server.jar 78 | 79 | # Install a script from a Git repo 80 | $ bbin install https://gist.github.com/1d7670142f8117fa78d7db40a9d6ee80.git 81 | $ bbin install git@gist.github.com:1d7670142f8117fa78d7db40a9d6ee80.git 82 | 83 | # Install a script from a local file 84 | $ bbin install foo.clj 85 | $ bbin install http-server.jar 86 | 87 | # Install a script from a local root (with no lib name) 88 | $ git clone https://github.com/babashka/bbin.git ~/src/bbin 89 | $ bbin install ~/src/bbin --as bbin-dev 90 | 91 | # Install a script from a local root (with lib name) 92 | $ bbin install io.github.babashka/bbin --local/root ~/src/bbin --as bbin-dev 93 | 94 | # Remove a script 95 | $ bbin uninstall watch 96 | 97 | # Show installed scripts 98 | $ bbin ls 99 | 100 | # Show the bin path 101 | $ bbin bin 102 | ``` 103 | 104 | ## Docs 105 | 106 | - [CLI Docs](#cli) 107 | - [FAQ](docs/faq.md) 108 | - [Design Docs](docs/design.md) 109 | - [Community Scripts and Projects](https://github.com/babashka/bbin/wiki/Scripts-and-Projects) 110 | - [Auto-Completion](docs/auto-completion.md) 111 | 112 | ## CLI 113 | 114 | - [`bbin install [script]`](#bbin-install-script) 115 | - [`bbin uninstall [script]`](#bbin-uninstall-script) 116 | - [`bbin ls`](#bbin-ls) 117 | - [`bbin bin`](#bbin-bin) 118 | - [`bbin version`](#bbin-version) 119 | - [`bbin help`](#bbin-help) 120 | 121 | --- 122 | 123 | ### `bbin install [script]` 124 | 125 | **Install a script** 126 | 127 | - By default, scripts will be installed to `~/.local/bin` 128 | - If `$BABASHKA_BBIN_BIN_DIR` is set, then use `$BABASHKA_BBIN_BIN_DIR` (explicit override) 129 | - Each bin script is a self-contained shell script that fetches deps and invokes `bb` with the correct arguments. 130 | - The bin scripts can be configured using the CLI options or the `:bbin/bin` key in `bb.edn` 131 | - [See the FAQ for additional info on setting up your code to work with bbin](docs/faq.md#how-do-i-get-my-software-onto-bbin) 132 | 133 | **Example `bb.edn` Config:** 134 | 135 | ```clojure 136 | {:bbin/bin {neil {:main-opts ["-f" "neil"]}}} 137 | ``` 138 | 139 | **Supported Options:** 140 | 141 | *Note:* `bbin` will throw an error if conflicting options are provided, such as using both `--git/sha` and `--mvn/version` at the same time. 142 | 143 | If no `--git/tag` or `--git/sha` is provided, the latest tag from the Git repo will be used. If no tags exist, the latest SHA will be used. 144 | 145 | - `--as` 146 | - The name of the script to be saved in the `bbin bin` path 147 | - `--git/sha` 148 | - The SHA for a Git repo 149 | - `--git/tag` 150 | - The tag for a Git repo 151 | - `--git/url` 152 | - The URL for a Git repo 153 | - `--latest-sha` 154 | - If provided, find the latest SHA from the Git repo 155 | - `--local/root` 156 | - The path of a local directory containing a `deps.edn` file 157 | - `--main-opts` 158 | - The provided options (EDN format) will be passed to the `bb` command-line when the installed script is run 159 | - By default, `--main-opts` will be set to `["-m" ...]`, inferring the main function from the lib name 160 | - For example, if you provide a lib name like `io.github.rads/watch`, `bbin` will infer `rads.watch/-main` 161 | - Project authors can provide a default in the `:bbin/bin` key in `bb.edn` 162 | - `--mvn/version` 163 | - The version of a Maven dependency 164 | - `--ns-default` 165 | - The namespace to use to find functions (tool mode only) 166 | - Project authors can provide a default in the `:bbin/bin` key in `bb.edn` 167 | - `--tool` 168 | - If this option is provided, the script will be installed using **tool mode** 169 | - When enabled, the installed script acts as an entry point for functions in a namespace, similar to `clj -T` 170 | - If no function is provided, the installed script will infer a help message based on the function docstrings 171 | --- 172 | 173 | ### `bbin uninstall [script]` 174 | 175 | **Remove a script** 176 | 177 | --- 178 | 179 | ### `bbin ls` 180 | 181 | **List installed scripts** 182 | 183 | --- 184 | 185 | ### `bbin bin` 186 | 187 | **Display bbin bin folder** 188 | 189 | - The default folder is `~/.local/bin` 190 | 191 | --- 192 | 193 | ### `bbin version` 194 | 195 | **Display bbin version** 196 | 197 | --- 198 | 199 | ### `bbin help` 200 | 201 | **Display bbin help** 202 | 203 | --- 204 | 205 | ### `bbin migrate` 206 | 207 | **Migrate from bbin v0.1.x** 208 | 209 | --- 210 | 211 | ## Dev 212 | 213 | -To install a development version of bbin, first install bbin stable, then install bbin with bbin. 214 | 215 | ``` 216 | $ bbin install . --as bbin-dev --main-opts '["-m" babashka.bbin.cli/-main]' 217 | ``` 218 | 219 | You can now run your development copy of bbin with `bbin-dev`. 220 | Rebuilding is not required for `bbin-dev` installed this way, changes in Clojure source code is reflected instantly. 221 | 222 | ## Contributing 223 | 224 | If you'd like to contribute to `bbin`, you're welcome to create [issues for ideas, feature requests, and bug reports](https://github.com/babashka/bbin/issues). 225 | 226 | ## License 227 | 228 | `bbin` is released under the [MIT License](LICENSE). 229 | -------------------------------------------------------------------------------- /src/babashka/bbin/migrate.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.migrate 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.scripts :as scripts] 4 | [babashka.bbin.util :as util] 5 | [babashka.fs :as fs] 6 | [clojure.string :as str])) 7 | 8 | (def templates 9 | {:found-scripts 10 | (fn [{:keys [cli-opts]}] 11 | (let [{:keys [force]} cli-opts] 12 | (println (util/bold "We found scripts in ~/.babashka/bbin/bin." cli-opts)) 13 | (if force 14 | (println "The --force option was enabled. We'll continue the migration without prompting.") 15 | (do 16 | (println "We'll ask you to confirm each script individually and back up the original before replacement.") 17 | (println "Re-run this command with --force to migrate all scripts without confirming."))))) 18 | 19 | :printable-scripts 20 | (fn [{:keys [scripts cli-opts]}] 21 | (util/print-scripts (util/printable-scripts scripts) cli-opts)) 22 | 23 | :prompt-move 24 | (fn [{:keys [cli-opts]}] 25 | (println (util/bold "Would you like to move your scripts to ~/.local/bin? (yes/no)" cli-opts)) 26 | (print "> ")) 27 | 28 | :migrating 29 | (fn [{:keys [cli-opts]}] 30 | (println (util/bold "Migrating..." cli-opts))) 31 | 32 | :up-to-date 33 | (fn [_] 34 | (println "Up-to-date.")) 35 | 36 | :copying 37 | (fn [{:keys [src dest]}] 38 | (println "Copying" src "to" dest)) 39 | 40 | :moving 41 | (fn [{:keys [src dest]}] 42 | (println "Moving" src "to" dest)) 43 | 44 | :canceled 45 | (fn [{:keys [cli-opts]}] 46 | (println (util/bold "Migration canceled." cli-opts))) 47 | 48 | :done 49 | (fn [{:keys [cli-opts]}] 50 | (println (util/bold "Migration complete." cli-opts))) 51 | 52 | :confirm-replace 53 | (fn [{:keys [dest cli-opts]}] 54 | (println (util/bold (str dest " already exists. Do you want to replace it? (yes/no) ") 55 | cli-opts)) 56 | (print "> ") 57 | (flush)) 58 | 59 | :skipping 60 | (fn [{:keys [src]}] 61 | (println "Skipping" src))}) 62 | 63 | (defn- printer [log-path cli-opts] 64 | (fn [k & {:as opts}] 65 | (let [cmd (if opts [k opts] [k])] 66 | (when log-path 67 | (spit log-path (prn-str cmd) :append true)) 68 | (if (:edn cli-opts) 69 | (prn cmd) 70 | ((get templates k) (assoc opts :cli-opts cli-opts)))))) 71 | 72 | (defn log-path [s migration-id] 73 | (str s "." migration-id ".log")) 74 | 75 | (defn src-backup-path [s migration-id] 76 | (str s ".src." migration-id ".backup")) 77 | 78 | (defn dest-backup-path [s migration-id] 79 | (str s ".dest." migration-id ".backup")) 80 | 81 | (defn- confirm-one-script [script-name {:keys [force] :as cli-opts} t] 82 | (let [bin-dest (str (fs/file (dirs/xdg-bin-dir cli-opts) (name script-name)))] 83 | (if (or force (not (fs/exists? bin-dest))) 84 | true 85 | (do 86 | (println) 87 | (t :confirm-replace {:dest bin-dest}) 88 | (= "yes" (str/trim (read-line))))))) 89 | 90 | (defn- confirm-all-scripts [scripts cli-opts t] 91 | (->> scripts 92 | (map (fn [[script-name _]] 93 | [script-name (confirm-one-script script-name cli-opts t)])) 94 | (remove (fn [[_ confirmed]] (nil? confirmed))) 95 | doall)) 96 | 97 | (defn- copy-script [script-name confirmed migration-id cli-opts t] 98 | (let [bin-src (str (fs/file (dirs/legacy-bin-dir) (name script-name))) 99 | bin-dest (str (fs/file (dirs/xdg-bin-dir cli-opts) (name script-name))) 100 | bin-dest-backup (str (fs/file (dest-backup-path (dirs/legacy-bin-dir) migration-id) 101 | (name script-name))) 102 | bat-src (str (fs/file (dirs/legacy-bin-dir) (str script-name ".bat"))) 103 | bat-dest (str (fs/file (dirs/xdg-bin-dir cli-opts) (str script-name ".bat"))) 104 | bat-dest-backup (str (fs/file (dest-backup-path (dirs/legacy-bin-dir) migration-id) 105 | (str script-name ".bat"))) 106 | jar-src (str (fs/file (dirs/legacy-jars-dir) (str script-name ".jar"))) 107 | jar-dest (str (fs/file (dirs/xdg-jars-dir cli-opts) (str script-name ".jar"))) 108 | jar-dest-backup (str (fs/file (dest-backup-path (dirs/legacy-jars-dir) migration-id) 109 | (str script-name ".jar"))) 110 | copy-bin (fn [] 111 | (when (fs/exists? bin-dest) 112 | (fs/create-dirs (fs/parent bin-dest-backup)) 113 | (t :copying {:src bin-dest :dest bin-dest-backup}) 114 | (fs/copy bin-dest bin-dest-backup {:replace-existing true})) 115 | (t :copying {:src bin-src :dest bin-dest}) 116 | (fs/copy bin-src bin-dest {:replace-existing true})) 117 | copy-bat (fn [] 118 | (when (fs/exists? bat-src) 119 | (when (fs/exists? bat-dest) 120 | (fs/create-dirs (fs/parent bat-dest-backup)) 121 | (t :copying {:src bat-dest :dest bat-dest-backup}) 122 | (fs/copy bat-dest bat-dest-backup {:replace-existing true})) 123 | (t :copying {:src bat-src :dest bat-dest}) 124 | (fs/copy bat-src bat-dest {:replace-existing true}))) 125 | copy-jar (fn [] 126 | (when (fs/exists? jar-src) 127 | (when (fs/exists? jar-dest) 128 | (fs/create-dirs (fs/parent jar-dest-backup)) 129 | (t :copying {:src jar-dest :dest jar-dest-backup}) 130 | (fs/copy jar-dest jar-dest-backup {:replace-existing true})) 131 | (t :copying {:src jar-src :dest jar-dest}) 132 | (spit bin-dest (str/replace (slurp bin-dest) jar-src jar-dest)) 133 | (fs/copy jar-src jar-dest {:replace-existing true})))] 134 | (if-not confirmed 135 | (t :skipping {:src bin-src}) 136 | (do 137 | (copy-bin) 138 | (copy-bat) 139 | (copy-jar))))) 140 | 141 | (defn move-legacy-dirs [t migration-id] 142 | (let [bin-src (str (dirs/legacy-bin-dir)) 143 | bin-dest (src-backup-path bin-src migration-id) 144 | jars-src (str (dirs/legacy-jars-dir)) 145 | jars-dest (src-backup-path jars-src migration-id)] 146 | (t :moving {:src bin-src :dest bin-dest}) 147 | (fs/move bin-src bin-dest) 148 | (when (fs/exists? jars-src) 149 | (t :moving {:src jars-src :dest jars-dest}) 150 | (fs/move jars-src jars-dest)))) 151 | 152 | (defn migrate-auto [{:keys [force] :as cli-opts}] 153 | (let [migration-id (inst-ms (util/now)) 154 | logp (when (dirs/using-legacy-paths?) 155 | (log-path (dirs/legacy-bin-dir) migration-id)) 156 | t (printer logp cli-opts)] 157 | (if-not (dirs/using-legacy-paths?) 158 | (t :up-to-date) 159 | (let [scripts (scripts/load-scripts (dirs/legacy-bin-dir))] 160 | (when (seq scripts) 161 | (println) 162 | (t :printable-scripts {:scripts scripts}) 163 | (println) 164 | (t :found-scripts) 165 | (when-not force 166 | (println) 167 | (t :prompt-move))) 168 | (flush) 169 | (if (or force (= "yes" (str/trim (read-line)))) 170 | (do 171 | (dirs/ensure-xdg-dirs cli-opts) 172 | (if-not (seq scripts) 173 | (do 174 | (println) 175 | (t :migrating) 176 | (println) 177 | (move-legacy-dirs t migration-id) 178 | (println) 179 | (t :done) 180 | (println)) 181 | (do 182 | (flush) 183 | (let [confirm-results (confirm-all-scripts scripts cli-opts t)] 184 | (println) 185 | (t :migrating) 186 | (doseq [[script-name confirmed] confirm-results] 187 | (copy-script script-name confirmed migration-id cli-opts t))) 188 | (move-legacy-dirs t migration-id) 189 | (println) 190 | (t :done) 191 | (println)))) 192 | (do 193 | (println) 194 | (t :canceled) 195 | (println))))))) 196 | 197 | (defn migrate-help [_] 198 | (println (str/triml " 199 | In bbin 0.2.0, we now use the XDG Base Directory Specification by default. 200 | This means the ~/.babashka/bbin/bin path is deprecated in favor of ~/.local/bin. 201 | 202 | To migrate your scripts automatically, run `bbin migrate auto`. We won't make 203 | any changes without asking first. 204 | 205 | Otherwise, you can either a) migrate manually or b) override: 206 | 207 | a) Migrate manually: 208 | - Move files in ~/.babashka/bbin/bin to ~/.local/bin 209 | - Move files in ~/.babashka/bbin/jars to ~/.cache/babashka/bbin/jars 210 | - For each script that uses a JAR, edit the line containing 211 | `(def script-jar ...)` to use the new \"~/.cache/babashka/bbin/jars\" path. 212 | 213 | b) Override: 214 | - Set the BABASHKA_BBIN_BIN_DIR env variable to \"$HOME/.babashka/bbin\" 215 | - Set the BABASHKA_BBIN_JARS_DIR env variable to \"$HOME/.babashka/jars\" 216 | "))) 217 | 218 | (defn migrate 219 | ([cli-opts] (migrate :root cli-opts)) 220 | ([command cli-opts] 221 | (case command 222 | :root (migrate-help cli-opts) 223 | :auto (migrate-auto cli-opts)))) 224 | -------------------------------------------------------------------------------- /test/babashka/bbin/migrate_test.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.migrate-test 2 | (:require [babashka.bbin.dirs :as dirs] 3 | [babashka.bbin.migrate :as migrate] 4 | [babashka.bbin.scripts :as scripts] 5 | [babashka.bbin.test-util 6 | :refer [bbin-dirs-fixture reset-test-dir test-dir]] 7 | [babashka.bbin.util :as util] 8 | [babashka.fs :as fs] 9 | [clojure.edn :as edn] 10 | [clojure.string :as str] 11 | [clojure.test :refer [deftest is testing use-fixtures]]) 12 | (:import (java.time Instant))) 13 | 14 | (use-fixtures :once (bbin-dirs-fixture)) 15 | 16 | (defn- parse-edn-out [s] 17 | (->> (str/split-lines s) 18 | (remove str/blank?) 19 | (mapv edn/read-string))) 20 | 21 | (deftest migrate-test 22 | (testing "migrate" 23 | (is (with-out-str (migrate/migrate {})))) 24 | 25 | (testing "migrate auto" 26 | 27 | (testing "scenario: no changes needed" 28 | (reset-test-dir) 29 | (let [commands [[:up-to-date]]] 30 | (is (= commands 31 | (->> (migrate/migrate :auto {:edn true}) 32 | (with-in-str "") 33 | with-out-str 34 | parse-edn-out))))) 35 | 36 | (testing "scenario: changes needed, user declines" 37 | (reset-test-dir) 38 | (fs/create-dirs (dirs/legacy-bin-dir)) 39 | (let [test-script (doto (fs/file test-dir "hello.clj") 40 | (spit "#!/usr/bin/env bb\n(println \"Hello world\")"))] 41 | (with-out-str (scripts/install {:script/lib (str (fs/canonicalize test-script))})) 42 | (fs/create-dirs (dirs/legacy-bin-dir)) 43 | (binding [util/*now* (Instant/ofEpochSecond 123)] 44 | (let [parsed-script (scripts/parse-script 45 | (slurp (fs/file (dirs/legacy-bin-dir) "hello"))) 46 | commands [[:printable-scripts {:scripts {'hello parsed-script}}] 47 | [:found-scripts] 48 | [:prompt-move] 49 | [:canceled]]] 50 | (is (= commands 51 | (->> (migrate/migrate :auto {:edn true}) 52 | (with-in-str "no\n") 53 | with-out-str 54 | parse-edn-out))) 55 | (is (= commands 56 | (parse-edn-out (slurp (migrate/log-path (dirs/legacy-bin-dir) 57 | (inst-ms (util/now))))))))))) 58 | 59 | (testing "scenario: changes needed, user accepts, no conflicts" 60 | (reset-test-dir) 61 | (fs/create-dirs (dirs/legacy-bin-dir)) 62 | (let [test-script (doto (fs/file test-dir "hello.clj") 63 | (spit "#!/usr/bin/env bb\n(println \"Hello world\")"))] 64 | (with-out-str (scripts/install {:script/lib (str (fs/canonicalize test-script))})) 65 | (binding [util/*now* (Instant/ofEpochSecond 123)] 66 | (let [parsed-script (scripts/parse-script 67 | (slurp (fs/file (dirs/legacy-bin-dir) "hello"))) 68 | commands-1 (filter identity 69 | [[:printable-scripts {:scripts {'hello parsed-script}}] 70 | [:found-scripts] 71 | [:prompt-move] 72 | [:migrating] 73 | [:copying {:src (str (fs/file (dirs/legacy-bin-dir) "hello")) 74 | :dest (str (fs/file (dirs/xdg-bin-dir nil) "hello"))}] 75 | (when (fs/windows?) 76 | [:copying {:src (str (fs/file (dirs/legacy-bin-dir) "hello.bat")) 77 | :dest (str (fs/file (dirs/xdg-bin-dir nil) "hello.bat"))}]) 78 | [:moving {:src (str (dirs/legacy-bin-dir)) 79 | :dest (migrate/src-backup-path 80 | (dirs/legacy-bin-dir) 81 | (inst-ms (util/now)))}] 82 | [:done]]) 83 | commands-2 [[:up-to-date]]] 84 | (is (= commands-1 85 | (->> (migrate/migrate :auto {:edn true}) 86 | (with-in-str "yes\n") 87 | with-out-str 88 | parse-edn-out))) 89 | (is (= commands-1 90 | (parse-edn-out (slurp (migrate/log-path (dirs/legacy-bin-dir) 91 | (inst-ms (util/now))))))) 92 | (is (= commands-2 93 | (->> (migrate/migrate :auto {:edn true}) 94 | (with-in-str "yes\n") 95 | with-out-str 96 | parse-edn-out))))))) 97 | 98 | (testing "scenario: changes needed, user accepts, script exists, skip" 99 | (reset-test-dir) 100 | (fs/create-dirs (dirs/legacy-bin-dir)) 101 | (let [test-script (doto (fs/file test-dir "hello.clj") 102 | (spit "#!/usr/bin/env bb\n(println \"Hello world\")"))] 103 | (with-out-str (scripts/install {:script/lib (str (fs/canonicalize test-script))})) 104 | (dirs/ensure-xdg-dirs nil) 105 | (fs/copy (fs/file (dirs/legacy-bin-dir) "hello") (fs/file (dirs/xdg-bin-dir nil) "hello")) 106 | (binding [util/*now* (Instant/ofEpochSecond 123)] 107 | (let [parsed-script (scripts/parse-script 108 | (slurp (fs/file (dirs/legacy-bin-dir) "hello"))) 109 | commands-1 [[:printable-scripts {:scripts {'hello parsed-script}}] 110 | [:found-scripts] 111 | [:prompt-move] 112 | [:confirm-replace {:dest (str (fs/file (dirs/xdg-bin-dir nil) "hello"))}] 113 | [:migrating] 114 | [:skipping {:src (str (fs/file (dirs/legacy-bin-dir) "hello"))}] 115 | [:moving {:src (str (dirs/legacy-bin-dir)) 116 | :dest (migrate/src-backup-path 117 | (dirs/legacy-bin-dir) 118 | (inst-ms (util/now)))}] 119 | [:done]] 120 | commands-2 [[:up-to-date]]] 121 | (is (= commands-1 122 | (->> (migrate/migrate :auto {:edn true}) 123 | (with-in-str "yes\nno\n") 124 | with-out-str 125 | parse-edn-out))) 126 | (is (= commands-1 127 | (parse-edn-out (slurp (migrate/log-path (dirs/legacy-bin-dir) 128 | (inst-ms (util/now))))))) 129 | (is (= commands-2 130 | (->> (migrate/migrate :auto {:edn true}) 131 | (with-in-str "yes\n") 132 | with-out-str 133 | parse-edn-out))))))) 134 | 135 | (testing "scenario: changes needed, user accepts, script exists, overwrite" 136 | (reset-test-dir) 137 | (fs/create-dirs (dirs/legacy-bin-dir)) 138 | (let [test-script (str "test-resources" fs/file-separator "hello.jar")] 139 | (with-out-str (scripts/install {:script/lib (str (fs/canonicalize test-script))})) 140 | (dirs/ensure-xdg-dirs nil) 141 | (fs/copy (fs/file (dirs/legacy-bin-dir) "hello") (fs/file (dirs/xdg-bin-dir nil) "hello")) 142 | (binding [util/*now* (Instant/ofEpochSecond 123)] 143 | (let [parsed-script (scripts/parse-script 144 | (slurp (fs/file (dirs/legacy-bin-dir) "hello"))) 145 | commands-1 (filter identity 146 | [[:printable-scripts {:scripts {'hello parsed-script}}] 147 | [:found-scripts] 148 | [:prompt-move] 149 | [:confirm-replace {:dest (str (fs/file (dirs/xdg-bin-dir nil) "hello"))}] 150 | [:migrating] 151 | [:copying {:src (str (fs/file (dirs/xdg-bin-dir nil) "hello")) 152 | :dest (str (fs/file (migrate/dest-backup-path 153 | (dirs/legacy-bin-dir) 154 | (inst-ms (util/now))) 155 | "hello"))}] 156 | [:copying {:src (str (fs/file (dirs/legacy-bin-dir) "hello")) 157 | :dest (str (fs/file (dirs/xdg-bin-dir nil) "hello"))}] 158 | (when (fs/windows?) 159 | [:copying {:src (str (fs/file (dirs/legacy-bin-dir) "hello.bat")) 160 | :dest (str (fs/file (dirs/xdg-bin-dir nil) "hello.bat"))}]) 161 | [:copying {:src (str (fs/file (dirs/legacy-jars-dir) "hello.jar")) 162 | :dest (str (fs/file (dirs/xdg-jars-dir nil) "hello.jar"))}] 163 | [:moving {:src (str (dirs/legacy-bin-dir)) 164 | :dest (migrate/src-backup-path 165 | (dirs/legacy-bin-dir) 166 | (inst-ms (util/now)))}] 167 | [:moving {:src (str (dirs/legacy-jars-dir)) 168 | :dest (migrate/src-backup-path 169 | (dirs/legacy-jars-dir) 170 | (inst-ms (util/now)))}] 171 | [:done]]) 172 | commands-2 [[:up-to-date]]] 173 | (is (= commands-1 174 | (->> (migrate/migrate :auto {:edn true}) 175 | (with-in-str "yes\nyes\n") 176 | with-out-str 177 | parse-edn-out))) 178 | (is (= commands-1 179 | (parse-edn-out (slurp (migrate/log-path (dirs/legacy-bin-dir) 180 | (inst-ms (util/now))))))) 181 | (is (= commands-2 182 | (->> (migrate/migrate :auto {:edn true}) 183 | (with-in-str "yes\n") 184 | with-out-str 185 | parse-edn-out))))))))) 186 | 187 | (comment 188 | (clojure.test/run-test-var #'migrate-test) 189 | (clojure.test/run-tests)) 190 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [bbin](https://github.com/babashka/bbin): Install any Babashka script or project with one command 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ## 0.2.5 14 | 15 | - [Fix #78: Installation fails if $GITLIBS are set](https://github.com/babashka/bbin/issues/73) ([@Ramblurr](https://github.com/babashka/bbin/issues/73)) 16 | - [Fix #98: main-opts is limited to two values?](https://github.com/babashka/bbin/issues/98) 17 | 18 | ## 0.2.4 19 | 20 | - [Fix #88: NPE when using `bbin ls` in dirs with zero-length files](https://github.com/babashka/bbin/issues/88) 21 | 22 | ## 0.2.3 23 | 24 | - [Fix error in compiled script when installing from Homebrew (again)](https://github.com/babashka/bbin/commit/f0a3096a1e57408af77eed35f86a3d71cccccb07) 25 | 26 | ## 0.2.2 27 | 28 | - [Fix #62: bbin ls is unnecessarily slow](https://github.com/babashka/bbin/issues/62) 29 | - [Fix #72: bbin install [LOCAL-FILE] should not be restricted to files with the .clj extension](https://github.com/babashka/bbin/issues/72) 30 | 31 | ## 0.2.1 32 | 33 | - [Fix error in compiled script when installing from Homebrew](https://github.com/babashka/bbin/commit/ba1749a3308744c9dcecc1f032214aeb109bb073) 34 | 35 | ## 0.2.0 36 | 37 | **BREAKING CHANGES:** 38 | 39 | - `bbin` now follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). 40 | - The `BABASHKA_BBIN_FLAG_XDG` flag is no longer used. 41 | - If you're still using `~/.babashka/bbin/bin`, `bbin` will print a warning. 42 | - To remove this warning, run `bbin migrate` for instructions on how to 43 | - run an automatic migration 44 | - migrate manually 45 | - revert to existing paths 46 | - `bbin ls` and `bbin install` now print human-readable text by default. 47 | - The `BABASHKA_BBIN_FLAG_PRETTY_OUTPUT` flag is no longer used. 48 | - Pass in the `--edn` option to revert to the `0.1.x` behavior. 49 | 50 | **Changed paths:** 51 | 52 | - New: 53 | - Scripts: `~/.local/bin` 54 | - Cached JARs: `~/.cache/babashka/bbin/jars` 55 | - Old: 56 | - Scripts: `~/.babashka/bbin/bin` 57 | - Cached JARs: `~/.babashka/bbin/jars` 58 | 59 | **Fixed issues:** 60 | 61 | - [Fix #35: Use Freedesktop specification for default paths](https://github.com/babashka/bbin/issues/35) 62 | - [Fix #53: bbin should print human-readable text first and edn as an optional format](https://github.com/babashka/bbin/issues/53) 63 | - [Fix #65: BUG: uninstall not working for some scripts](https://github.com/babashka/bbin/issues/65) 64 | 65 | ## 0.1.13 66 | 67 | - [Fix #61: Disable `*print-namespace-maps*` when printing EDN](https://github.com/babashka/bbin/issues/61) ([@eval](http://github.com/eval)) 68 | - [Upcoming fixes for #53: bbin should print human-readable text first and edn as an optional format](https://github.com/babashka/bbin/issues/53) 69 | - [#54: `bbin ls` prints human readable text](https://github.com/babashka/bbin/pull/54) ([@eval](http://github.com/eval)) 70 | - The new output format is currently disabled by default in `0.1.x` releases. 71 | - Set `BABASHKA_BBIN_FLAG_PRETTY_OUTPUT=true` to enable the new behavior. See the PR for [updated docs](https://github.com/eval/bbin/blob/afd33ed720f84dccae907f1b59d51c19536448e5/README.md#bbin-ls). 72 | - Since changing the default output format is a breaking change, the flag will be removed in an upcoming `0.2.0` release. 73 | - We're adding an `--edn` option to existing `bbin` commands to support raw data as output. 74 | 75 | ## 0.1.12 76 | 77 | - [Fix #60: `XDG_DATA_HOME` does not work](https://github.com/babashka/bbin/issues/60) 78 | 79 | ## 0.1.11 80 | 81 | - [Fix #55: Error when installing from a Git repo with no tags](https://github.com/babashka/bbin/issues/55) 82 | - [infer: Use latest SHA when no tag is found](https://github.com/rads/deps-info/commit/6d323ba978502635c4cf2f8f1da4ff04f48240ca) 83 | 84 | ## 0.1.10 85 | 86 | - [Fix #57: escaping issue with local/root install on Windows](https://github.com/babashka/bbin/issues/57) 87 | - [Fix #52: git url install does not handle a dot in the name](https://github.com/babashka/bbin/issues/52) 88 | 89 | ## 0.1.9 90 | 91 | - [Bump `deps-info` version to `0.1.0`](https://github.com/babashka/bbin/commit/a1291ab9a61996bcafb135ebadc775a3a07f92b0) 92 | - Fixes an error when encountering Git tags named without a `v` prefix (thanks [@eval](https://github.com/eval)) 93 | - [Upgrade dependency versions](https://github.com/babashka/bbin/commit/178fadcc4cfd0e239b279651a4dcfe5e85ab9633) 94 | 95 | ## 0.1.8 96 | 97 | - [Fix missing `babashka.bbin.specs` ns in generated script](https://github.com/babashka/bbin/commit/9a67b1ed2e7c90fc4eedf2710d9ee8df34ea896b) 98 | 99 | ## 0.1.7 100 | 101 | - [Validate `:bbin/bin` config](https://github.com/babashka/bbin/commit/b6fe7dc6ce2bc4ee56205e181e71634d503cca02) 102 | - [Add support for Git URLs without explicit lib name](https://github.com/babashka/bbin/commit/2ef56e19109fab3e8150819a0eaa8f63298da43b) 103 | - [Run jars without process/exec](https://github.com/babashka/bbin/commit/af7d140d4dfb109ca1930e1f77115289ec067967) (thanks [@jeroenvandijk](https://github.com/jeroenvandijk)!) 104 | 105 | ## 0.1.6 106 | 107 | - [Bump `io.github.rads/deps-info` to `v0.0.11`](https://github.com/babashka/bbin/commit/82dbee1e0f472cf4f6dda82858c848ed5b4a0709) 108 | - New features: 109 | - [Support inference for private Git repos](https://github.com/babashka/bbin/issues/48) 110 | - [Support all possible "lib to url" cases](https://github.com/babashka/bbin/issues/3) 111 | 112 | ## 0.1.5 113 | 114 | - [Support installing script files without shebang](https://github.com/babashka/bbin/commit/d4103e26db3c5c94f9ed7414c1d5fcd988b40e34) 115 | 116 | ## 0.1.4 117 | 118 | - [Replace `babashka.curl` with `org.httpkit.client`](https://github.com/babashka/bbin/commit/55f942bfccb8e3095ba715e242c99a1c030cf0e9) 119 | - [Add opt-in flag for "Use Freedesktop specification for default paths"](https://github.com/babashka/bbin/commit/6fde1b1dbfaef3063eb1eba4899a730bf703c792) 120 | - We're currently working on making `bbin` follow the Freedesktop spec more closely, which means we need to change the default bin path ([\#35](https://github.com/babashka/bbin/issues/35)) 121 | - In a future `0.2.0` release, `bbin` will change its default bin path from `~/.babashka/bbin/bin` to `~/.local/bin` 122 | - For versions `>=0.1.4`, the new default behavior can be enabled by setting an env variable: 123 | ``` 124 | $ bbin bin 125 | /Users/rads/.babashka/bbin/bin 126 | 127 | $ BABASHKA_BBIN_FLAG_XDG=true bbin bin 128 | /Users/rads/.local/bin 129 | ``` 130 | - The flag will not have any effect when used with `0.2.0`. It's only for previewing the upcoming changes 131 | - [Fix "local installs without aliases throw exception on Windows"](https://github.com/babashka/bbin/commit/748722178824d7e2ff76544bfc7c23def8ce708c) (thanks [@bobisageek](https://github.com/bobisageek)!) 132 | 133 | ## 0.1.3 134 | 135 | - [Fix script args being ignored when installing JARs](https://github.com/babashka/bbin/commit/ac85b8f984c8a30683c219d8d0faa32ef91e93e2) 136 | 137 | ## 0.1.2 138 | 139 | - [Add support for HTTP and local JARs](https://github.com/babashka/bbin/commit/58d48df19969aaf5e7ff8ea0b87330e2d1e67568) 140 | - [Add support for installing local directories](https://github.com/babashka/bbin/commit/268de01de73f26e8256498d33f508c61a3c5663d) 141 | 142 | ## 0.1.1 143 | 144 | - [Add support for installing local files](https://github.com/babashka/bbin/commit/675c5826a633e10a1e997870dcf0cb28867c411f) 145 | - [Coerce script names to snake-case](https://github.com/babashka/bbin/commit/7235b2c291400f7074232c2ef1230fe3e9652f23) 146 | 147 | ## 0.1.0 148 | 149 | - [Remove alpha status warning](https://github.com/babashka/bbin/commit/ea7dc1999a0e928ce749520d64d6a833e8bba686) 150 | - [Add docs for all supported options](https://github.com/babashka/bbin/commit/add4e2b2613a7503e49110ca711f7815734e9aed) 151 | - [Add support for overriding bbin root via env variables](https://github.com/babashka/bbin/commit/a24775cfd8637541caee42d320be4a3882bf5219) 152 | 153 | ## 0.0.12 154 | 155 | - [Change root dir from `~/.bbin` to `~/.babashka/bbin`](https://github.com/babashka/bbin/commit/99a5d2684f4e979ff8f183a2ce8088f3df26b405) 156 | - [Improve script readability](https://github.com/babashka/bbin/commit/04c2e1851eae335a8a1b57118b7a4af78c3f4b1c) 157 | - [Remove bash scripts](https://github.com/babashka/bbin/commit/f889f1a53620f87ac42af54015d85ecf0f70c7d0) 158 | - [Do not stringify args (#24)](https://github.com/babashka/bbin/commit/e5b8daf6b71e5e51e8fb948ba677eaa748416218) (thanks [@borkdude](https://github.com/borkdude)!) 159 | 160 | ## 0.0.11 161 | 162 | - [Windows Support](https://github.com/babashka/bbin/commit/378e7e7728d19b7800798afe73f2d1d2e4831273) (thanks [@bobisageek](https://github.com/bobisageek)!) 163 | - [Bump `:min-bb-version` to `0.9.162`](https://github.com/babashka/bbin/commit/52bc0d053abef6c0a7744d0eb2045096ad0dc533) 164 | - [Add `bbin version`](https://github.com/babashka/bbin/commit/7e5bef4d077afc1f20a5aa288f317ffe1bb1a8e1) 165 | - [Add `bbin --version`](https://github.com/babashka/bbin/commit/6e066bd2005d930d5f5171e8a678beb16bce8546) 166 | 167 | ## 0.0.10 168 | 169 | - [Remove `bbin trust` and `bbin revoke`](https://github.com/babashka/bbin/commit/6c1b44cd5d09415779557084d63ca4af325acae1) 170 | - [Remove script name checks](https://github.com/babashka/bbin/commit/3c0730011b1c74514600beb476fa7713d2c30671) 171 | 172 | ## 0.0.9 173 | 174 | - [Fix `bbin commands`](https://github.com/babashka/bbin/commit/c341c270ea2d5744c156fc719a6579f6c48549d2) 175 | - [Check for reserved script names](https://github.com/babashka/bbin/commit/52887c3f9948a8b4e466766dfd06d3a35d443277) 176 | - [Support Git and local installs when `bb.edn` file is missing](https://github.com/babashka/bbin/commit/a0bc556fc44c2d70e83bc0c387a3f3c716c25743) 177 | 178 | ## 0.0.8 179 | 180 | - [Restrict `bbin` alias to installs from official repo](https://github.com/babashka/bbin/commit/d343c37d7d045f294cff928319ffe7f9fa39617a) 181 | - [Add info message about password request](https://github.com/babashka/bbin/commit/28911b65a21ac96b66fa47e10b16ffdf5680e4ab) 182 | 183 | ## 0.0.7 184 | 185 | - [Fix `sudo` being required in `bbin install` command](https://github.com/babashka/bbin/commit/8fb8a8d2b8186ab0e22cde978cfeae3ce7ce4d1d) 186 | 187 | ## 0.0.6 188 | 189 | - [Use sudo user and group instead of hard-coded `root:wheel`](https://github.com/babashka/bbin/commit/e3d77ac6e26b9676bf898e60142499c9738c1877) 190 | - [Fix missing `util` ns in `gen-script`](https://github.com/babashka/bbin/commit/96c54c3e7ad3ab3d4af9cff0d830fc7c5f0ca5a8) 191 | 192 | ## 0.0.5 193 | 194 | - [Require privileged access for `~/.bbin/trust`](https://github.com/babashka/bbin/commit/ae6ca2fb2ac5a8c763ebb475151b5eddd4426809) 195 | 196 | ## 0.0.4 197 | 198 | - [Rename to `babashka.bbin`](https://github.com/babashka/bbin/commit/6322b4d2bbb6e44589875057123c8e59cc5dfe6d) 199 | 200 | ## 0.0.3 201 | 202 | - [Add `bbin trust` and `bbin revoke`](https://github.com/babashka/bbin/commit/3aa49be0a35bd8f77a72b26ffc4ac452bec75684) 203 | 204 | ## 0.0.2 205 | 206 | - [Add `:min-bb-version` to `bb.edn`](https://github.com/babashka/bbin/commit/af09bebea56720118ca80aacb0fedcd96acc9624) 207 | - [Add support for `:mvn/version` coordinates](https://github.com/babashka/bbin/commit/a30f1747b2147616f949b947d01b6023e56ce477) 208 | - [Use `:bbin/url` instead of `:http/url`](https://github.com/babashka/bbin/commit/c09a955c473a8838de79509190d6bc088931afba) 209 | - [Use `deps.edn` for deps instead of `bb.edn`](https://github.com/babashka/bbin/commit/6295ae344e455b0e85fbe7da96ddda9acf7fdf89) 210 | 211 | ## 0.0.1 212 | 213 | - First release 214 | -------------------------------------------------------------------------------- /src/babashka/bbin/util.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.util 2 | (:require [babashka.bbin.meta :as meta] 3 | [babashka.fs :as fs] 4 | [babashka.process :as p] 5 | [clojure.edn :as edn] 6 | [clojure.pprint :as pprint] 7 | [clojure.string :as str] 8 | [taoensso.timbre :as log]) 9 | (:import (java.time Instant))) 10 | 11 | (def ^:dynamic *now* nil) 12 | 13 | (defn now [] 14 | (or *now* (Instant/now))) 15 | 16 | (defn canonicalized-cli-opts [cli-opts] 17 | (merge cli-opts 18 | (when-let [v (:local/root cli-opts)] 19 | {:local/root (str (fs/canonicalize v {:nofollow-links true}))}))) 20 | 21 | (defn is-tty 22 | [fd key] 23 | (-> ["test" "-t" (str fd)] 24 | (p/process {key :inherit :env {}}) 25 | deref 26 | :exit 27 | (= 0))) 28 | 29 | (def tty-out? (memoize #(is-tty 1 :out))) 30 | 31 | (defn terminal-dimensions 32 | "Yields e.g. `{:cols 30 :rows 120}`" 33 | [] 34 | (-> 35 | (p/process ["stty" "size"] {:inherit true :out :string}) 36 | deref 37 | :out 38 | str/trim 39 | (str/split #" ") 40 | (->> (map #(Integer/parseInt %)) 41 | (zipmap [:rows :cols])))) 42 | 43 | (defn sh [cmd & {:as opts}] 44 | (doto (p/sh cmd (merge {:err :inherit} opts)) 45 | p/check)) 46 | 47 | (defn set-logging-config! [{:keys [debug]}] 48 | (log/merge-config! {:min-level (if debug :debug :warn)})) 49 | 50 | (defn pprint [x & _] 51 | (binding [*print-namespace-maps* false] 52 | (pprint/pprint x))) 53 | 54 | (defn upgrade-enabled? [] 55 | (some-> (System/getenv "BABASHKA_BBIN_FLAG_UPGRADE") 56 | edn/read-string)) 57 | 58 | (defn truncate 59 | "Truncates `s` when it exceeds length `truncate-to` by inserting `omission` at the given `omission-position`. 60 | 61 | The result's length will equal `truncate-to`, unless `truncate-to` < `omission`-length, in which case the result equals `omission`. 62 | 63 | Examples: 64 | ```clojure 65 | (truncate \"1234567\" {:truncate-to 7}) 66 | # => \"1234567\" 67 | 68 | (truncate \"1234567\" {:truncate-to 5}) 69 | # => \"12...\" 70 | 71 | (truncate \"1234567\" {:truncate-to 5 :omission \"(continued)\"}) 72 | # => \"(continued)\" 73 | 74 | (truncate \"example.org/path/to/release/v1.2.3/server.jar\" 75 | {:omission \"…\" :truncate-to 35 :omission-position :center}) 76 | # => \"example.org/path/…v1.2.3/server.jar\" 77 | ``` 78 | 79 | Options: 80 | - `truncate-to` (`30`) length above which truncating will occur. The resulting string will have this length (assuming `(> truncate-to (count omission))`). 81 | - `omission` (`\"...\"`) what to use as omission. 82 | - `omission-position` (`:end`) where to put omission. Options: `#{:center :end}`. 83 | " 84 | [s {:keys [omission truncate-to omission-position] 85 | :or {omission "..." truncate-to 30 omission-position :end}}] 86 | (if-not (> (count s) truncate-to) 87 | s 88 | (let [truncated-s-length (max 0 (- truncate-to (count omission))) 89 | [lsub-len rsub-len] (case omission-position 90 | :end [truncated-s-length 0] 91 | :center (if (even? truncated-s-length) 92 | [(/ truncated-s-length 2) (/ truncated-s-length 2)] 93 | [(/ (inc truncated-s-length) 2) (/ (dec truncated-s-length) 2)]))] 94 | (str (subs s 0 lsub-len) 95 | omission 96 | (subs s (- (count s) rsub-len) (count s)))))) 97 | 98 | (defn plain-mode? [{:keys [plain] :as _cli-opts}] 99 | (or (fs/windows?) plain (not (tty-out?)))) 100 | 101 | (defn no-color? [{:keys [color] :as cli-opts}] 102 | (or (false? color) 103 | (plain-mode? cli-opts) 104 | (System/getenv "NO_COLOR") 105 | (= "dumb" (System/getenv "TERM")))) 106 | 107 | (defn bold [s cli-opts] 108 | (if (no-color? cli-opts) s (str "\033[1m" s "\033[0m"))) 109 | 110 | (defn printable-scripts [scripts] 111 | (map (fn [[bin {{lroot :local/root 112 | gtag :git/tag 113 | gsha :git/sha 114 | gurl :git/url 115 | burl :bbin/url} :coords}]] 116 | (cond-> (assoc {} :bin bin) 117 | gurl (assoc :location gurl) 118 | burl (assoc :location burl) 119 | lroot (assoc :location (str "file://" lroot)) 120 | gsha (assoc :version gsha) 121 | gtag (assoc :version gtag))) 122 | scripts)) 123 | 124 | (defn print-table 125 | "Print table to stdout. 126 | 127 | Examples: 128 | ```clojure 129 | ;; Extract columns from rows 130 | (print-table [{:a \"one\" :b \"two\"}]) 131 | 132 | a b 133 | ─── ─── 134 | one two 135 | 136 | ;; Provide columns (as b is an empty column, it will be skipped) 137 | (print-table [:a :b] [{:a \"one\" :b nil}]) 138 | 139 | a 140 | ─── 141 | one 142 | 143 | ;; Ensure all columns being shown: 144 | (print-table [:a :b] [{:a \"one\"}] {:show-empty-columns true}) 145 | 146 | ;; Provide columns with labels and apply column coercion 147 | (print-table {:a \"option A\" :b \"option B\"} [{:a \"one\" :b nil}] 148 | {:column-coercions {:b (fnil boolean false)}}) 149 | 150 | option A option B 151 | ──────── ──────── 152 | one false 153 | 154 | ;; Provide `max-width` and `:width-reduce-column` to try to make the table fit smaller screens. 155 | (print-table {:a \"123456\"} {:max-width 5 :width-reduce-column :a}) 156 | 157 | a 158 | ───── 159 | 12... 160 | 161 | ;; A custom `width-reduce-fn` can be provided. See options for details. 162 | (print-table {:a \"123456\"} {:max-width 5 163 | :width-reduce-column :a 164 | :width-reduce-fn #(subs %1 0 %2)}) 165 | a 166 | ───── 167 | 12345 168 | 169 | ``` 170 | 171 | Options: 172 | - `column-coercions` (`{}`) fn that given a key `k` yields an fn to be applied to every `(k row)` *iff* row contains key `k`. 173 | See example above. 174 | - `skip-header` (`false`) don't print column names and divider (typically use this when stdout is no tty). 175 | - `show-empty-columns` (`false`) print every column, even if it results in empty columns. 176 | - `no-color` (`false`) prevent printing escape characters to stdout. 177 | - `max-width` (`nil`) when width of the table exceeds this value, `width-reduce-fn` will be applied to all cells of column `width-reduce-column`. NOTE: providing this, requires `width-reduce-column` to be provided as well. 178 | - `width-reduce-column` (`nil`) column that `width-reduce-fn` will be applied to when table width exceeds `max-width`. 179 | - `width-reduce-fn` (`#(truncate %1 {:truncate-to %2})`) function that is applied to all cells of column `width-reduce-column` when the table exceeds width `max-width`. 180 | The function should have 2-arity: a string (representing the cell value) and an integer (representing the max size of the cell contents in order for the table to stay within `max-width`)." 181 | ([rows] 182 | (print-table rows {})) 183 | ([ks-rows rows-opts] 184 | (let [rows->ks #(-> % first keys) 185 | [ks rows opts] (if (map? rows-opts) 186 | [(rows->ks ks-rows) ks-rows rows-opts] 187 | [ks-rows rows-opts {}])] 188 | (print-table ks rows opts))) 189 | ([ks rows {:as opts 190 | :keys [show-empty-columns skip-header no-color column-coercions 191 | max-width width-reduce-column width-reduce-fn] 192 | :or {show-empty-columns false skip-header false no-color false column-coercions {}}}] 193 | (assert (or (not max-width) (and max-width ((set ks) width-reduce-column))) 194 | (str "Option :max-width requires option :width-reduce-column to be one of " (pr-str ks))) 195 | (let [wrap-bold (fn [s] (if no-color s (str "\033[1m" s "\033[0m"))) 196 | row-get (fn [row k] 197 | (when (contains? row k) 198 | ((column-coercions k identity) (get row k)))) 199 | key->label (if (map? ks) ks #(subs (str (keyword %)) 1)) 200 | header-keys (if (map? ks) (keys ks) ks) 201 | ;; ensure all header-keys exist for every row and every value is a string 202 | rows (map (fn [row] 203 | (reduce (fn [acc k] 204 | (assoc acc k (str (row-get row k)))) {} header-keys)) rows) 205 | header-keys (if show-empty-columns 206 | header-keys 207 | (let [non-empty-cols (remove 208 | (fn [k] (every? str/blank? (map #(get % k) rows))) 209 | header-keys)] 210 | (filter (set non-empty-cols) header-keys))) 211 | header-labels (map key->label header-keys) 212 | column-widths (reduce (fn [acc k] 213 | (let [val-widths (map count (cons (key->label k) 214 | (map #(get % k) rows)))] 215 | (assoc acc k (apply max val-widths)))) {} header-keys) 216 | row-fmt (str/join " " (map #(str "%-" (column-widths %) "s") header-keys)) 217 | cells->formatted-row #(apply format row-fmt %) 218 | plain-header-row (cells->formatted-row header-labels) 219 | required-width (count plain-header-row) 220 | header-row (wrap-bold plain-header-row) 221 | max-width-exceeded? (and max-width 222 | (> required-width max-width)) 223 | div-row (wrap-bold 224 | (cells->formatted-row 225 | (map (fn [k] 226 | (apply str (take (column-widths k) (repeat \u2500)))) header-keys))) 227 | data-rows (map #(cells->formatted-row (map % header-keys)) rows)] 228 | (if-not max-width-exceeded? 229 | (when (seq header-keys) 230 | (let [header (if skip-header (vector) (vector header-row div-row))] 231 | (println (apply str (interpose \newline (into header data-rows)))))) 232 | (let [overflow (- required-width max-width) 233 | max-column-width (max 0 (- (column-widths width-reduce-column) overflow)) 234 | width-reduce-fn (or width-reduce-fn #(truncate %1 {:truncate-to %2})) 235 | coercion-fn #(width-reduce-fn % max-column-width)] 236 | (recur ks rows (assoc opts 237 | :max-width nil 238 | :column-coercions {width-reduce-column coercion-fn}))))))) 239 | 240 | (defn edn? [cli-opts] 241 | (:edn cli-opts)) 242 | 243 | (defn print-scripts [printable-scripts cli-opts] 244 | (let [no-color? (no-color? cli-opts) 245 | plain-mode? (plain-mode? cli-opts) 246 | skip-header? plain-mode? 247 | column-atts '(:bin :version :location) 248 | column-coercions {:version #(if (or plain-mode? (not= 40 (count %))) 249 | % 250 | (subs % 0 7))} 251 | max-width (when-not plain-mode? 252 | (:cols (terminal-dimensions))) 253 | location-truncate #(-> %1 254 | (str/replace #"^(file|https?):\/\/" "") 255 | (truncate {:truncate-to %2 256 | :omission "…" 257 | :omission-position :center}))] 258 | (print-table column-atts (sort-by :bin printable-scripts) {:skip-header skip-header? 259 | :max-width max-width 260 | :width-reduce-column :location 261 | :width-reduce-fn location-truncate 262 | :column-coercions column-coercions 263 | :no-color no-color?}))) 264 | 265 | (def help-commands 266 | (->> [{:command "bbin install" :doc "Install a script"} 267 | (when (upgrade-enabled?) 268 | {:command "bbin upgrade" :doc "Upgrade a script"}) 269 | {:command "bbin uninstall" :doc "Remove a script"} 270 | {:command "bbin ls" :doc "List installed scripts"} 271 | {:command "bbin bin" :doc "Display bbin bin folder"} 272 | {:command "bbin version" :doc "Display bbin version"} 273 | {:command "bbin help" :doc "Display bbin help"} 274 | {:command "bbin migrate" :doc "Migrate from bbin v0.1.x"}] 275 | (remove nil?))) 276 | 277 | (defn print-help [& _] 278 | (let [max-width (apply max (map #(count (:command %)) help-commands)) 279 | lines (->> help-commands 280 | (map (fn [{:keys [command doc]}] 281 | (format (str " %-" (inc max-width) "s %s") command doc))))] 282 | (println (str "Version: " meta/version)) 283 | (println) 284 | (println (str "Usage: bbin \n\n" (str/join "\n" lines))))) 285 | 286 | (def windows? 287 | (some-> (System/getProperty "os.name") 288 | (str/lower-case) 289 | (str/index-of "win"))) 290 | 291 | (defn print-version [& {:as opts}] 292 | (if (:help opts) 293 | (print-help) 294 | (println "bbin" meta/version))) 295 | 296 | (defn- parse-version [version] 297 | (mapv #(Integer/parseInt %) 298 | (-> version 299 | (str/replace "-SNAPSHOT" "") 300 | (str/split #"\.")))) 301 | 302 | (defn- satisfies-min-version? [current-version min-version] 303 | (let [[major-current minor-current patch-current] (parse-version current-version) 304 | [major-min minor-min patch-min] (parse-version min-version)] 305 | (or (> major-current major-min) 306 | (and (= major-current major-min) 307 | (or (> minor-current minor-min) 308 | (and (= minor-current minor-min) 309 | (>= patch-current patch-min))))))) 310 | 311 | (defn check-min-bb-version [] 312 | (let [current-bb-version (System/getProperty "babashka.version")] 313 | (when (and meta/min-bb-version (not= meta/min-bb-version :version-not-set)) 314 | (when-not (satisfies-min-version? current-bb-version meta/min-bb-version) 315 | (binding [*out* *err*] 316 | (println (str "WARNING: this project requires babashka " 317 | meta/min-bb-version " or newer, but you have: " 318 | current-bb-version))))))) 319 | 320 | (defn snake-case [s] 321 | (str/replace s "_" "-")) 322 | -------------------------------------------------------------------------------- /src/babashka/bbin/scripts/common.clj: -------------------------------------------------------------------------------- 1 | (ns babashka.bbin.scripts.common 2 | (:require [babashka.bbin.deps :as bbin-deps] 3 | [babashka.bbin.dirs :as dirs] 4 | [babashka.bbin.specs] 5 | [babashka.bbin.util :as util :refer [sh]] 6 | [babashka.deps :as deps] 7 | [babashka.fs :as fs] 8 | [clojure.edn :as edn] 9 | [clojure.main :as main] 10 | [clojure.spec.alpha :as s] 11 | [clojure.string :as str] 12 | [selmer.parser :as selmer] 13 | [selmer.util :as selmer-util]) 14 | (:import (java.util.jar JarFile))) 15 | 16 | (def bb-shebang-str "#!/usr/bin/env bb") 17 | 18 | (defn- bb-shebang? [s] 19 | (str/starts-with? s bb-shebang-str)) 20 | 21 | (defn insert-script-header [script-contents header] 22 | (let [prev-lines (str/split-lines script-contents) 23 | [prefix [shebang & code]] (split-with #(not (bb-shebang? %)) prev-lines) 24 | header (concat ["" 25 | "; :bbin/start" 26 | ";"] 27 | (map #(str "; " %) 28 | (str/split-lines 29 | (with-out-str 30 | (util/pprint header)))) 31 | [";" 32 | "; :bbin/end" 33 | ""]) 34 | next-lines (if shebang 35 | (concat prefix [shebang] header code) 36 | (concat [bb-shebang-str] header prefix))] 37 | (str/join "\n" next-lines))) 38 | 39 | (defn file-path->script-name [file-path] 40 | (-> file-path 41 | fs/file-name 42 | fs/strip-ext 43 | util/snake-case)) 44 | 45 | (defn http-url->script-name [http-url] 46 | (util/snake-case 47 | (first 48 | (str/split (last (str/split http-url #"/")) 49 | #"\.")))) 50 | 51 | (def windows-wrapper-extension ".bat") 52 | 53 | (defn install-script 54 | "Spits `contents` to `path` (adding an extension on Windows), or 55 | pprints them if `dry-run?` is truthy. 56 | Side-effecting." 57 | [script-name header path contents & {:keys [dry-run] :as cli-opts}] 58 | (let [path-str (str path)] 59 | (if dry-run 60 | (util/pprint {:script-file path-str 61 | :script-contents contents} 62 | dry-run) 63 | (do 64 | (spit path-str contents) 65 | (when-not util/windows? (sh ["chmod" "+x" path-str])) 66 | (when util/windows? 67 | (spit (str path-str windows-wrapper-extension) 68 | (str "@bb -f %~dp0" (fs/file-name path-str) " -- %*"))) 69 | (if (util/edn? cli-opts) 70 | (util/pprint header) 71 | (do 72 | (println) 73 | (util/print-scripts (util/printable-scripts {script-name header}) 74 | cli-opts) 75 | (println) 76 | (println (util/bold "Install complete." cli-opts)) 77 | (println))) 78 | nil)))) 79 | 80 | (defn- generate-deps-lib-name [git-url] 81 | (let [s (str "script-" 82 | (.hashCode git-url) 83 | "-" 84 | (-> git-url 85 | (str/replace #"[^a-zA-Z0-9-]" "-") 86 | (str/replace #"--+" "-")))] 87 | (symbol "org.babashka.bbin" s))) 88 | 89 | (defn local-lib-path 90 | ([script-deps] 91 | (local-lib-path script-deps (System/getenv "GITLIBS"))) 92 | ([script-deps gitlibs-env] 93 | (let [lib (key (first script-deps)) 94 | coords (val (first script-deps)) 95 | gitlibs-root (or (not-empty gitlibs-env) 96 | (str (fs/path (fs/home) ".gitlibs")))] 97 | (if (#{::no-lib} lib) 98 | (:local/root coords) 99 | (fs/expand-home (str/join fs/file-separator [gitlibs-root "libs" (namespace lib) (name lib) (:git/sha coords)])))))) 100 | 101 | (defn- load-bin-config [script-root] 102 | (let [bb-file (fs/file script-root "bb.edn") 103 | bb-edn (when (fs/exists? bb-file) 104 | (some-> bb-file slurp edn/read-string)) 105 | bin-config (:bbin/bin bb-edn)] 106 | (when bin-config 107 | (if (s/valid? :bbin/bin bin-config) 108 | bin-config 109 | (throw (ex-info (s/explain-str :bbin/bin bin-config) 110 | {:bbin/bin bin-config})))))) 111 | 112 | (defn default-script-config [cli-opts] 113 | (let [[ns name] (str/split (:script/lib cli-opts) #"/") 114 | top (last (str/split ns #"\."))] 115 | {:main-opts ["-m" (str top "." name)] 116 | :ns-default (str top "." name)})) 117 | 118 | (defn process-main-opts 119 | "Process main-opts, canonicalizing file paths that follow -f flags." 120 | [main-opts script-root] 121 | (->> main-opts 122 | (map-indexed (fn [i arg] 123 | (if (and (pos? i) (= "-f" (nth main-opts (dec i)))) 124 | (str (fs/canonicalize (fs/file script-root arg) 125 | {:nofollow-links true})) 126 | arg))) 127 | vec)) 128 | 129 | (def comment-char ";") 130 | 131 | (def ^:private local-dir-tool-template-str 132 | (str/trim " 133 | #!/usr/bin/env bb 134 | 135 | ; :bbin/start 136 | ; 137 | {{script/meta}} 138 | ; 139 | ; :bbin/end 140 | 141 | (require '[babashka.process :as process] 142 | '[babashka.fs :as fs] 143 | '[clojure.string :as str]) 144 | 145 | (def script-root {{script/root|pr-str}}) 146 | (def script-ns-default '{{script/ns-default}}) 147 | (def script-name (fs/file-name *file*)) 148 | 149 | (def tmp-edn 150 | (doto (fs/file (fs/temp-dir) (str (gensym \"bbin\"))) 151 | (spit (str \"{:deps {local/deps {:local/root \" (pr-str script-root) \"}}}\")) 152 | (fs/delete-on-exit))) 153 | 154 | (def base-command 155 | [\"bb\" \"--deps-root\" script-root \"--config\" (str tmp-edn)]) 156 | 157 | (defn help-eval-str [] 158 | (str \"(require '\" script-ns-default \") 159 | (def fns (filter #(fn? (deref (val %))) (ns-publics '\" script-ns-default \"))) 160 | (def max-width (->> (keys fns) (map (comp count str)) (apply max))) 161 | (defn pad-right [x] (format (str \\\"%-\\\" max-width \\\"s\\\") x)) 162 | (println (str \\\"Usage: \" script-name \" \\\")) 163 | (newline) 164 | (doseq [[k v] fns] 165 | (println 166 | (str \\\" \" script-name \" \\\" (pad-right k) \\\" \\\" 167 | (when (:doc (meta v)) 168 | (first (str/split-lines (:doc (meta v))))))))\")) 169 | 170 | (def first-arg (first *command-line-args*)) 171 | (def rest-args (rest *command-line-args*)) 172 | 173 | (if first-arg 174 | (process/exec 175 | (vec (concat base-command 176 | [\"-x\" (str script-ns-default \"/\" first-arg)] 177 | rest-args))) 178 | (process/exec (into base-command [\"-e\" (help-eval-str)]))) 179 | ")) 180 | 181 | (def ^:private deps-tool-template-str 182 | (str/trim " 183 | #!/usr/bin/env bb 184 | 185 | ; :bbin/start 186 | ; 187 | {{script/meta}} 188 | ; 189 | ; :bbin/end 190 | 191 | (require '[babashka.process :as process] 192 | '[babashka.fs :as fs] 193 | '[clojure.string :as str]) 194 | 195 | (def script-root {{script/root|pr-str}}) 196 | (def script-lib '{{script/lib}}) 197 | (def script-coords {{script/coords|str}}) 198 | (def script-ns-default '{{script/ns-default}}) 199 | (def script-name (fs/file-name *file*)) 200 | 201 | (def tmp-edn 202 | (doto (fs/file (fs/temp-dir) (str (gensym \"bbin\"))) 203 | (spit (str \"{:deps {\" script-lib script-coords \"}}\")) 204 | (fs/delete-on-exit))) 205 | 206 | (def base-command 207 | [\"bb\" \"--deps-root\" script-root \"--config\" (str tmp-edn)]) 208 | 209 | (defn help-eval-str [] 210 | (str \"(require '\" script-ns-default \") 211 | (def fns (filter #(fn? (deref (val %))) (ns-publics '\" script-ns-default \"))) 212 | (def max-width (->> (keys fns) (map (comp count str)) (apply max))) 213 | (defn pad-right [x] (format (str \\\"%-\\\" max-width \\\"s\\\") x)) 214 | (println (str \\\"Usage: \" script-name \" \\\")) 215 | (newline) 216 | (doseq [[k v] fns] 217 | (println 218 | (str \\\" \" script-name \" \\\" (pad-right k) \\\" \\\" 219 | (when (:doc (meta v)) 220 | (first (str/split-lines (:doc (meta v))))))))\")) 221 | 222 | (def first-arg (first *command-line-args*)) 223 | (def rest-args (rest *command-line-args*)) 224 | 225 | (if first-arg 226 | (process/exec 227 | (vec (concat base-command 228 | [\"-x\" (str script-ns-default \"/\" first-arg)] 229 | rest-args))) 230 | (process/exec (into base-command [\"-e\" (help-eval-str)]))) 231 | ")) 232 | 233 | (def ^:private local-dir-template-str 234 | (str/trim " 235 | #!/usr/bin/env bb 236 | 237 | ; :bbin/start 238 | ; 239 | {{script/meta}} 240 | ; 241 | ; :bbin/end 242 | 243 | (require '[babashka.process :as process] 244 | '[babashka.fs :as fs] 245 | '[clojure.string :as str]) 246 | 247 | (def script-root {{script/root|pr-str}}) 248 | (def script-main-opts {{script/main-opts}}) 249 | 250 | (def tmp-edn 251 | (doto (fs/file (fs/temp-dir) (str (gensym \"bbin\"))) 252 | (spit (str \"{:deps {local/deps {:local/root \" (pr-str script-root) \"}}}\")) 253 | (fs/delete-on-exit))) 254 | 255 | (def base-command 256 | (vec (concat [\"bb\" \"--deps-root\" script-root \"--config\" (str tmp-edn)] 257 | script-main-opts 258 | [\"--\"]))) 259 | 260 | (process/exec (into base-command *command-line-args*)) 261 | ")) 262 | 263 | (def ^:private git-or-local-template-str 264 | (str/trim " 265 | #!/usr/bin/env bb 266 | 267 | ; :bbin/start 268 | ; 269 | {{script/meta}} 270 | ; 271 | ; :bbin/end 272 | 273 | (require '[babashka.process :as process] 274 | '[babashka.fs :as fs] 275 | '[clojure.string :as str]) 276 | 277 | (def script-root {{script/root|pr-str}}) 278 | (def script-lib '{{script/lib}}) 279 | (def script-coords {{script/coords|str}}) 280 | (def script-main-opts {{script/main-opts}}) 281 | 282 | (def tmp-edn 283 | (doto (fs/file (fs/temp-dir) (str (gensym \"bbin\"))) 284 | (spit (str \"{:deps {\" script-lib script-coords \"}}\")) 285 | (fs/delete-on-exit))) 286 | 287 | (def base-command 288 | (vec (concat [\"bb\" \"--deps-root\" script-root \"--config\" (str tmp-edn)] 289 | script-main-opts 290 | [\"--\"]))) 291 | 292 | (process/exec (into base-command *command-line-args*)) 293 | ")) 294 | 295 | (defn install-deps-git-or-local [cli-opts {:keys [procurer] :as _summary}] 296 | (let [script-deps (cond 297 | (and (#{:local} procurer) (not (:local/root cli-opts))) 298 | {::no-lib {:local/root (str (fs/canonicalize (:script/lib cli-opts) {:nofollow-links true}))}} 299 | 300 | (bbin-deps/git-repo-url? (:script/lib cli-opts)) 301 | (bbin-deps/infer 302 | (cond-> (assoc cli-opts :lib (str (generate-deps-lib-name (:script/lib cli-opts))) 303 | :git/url (:script/lib cli-opts)) 304 | (not (some cli-opts [:latest-tag :latest-sha :git/sha :git/tag])) 305 | (assoc :latest-sha true))) 306 | 307 | :else 308 | (bbin-deps/infer (assoc cli-opts :lib (:script/lib cli-opts)))) 309 | lib (key (first script-deps)) 310 | coords (val (first script-deps)) 311 | header (merge {:coords coords} (when-not (#{::no-lib} lib) {:lib lib})) 312 | header' (if (#{::no-lib} lib) 313 | {:coords {:bbin/url (str "file://" (get-in header [:coords :local/root]))}} 314 | header) 315 | _ (when-not (#{::no-lib} lib) 316 | (deps/add-deps {:deps script-deps})) 317 | script-root (fs/canonicalize (or (get-in header [:coords :local/root]) 318 | (local-lib-path script-deps)) 319 | {:nofollow-links true}) 320 | bin-config (load-bin-config script-root) 321 | script-name (or (:as cli-opts) 322 | (some-> bin-config first key str) 323 | (and (not (#{::no-lib} lib)) 324 | (second (str/split (:script/lib cli-opts) #"/")))) 325 | _ (when (str/blank? script-name) 326 | (throw (ex-info "Script name not found. Use --as or :bbin/bin to provide a script name." 327 | header))) 328 | script-config (merge (when-not (#{::no-lib} lib) 329 | (default-script-config cli-opts)) 330 | (some-> bin-config first val) 331 | (when (:ns-default cli-opts) 332 | {:ns-default (edn/read-string (:ns-default cli-opts))})) 333 | script-edn-out (with-out-str 334 | (binding [*print-namespace-maps* false] 335 | (util/pprint header'))) 336 | tool-mode (or (:tool cli-opts) 337 | (and (some-> bin-config first val :ns-default) 338 | (not (some-> bin-config first val :main-opts)))) 339 | main-opts (or (some-> (:main-opts cli-opts) edn/read-string) 340 | (:main-opts script-config)) 341 | _ (when (and (not tool-mode) (not (seq main-opts))) 342 | (throw (ex-info "Main opts not found. Use --main-opts or :bbin/bin to provide main opts." 343 | {}))) 344 | template-opts {:script/meta (->> script-edn-out 345 | str/split-lines 346 | (map #(str comment-char " " %)) 347 | (str/join "\n")) 348 | :script/root script-root 349 | :script/lib (pr-str (key (first script-deps))) 350 | :script/coords (binding [*print-namespace-maps* false] (pr-str (val (first script-deps))))} 351 | template-opts' (if tool-mode 352 | (assoc template-opts :script/ns-default (:ns-default script-config)) 353 | (assoc template-opts :script/main-opts 354 | (process-main-opts main-opts script-root))) 355 | template-str (if tool-mode 356 | (if (#{::no-lib} lib) 357 | local-dir-tool-template-str 358 | deps-tool-template-str) 359 | (if (#{::no-lib} lib) 360 | local-dir-template-str 361 | git-or-local-template-str)) 362 | template-out (selmer-util/without-escaping 363 | (selmer/render template-str template-opts')) 364 | script-file (fs/canonicalize (fs/file (dirs/bin-dir cli-opts) script-name) {:nofollow-links true})] 365 | (install-script script-name header' script-file template-out cli-opts))) 366 | 367 | (defn jar->main-ns [jar-path] 368 | (with-open [jar-file (JarFile. (fs/file jar-path))] 369 | (let [main-attributes (some-> jar-file .getManifest .getMainAttributes) 370 | ;; TODO After July 17th 2023: Remove workaround below and start using (.getValue "Main-Class") instead 371 | ;; (see https://github.com/babashka/bbin/pull/47#discussion_r1071348344) 372 | main-class (some (fn [[k v]] (when (str/includes? k "Main-Class") v)) 373 | main-attributes)] 374 | (if main-class 375 | (main/demunge main-class) 376 | (throw (ex-info "jar has no Main-Class" {:jar-path jar-path})))))) 377 | 378 | (defn delete-files [cli-opts] 379 | (let [script-name (:script/lib cli-opts) 380 | script-file (fs/canonicalize (fs/file (dirs/bin-dir cli-opts) script-name) {:nofollow-links true})] 381 | (when (fs/delete-if-exists script-file) 382 | (when util/windows? (fs/delete-if-exists (fs/file (str script-file windows-wrapper-extension)))) 383 | (fs/delete-if-exists (fs/file (dirs/jars-dir cli-opts) (str script-name ".jar"))) 384 | (println "Removing" (str script-file))))) 385 | --------------------------------------------------------------------------------