diff --git a/doc/MANAGED_DEPS.md b/doc/MANAGED_DEPS.md new file mode 100644 index 00000000..77ee96df --- /dev/null +++ b/doc/MANAGED_DEPS.md @@ -0,0 +1,105 @@ + + + +# Managed Dependencies With Leiningen + +Maven (and now Leiningen) provides a capability called "Dependency Management". +The idea is to provide a way to specify a version number for common library +dependencies in a single location, and re-use those version numbers from other +discrete maven/lein projects. This makes it easy to, e.g., update your `clj-time` +dependency across a large number of projects without having to be mindful +of every common dependency version across all of your libraries. + +When using `:pedantic? :abort` in your projects, to ensure that you are producing +a consist and predictable build, it can be very cumbersome to play the "dependency +version whack-a-mole" game that arises whenever an upstream library bumps a version +of one of its dependencies. `:managed-dependencies` can help alleviate this issue +by allowing you to keep the dependency version numbers centralized. + +## `:managed-dependencies` + +The `:managed-dependencies` section of your `project.clj` file is just like the +regular `:dependencies` section, with two exceptions: + +1. It does not actually introduce any dependencies to your project. It only says, + "hey leiningen, if you encounter one of these dependencies later, here are the + versions that you should fall back to if the version numbers aren't explicitly + specified." +2. It allows the version number to be omitted from the `:dependencies` section, + for any artifact that you've listed in your `:managed-dependencies` section. + +Here's an example: + +```clj +(defproject superfun/happyslide "1.0.0-SNAPSHOT" + :description "A Clojure project with managed dependencies" + :min-lein-version "2.7.0" + :managed-dependencies [[clj-time "0.12.0"] + [me.raynes/fs "1.4.6"] + [ring/ring-codec "1.0.1"]] + :dependencies [[clj-time] + [me.raynes/fs]]) +``` + +In the example above, the final, resolved project will end up using the specified + versions of `clj-time` and `me.raynes/fs`. It will not have an actual dependency + on `ring/ring-codec` at all, since that is not mentioned in the "real" `:dependencies` + section. + +This feature is not all that useful on its own, because in the example above, +we're specifying the `:managed-dependencies` and `:dependencies` sections right +alongside one another, and you could just as easily include the version numbers +directly in the `:dependencies` section. The feature becomes more powerful +when your build workflow includes some other way of sharing the `:managed-dependencies` +section across multiple projects. + +## Lein "parent" projects + +One way of leveraging `:managed-dependencies` across multiple projects is to use +the [`lein-parent` plugin](https://github.com/achin/lein-parent). This plugin +will allow you to define a single "parent" project that is inherited by multiple +"child" projects; e.g.: + +```clj +(defproject superfun/myparent "1.0.0" + :managed-dependencies [[clj-time "0.12.0"] + [me.raynes/fs "1.4.6"] + [ring/ring-codec "1.0.1"]]) + +(defproject superfun/kid-a "1.0.0-SNAPSHOT" + :parent-project [:coords [superfun/myparent "1.0.0"] + :inherits [:managed-dependencies]] + :dependencies [[clj-time] + [me.raynes/fs]]) + +(defproject superfun/kid-b "1.0.0-SNAPSHOT" + :parent-project [:coords [superfun/myparent "1.0.0"] + :inherits [:managed-dependencies]] + :dependencies [[clj-time] + [ring/ring-codec]]) +``` + +In this example, we've consolidated the task of managing common version dependencies +in the parent project, and defined two child projects that will inherit those +dependency versions from the parent without needing to specify them explicitly. + +This makes it easier to ensure that all of your projects are using the same versions +of your common dependencies, which can help make sure that your uberjar builds are +more predictable and repeatable. + +## Other ways to share 'managed-dependencies' + +Since the `defproject` form is a macro, it would be possible to write other plugins +that generated the value for a `:managed-dependencies` section dynamically. That +could provide other useful ways to take advantage of the `:managed-dependencies` +functionality without needing to explicitly populate that section in all of your +`project.clj` files. + +## Future integration + +It is likely that the functionality provided by the `lein-parent` plugin may integrated +into the leiningen core in a future release; for now we have added only the `:managed-dependencies` +functionality because it is necessary in order for the plugin to leverage it. We +will be experimenting with different ideas for implementation / API in plugins and +making sure that we find an API that works well before submitting for inclusion +into core leiningen. diff --git a/leiningen-core/src/leiningen/core/classpath.clj b/leiningen-core/src/leiningen/core/classpath.clj index 88f6dd04..9ef7e640 100644 --- a/leiningen-core/src/leiningen/core/classpath.clj +++ b/leiningen-core/src/leiningen/core/classpath.clj @@ -250,10 +250,12 @@ (def ^:private get-dependencies-memoized (memoize - (fn [dependencies-key {:keys [repositories local-repo offline? update - checksum mirrors] :as project} + (fn [dependencies-key managed-dependencies-key + {:keys [repositories local-repo offline? update + checksum mirrors] :as project} {:keys [add-classpath? repository-session-fn] :as args}] - {:pre [(every? vector? (get project dependencies-key))]} + {:pre [(every? vector? (get project dependencies-key)) + (every? vector? (get project managed-dependencies-key))]} (try ((if add-classpath? pomegranate/add-dependencies @@ -264,6 +266,7 @@ :repositories (->> repositories (map add-repo-auth) (map (partial update-policies update checksum))) + :managed-coordinates (get project managed-dependencies-key) :coordinates (get project dependencies-key) :mirrors (->> mirrors (map add-repo-auth) @@ -407,13 +410,17 @@ #(-> % aether/repository-session (pedantic/use-transformer ranges overrides)))) -(defn ^:internal get-dependencies [dependencies-key project & args] +(defn ^:internal get-dependencies [dependencies-key managed-dependencies-key + project & args] (let [ranges (atom []), overrides (atom []) session (pedantic-session project ranges overrides) args (assoc (apply hash-map args) :repository-session-fn session) - trimmed (select-keys project [dependencies-key :repositories :checksum - :local-repo :offline? :update :mirrors]) - deps-result (get-dependencies-memoized dependencies-key trimmed args)] + trimmed (select-keys project [dependencies-key managed-dependencies-key + :repositories :checksum :local-repo :offline? + :update :mirrors]) + deps-result (get-dependencies-memoized dependencies-key + managed-dependencies-key + trimmed args)] (pedantic-do (:pedantic? project) @ranges @overrides) deps-result)) @@ -486,16 +493,21 @@ (doseq [[_ {:keys [native-prefix file]}] snap-deps] (extract-native-dep! native-path file native-prefix)))))) -(defn resolve-dependencies +(defn resolve-managed-dependencies "Delegate dependencies to pomegranate. This will ensure they are downloaded into ~/.m2/repository and that native components of dependencies have been extracted to :native-path. If :add-classpath? is logically true, will add the resolved dependencies to Leiningen's classpath. + Supports inheriting 'managed' dependencies, e.g. to allow common dependency + versions to be specified from an alternate location in the project file, or + from a parent project file. + Returns a seq of the dependencies' files." - [dependencies-key {:keys [native-path] :as project} & rest] - (let [dependencies-tree (apply get-dependencies dependencies-key project rest) + [dependencies-key managed-dependencies-key project & rest] + (let [dependencies-tree (apply get-dependencies dependencies-key + managed-dependencies-key project rest) jars (->> dependencies-tree (aether/dependency-files) (filter #(re-find #"\.(jar|zip)$" (.getName %))))] @@ -504,13 +516,49 @@ (extract-native-dependencies project jars dependencies-tree)) jars)) +(defn ^:deprecated resolve-dependencies + "Delegate dependencies to pomegranate. This will ensure they are + downloaded into ~/.m2/repository and that native components of + dependencies have been extracted to :native-path. If :add-classpath? + is logically true, will add the resolved dependencies to Leiningen's + classpath. + + Returns a seq of the dependencies' files. + + NOTE: deprecated in favor of `resolve-managed-dependencies`." + [dependencies-key project & rest] + (let [managed-dependencies-key (if (= dependencies-key :dependencies) + :managed-dependencies)] + (apply resolve-managed-dependencies dependencies-key managed-dependencies-key project rest))) + +(defn merge-versions-from-managed-coords + [deps managed-deps] + ;; NOTE: there is a new function in the 0.3.1 release of pomegranate that + ;; is needed here, but was accidentally marked as private. Calling it + ;; via the symbol dereference for now, but this can be changed to a + ;; regular function call once https://github.com/cemerick/pomegranate/pull/74 + ;; is merged. + (#'aether/merge-versions-from-managed-coords deps managed-deps)) + +(defn managed-dependency-hierarchy + "Returns a graph of the project's dependencies. + + Supports inheriting 'managed' dependencies, e.g. to allow common dependency + versions to be specified from an alternate location in the project file, or + from a parent project file." + [dependencies-key managed-dependencies-key project & options] + (if-let [deps-list (merge-versions-from-managed-coords + (get project dependencies-key) + (get project managed-dependencies-key))] + (aether/dependency-hierarchy deps-list + (apply get-dependencies dependencies-key + managed-dependencies-key + project options)))) + (defn dependency-hierarchy "Returns a graph of the project's dependencies." [dependencies-key project & options] - (if-let [deps-list (get project dependencies-key)] - (aether/dependency-hierarchy deps-list - (apply get-dependencies dependencies-key - project options)))) + (apply managed-dependency-hierarchy dependencies-key nil project options)) (defn- normalize-path [root path] (let [f (io/file path) ; http://tinyurl.com/ab5vtqf @@ -533,7 +581,7 @@ (seq (->> (filter ext-dependency? (:dependencies project)) (assoc project :dependencies) - (resolve-dependencies :dependencies) + (resolve-managed-dependencies :dependencies :managed-dependencies) (map (memfn getAbsolutePath))))) (defn ^:internal checkout-deps-paths @@ -561,7 +609,8 @@ (:resource-paths project) [(:compile-path project)] (checkout-deps-paths project) - (for [dep (resolve-dependencies :dependencies project)] + (for [dep (resolve-managed-dependencies + :dependencies :managed-dependencies project)] (.getAbsolutePath dep))) :when path] (normalize-path (:root project) path))) diff --git a/leiningen-core/src/leiningen/core/eval.clj b/leiningen-core/src/leiningen/core/eval.clj index dd69ee1c..3ab7f916 100644 --- a/leiningen-core/src/leiningen/core/eval.clj +++ b/leiningen-core/src/leiningen/core/eval.clj @@ -83,7 +83,7 @@ ((juxt :source-paths :test-paths :resource-paths) project))] (.mkdirs (io/file path)))) (write-pom-properties project) - (classpath/resolve-dependencies :dependencies project) + (classpath/resolve-managed-dependencies :dependencies :managed-dependencies project) (run-prep-tasks project) (deliver @prep-blocker true) (reset! prep-blocker (promise))) @@ -221,7 +221,7 @@ (defn ^:internal classpath-arg [project] (let [classpath-string (string/join java.io.File/pathSeparatorChar (classpath/get-classpath project)) - agent-tree (classpath/get-dependencies :java-agents project) + agent-tree (classpath/get-dependencies :java-agents nil project) ;; Seems like you'd expect dependency-files to walk the whole tree ;; here, but it doesn't, which is what we want. but maybe a bug? agent-jars (aether/dependency-files (aether/dependency-hierarchy @@ -334,7 +334,7 @@ (when (:debug project) (System/setProperty "clojure.debug" "true")) ;; :dependencies are loaded the same way as plugins in eval-in-leiningen - (project/load-plugins project :dependencies) + (project/load-plugins project :dependencies :managed-dependencies) (doseq [path (classpath/get-classpath project)] (pomegranate/add-classpath path)) (doseq [opt (get-jvm-args project) diff --git a/leiningen-core/src/leiningen/core/project.clj b/leiningen-core/src/leiningen/core/project.clj index 03604d99..f539b66b 100644 --- a/leiningen-core/src/leiningen/core/project.clj +++ b/leiningen-core/src/leiningen/core/project.clj @@ -686,13 +686,14 @@ (def ^:private registered-wagon-files (atom #{})) (defn load-plugins - ([project key] - (when (seq (get project key)) + ([project dependencies-key managed-dependencies-key] + (when (seq (get project dependencies-key)) (ensure-dynamic-classloader) (let [repos-project (update-in project [:repositories] meta-merge (:plugin-repositories project))] - (classpath/resolve-dependencies key repos-project - :add-classpath? true))) + (classpath/resolve-managed-dependencies + dependencies-key managed-dependencies-key repos-project + :add-classpath? true))) (doseq [wagon-file (-> (.getContextClassLoader (Thread/currentThread)) (.getResources "leiningen/wagons.clj") (enumeration-seq)) @@ -701,6 +702,7 @@ (aether/register-wagon-factory! hint (eval factory)) (swap! registered-wagon-files conj wagon-file)) project) + ([project dependencies-key] (load-plugins project dependencies-key nil)) ([project] (load-plugins project :plugins))) (defn plugin-vars [project type] diff --git a/leiningen-core/test/leiningen/core/test/classpath.clj b/leiningen-core/test/leiningen/core/test/classpath.clj index 594b14b1..fb693c6e 100644 --- a/leiningen-core/test/leiningen/core/test/classpath.clj +++ b/leiningen-core/test/leiningen/core/test/classpath.clj @@ -2,7 +2,6 @@ (:use [clojure.test] [leiningen.core.classpath]) (:require [clojure.java.io :as io] - [clojure.set :as set] [leiningen.core.user :as user] [leiningen.test.helper :as lthelper] [leiningen.core.project :as project])) @@ -17,7 +16,8 @@ (defn m2-file [f] (io/file (System/getProperty "user.home") ".m2" "repository" f)) -(def project {:dependencies '[[org.clojure/clojure "1.3.0"] +(def project {:managed-dependencies '[[org.clojure/clojure "1.3.0"]] + :dependencies '[[org.clojure/clojure] [ring/ring-core "1.0.0" :exclusions [commons-codec]]] :checkout-deps-shares [:source-paths :resource-paths @@ -38,7 +38,9 @@ (m2-file "ring/ring-core/1.0.0/ring-core-1.0.0.jar") (m2-file (str "commons-fileupload/commons-fileupload/1.2.1/" "commons-fileupload-1.2.1.jar"))} - (set (resolve-dependencies :dependencies project))))) + (set (resolve-managed-dependencies :dependencies + :managed-dependencies + project))))) (deftest test-dependency-hierarchy (doseq [f (reverse (file-seq (io/file (:root project))))] @@ -49,7 +51,9 @@ {[commons-fileupload "1.2.1"] nil [commons-io "1.4"] nil [javax.servlet/servlet-api "2.5"] nil}} - (dependency-hierarchy :dependencies project)))) + (managed-dependency-hierarchy :dependencies + :managed-dependencies + project)))) (def directories (vec (map lthelper/pathify diff --git a/leiningen-core/test/leiningen/core/test/eval.clj b/leiningen-core/test/leiningen/core/test/eval.clj index 13198516..cf12281f 100644 --- a/leiningen-core/test/leiningen/core/test/eval.clj +++ b/leiningen-core/test/leiningen/core/test/eval.clj @@ -8,7 +8,8 @@ [leiningen.core.project :as project]) (:import (java.io File))) -(def project {:dependencies '[[org.clojure/clojure "1.3.0"]] +(def project {:managed-dependencies '[[org.clojure/clojure "1.3.0"]] + :dependencies '[[org.clojure/clojure]] :root "/tmp/lein-sample-project" :repositories project/default-repositories :target-path "/tmp/lein-sample-project/target" diff --git a/sample.project.clj b/sample.project.clj index 7095afa4..d50ae0c6 100644 --- a/sample.project.clj +++ b/sample.project.clj @@ -55,6 +55,22 @@ ;; LWJGL stores natives in the root of the jar; this ;; :native-prefix will extract them. :native-prefix ""]] + ;; "Managed Dependencies" are a concept borrowed from maven pom files; see + ;; https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Dependency_Management + ;; Managed dependencies allow you to specify a desired version number for a dependency + ;; *if* the dependency exists (often transitively), but a managed dependency + ;; will not actually cause the described artifact to be a dependency on its own. + ;; This feature is most useful in combination with some other mechanism for + ;; defining a "parent project"; e.g. you can have a "parent project" that specifies + ;; managed dependencies for common libraries that you use frequently in your other + ;; projects, and then the downstream/child projects can specify a normal dependency on + ;; those libraries *without specifying a version number*, and thus will inherit + ;; the version number from the parent. This provides a simpler means of keeping + ;; common dependency versions in sync across a large number of clojure libraries. + ;; For more info see ./doc/MANAGED_DEPS.md and https://github.com/achin/lein-parent + :managed-dependencies [[clj-time "0.12.0"] + [me.raynes/fs "1.4.6"]] + ;; What to do in the case of version issues. Defaults to :ranges, which ;; warns when version ranges are present anywhere in the dependency tree, ;; but can be set to true to warn for both ranges and overrides, or :abort diff --git a/src/leiningen/deps.clj b/src/leiningen/deps.clj index 9cc7defb..5f569aff 100644 --- a/src/leiningen/deps.clj +++ b/src/leiningen/deps.clj @@ -2,13 +2,10 @@ "Download all dependencies." (:require [leiningen.core.classpath :as classpath] [leiningen.core.main :as main] - [leiningen.core.eval :as eval] [leiningen.core.project :as project] [leiningen.core.user :as user] [leiningen.core.utils :as utils] - [cemerick.pomegranate.aether :as aether] - [clojure.pprint :as pp] - [clojure.java.io :as io]) + [cemerick.pomegranate.aether :as aether]) (:import (org.sonatype.aether.resolution DependencyResolutionException))) (defn- walk-deps @@ -69,8 +66,8 @@ (def tree-command "A mapping from the tree-command to the dependency key it should print a tree for." - {":tree" :dependencies - ":plugin-tree" :plugins}) + {":tree" [:dependencies :managed-dependencies] + ":plugin-tree" [:plugins nil]}) @@ -121,16 +118,22 @@ force them to be updated, use `lein -U $TASK`." (let [project (project/merge-profiles project [{:pedantic? (quote ^:displace warn)}]) - hierarchy (classpath/dependency-hierarchy - (tree-command command) + [dependencies-key managed-dependencies-key] (tree-command command) + hierarchy (classpath/managed-dependency-hierarchy + dependencies-key + managed-dependencies-key project)] (walk-deps hierarchy print-dep)) (= command ":verify") (if (user/gpg-available?) - (walk-deps (classpath/dependency-hierarchy :dependencies project) + (walk-deps (classpath/managed-dependency-hierarchy + :dependencies + :managed-dependencies + project) (partial verify project)) (main/abort (str "Could not verify - gpg not available.\n" "See `lein help gpg` for how to setup gpg."))) - :else (classpath/resolve-dependencies :dependencies project)) + :else (classpath/resolve-managed-dependencies + :dependencies :managed-dependencies project)) (catch DependencyResolutionException e (main/abort (.getMessage e)))))) diff --git a/src/leiningen/pom.clj b/src/leiningen/pom.clj index 558cdc1a..420f9ddc 100644 --- a/src/leiningen/pom.clj +++ b/src/leiningen/pom.clj @@ -330,6 +330,7 @@ (project/unmerge-profiles profile-kws) (project/merge-profiles [:test]) relativize) + managed-deps (:managed-dependencies test-project) deps (:dependencies test-project)] (list [:project {:xsi:schemaLocation @@ -354,6 +355,8 @@ ;; TODO: this results in lots of duplicate entries (xml-tags :build [project test-project]) (xml-tags :repositories (:repositories project)) + (xml-tags :dependencyManagement + (xml-tags :dependencies (distinct-key dep-key managed-deps))) (xml-tags :dependencies (distinct-key dep-key deps)) (and (:pom-addition project) (:pom-addition project))])))) diff --git a/src/leiningen/uberjar.clj b/src/leiningen/uberjar.clj index 5891aa44..6810a0ba 100644 --- a/src/leiningen/uberjar.clj +++ b/src/leiningen/uberjar.clj @@ -177,7 +177,8 @@ be deactivated." (let [whitelisted (select-keys project project/whitelist-keys) project (-> (project/unmerge-profiles project [:default]) (merge whitelisted)) - deps (->> (classpath/resolve-dependencies :dependencies project) + deps (->> (classpath/resolve-managed-dependencies + :dependencies :managed-dependencies project) (filter #(.endsWith (.getName %) ".jar"))) jars (cons (io/file jar) deps)] (write-components project jars out))) diff --git a/test/leiningen/test/deps.clj b/test/leiningen/test/deps.clj index 8b65447d..9973a84d 100644 --- a/test/leiningen/test/deps.clj +++ b/test/leiningen/test/deps.clj @@ -1,13 +1,14 @@ (ns leiningen.test.deps (:use [clojure.test] [leiningen.deps] - [leiningen.test.helper :only [sample-project m2-dir native-project + [leiningen.test.helper :only [sample-project m2-dir m2-file native-project + managed-deps-project delete-file-recursively]]) (:require [clojure.java.io :as io] - [leiningen.core.main :as main] - [leiningen.core.classpath :as classpath] [leiningen.core.utils :as utils] - [leiningen.core.eval :as eval])) + [leiningen.core.eval :as eval] + [leiningen.core.classpath :as classpath] + [cemerick.pomegranate.aether :as aether])) (deftest ^:online test-deps (let [sample-deps [["rome" "0.9"] ["jdom" "1.0"]]] @@ -128,3 +129,77 @@ (set (for [f (rest (file-seq (io/file (first (eval/native-arch-paths native-project)))))] (.getName f)))))) + +(defn coordinates-match? + [dep1 dep2] + ;; NOTE: there is a new function in the 0.3.1 release of pomegranate that + ;; is useful here, but it is private. Calling it via the symbol dereference + ;; for now, but might consider making it public upstream. Haven't done so + ;; yet since it is only used for tests. + (#'aether/coordinates-match? dep1 dep2)) + +(deftest ^:online test-managed-deps + (let [is-clojure-dep? #(#{'org.clojure/clojure + 'org.clojure/tools.nrepl} + (first %)) + remove-clojure-deps #(remove is-clojure-dep? %) + managed-deps (remove-clojure-deps (:managed-dependencies managed-deps-project)) + ;; find deps from normal "deps" section which explicitly specify their + ;; version number rather than inheriting it from managed-deps + versioned-unmanaged-deps (filter + (fn [dep] + (and (> (count dep) 1) + (string? (nth dep 1)) + (not (is-clojure-dep? dep)))) + (:dependencies managed-deps-project)) + ;; the list of final, used deps w/versions + merged-deps (remove-clojure-deps + (classpath/merge-versions-from-managed-coords + (:dependencies managed-deps-project) + (:managed-dependencies managed-deps-project))) + ;; the list of deps from the managed deps section that aren't used + unused-managed-deps (-> (remove + (fn [dep] + (or (some (partial coordinates-match? dep) merged-deps) + ;; special-casing to remove tools.reader, which is a common transitive dep + ;; of two of our normal dependencies + (= 'org.clojure/tools.reader (first dep)))) + managed-deps)) + ;; deps that have classifiers + classified-deps (filter + #(some #{:classifier} %) + merged-deps)] + ;; make sure the sample data has some unmanaged deps, some unused managed deps, + ;; and some classified deps, for completeness + (is (seq versioned-unmanaged-deps)) + (is (seq unused-managed-deps)) + (is (seq classified-deps)) + ;; delete all of the existing artifacts for merged deps + (doseq [[n v] merged-deps] + (delete-file-recursively (m2-dir n v) :silently)) + ;; delete all of the artifacts for the managed deps too + (doseq [[n v] managed-deps] + (delete-file-recursively (m2-dir n v) :silently)) + ;; delete all copies of tools.reader so we know that the managed dependency + ;; for it is taking precedence + (delete-file-recursively (m2-dir 'org.clojure/tools.reader) :silently) + (deps managed-deps-project) + ;; artifacts should be available for all merged deps + (doseq [[n v] merged-deps] + (is (.exists (m2-dir n v)) (str n " was not downloaded (missing dir '" (m2-dir n v) "')."))) + ;; artifacts should *not* have been downloaded for unused managed deps + (doseq [[n v] unused-managed-deps] + (is (not (.exists (m2-dir n v))) (str n " was unexpectedly downloaded (found unexpected dir '" (m2-dir n v) "')."))) + ;; artifacts with classifiers should be available + (doseq [[n v _ classifier] classified-deps] + (let [f (m2-file n v classifier)] + (is (.exists f) (str f " was not downloaded.")))) + ;; check tools.reader explicitly, since it is our special transitive dependency + (let [tools-reader-versions (into [] (.listFiles (m2-dir 'org.clojure/tools.reader)))] + (is (= 1 (count tools-reader-versions))) + (is (= (first tools-reader-versions) (m2-dir 'org.clojure/tools.reader + (->> managed-deps + (filter + (fn [dep] (= 'org.clojure/tools.reader (first dep)))) + first + second))))))) diff --git a/test/leiningen/test/helper.clj b/test/leiningen/test/helper.clj index 95403ab1..bb403ea9 100644 --- a/test/leiningen/test/helper.clj +++ b/test/leiningen/test/helper.clj @@ -2,7 +2,8 @@ (:require [leiningen.core.project :as project] [leiningen.core.user :as user] [leiningen.core.test.helper :as helper] - [clojure.java.io :as io]) + [clojure.java.io :as io] + [clojure.string :as str]) (:import (java.io ByteArrayOutputStream PrintStream FileDescriptor FileOutputStream))) @@ -11,9 +12,16 @@ (def tmp-dir (System/getProperty "java.io.tmpdir")) -(defn m2-dir [n v] - (io/file local-repo - (if (string? n) n (or (namespace n) (name n))) (name n) v)) +(defn m2-dir + ([n] + (let [group (-> (if (string? n) n (or (namespace n) (name n))) + (str/replace "." "/"))] + (io/file local-repo group (name n)))) + ([n v] + (io/file (m2-dir n) v))) + +(defn m2-file [n v classifier] + (io/file (m2-dir n v) (str (name n) "-" v "-" classifier ".jar"))) (defn read-test-project-with-user-profiles [name user-profiles] (with-redefs [user/profiles (constantly user-profiles)] @@ -65,6 +73,8 @@ (def with-classifiers-project (read-test-project "with-classifiers")) +(def managed-deps-project (read-test-project "managed-deps")) + (defn abort-msg "Catches main/abort thrown by calling f on its args and returns its error message." diff --git a/test/leiningen/test/pom.clj b/test/leiningen/test/pom.clj index 8e3ce942..dd7dd9e2 100644 --- a/test/leiningen/test/pom.clj +++ b/test/leiningen/test/pom.clj @@ -4,7 +4,8 @@ [leiningen.pom :only [make-pom pom snapshot?]] [leiningen.core.user :as user] [leiningen.test.helper - :only [sample-project sample-profile-meta-project] + :only [sample-project sample-profile-meta-project + managed-deps-project] :as lthelper]) (:require [clojure.data.xml :as xml] [leiningen.core.project :as project] @@ -352,3 +353,33 @@ (testing "Version containing anything else is not a snapshot " (is (not (snapshot? {:version "foo"}))) (is (not (snapshot? nil))))) + +(deftest test-managed-dependencies + (let [xml (xml/parse-str + (make-pom managed-deps-project))] + (testing "normal dependencies are written to pom properly" + (is (= ["org.clojure" "rome" "ring" "ring" "commons-codec" "commons-math" + "org.clojure" "org.clojure"] + (map #(first-in % [:dependency :groupId]) + (deep-content xml [:project :dependencies])))) + (is (= ["clojure" "rome" "ring" "ring-codec" "commons-codec" "commons-math" + "tools.emitter.jvm" "tools.namespace"] + (map #(first-in % [:dependency :artifactId]) + (deep-content xml [:project :dependencies])))) + (is (= [nil nil nil nil "1.6" nil "0.1.0-beta5" "0.3.0-alpha3"] + (map #(first-in % [:dependency :version]) + (deep-content xml [:project :dependencies]))))) + (testing "managed dependencies are written to pom properly" + (is (= ["org.clojure" "rome" "ring" "ring" "commons-math" "ring" "org.clojure"] + (map #(first-in % [:dependency :groupId]) + (deep-content xml [:project :dependencyManagement :dependencies])))) + (is (= ["clojure" "rome" "ring" "ring-codec" "commons-math" "ring-defaults" + "tools.reader"] + (map #(first-in % [:dependency :artifactId]) + (deep-content xml [:project :dependencyManagement :dependencies])))) + (is (= ["1.3.0" "0.9" "1.0.0" "1.0.1" "1.2" "0.2.1" "1.0.0-beta3"] + (map #(first-in % [:dependency :version]) + (deep-content xml [:project :dependencyManagement :dependencies])))) + (is (= [nil nil nil nil "sources" nil nil] + (map #(first-in % [:dependency :classifier]) + (deep-content xml [:project :dependencyManagement :dependencies]))))))) \ No newline at end of file diff --git a/test/leiningen/test/uberjar.clj b/test/leiningen/test/uberjar.clj index 5426d76e..91231ce6 100644 --- a/test/leiningen/test/uberjar.clj +++ b/test/leiningen/test/uberjar.clj @@ -6,7 +6,8 @@ [clojure.xml :as xml] [leiningen.test.helper :refer [sample-no-aot-project uberjar-merging-project - provided-project]]) + provided-project + managed-deps-project]]) (:import (java.io File FileOutputStream) (java.util.zip ZipFile))) @@ -64,3 +65,10 @@ _ (uberjar provided-project)] (is (= 1 (:exit (sh "java" "-jar" filename)))) (is (= 0 (:exit (sh "java" bootclasspath "-jar" filename)))))) + +(deftest test-uberjar-managed-dependencies + (uberjar managed-deps-project) + (let [filename (str "test_projects/managed-deps/target/" + "mgmt-0.99.0-SNAPSHOT-standalone.jar") + uberjar-file (File. filename)] + (is (= true (.exists uberjar-file))))) \ No newline at end of file diff --git a/test_projects/managed-deps/project.clj b/test_projects/managed-deps/project.clj new file mode 100644 index 00000000..df1deee4 --- /dev/null +++ b/test_projects/managed-deps/project.clj @@ -0,0 +1,22 @@ +(def clj-version "1.3.0") + +(defproject mgmt "0.99.0-SNAPSHOT" + :description "A test project" + + :managed-dependencies [[~(symbol "org.clojure" "clojure") ~clj-version] + [rome ~(str "0." "9")] + [ring/ring "1.0.0"] + [ring/ring-codec "1.0.1"] + [commons-math/commons-math "1.2" :classifier "sources"] + [ring/ring-defaults "0.2.1"] + [org.clojure/tools.reader "1.0.0-beta3"]] + + :dependencies [[org.clojure/clojure] + [rome/rome nil] + [ring] + [ring/ring-codec nil :exclusions [commons-codec]] + [commons-codec "1.6"] + [commons-math nil :classifier "sources"] + [org.clojure/tools.emitter.jvm "0.1.0-beta5"] ; depends on tools.reader 0.8.5 + [org.clojure/tools.namespace "0.3.0-alpha3"] ; depends on tools.reader 0.10.0 + ])