Skip to content

Commit

Permalink
Discover child module versions automatically #29
Browse files Browse the repository at this point in the history
This involved quite a bit of refactoring of the progeny stuff into
common so that versionization could use it, too. It looks for the
top-most project and then all of that project's children. This can lead
to an infinite loop when middleware is invoked so we introduce some
macros to disable middleware when parents and children are being read.

Unfortunately, discovering all the children can add quite a bit of time
to the build, almost double for some of the tasks in the Immutant build.

Parking on a branch for now.
  • Loading branch information
jcrossley3 committed Jun 9, 2015
1 parent 10c7eff commit 1578dd1
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 78 deletions.
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,9 @@ But there are some important differences:
`:subprocess` config option, which defaults to "lein"
* lein-modules supports automatic discovery of child modules so that
you don't have to set `:dirs` at all
* lein-modules simplifies your release process by eliminating the
redundant references to your project's current version in
interdependent modules, e.g.
`[:dependencies [[your-project/common :version]]`, resolving the
`:version` keyword to the value from the dependent's own project map
* lein-modules resolves interdependent child modules automatically, so
you only need to specify the version of each child once, in its own
`project.clj`

### Release Management

Expand Down Expand Up @@ -188,9 +186,8 @@ any of the following keys:
versions of your child modules' shared dependencies in a single
place. And like the `:inherited` profile, when multiple `:versions`
maps are found among ancestors, the most immediate take precedence.
The *project* map's `:version` is automatically included in the
`:versions` map, so your interdependent modules may use that without
configuring this option at all.
The versions of all child modules are automatically discovered and
included in this map for you.

* `:dirs` - A vector of strings denoting the relative paths to the
project's child modules. Normally, they're discovered automatically
Expand Down Expand Up @@ -267,16 +264,15 @@ version itself will be tried as a key.
midje "1.6.0"
ring "1.2.1"
:jbossas "7.2.x.slim.incremental.12"
org.jboss.as :jbossas
org.immutant :version}})
org.jboss.as :jbossas}})
```

### Child
```clj
(defproject org.immutant/web "1.0.3-SNAPSHOT"
:plugins [[lein-modules "0.3.11"]]
:description "The web component"
:dependencies [[org.immutant/core :version]
:dependencies [[org.immutant/core "_"]
[ring/ring-servlet "_"]
[potemkin "0.3.4"]])
```
Expand Down
76 changes: 72 additions & 4 deletions src/lein_modules/common.clj
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
(ns lein-modules.common
(:require [leiningen.core.project :as prj]
[leiningen.core.main :refer (version-satisfies? leiningen-version)]
[leiningen.core.utils :as utils]
[clojure.java.io :as io]
[lein-modules.compression :refer (compressed-profiles)]))

(def read-project (if (version-satisfies? (leiningen-version) "2.5")
(load-string "#(prj/init-profiles (prj/project-with-profiles (prj/read-raw %)) [:default])")
prj/read))

(def ^:dynamic *middleware-disabled* false)

(defmacro without-middleware [& body]
`(binding [*middleware-disabled* true]
~@body))

