Do not use version ranges in project.clj

Build tools like lein and maven make it very easy to use libraries in clojure. Just go to clojars or central and find the one you want. However, there are several situations where dependency resolution does not go as planned. A lot of these are a result of using version ranges for a dependency.

I’m going to pick on Christophe Grand and use his regex library for some examples of what can happen. It contains a version range for clojure of [1.2.0,). This means it wants version 1.2.0 or later.

Version ranges check a pom for every version

Given this project.clj:

(defproject tester "0.0.1"
  :dependencies [[org.clojure/clojure "1.4.0"]
                 [net.cgrand/regex "1.1.0"]])

Running lein deps produces:

Retrieving org/clojure/clojure/1.3.0/clojure-1.3.0.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/sonatype/oss/oss-parent/5/oss-parent-5.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.2.1/clojure-1.2.1.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.3.0/clojure-1.3.0.jar (3022k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0/clojure-1.4.0.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/maven-metadata.xml (1k)
    from http://repo1.maven.org/maven2/
Could not find metadata org.clojure:clojure/maven-metadata.xml in clojars (https://clojars.org/repo/)
Retrieving org/clojure/clojure/1.2.0/clojure-1.2.0.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.3.0-alpha5/clojure-1.3.0-alpha5.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.3.0-alpha6/clojure-1.3.0-alpha6.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.3.0-alpha7/clojure-1.3.0-alpha7.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.3.0-alpha8/clojure-1.3.0-alpha8.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.3.0-beta1/clojure-1.3.0-beta1.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.3.0-beta2/clojure-1.3.0-beta2.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.3.0-beta3/clojure-1.3.0-beta3.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.3.0-RC0/clojure-1.3.0-RC0.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-alpha1/clojure-1.4.0-alpha1.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-alpha2/clojure-1.4.0-alpha2.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-alpha3/clojure-1.4.0-alpha3.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-alpha4/clojure-1.4.0-alpha4.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-alpha5/clojure-1.4.0-alpha5.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-beta1/clojure-1.4.0-beta1.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-beta2/clojure-1.4.0-beta2.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-beta3/clojure-1.4.0-beta3.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-beta4/clojure-1.4.0-beta4.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-beta5/clojure-1.4.0-beta5.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-beta6/clojure-1.4.0-beta6.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0-beta7/clojure-1.4.0-beta7.pom (1k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.5.0-alpha1/clojure-1.5.0-alpha1.pom (2k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.5.0-alpha2/clojure-1.5.0-alpha2.pom (2k)
    from http://repo1.maven.org/maven2/
Retrieving org/clojure/clojure/1.4.0/clojure-1.4.0.jar (3045k)
    from http://repo1.maven.org/maven2/

This produces a large number of requests for poms. Unfortunately Aether, the library underneath lein2/maven3, cannot merge the two dependencies on clojure without downloading everything. This is a rather slight annoyance, but it makes clean builds, such as on travis-ci, very noisy.

Version ranges cause surprises

(defproject tester "0.0.1"
  :dependencies [[org.clojure/clojure "1.1.0"]
                 [net.cgrand/regex "1.1.0"]])

lein deps :tree shows:

[net.cgrand/regex "1.1.0"]
  [org.clojure/clojure "1.5.0-alpha2"]

Here is a project with a direct dependency on clojure 1.1.0. This is outside of the range desired by net.cgrand/regex. Aether lets version ranges take priority over normal “soft” dependencies, and the project uses 1.5.0-alpha2 instead. Normally a direct dependency will take priority over any transitive dependencies. This situation causes a surprise for the user, who would not be expecting a transitive version range. The user has to go explicitly add an :exclusion to get the desired version of clojure.

Version ranges are hard to track down

(defproject tester "0.0.1"
  :dependencies [[org.clojure/clojure "1.1.0"]
                 [serializable-fn "1.1.2"]
                 [net.cgrand/regex "1.1.0"]])

lein deps :tree shows:

[net.cgrand/regex "1.1.0"]
[serializable-fn "1.1.2"]
  [org.clojure/clojure "1.3.0"]

Here the project has another dependency – serializable-fn. It wants clojure 1.3.0. The dependency tree shows that clojure 1.3.0 got chosen because of serializable-fn. However, this only happens because net.cgrand/regex has a version range. This association of why a dependency was downloaded does not help the user determine where an :exclusion should go. Using the data they would place an :exclusion on serializable-fn, but that would just lead to getting 1.5.0-alpha2 as above.

Version ranges cause unrepeatable builds

Open ended version ranges also create unrepeatable builds. Since net.cgrand/regex uses the latest above 1.2.0, building a version now, versus building a version a week from now may result in different clojure versions running the test suite.

Do not use version ranges

Do not use version ranges. There have been several instances of people asking for help on #clojure or #leiningen and a version range being the underlying issue. They cause problems for other developers and users of your libraries.