Pomegranate is a library that provides:
-
A sane Clojure API for the Maven Artifact Resolver.
-
A re-implementation of Clojure core’s deprecated
add-classpath
that:-
is a little more comprehensive, it should work as expected in more circumstances
-
optionally uses the Maven Artifact Resolver to add a Maven artifact (and all of its transitive dependencies) to your Clojure runtime’s classpath dynamically.
-
Note
|
When Pomegranate was created, the Maven Artifact Resolver was called Aether, so you’ll see aether in our APIs and docs.
|
If Pomegranate is not your cup of tea, consider having a look at:
-
Like Pomegranate, Clojure’s tools.deps resolves dependencies using the Maven Artifact Resolver. Unlike Pomegranate, by design, it is focused on runtime dependencies only.
Clojure v1.12, in alpha at the time of this writing, includes support for adding libraries at runtime.
-
Clojure v1.12’s add-lib feature for leiningen and/or other environments without a specific version of the clojure CLI
-
Experimental utilities for dealing with "the classpath", and dynamically loading libraries.
To get the latest changes that are not yet released to Clojars, you can use this library as a git dependency, for example:
$ cat deps.edn
{:deps {clj-commons/pomegranate {:git/url "https://github.com/clj-commons/pomegranate.git"
:git/sha "4db42b2091f363bff48cbb80bc5230c3afa598d9"}}}
Replace the :git/sha
value as appropriate.
To set the stage: you’re at the REPL, and you’ve got some useful data that you’d like to munge and analyze in various ways. Maybe it’s something you’ve generated locally, maybe it’s data on a production machine, and you’re logged in via nREPL. In any case, you’d like to work with the data, but realize that you don’t have the libraries you need do what you want. Your choices at this point are:
-
Dump the data to disk via
pr
(assuming it’s just Clojure data structures!), and start up a new Clojure process with the appropriate libraries on the classpath. This can really suck if the data is in a remote environment. -
There is no second choice. You could use Clojure’s deprecated
add-claspath
, but the libraries you want to add have 12 bajillion dependencies, and there’s no way you’re going to hunt them down manually.
Let’s say we want to use Incanter.
Incanter has roughly 40 dependencies — far too many for us to reasonably locate and add via add-classpath
manually:
user=> (require '[incanter core stats charts])
Execution error (FileNotFoundException) at user/eval1 (REPL:1).
Could not locate incanter/core__init.class, incanter/core.clj or incanter/core.cljc on classpath.
Looks bleak. Assuming you’ve got Pomegranate on your classpath already, you can do this though:
user=> (require '[cemerick.pomegranate :as pom]
'[cemerick.pomegranate.aether :as aether])
nil
user=> (pom/add-dependencies :coordinates '[[incanter "1.9.2"]]
:repositories (merge aether/maven-central
{"clojars" "https://clojars.org/repo"}))
;...add-dependencies returns full dependency graph here...
user=> (require '[incanter core stats charts])
nil
Now you can analyze and chart away, Incanter having been added to your runtime.
Note that add-dependencies
may crunch along for a while — it may need to download dependencies, so you’re waiting on the network.
All resolved dependencies are stored in the default local maven repository (~/.m2/repository
).
A dependency is only downloaded if it does not already exist in the local repository.
The arguments to add-dependencies
look like Leiningen-style notation, and they are.
Tip
|
There are a number of scenarios in which |
When Pomegranate was created, the JDK was amenable to inspecting and modifying class loaders. This changed starting with JDK version 9. Reflection API restrictions, modules, and encapsulation have given us less wiggle room.
Pomegranate 1.0.0
adapted to the new reality by no longer attempting to modify java.net.URLClassLoader
instances via reflection.
Pomegranate now leans on the modifiability of clojure.lang.DynamicClassLoader
.
As long as this classloader is available, we can modify the classpath.
If you find yourself in a situation where you want to use Pomegranate but have no dynamic classloader available, you might consider:
-
creating your own modifiable classloader, per the dynapath README, like boot did.
-
ensuring Clojure’s dynamic classloader available like like kaocha did.
Here we go over some simple example usages to get your feet wet. Please consult the API docs, they describe all available options.
We’ll do some setup in our REPL first:
(require '[cemerick.pomegranate.aether :as aether])
;; by default Pomegranate consults maven central, let's include clojars:
(alter-var-root #'aether/maven-central assoc "clojars" "https://repo.clojars.org")
;; => {"central" "https://repo1.maven.org/maven2/", "clojars" "https://repo.clojars.org"}
Let’s try resolving an artifact:
(aether/resolve-artifacts :coordinates '[[metosin/malli "0.10.0"]])
;; => ([metosin/malli "0.10.0"])
Ok not too exiting maybe, but now resolve dependencies for that artifact:
(aether/resolve-dependencies :coordinates '[[metosin/malli "0.10.0"]])
;; => {[org.clojure/clojure "1.8.0"] nil,
;; [org.clojure/test.check "1.1.1"] nil,
;; [org.clojure/core.rrb-vector "0.1.2"] nil,
;; [fipp "0.6.26"] #{[org.clojure/clojure "1.8.0"] [org.clojure/core.rrb-vector "0.1.2"]},
;; [borkdude/edamame "1.0.0"] #{[org.clojure/tools.reader "1.3.4"]},
;; [metosin/malli "0.10.0"] #{[org.clojure/test.check "1.1.1"]
;; [fipp "0.6.26"]
;; [borkdude/edamame "1.0.0"]
;; [borkdude/dynaload "0.3.5"]
;; [mvxcvi/arrangement "2.0.0"]},
;; [org.clojure/tools.reader "1.3.4"] nil,
;; [borkdude/dynaload "0.3.5"] nil,
;; [mvxcvi/arrangement "2.0.0"] nil}
Interesting. Also note that there’s some details hiding in metadata:
(-> (aether/resolve-dependencies :coordinates '[[metosin/malli "0.10.0"]])
ffirst
((juxt identity meta)))
;; => [[org.clojure/clojure "1.8.0"]
;; {:dependency
;; #object[org.eclipse.aether.graph.Dependency 0x7e70e8a0 "org.clojure:clojure:jar:1.8.0 (compile)"],
;; :file
;; #object[java.io.File 0x501ed01a "/home/lee/.m2/repository/org/clojure/clojure/1.8.0/clojure-1.8.0.jar"]}]
We can conveniently get to the :file
info like so:
(->> (aether/resolve-dependencies :coordinates '[[metosin/malli "0.10.0"]])
aether/dependency-files
(map str))
;; => ("/home/lee/.m2/repository/org/clojure/clojure/1.8.0/clojure-1.8.0.jar"
;; "/home/lee/.m2/repository/org/clojure/test.check/1.1.1/test.check-1.1.1.jar"
;; "/home/lee/.m2/repository/org/clojure/core.rrb-vector/0.1.2/core.rrb-vector-0.1.2.jar"
;; "/home/lee/.m2/repository/fipp/fipp/0.6.26/fipp-0.6.26.jar"
;; "/home/lee/.m2/repository/borkdude/edamame/1.0.0/edamame-1.0.0.jar"
;; "/home/lee/.m2/repository/metosin/malli/0.10.0/malli-0.10.0.jar"
;; "/home/lee/.m2/repository/org/clojure/tools.reader/1.3.4/tools.reader-1.3.4.jar"
;; "/home/lee/.m2/repository/borkdude/dynaload/0.3.5/dynaload-0.3.5.jar"
;; "/home/lee/.m2/repository/mvxcvi/arrangement/2.0.0/arrangement-2.0.0.jar")
Let’s have Pomegranate express dependencies for malli using malli as the root dependency:
(->> (aether/resolve-dependencies :coordinates '[[metosin/malli "0.10.0"]])
(aether/dependency-hierarchy '[[metosin/malli "0.10.0"]]))
;; => {[metosin/malli "0.10.0"]
;; {[borkdude/dynaload "0.3.5"] nil,
;; [borkdude/edamame "1.0.0"] {[org.clojure/tools.reader "1.3.4"] nil},
;; [fipp "0.6.26"] {[org.clojure/clojure "1.8.0"] nil,
;; [org.clojure/core.rrb-vector "0.1.2"] nil},
;; [mvxcvi/arrangement "2.0.0"] nil,
;; [org.clojure/test.check "1.1.1"] nil}}
Cool!
Out of the box, Pomegranate can communicate with maven repositories over HTTPS.
If you need to hit a maven repository that speaks some other protocol, you can do so via Maven Wagon.
For example, by default, for security reasons, Pomegranate no longer has plain old unsecure HTTP support built available. But, if you understand the risks (don’t do this if you don’t), and want to re-enable this support, you can do so by registering an HTTP wagon like so:
(aether/register-wagon-factory! "http" #(org.apache.maven.wagon.providers.http.HttpWagon.))
And now you can hit your unsecure HTTP maven repo too. Maybe you are running a local instance for caching.
(aether/resolve-artifacts :coordinates '[[metosin/malli "0.10.0"]]
:repositories {"local-nexus" "http://localhost:8081/repository/maven-public"})
Tip
|
If you want a tool that does this well that uses the Pomegranate to do so, consider using deps-deploy. Fun fact: To deploy itself to clojars, Pomegranate uses deps-deploy, which uses Pomegranate. |
Assuming pom.xml
and target/some-library.jar
files, exist:
(aether/install :coordinates '[lread/mucking-around "1.2.3"]
:jar-file (io/file "target" "some-library.jar")
:pom-file (io/file "pom.xml"))
After this completes, you’ll see something like:
$ tree ~/.m2/repository/lread/mucking-around
/home/lee/.m2/repository/lread/mucking-around
├── 1.2.3
│ ├── mucking-around-1.2.3.jar
│ ├── mucking-around-1.2.3.pom
│ └── _remote.repositories
└── maven-metadata-local.xml
1 directory, 4 files
Assuming pom.xml
and target/some-library.jar
, exist, a deploy to clojars could look something like this:
(aether/deploy :coordinates '[lread/mucking-around "1.2.3"]
:jar-file (io/file "target" "some-library.jar")
:pom-file (io/file "pom.xml")
:repository {"clojars" {:url "https://repo.clojars.org"
:username (System/getenv "CLOJARS_USERNAME")
:password (System/getenv "CLOJARS_PASSWORD")}})