Skip to content

Latest commit

 

History

History
303 lines (234 loc) · 12.6 KB

01-user-guide.adoc

File metadata and controls

303 lines (234 loc) · 12.6 KB

User Guide

Audience

You want to learn more about how to use the Pomegranate library from your app or library.

Introduction

Pomegranate is a library that provides:

  1. A sane Clojure API for the Maven Artifact Resolver.

  2. 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.

Interesting Alternatives

If Pomegranate is not your cup of tea, consider having a look at:

  • clojure/tools.deps

    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.

  • borkdude/deps.add-lib

    Clojure v1.12’s add-lib feature for leiningen and/or other environments without a specific version of the clojure CLI

  • com.lambdaisland.classpath

    Experimental utilities for dealing with "the classpath", and dynamically loading libraries.

History

  • Oct 2011 - The first version of cemerick/pomegranate is released to Maven Central.

  • Jan 2020 - Clj-commons adopts Pomegranate where it can get some ongoing love and care. It makes its first release under clj-commons/pomegranate to Clojars.

Installation

Leiningen project.clj

[clj-commons/pomegranate "1.2.23"]

Clojure CLI deps.edn

clj-commons/pomegranate {:mvn/version "1.2.23"}

As a Git Dependency

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.

Usage

Modifying the Classpath

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:

  1. 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.

  2. 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 add-dependencies will not work, or will not work as you’d expect. Many of these are due to the nature of JVM classloaders (e.g. adding jars containing conflicting versions of a particular dependency will rarely end well), which Pomegranate does not currently attempt to hide. Thus, add-classpath and add-dependencies should be considered escape hatches to be used when necessary, rather than a regular part of your development workflow.

Modifying the Classpath and JDK 9+

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:

The Aether API

Here we go over some simple example usages to get your feet wet. Please consult the API docs, they describe all available options.

Dependency Resolution

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!

Supporting other Protocols via Wagons

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"})

Deploying Artifacts

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.
To Local Maven Repo

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
To Remote Maven Repo

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")}})