(defmacro with-middleware [project & body]
`(if (or *middleware-disabled* (-> ~project meta ::middleware-applied))
~project
(let [~project (vary-meta ~project assoc ::middleware-applied true)]
(vary-meta (do ~@body) dissoc ::middleware-applied))))

(defn with-profiles
"Apply profiles to project"
[project profiles]
Expand Down Expand Up @@ -36,7 +49,62 @@
"Traverse all parents to accumulate a list of :modules config,
ordered by least to most immediate ancestors"
[project]
(loop [p project, acc '()]
(if (nil? p)
(remove nil? acc)
(recur (parent p) (conj acc (-> p :modules))))))
(without-middleware
(loop [p project, acc '()]
(if (nil? p)
(remove nil? acc)
(recur (parent p) (conj acc (-> p :modules)))))))

(defn id
"Returns fully-qualified symbol identifier for project"
[project]
(if project
(symbol (:group project) (:name project))))

(defn child?
"Return true if child is an immediate descendant of project"
[project child]
(= (:root project) (:root (parent child))))

(defn file-seq-sans-symlinks
"A tree seq on java.io.Files that aren't symlinks"
[dir]
(tree-seq
(fn [^java.io.File f] (and (.isDirectory f) (not (utils/symlink? f))))
(fn [^java.io.File d] (seq (.listFiles d)))
dir))

(defn children
"Return the child maps for a project according to its active profiles"
[project]
(if-let [dirs (-> project :modules :dirs)]
(remove nil?
(map (comp #(try (read-project %) (catch Exception e (println (.getMessage e))))
(memfn getCanonicalPath)
#(io/file (:root project) % "project.clj"))
dirs))
(->> (file-seq-sans-symlinks (io/file (:root project)))
(filter #(= "project.clj" (.getName %)))
(remove #(= (:root project) (.getParent %)))
(map (comp #(try (read-project %) (catch Exception e (println (.getMessage e)))) str))
(remove nil?)
(filter #(child? project (with-profiles % (compressed-profiles project)))))))

(defn progeny
"Recursively return the project's children in a map keyed by id"
([project]
(progeny project (compressed-profiles project)))
([project profiles]
(let [kids (children (with-profiles project profiles))]
(apply merge
(into {} (map (juxt id identity) kids))
(->> kids
(remove #(= (:root project) (:root %))) ; in case "." in :dirs
(map #(progeny % profiles)))))))

(defn primogenitor
"The top-most parent of a project"
[project]
(if-let [p (parent project nil)]
(recur p)
project))
5 changes: 3 additions & 2 deletions src/lein_modules/inheritance.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(ns lein-modules.inheritance
(:use [lein-modules.common :only (config parent)]
(:use [lein-modules.common :only (config parent without-middleware)]
[lein-modules.compression :only (compressed-profiles)])
(:require [leiningen.core.project :as prj]))

Expand Down Expand Up @@ -42,13 +42,14 @@
([project]
(compositize-profiles project (compressed-profiles project)))
([project active-profiles]
(without-middleware
(loop [p project, result nil]
(if (nil? p)
result
(recur (parent p active-profiles)
(reduce (compositor p) result
(conj (select-keys (:modules p) [:inherited])
(filter-profiles (:profiles (meta p))))))))))
(filter-profiles (:profiles (meta p)))))))))))

(defn inherit
"Add profiles from parents, setting any :inherited ones if found,
Expand Down
10 changes: 4 additions & 6 deletions src/lein_modules/plugin.clj
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
(ns lein-modules.plugin
(:use [lein-modules.versionization :only (versionize)]
[lein-modules.inheritance :only (inherit)]
[lein-modules.common :only (config)]))
[lein-modules.common :only (with-middleware)]))

(defn middleware
"Implicit Leiningen middleware, guarding recursive
middleware calls with a metadata flag.
See https://github.com/technomancy/leiningen/issues/1151"
[project]
(if (-> project meta ::middleware-applied)
project
(with-middleware project
(-> project
(vary-meta assoc ::middleware-applied true)
inherit
versionize
(vary-meta dissoc ::middleware-applied))))
versionize)))

14 changes: 12 additions & 2 deletions src/lein_modules/versionization.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(ns lein-modules.versionization
(:use [lein-modules.common :only (config)]
[leiningen.core.project :only (artifact-map)]))
[leiningen.core.project :only (artifact-map)]
[lein-modules.common :only (primogenitor progeny without-middleware)]))

(defn versions
"Merge dependency management maps of :versions from the
Expand Down Expand Up @@ -33,11 +34,20 @@
ver)
opts)))

(def related-module-versions
"Return a mapping of all related project symbols to their versions"
(memoize
(fn [project]
(without-middleware
(into {}
(for [[k v] (-> project primogenitor progeny)]
[k (:version v)]))))))

(defn versionize
"Substitute versions in dependency vectors with actual versions from
the :versions modules config"
[project]
(let [vmap (merge (select-keys project [:version]) (versions project))
(let [vmap (merge (related-module-versions project) (versions project))
f #(with-meta (for [d %] (expand-version d vmap)) (meta %))]
(-> project
(update-in [:dependencies] f)
Expand Down
50 changes: 1 addition & 49 deletions src/leiningen/modules.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,9 @@
[leiningen.core.utils :as utils]
[clojure.java.io :as io]
[clojure.string :as s])
(:use [lein-modules.inheritance :only (inherit)]
[lein-modules.common :only (parent with-profiles read-project)]
(:use [lein-modules.common :only (progeny)]
[lein-modules.compression :only (compressed-profiles)]))

(defn child?
"Return true if child is an immediate descendant of project"
[project child]
(= (:root project) (:root (parent child))))

(defn file-seq-sans-symlinks
"A tree seq on java.io.Files that aren't symlinks"
[dir]
(tree-seq
(fn [^java.io.File f] (and (.isDirectory f) (not (utils/symlink? f))))
(fn [^java.io.File d] (seq (.listFiles d)))
dir))

(defn children
"Return the child maps for a project according to its active profiles"
[project]
(if-let [dirs (-> project :modules :dirs)]
(remove nil?
(map (comp #(try (read-project %) (catch Exception e (println (.getMessage e))))
(memfn getCanonicalPath)
#(io/file (:root project) % "project.clj"))
dirs))
(->> (file-seq-sans-symlinks (io/file (:root project)))
(filter #(= "project.clj" (.getName %)))
(remove #(= (:root project) (.getParent %)))
(map (comp #(try (read-project %) (catch Exception e (println (.getMessage e)))) str))
(remove nil?)
(filter #(child? project (with-profiles % (compressed-profiles project)))))))

(defn id
"Returns fully-qualified symbol identifier for project"
[project]
(if project
(symbol (:group project) (:name project))))

(defn progeny
"Recursively return the project's children in a map keyed by id"
([project]
(progeny project (compressed-profiles project)))
([project profiles]
(let [kids (children (with-profiles project profiles))]
(apply merge
(into {} (map (juxt id identity) kids))
(->> kids
(remove #(= (:root project) (:root %))) ; in case "." in :dirs
(map #(progeny % profiles)))))))

(defn interdependence
"Turn a progeny map (symbols to projects) into a mapping of projects
to their dependent projects"
Expand Down
2 changes: 1 addition & 1 deletion test-resources/grandparent/parent/child/project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
[foo/b _]
[foo/c _]
[foo/d :ver]
[sibling "0.1.0-SNAPSHOT"]]
[sibling _]]
:modules {:versions {parent "3.0"}}
:profiles {:by-child {}})
2 changes: 1 addition & 1 deletion test-resources/grandparent/parent/sibling/project.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(defproject sibling "0.1.0-SNAPSHOT"
(defproject sibling "0.2.0-SNAPSHOT"
:description "sibling"

:profiles {:by-child {:modules {:parent nil}}})
3 changes: 2 additions & 1 deletion test/lein_modules/versionization_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
'[foo/a "1"]
'[foo/b "2"]
'[foo/c "3"]
'[foo/d "2.0"])))
'[foo/d "2.0"]
'[sibling/sibling "0.2.0-SNAPSHOT"])))

(deftest partial-composites-should-work
(let [p (-> (prj/read "test-resources/lambda/project.clj")
Expand Down
2 changes: 1 addition & 1 deletion test/leiningen/modules_test.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(ns leiningen.modules-test
(:use clojure.test
leiningen.modules
[lein-modules.common :only (parent)]
[lein-modules.common :only (parent child? children progeny)]
[lein-modules.inheritance :only (inherit)])
(:require [leiningen.core.project :as prj]
[leiningen.clean :refer [delete-file-recursively]]
Expand Down

0 comments on commit 1578dd1

Please sign in to comment.