├── CODEOWNERS ├── README.md ├── circle.yml ├── project.clj ├── resources └── bin │ └── real-time.sh ├── src └── leiningen │ └── jarbin.clj └── test └── leiningen └── jarbin_test.clj /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Placeholder CODEOWNERS file as this repo did not have one 2 | # Please make a PR adding the correct owner or request that this repo be archived 3 | * @circleci/default-security-operations 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lein-jarbin 2 | 3 | A lein plugin to run binaries contained a jar. 4 | 5 | This can be used for e.g. complicated system startup procedures, or daemonizing. Think of it as similar to lein-init-script, but without the installation process, or lein-daemon but less opinionated (and more reliable!). 6 | 7 | 8 | ## Usage 9 | 10 | `:plugins [[circleci/lein-jarbin "0.1.0"]]` 11 | 12 | $ lein jarbin [foo/bar "1.2.3"] bbq.sh 13 | $ lein jarbin ./foo-bar-1.2.3.jar bbq.sh 14 | 15 | Will run the script in resources/bin/bbq.sh, or in the jar. 16 | 17 | In the first usage, jarbin will download the jar if necessary. The jar should be resolvable from standard jar locations, or add a repo to your ~/.lein/profiles.clj 18 | 19 | The second usage takes a path to a local jar on the filesystem. The third usage is for running from the source tree. 20 | 21 | Extra args can be included on the command line: 22 | 23 | $ lein jarbin [foo/bar "1.2.3"] bbq foo 2 3 24 | 25 | Environment variables can be specified in the project.clj: 26 | 27 | :jarbin {:bin-dir "bin" ;; relative to resources. optional, defaults to "bin", 28 | :scripts {"bbq.sh" {:env {:foo "bar" 29 | :name :lein/name 30 | :version :lein/version 31 | :JVM_OPTS :lein/jvm-opts}}} 32 | 33 | Environment variables with the 'lein' namespace, like :lein/name will take their values from the same key in the project.clj 34 | 35 | There are a few more 'special' env vars: 36 | - :jarbin/coord, the coordinate passed to jarbin, i.e. "[foo/bar 1.2.3]" 37 | - :jarbin/jar-path, the path to the resolved jar 38 | 39 | ## Limitations 40 | Jarbin creates a temp directory, containing your project.clj and the script. Due to races, and jarbin not knowing when you're 'done' with the code, it never cleans up the temp directory. 41 | 42 | ## See Also 43 | 44 | lein-jartask. Very similar, but runs lein tasks, rather than scripts 45 | 46 | ## License 47 | 48 | Copyright © 2014 CircleCI 49 | 50 | Distributed under the Eclipse Public License either version 1.0 or (at 51 | your option) any later version. 52 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | override: 3 | - lein test 4 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject circleci/lein-jarbin "0.1.3-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[me.raynes/conch "0.6.0"]] 7 | :eval-in-leiningen true) 8 | -------------------------------------------------------------------------------- /resources/bin/real-time.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "." 3 | sleep 1 4 | echo "." 5 | sleep 1 6 | echo "." 7 | sleep 1 8 | echo "done!" 9 | -------------------------------------------------------------------------------- /src/leiningen/jarbin.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.jarbin 2 | (:require [leiningen.core.main :as main] 3 | [leiningen.core.project :as project] 4 | [cemerick.pomegranate.aether :as aether] 5 | [clojure.string :as str] 6 | [clojure.pprint :refer (pprint)] 7 | [clojure.java.io :as io] 8 | [me.raynes.conch.low-level :as sh]) 9 | (:import java.util.jar.JarFile 10 | (java.io PushbackReader 11 | StringReader 12 | File) 13 | (java.nio.file Files 14 | Path 15 | attribute.FileAttribute))) 16 | 17 | (defn project-repo-map 18 | "Returns the map of all repos. Use for resolving, not deploying" 19 | [project] 20 | (into {} (map (fn [[name repo]] 21 | [name (leiningen.core.user/resolve-credentials repo)]) (:repositories project)))) 22 | 23 | (defn resolve-coord 24 | "Resolve a single coordinate" 25 | [project coord] 26 | (aether/resolve-dependencies :coordinates [coord] :repositories (project-repo-map project))) 27 | 28 | (defn resolve-jar-path 29 | "Resolve the coordinates, returns the path to the jar file locally" 30 | [project coord] 31 | (->> (resolve-coord project coord) 32 | keys 33 | (filter #(= % coord)) 34 | first 35 | meta 36 | :file)) 37 | 38 | (def empty-file-attrs 39 | ;; several Files/ methods need this 40 | (into-array FileAttribute [])) 41 | 42 | (defn create-temp-dir [prefix] 43 | (str (Files/createTempDirectory prefix empty-file-attrs))) 44 | 45 | (defn extract-file-from-jar [jar dest-dir filename] 46 | (with-open [jar (JarFile. jar)] 47 | (let [entry (.getEntry jar filename) 48 | dest-name (.getName entry) 49 | dest-file (File. (str dest-dir File/separator dest-name)) 50 | dest-path (.toPath dest-file)] 51 | (Files/createDirectories (.getParent dest-path) empty-file-attrs) 52 | (with-open [i (.getInputStream jar entry)] 53 | (io/copy i dest-file))))) 54 | 55 | (defn parse-coord-str [coord-str] 56 | (let [[_ name version] (re-find #"\[([-./\w]+) (.+)\]" coord-str)] 57 | (if name 58 | [(symbol name) (str version)] 59 | (throw (Exception. (format "couldn't parse %s" coord-str)))))) 60 | 61 | (defn resolve-lein-env-var [project v] 62 | (let [resp (get project (keyword (name v)))] 63 | (if (sequential? resp) 64 | (str/join " " resp) 65 | resp))) 66 | 67 | (defn resolve-lein-env-vars 68 | "Returns a map of env vars and their resolved values" 69 | [project jarbin-env-vars script-name] 70 | (->> (get-in project [:jarbin :scripts script-name :env]) 71 | (map (fn [[k v]] 72 | (cond 73 | (and (keyword? v) (= "lein" (namespace v))) [(name k) (resolve-lein-env-var project v)] 74 | (and (keyword? v) (= "jarbin" (namespace v))) [(name k) (get jarbin-env-vars (name v))] 75 | :else [(name k) v]))) 76 | (into {}))) 77 | 78 | (defn parse-coord [args] 79 | (when-let [[_ coord rest-args] (re-find #"^(\[.+\])(.+)" (str/join " " args))] 80 | (let [rest-args (str/split (str/trim rest-args) #" ")] 81 | {:coord (parse-coord-str coord) 82 | :bin (first rest-args) 83 | :bin-args (rest rest-args)}))) 84 | 85 | 86 | (defn parse-local-jar [args] 87 | (when-let [path (first args)] 88 | (when (.isFile (File. path)) 89 | {:local-jar path 90 | :bin (second args) 91 | :bin-args (drop 2 args)}))) 92 | 93 | (defn parse-local-src [args] 94 | (when (= "." (first args)) 95 | {:local-src (first args) 96 | :bin (second args) 97 | :bin-args (drop 2 args)})) 98 | 99 | (defn parse-args [args] 100 | (or (parse-coord args) 101 | (parse-local-jar args) 102 | (throw (Exception. (str "Unsupported jar path: " (first args)))))) 103 | 104 | (defn bin-path-in-jar 105 | "Given the name of a script, return the in-jar location of the bin" 106 | [project bin-name] 107 | (let [bin-path (get-in project [:jarbin :bin-dir] "bin")] 108 | (str bin-path "/" bin-name))) 109 | 110 | (defn exec [{:keys [env dir cmd] :as args}] 111 | (let [proc (apply sh/proc (concat cmd [:dir (str dir) :env env])) 112 | stdout (future (sh/stream-to-out proc :out)) 113 | stderr (future (sh/stream-to-out proc :err)) 114 | exit (future (sh/exit-code proc))] 115 | (println "exit:" @exit) 116 | @stdout 117 | @stderr 118 | (main/exit @exit))) 119 | 120 | (defn setup-exec [jarbin-project {:keys [local-jar coord bin bin-args]}] 121 | (let [project-dir (create-temp-dir "jarbin") 122 | jar-path (or local-jar (resolve-jar-path jarbin-project coord)) 123 | _ (assert jar-path) 124 | _ (extract-file-from-jar jar-path project-dir "project.clj") 125 | project-path (str/join "/" [project-dir "project.clj"]) 126 | target-project (project/read project-path) 127 | bin-path (str/join "/" [project-dir (bin-path-in-jar target-project bin)]) 128 | jarbin-env-vars {"jar-path" (str jar-path) 129 | "coord" (str coord)}] 130 | (extract-file-from-jar jar-path project-dir (bin-path-in-jar target-project bin)) 131 | (sh/proc "chmod" "+x" (str bin-path)) 132 | {:dir project-dir 133 | :env (merge (into {} (System/getenv)) (resolve-lein-env-vars target-project jarbin-env-vars bin)) 134 | :cmd (concat [bin-path] bin-args)})) 135 | 136 | (defn ^:no-project-needed ^:higher-order jarbin 137 | "Run a script contained in jar 138 | 139 | Usage: 140 | 141 | lein jarbin [foo/bar \"1.2.3\"] bbq 142 | lein jarbin ./foo-bar-1.2.3.jar bbq 143 | " 144 | [project & args] 145 | (when (not-empty args) 146 | (as-> args % 147 | (parse-args %) 148 | (setup-exec project %) 149 | (exec %)))) 150 | -------------------------------------------------------------------------------- /test/leiningen/jarbin_test.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.jarbin-test 2 | (:require [clojure.test :refer :all] 3 | [leiningen.jarbin :as jarbin])) 4 | 5 | (deftest parse-coord-works 6 | (is (= '[foo/bar "1.2.3"] (jarbin/parse-coord-str "[foo/bar 1.2.3]"))) 7 | (is (= '[foo/bar-bar "1.2.3"] (jarbin/parse-coord-str "[foo/bar-bar 1.2.3]")))) 8 | 9 | (deftest parse-args-works 10 | (testing "coord" 11 | (is (= {:coord '[foo/bar "1.2.3"] 12 | :bin "bbq" 13 | :bin-args ["baz" "1"]}) (jarbin/parse-args ["[foo/bar" "1.2.3]" "bbq" "baz" "1"])))) 14 | 15 | (def test-project {:name "foo" 16 | :group "bar" 17 | :version "1.2.3" 18 | :jvm-opts ["-server" 19 | "-Xmx1024m"] 20 | :jarbin {:scripts {:bbq {:env {:FOO "bar" 21 | :NAME :lein/name 22 | :VERSION :lein/version 23 | :JVM_OPTS :lein/jvm-opts 24 | :JAR_PATH :jarbin/jar-path 25 | :COORD :jarbin/coord}}}}}) 26 | 27 | (deftest resolve-single-lein-env-var 28 | (is (= "foo" (jarbin/resolve-lein-env-var test-project :lein/name))) 29 | (is (= "-server -Xmx1024m" (jarbin/resolve-lein-env-var test-project :lein/jvm-opts)))) 30 | 31 | (deftest env-map-works 32 | (let [resp (jarbin/resolve-lein-env-vars test-project {} :bbq)] 33 | (is (= "bar" (get resp "FOO"))) 34 | (is (= "1.2.3" (get resp "VERSION"))))) 35 | 36 | (deftest env-map-exposes-jarbin 37 | (let [resp (jarbin/resolve-lein-env-vars test-project {"jar-path" "/foo/bar"} :bbq)] 38 | (is (= "/foo/bar" (get resp "JAR_PATH"))))) 39 | --------------------------------------------------------------------------------