├── .gitignore ├── README.md ├── build.boot └── src └── boot_bundle.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /.nrepl-history 2 | /.nrepl-port 3 | /build.boot.sample 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # boot-bundle 2 | Boot-bundle: managed dependencies for [boot](https://github.com/boot-clj/boot). 3 | 4 | [![Clojars Project](https://img.shields.io/clojars/v/boot-bundle.svg)](https://clojars.org/boot-bundle) 5 | 6 | Don't repeat yourself for library coordinates. Upgrade once, upgrade everywhere. 7 | 8 | ## Why 9 | The most common scenario for usage of this library is when you have a repository with multiple boot projects and these projects have overlapping dependencies that you want to manage in one place. That one place is the bundle file. 10 | 11 | ## Usage 12 | Define a bundle file that contains a map of keywords to either: 13 | - a single dependency 14 | - a vector of dependencies and/or keywords 15 | 16 | Example: 17 | ```clojure 18 | {:clojure [[org.clojure/clojure "1.8.0"] 19 | [clojure-future-spec "1.9.0-alpha13"] 20 | [org.clojure/test.check "0.9.0"] 21 | [org.clojure/core.async "0.2.391"]] 22 | :schema [prismatic/schema "1.1.3"] 23 | :component [[com.stuartsierra/component "0.3.1"] 24 | [org.clojure/tools.nrepl "0.2.12"] 25 | [reloaded.repl "0.2.3"]] 26 | :base [:clojure 27 | :schema 28 | :component 29 | [com.taoensso/timbre "4.7.4"]] 30 | :clojurescript [org.clojure/clojurescript "1.9.229"]} 31 | ``` 32 | 33 | Load boot-bundle before you load your other dependencies in `build.boot`: 34 | 35 | ```clojure 36 | (set-env! :dependencies 37 | '[[boot-bundle "0.1.1" :scope "test"] 38 | ;; if you share your bundle via clojars, uncomment and change: 39 | ;; [your-bundle "0.1.1" :scope "test"] 40 | ] 41 | ;; if you use a bundle file from the current project's classpath, uncomment: 42 | ;; :resource-paths #{"resources"} 43 | ) 44 | 45 | (require '[boot-bundle :refer [expand-keywords]]) 46 | ``` 47 | 48 | Wrap the `dependencies` vector in `set-env!` with `expand-keywords`: 49 | 50 | ```clojure 51 | (set-env! 52 | :source-paths #{"src"} 53 | :dependencies 54 | (expand-keywords 55 | '[:base 56 | :clojurescript 57 | ;; combine this with your remaining dependencies: 58 | [reagent "0.6.0"] 59 | ;; ... 60 | ])) 61 | ``` 62 | By default boot-bundle searches for the file `boot.bundle.edn` on the classpath. 63 | This can be overriden by setting either 64 | 65 | - the system property `boot.bundle.file`: 66 | ``` 67 | BOOT_JVM_OPTIONS="-Dboot.bundle.file=../bundle.edn" 68 | ``` 69 | - the environment variable `BOOT_BUNDLE_FILE`: 70 | 71 | ``` clojure 72 | BOOT_BUNDLE_FILE="../bundle.edn" 73 | ``` 74 | 75 | - the atom `bundle-file-path`: 76 | 77 | ``` clojure 78 | (reset! boot-bundle/bundle-file-path "../bundle.edn") 79 | ``` 80 | 81 | Searching the local file system has priority over searching the classpath. 82 | 83 | That's it. You can now use boot as you normally would. 84 | 85 | ## Advanced usage 86 | 87 | ### Manipulating the bundle map 88 | Boot-bundle lets you set the bundle map if you want to. For example, just write 89 | 90 | ```clojure 91 | (reset! boot-bundle/bundle-map 92 | (boot-bundle/read-from-file "../bundle.edn")) 93 | ``` 94 | Note that validation only happens when using `read-from-file`, so when doing 95 | something else, you may want to validate yourself: 96 | 97 | ```clojure 98 | (swap! boot-bundle/bundle-map 99 | #(boot-bundle/validate-bundle 100 | (assoc % :schema '[prismatic/schema "1.1.3"]))) 101 | ``` 102 | 103 | ### Versions 104 | 105 | The function `get-version` returns the version for a dependency by its keyword. This can be used to define the version of a project in `build.boot`. 106 | 107 | For example, in `boot.bundle.edn`: 108 | 109 | ```clojure 110 | {:myproject [myproject "0.1.0-SNAPSHOT"]} 111 | ``` 112 | 113 | In `myproject`'s `build.boot`: 114 | 115 | ```clojure 116 | (set-env! :dependencies 117 | '[[boot-bundle "0.1.1" :scope "test"]]) 118 | (require '[boot-bundle :refer [expand-keywords get-version]]) 119 | (def +version+ (get-version :myproject)) 120 | ``` 121 | 122 | Boot-bundle also supports version keywords. They are convenient if you need the same version on multiple dependencies. Version keywords are qualified with `version` and must refer to a string. 123 | 124 | Example usage: 125 | 126 | In `boot.bundle.edn`: 127 | 128 | ```clojure 129 | {:version/pedestal "0.5.1" 130 | :pedestal [[io.pedestal/pedestal.service :version/pedestal] 131 | [io.pedestal/pedestal.service-tools :version/pedestal] 132 | [io.pedestal/pedestal.jetty :version/pedestal] 133 | [io.pedestal/pedestal.immutant :version/pedestal] 134 | [io.pedestal/pedestal.tomcat :version/pedestal]]} 135 | ``` 136 | With every new Pedestal release, you only have to change the version in one place. 137 | 138 | ## Funding 139 | 140 | This software was commissioned and sponsored by [Doctor Evidence](http://doctorevidence.com/). The Doctor Evidence mission is to improve clinical outcomes by finding and delivering medical evidence to healthcare professionals, medical associations, policy makers and manufacturers through revolutionary solutions that enable anyone to make informed decisions and policies using medical data that is more accessible, relevant and readable. 141 | 142 | ## FAQ 143 | ### How can I distribute my bundle via clojars? 144 | 145 | Check out [this example](https://github.com/borkdude/boot.bundle.edn). 146 | 147 | ### Why isn't boot-bundle eating its own dog food? 148 | 149 | Boot-bundle is a lightweight library without any external dependencies. 150 | 151 | ### Can I use multiple bundles and merge them? 152 | 153 | Sure! 154 | ```clojure 155 | (reset! boot-bundle/bundle-map 156 | (merge 157 | (boot-bundle/read-from-file "bundle1.edn") 158 | (boot-bundle/read-from-file "bundle2.edn"))) 159 | ``` 160 | ### How do you use it? 161 | At work we use it in a multi-project repository. We have a `bundle.edn` file in the root and refer to it from most of the Clojure projects. 162 | 163 | ### How can I opt out? 164 | 165 | Start a REPL, eval the call to `expand-keywords` and substitute this result back into your `build.boot`. 166 | 167 | ```clojure 168 | $ boot repl 169 | boot.user=> (use 'clojure.pprint) 170 | boot.user=> (pprint (expand-keywords '[:clojure])) 171 | [[org.clojure/clojure "1.8.0"] 172 | [clojure-future-spec "1.9.0-alpha13"] 173 | [org.clojure/test.check "0.9.0"] 174 | [org.clojure/core.async "0.2.391"]] 175 | nil 176 | ``` 177 | 178 | ## License 179 | 180 | Copyright Michiel Borkent 2016. 181 | 182 | Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version. 183 | -------------------------------------------------------------------------------- /build.boot: -------------------------------------------------------------------------------- 1 | (def +version+ "0.1.1-SNAPSHOT") 2 | 3 | (set-env! 4 | :resource-paths #{"src"} 5 | :dependencies '[[adzerk/boot-test "1.1.2" :scope "test"] 6 | [adzerk/bootlaces "0.1.11" :scope "test"]]) 7 | 8 | (require '[adzerk.bootlaces :refer :all]) 9 | (bootlaces! +version+) 10 | 11 | (require '[adzerk.boot-test :refer :all]) 12 | 13 | (task-options! 14 | pom {:project 'boot-bundle 15 | :version +version+ 16 | :description "boot-bundle, DRY for dependencies" 17 | :url "https://github.com/borkdude/boot-bundle" 18 | :scm {:url "https://github.com/borkdude/boot-bundle"} 19 | :license {"EPL" "http://www.eclipse.org/legal/epl-v10.html"}}) 20 | -------------------------------------------------------------------------------- /src/boot_bundle.clj: -------------------------------------------------------------------------------- 1 | (ns boot-bundle 2 | (:require [clojure.edn :as edn] 3 | [clojure.java.io :as io] 4 | [clojure.test :refer [with-test 5 | is 6 | test-ns]])) 7 | 8 | (def bundle-file-path (atom nil)) 9 | (def bundle-map (atom nil)) 10 | 11 | (reset! bundle-file-path 12 | (or (System/getProperty "boot.bundle.file") 13 | (System/getenv "BOOT_BUNDLE_FILE") 14 | "boot.bundle.edn")) 15 | 16 | (with-test 17 | (defn version-key? [k] 18 | (and (keyword? k) 19 | (= "version" (namespace k)))) 20 | (is (false? (version-key? :clojure))) 21 | (is (true? (version-key? :version/clojure)))) 22 | 23 | (with-test 24 | (defn dependency-vector? [[group-artifact version]] 25 | (and (symbol? group-artifact) 26 | (or (string? version) 27 | (version-key? version)))) 28 | (is (false? (dependency-vector? '[1 2 3]))) 29 | (is (true? (dependency-vector? '[org.clojure/clojure "1.8.0"]))) 30 | (is (true? (dependency-vector? '[org.clojure/clojure :version/clojure])))) 31 | 32 | (with-test 33 | (defn version-key [[group-artifact version]] 34 | (when (version-key? version) 35 | version)) 36 | (is (= :version/clojure 37 | (version-key '[org.clojure/clojure :version/clojure]))) 38 | (is (nil? (version-key '[org.clojure/clojure "1.8.0"])))) 39 | 40 | (with-test 41 | (defn validate-bundle [m] 42 | (assert (map? m) "Bundle should be a map") 43 | (let [grouped (group-by 44 | (fn [[k v]] (version-key? k)) m) 45 | versions (into {} (grouped true)) 46 | dependencies (into {} (grouped false)) 47 | key-set (set (keys m)) 48 | dependency-key-set (set (keys dependencies)) 49 | dependency-values (vals dependencies) 50 | versions-key-set (set (keys versions)) 51 | valid-version-key? 52 | #(if-let [k (version-key %)] 53 | (contains? versions-key-set k) 54 | true) 55 | assert-vector #(assert 56 | (vector? %) 57 | (format "Bundle value should be vector: %s" %)) 58 | assert-valid-dependency 59 | #(do (assert (dependency-vector? %) 60 | (format "Invalid dependency vector: %s" %)) 61 | (assert (valid-version-key? %) 62 | (format "Invalid version key in: %s" %)))] 63 | ;; validate versions 64 | (doseq [[k v] versions] 65 | (assert (string? v) 66 | (format "Version value should be string: %s" v))) 67 | ;; validate dependencies 68 | (assert (every? keyword? key-set) 69 | "Bundle keys should be keywords.") 70 | (doseq [value dependency-values] 71 | (assert-vector value) 72 | (if (dependency-vector? value) 73 | (assert-valid-dependency value) 74 | (doseq [v value] 75 | (if (keyword? v) 76 | (assert (contains? dependency-key-set v) 77 | (format "Invalid key reference: %s" v)) 78 | (assert-valid-dependency v)))))) 79 | m) 80 | (is (validate-bundle {})) 81 | (is (validate-bundle '{:clj-time [clj-time "0.12.0"]})) 82 | (is (validate-bundle '{:clojure [[org.clojure/clojure "1.8.0"] 83 | [clojure-future-spec "1.9.0-alpha13"] 84 | [org.clojure/test.check "0.9.0"] 85 | [org.clojure/core.async "0.2.391"]]})) 86 | (is (validate-bundle '{:spec [clojure-future-spec "1.9.0-alpha13"] 87 | :clojure [[org.clojure/clojure "1.8.0"] 88 | :spec]})) 89 | (is (validate-bundle '{:version/util "0.3.5" 90 | :util [util :version/util]})) 91 | (is (thrown? java.lang.AssertionError 92 | (validate-bundle 1))) 93 | (is (thrown? java.lang.AssertionError 94 | (validate-bundle {:foo 1}))) 95 | (is (thrown? java.lang.AssertionError 96 | (validate-bundle {1 :foo}))) 97 | (is (thrown? java.lang.AssertionError 98 | (validate-bundle {:foo [:bar]}))) 99 | (is (thrown? java.lang.AssertionError 100 | (validate-bundle 101 | '{:clojure [org.clojure/clojure :version/clojure]})))) 102 | 103 | (defn read-from-file 104 | "Reads bundle file" 105 | ([] 106 | (read-from-file @bundle-file-path)) 107 | ([file-path] 108 | (let [file (io/file file-path)] 109 | (validate-bundle 110 | (if (.exists file) 111 | (do (println "boot-bundle found bundle file:" 112 | (.getAbsolutePath file)) 113 | (edn/read-string (slurp file))) 114 | (if-let [resource (io/resource file-path)] 115 | (do (println "boot-bundle found resource on classpath:" 116 | (str resource)) 117 | (edn/read-string (slurp resource))) 118 | (throw (RuntimeException. 119 | (str "boot-bundle file not found at " 120 | file-path))))))))) 121 | 122 | (defn get-bundle-map [] 123 | (or @bundle-map 124 | (reset! bundle-map (read-from-file)))) 125 | 126 | (declare expand-keywords) 127 | 128 | (with-test 129 | (defn get-version 130 | "Returns version defined by version key or version of single 131 | library defined by unqualified keyword." 132 | [k] 133 | (let [v (k (get-bundle-map))] 134 | (if (version-key? k) v 135 | (when (dependency-vector? v) 136 | (second v))))) 137 | (with-redefs [get-bundle-map (constantly 138 | '{:version/util "0.1.0" 139 | :clojure [org.clojure/clojure "1.8.0"]})] 140 | (is (= "0.1.0" (get-version :version/util))) 141 | (is (= "1.8.0" (get-version :clojure))) 142 | (is (nil? (get-version :foo))))) 143 | 144 | (with-test 145 | (defn expand-version [[group-artifact version & rest :as dependency]] 146 | (if (version-key? version) 147 | (into [group-artifact (get-version version)] 148 | rest) 149 | dependency)) 150 | (with-redefs [get-bundle-map (constantly '{:version/util "0.1.0"})] 151 | (is (= '[util "0.1.0" :scope "test"] 152 | (expand-version '[util "0.1.0" :scope "test"]))) 153 | (is (= '[util "0.1.0" :scope "test"] 154 | (expand-version '[util :version/util :scope "test"]))))) 155 | 156 | (with-test 157 | (defn expand-single-keyword 158 | "Expands keyword defined in bundle. 159 | Returns vector of dependencies." 160 | [k] 161 | (if-let [deps (k (get-bundle-map))] 162 | (mapv expand-version 163 | (expand-keywords 164 | (if (dependency-vector? deps) 165 | [deps] 166 | deps))) 167 | (throw 168 | (RuntimeException. 169 | (format "Invalid bundle key: %s" 170 | k))))) 171 | (with-redefs 172 | [get-bundle-map 173 | (constantly 174 | '{:spec [clojure-future-spec "1.9.0-alpha13"] 175 | :clojure [[org.clojure/clojure "1.8.0"] 176 | :spec] 177 | :version/pedestal "0.5.1" 178 | :pedestal [[io.pedestal/pedestal.service :version/pedestal] 179 | [io.pedestal/pedestal.service-tools :version/pedestal] 180 | [io.pedestal/pedestal.jetty :version/pedestal] 181 | [io.pedestal/pedestal.immutant :version/pedestal] 182 | [io.pedestal/pedestal.tomcat :version/pedestal]]})] 183 | (is (thrown? RuntimeException 184 | (expand-single-keyword :foo))) 185 | (is (= '[[clojure-future-spec "1.9.0-alpha13"]] 186 | (expand-single-keyword :spec))) 187 | (is (= '[[io.pedestal/pedestal.service "0.5.1"] 188 | [io.pedestal/pedestal.service-tools "0.5.1"] 189 | [io.pedestal/pedestal.jetty "0.5.1"] 190 | [io.pedestal/pedestal.immutant "0.5.1"] 191 | [io.pedestal/pedestal.tomcat "0.5.1"]] 192 | (expand-single-keyword :pedestal))))) 193 | 194 | (with-test 195 | (defn expand-keywords 196 | "Expands keywords in the input seq to coordinates." 197 | [coordinates] 198 | (assert (coll? coordinates) 199 | (str "coordinates should be a collection of " 200 | "dependencies or keywords")) 201 | (reduce (fn [acc c] 202 | (if (keyword? c) 203 | (into acc (expand-single-keyword c)) 204 | (conj acc c))) 205 | [] 206 | coordinates)) 207 | (with-redefs [get-bundle-map 208 | (constantly 209 | '{:spec [clojure-future-spec "1.9.0-alpha13"] 210 | :clojure [[org.clojure/clojure "1.8.0"] 211 | :spec]})] 212 | (is (thrown? java.lang.AssertionError 213 | (expand-keywords :foo))) 214 | (is (= '[[org.clojure/clojure "1.8.0"] 215 | [clojure-future-spec "1.9.0-alpha13"]] 216 | (expand-keywords [:clojure]))))) 217 | 218 | #_(t/test-ns *ns*) 219 | --------------------------------------------------------------------------------