```
Looks bleak. Assuming you've got Pomegranate on your classpath already, you can do this though:
```clojure
=> (use '[cemerick.pomegranate :only (add-dependencies)])
nil
=> (add-dependencies :coordinates '[[incanter "1.2.3"]]
:repositories (merge cemerick.pomegranate.aether/maven-central
{"clojars" "http://clojars.org/repo"}))
;...add-dependencies returns full dependency graph...
=> (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 repository (`~/.m2/repository`), and if they are found there, then they are not downloaded.
The arguments to `add-dependencies` look like Leiningen-style notation, and they are.
Please note that **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.
## Status of Aether support
Pomegranate is being used by [Leiningen v2.x](http://leiningen.org) as
its sole dependency resolution library. This has prompted rapid
maturation of the scope and quality of Aether support. That said,
specific API points remain subject to change as we find the right
abstractions and conventions.
#### Supported features
* dependency resolution
** common dependency graph/hierarchy manipulation ops
* local installation
* remote deployment
* repository authentication for all of the above
* HTTP proxy configuration
* offline mode
* transfer listeners (with a sane Clojure fn veneer)
#### Not there yet
* repository listeners
* mirror support
* options to retrieve a single artifact (e.g. for obtaining
source/javadoc)
* tests; there's halfway decent coverage, but nowhere near the kind of comprehensive combinatorial testing that maven dependency resolution demands
## Changelog
See the `CHANGES.md` file at the top level of the repo.
## Need Help?
Ping `cemerick` on freenode irc or twitter if you have questions
or would like to contribute patches.
## License
Copyright © 2011-2012 [Chas Emerick](http://cemerick.com) and all other
contributors.
Licensed under the EPL. (See the file epl-v10.html.)
epl-v10.html 0000664 0000000 0000000 00000031163 12121704554 0013135 0 ustar 00root root 0000000 0000000
Eclipse Public License - Version 1.0
Eclipse Public License - v 1.0
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR
DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS
AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial
code and documentation distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program
originate from and are distributed by that particular Contributor. A
Contribution 'originates' from a Contributor if it was added to the
Program by such Contributor itself or anyone acting on such
Contributor's behalf. Contributions do not include additions to the
Program which: (i) are separate modules of software distributed in
conjunction with the Program under their own license agreement, and (ii)
are not derivative works of the Program.
"Contributor" means any person or entity that distributes
the Program.
"Licensed Patents" mean patent claims licensable by a
Contributor which are necessarily infringed by the use or sale of its
Contribution alone or when combined with the Program.
"Program" means the Contributions distributed in accordance
with this Agreement.
"Recipient" means anyone who receives the Program under
this Agreement, including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each
Contributor hereby grants Recipient a non-exclusive, worldwide,
royalty-free copyright license to reproduce, prepare derivative works
of, publicly display, publicly perform, distribute and sublicense the
Contribution of such Contributor, if any, and such derivative works, in
source code and object code form.
b) Subject to the terms of this Agreement, each
Contributor hereby grants Recipient a non-exclusive, worldwide,
royalty-free patent license under Licensed Patents to make, use, sell,
offer to sell, import and otherwise transfer the Contribution of such
Contributor, if any, in source code and object code form. This patent
license shall apply to the combination of the Contribution and the
Program if, at the time the Contribution is added by the Contributor,
such addition of the Contribution causes such combination to be covered
by the Licensed Patents. The patent license shall not apply to any other
combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor
grants the licenses to its Contributions set forth herein, no assurances
are provided by any Contributor that the Program does not infringe the
patent or other intellectual property rights of any other entity. Each
Contributor disclaims any liability to Recipient for claims brought by
any other entity based on infringement of intellectual property rights
or otherwise. As a condition to exercising the rights and licenses
granted hereunder, each Recipient hereby assumes sole responsibility to
secure any other intellectual property rights needed, if any. For
example, if a third party patent license is required to allow Recipient
to distribute the Program, it is Recipient's responsibility to acquire
that license before distributing the Program.
d) Each Contributor represents that to its knowledge it
has sufficient copyright rights in its Contribution, if any, to grant
the copyright license set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code
form under its own license agreement, provided that:
a) it complies with the terms and conditions of this
Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors
all warranties and conditions, express and implied, including warranties
or conditions of title and non-infringement, and implied warranties or
conditions of merchantability and fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors
all liability for damages, including direct, indirect, special,
incidental and consequential damages, such as lost profits;
iii) states that any provisions which differ from this
Agreement are offered by that Contributor alone and not by any other
party; and
iv) states that source code for the Program is available
from such Contributor, and informs licensees how to obtain it in a
reasonable manner on or through a medium customarily used for software
exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each
copy of the Program.
Contributors may not remove or alter any copyright notices contained
within the Program.
Each Contributor must identify itself as the originator of its
Contribution, if any, in a manner that reasonably allows subsequent
Recipients to identify the originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain
responsibilities with respect to end users, business partners and the
like. While this license is intended to facilitate the commercial use of
the Program, the Contributor who includes the Program in a commercial
product offering should do so in a manner which does not create
potential liability for other Contributors. Therefore, if a Contributor
includes the Program in a commercial product offering, such Contributor
("Commercial Contributor") hereby agrees to defend and
indemnify every other Contributor ("Indemnified Contributor")
against any losses, damages and costs (collectively "Losses")
arising from claims, lawsuits and other legal actions brought by a third
party against the Indemnified Contributor to the extent caused by the
acts or omissions of such Commercial Contributor in connection with its
distribution of the Program in a commercial product offering. The
obligations in this section do not apply to any claims or Losses
relating to any actual or alleged intellectual property infringement. In
order to qualify, an Indemnified Contributor must: a) promptly notify
the Commercial Contributor in writing of such claim, and b) allow the
Commercial Contributor to control, and cooperate with the Commercial
Contributor in, the defense and any related settlement negotiations. The
Indemnified Contributor may participate in any such claim at its own
expense.
For example, a Contributor might include the Program in a commercial
product offering, Product X. That Contributor is then a Commercial
Contributor. If that Commercial Contributor then makes performance
claims, or offers warranties related to Product X, those performance
claims and warranties are such Commercial Contributor's responsibility
alone. Under this section, the Commercial Contributor would have to
defend claims against the other Contributors related to those
performance claims and warranties, and if a court requires any other
Contributor to pay any damages as a result, the Commercial Contributor
must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION,
ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
responsible for determining the appropriateness of using and
distributing the Program and assumes all risks associated with its
exercise of rights under this Agreement , including but not limited to
the risks and costs of program errors, compliance with applicable laws,
damage to or loss of data, programs or equipment, and unavailability or
interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this Agreement, and without further action
by the parties hereto, such provision shall be reformed to the minimum
extent necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity
(including a cross-claim or counterclaim in a lawsuit) alleging that the
Program itself (excluding combinations of the Program with other
software or hardware) infringes such Recipient's patent(s), then such
Recipient's rights granted under Section 2(b) shall terminate as of the
date such litigation is filed.
All Recipient's rights under this Agreement shall terminate if it
fails to comply with any of the material terms or conditions of this
Agreement and does not cure such failure in a reasonable period of time
after becoming aware of such noncompliance. If all Recipient's rights
under this Agreement terminate, Recipient agrees to cease use and
distribution of the Program as soon as reasonably practicable. However,
Recipient's obligations under this Agreement and any licenses granted by
Recipient relating to the Program shall continue and survive.
Everyone is permitted to copy and distribute copies of this
Agreement, but in order to avoid inconsistency the Agreement is
copyrighted and may only be modified in the following manner. The
Agreement Steward reserves the right to publish new versions (including
revisions) of this Agreement from time to time. No one other than the
Agreement Steward has the right to modify this Agreement. The Eclipse
Foundation is the initial Agreement Steward. The Eclipse Foundation may
assign the responsibility to serve as the Agreement Steward to a
suitable separate entity. Each new version of the Agreement will be
given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version
of the Agreement is published, Contributor may elect to distribute the
Program (including its Contributions) under the new version. Except as
expressly stated in Sections 2(a) and 2(b) above, Recipient receives no
rights or licenses to the intellectual property of any Contributor under
this Agreement, whether expressly, by implication, estoppel or
otherwise. All rights in the Program not expressly granted under this
Agreement are reserved.
This Agreement is governed by the laws of the State of New York and
the intellectual property laws of the United States of America. No party
to this Agreement will bring a legal action under this Agreement more
than one year after the cause of action arose. Each party waives its
rights to a jury trial in any resulting litigation.
pom.xml 0000664 0000000 0000000 00000006737 12121704554 0012411 0 ustar 00root root 0000000 0000000
4.0.0
com.cemerick
pomegranate
0.2.0
pomegranate
http://github.com/cemerick/pomegranate
org.clojure
pom.contrib
0.0.20
Chas Emerick
http://cemerick.com
cemerick@snowtide.com
-5
scm:git:git@github.com:cemerick/pomegranate.git
scm:git:git@github.com:cemerick/pomegranate.git
git@github.com:cemerick/pomegranate.git
1.3.0
1.13.1
3.0.4
2.2
org.sonatype.aether
aether-api
${aetherVersion}
org.sonatype.aether
aether-util
${aetherVersion}
org.sonatype.aether
aether-impl
${aetherVersion}
org.sonatype.aether
aether-connector-file
${aetherVersion}
org.sonatype.aether
aether-connector-wagon
${aetherVersion}
org.apache.maven
maven-aether-provider
${mavenVersion}
org.tcrawley
dynapath
0.2.3
org.apache.maven.wagon
wagon-provider-api
${wagonVersion}
org.apache.maven.wagon
wagon-http
${wagonVersion}
org.apache.maven.wagon
wagon-ssh
${wagonVersion}
true
src/main/clojure
src/ 0000775 0000000 0000000 00000000000 12121704554 0011646 5 ustar 00root root 0000000 0000000 src/main/ 0000775 0000000 0000000 00000000000 12121704554 0012572 5 ustar 00root root 0000000 0000000 src/main/clojure/ 0000775 0000000 0000000 00000000000 12121704554 0014235 5 ustar 00root root 0000000 0000000 src/main/clojure/cemerick/ 0000775 0000000 0000000 00000000000 12121704554 0016017 5 ustar 00root root 0000000 0000000 src/main/clojure/cemerick/pomegranate.clj 0000664 0000000 0000000 00000011754 12121704554 0021023 0 ustar 00root root 0000000 0000000 (ns cemerick.pomegranate
(:import (clojure.lang DynamicClassLoader)
(java.net URL URLClassLoader))
(:require [clojure.java.io :as io]
[cemerick.pomegranate.aether :as aether]
[dynapath.util :as dp])
(:refer-clojure :exclude (add-classpath)))
;; call-method pulled from clojure.contrib.reflect, (c) 2010 Stuart Halloway & Contributors
(defn- call-method
"Calls a private or protected method.
params is a vector of classes which correspond to the arguments to
the method e
obj is nil for static methods, the instance object otherwise.
The method-name is given a symbol or a keyword (something Named)."
[klass method-name params obj & args]
(-> klass (.getDeclaredMethod (name method-name)
(into-array Class params))
(doto (.setAccessible true))
(.invoke obj (into-array Object args))))
(defn classloader-hierarchy
"Returns a seq of classloaders, with the tip of the hierarchy first.
Uses the current thread context ClassLoader as the tip ClassLoader
if one is not provided."
([] (classloader-hierarchy (.. Thread currentThread getContextClassLoader)))
([tip]
(->> tip
(iterate #(.getParent %))
(take-while boolean))))
(defn modifiable-classloader?
"Returns true iff the given ClassLoader is of a type that satisfies
the dynapath.dynamic-classpath/DynamicClasspath protocol, and it can
be modified."
[cl]
(dp/addable-classpath? cl))
(defn add-classpath
"A corollary to the (deprecated) `add-classpath` in clojure.core. This implementation
requires a java.io.File or String path to a jar file or directory, and will attempt
to add that path to the right classloader (with the search rooted at the current
thread's context classloader)."
([jar-or-dir classloader]
(if-not (dp/add-classpath-url classloader (.toURL (io/file jar-or-dir)))
(throw (IllegalStateException. (str classloader " is not a modifiable classloader")))))
([jar-or-dir]
(let [classloaders (classloader-hierarchy)]
(if-let [cl (last (filter modifiable-classloader? classloaders))]
(add-classpath jar-or-dir cl)
(throw (IllegalStateException. (str "Could not find a suitable classloader to modify from "
classloaders)))))))
(defn add-dependencies
"Resolves a set of dependencies, optionally against a set of additional Maven repositories,
and adds all of the resulting artifacts (jar files) to the current runtime via
`add-classpath`:
(add-dependencies :coordinates '[[incanter \"1.2.3\"]]
:repositories (merge cemerick.pomegranate.aether/maven-central
{\"clojars\" \"http://clojars.org/repo\"}))
Note that Maven central is used as the sole repository if none are specified.
If :repositories are provided, then you must merge in the `maven-central` map from
the cemerick.pomegranate.aether namespace yourself.
Acceptable arguments are the same as those for
`cemerick.pomegranate.aether/resolve-dependencies`; returns the dependency graph
returned from that function."
[& args]
(let [deps (apply aether/resolve-dependencies args)]
(doseq [artifact-file (aether/dependency-files deps)]
(add-classpath artifact-file))
deps))
(defn get-classpath
"Returns the effective classpath (i.e. _not_ the value of
(System/getProperty \"java.class.path\") as a seq of URL strings.
Produces the classpath from all classloaders by default, or from a
collection of classloaders if provided. This allows you to easily look
at subsets of the current classloader hierarchy, e.g.:
(get-classpath (drop 2 (classloader-hierarchy)))"
([classloaders]
(->> (reverse classloaders)
(mapcat #(dp/classpath-urls %))
(map str)))
([] (get-classpath (classloader-hierarchy))))
(defn classloader-resources
"Returns a sequence of [classloader url-seq] pairs representing all
of the resources of the specified name on the classpath of each
classloader. If no classloaders are given, uses the
classloader-heirarchy, in which case the order of pairs will be
such that the first url mentioned will in most circumstances match
what clojure.java.io/resource returns."
([classloaders resource-name]
(for [classloader (reverse classloaders)]
[classloader (enumeration-seq
(.getResources ^ClassLoader classloader resource-name))]))
([resource-name] (classloader-resources (classloader-hierarchy) resource-name)))
(defn resources
"Returns a sequence of URLs representing all of the resources of the
specified name on the effective classpath. This can be useful for
finding name collisions among items on the classpath. In most
circumstances, the first of the returned sequence will be the same
as what clojure.java.io/resource returns."
([classloaders resource-name]
(distinct (mapcat second (classloader-resources classloaders resource-name))))
([resource-name] (resources (classloader-hierarchy) resource-name)))
src/main/clojure/cemerick/pomegranate/ 0000775 0000000 0000000 00000000000 12121704554 0020321 5 ustar 00root root 0000000 0000000 src/main/clojure/cemerick/pomegranate/aether.clj 0000664 0000000 0000000 00000071422 12121704554 0022271 0 ustar 00root root 0000000 0000000 (ns cemerick.pomegranate.aether
(:refer-clojure :exclude [type proxy])
(:require [clojure.java.io :as io]
clojure.set
[clojure.string :as str])
(:import (org.apache.maven.repository.internal DefaultServiceLocator MavenRepositorySystemSession)
(org.sonatype.aether RepositorySystem)
(org.sonatype.aether.transfer TransferListener)
(org.sonatype.aether.artifact Artifact)
(org.sonatype.aether.connector.file FileRepositoryConnectorFactory)
(org.sonatype.aether.connector.wagon WagonProvider WagonRepositoryConnectorFactory)
(org.sonatype.aether.spi.connector RepositoryConnectorFactory)
(org.sonatype.aether.repository Proxy ArtifactRepository Authentication
RepositoryPolicy LocalRepository RemoteRepository
MirrorSelector)
(org.sonatype.aether.util.repository DefaultProxySelector DefaultMirrorSelector)
(org.sonatype.aether.graph Dependency Exclusion DependencyNode)
(org.sonatype.aether.collection CollectRequest)
(org.sonatype.aether.resolution DependencyRequest ArtifactRequest)
(org.sonatype.aether.util.graph PreorderNodeListGenerator)
(org.sonatype.aether.util.artifact DefaultArtifact SubArtifact
ArtifactProperties)
(org.sonatype.aether.deployment DeployRequest)
(org.sonatype.aether.installation InstallRequest)
(org.sonatype.aether.util.version GenericVersionScheme)))
(def ^{:private true} default-local-repo
(io/file (System/getProperty "user.home") ".m2" "repository"))
(def maven-central {"central" "http://repo1.maven.org/maven2/"})
; Using HttpWagon (which uses apache httpclient) because the "LightweightHttpWagon"
; (which just uses JDK HTTP) reliably flakes if you attempt to resolve SNAPSHOT
; artifacts from an HTTPS password-protected repository (like a nexus instance)
; when other un-authenticated repositories are included in the resolution.
; My theory is that the JDK HTTP impl is screwing up connection pooling or something,
; and reusing the same connection handle for the HTTPS repo as it used for e.g.
; central, without updating the authentication info.
; In any case, HttpWagon is what Maven 3 uses, and it works.
(def ^{:private true} wagon-factories (atom {"http" #(org.apache.maven.wagon.providers.http.HttpWagon.)
"https" #(org.apache.maven.wagon.providers.http.HttpWagon.)}))
(defn register-wagon-factory!
"Registers a new no-arg factory function for the given scheme. The function must return
an implementation of org.apache.maven.wagon.Wagon."
[scheme factory-fn]
(swap! wagon-factories (fn [m]
(when-let [fn (m scheme)]
(println (format "Warning: replacing existing support for %s repositories (%s) with %s" scheme fn factory-fn)))
(assoc m scheme factory-fn))))
(deftype PomegranateWagonProvider []
WagonProvider
(release [_ wagon])
(lookup [_ role-hint]
(when-let [f (get @wagon-factories role-hint)]
(f))))
(deftype TransferListenerProxy [listener-fn]
TransferListener
(transferCorrupted [_ e] (listener-fn e))
(transferFailed [_ e] (listener-fn e))
(transferInitiated [_ e] (listener-fn e))
(transferProgressed [_ e] (listener-fn e))
(transferStarted [_ e] (listener-fn e))
(transferSucceeded [_ e] (listener-fn e)))
(defn- transfer-event
[^org.sonatype.aether.transfer.TransferEvent e]
; INITIATED, STARTED, PROGRESSED, CORRUPTED, SUCCEEDED, FAILED
{:type (-> e .getType .name str/lower-case keyword)
; :get :put
:method (-> e .getRequestType str/lower-case keyword)
:transferred (.getTransferredBytes e)
:error (.getException e)
:data-buffer (.getDataBuffer e)
:data-length (.getDataLength e)
:resource (let [r (.getResource e)]
{:repository (.getRepositoryUrl r)
:name (.getResourceName r)
:file (.getFile r)
:size (.getContentLength r)
:transfer-start-time (.getTransferStartTime r)
:trace (.getTrace r)})})
(defn- default-listener-fn
[{:keys [type method transferred resource error] :as evt}]
(let [{:keys [name size repository transfer-start-time]} resource]
(case type
:started (do
(print (case method :get "Retrieving" :put "Sending")
name
(if (neg? size)
""
(format "(%sk)" (Math/round (double (max 1 (/ size 1024)))))))
(when (< 70 (+ 10 (count name) (count repository)))
(println) (print " "))
(println (case method :get "from" :put "to") repository))
(:corrupted :failed) (when error (println (.getMessage error)))
nil)))
(defn- repository-system
[]
(.getService (doto (DefaultServiceLocator.)
(.addService RepositoryConnectorFactory FileRepositoryConnectorFactory)
(.addService RepositoryConnectorFactory WagonRepositoryConnectorFactory)
(.addService WagonProvider PomegranateWagonProvider))
org.sonatype.aether.RepositorySystem))
(defn- construct-transfer-listener
[transfer-listener]
(cond
(instance? TransferListener transfer-listener) transfer-listener
(= transfer-listener :stdout)
(TransferListenerProxy. (comp default-listener-fn transfer-event))
(fn? transfer-listener)
(TransferListenerProxy. (comp transfer-listener transfer-event))
:else (TransferListenerProxy. (fn [_]))))
(defn repository-session
[{:keys [repository-system local-repo offline? transfer-listener mirror-selector]}]
(-> (MavenRepositorySystemSession.)
(.setLocalRepositoryManager (.newLocalRepositoryManager repository-system
(-> (io/file (or local-repo default-local-repo))
.getAbsolutePath
LocalRepository.)))
(.setMirrorSelector mirror-selector)
(.setOffline (boolean offline?))
(.setTransferListener (construct-transfer-listener transfer-listener))))
(def update-policies {:daily RepositoryPolicy/UPDATE_POLICY_DAILY
:always RepositoryPolicy/UPDATE_POLICY_ALWAYS
:never RepositoryPolicy/UPDATE_POLICY_NEVER})
(def checksum-policies {:fail RepositoryPolicy/CHECKSUM_POLICY_FAIL
:ignore RepositoryPolicy/CHECKSUM_POLICY_IGNORE
:warn RepositoryPolicy/CHECKSUM_POLICY_WARN})
(defn- policy
[policy-settings enabled?]
(RepositoryPolicy.
(boolean enabled?)
(update-policies (:update policy-settings :daily))
(checksum-policies (:checksum policy-settings :fail))))
(defn- set-policies
[repo settings]
(doto repo
(.setPolicy true (policy settings (:snapshots settings true)))
(.setPolicy false (policy settings (:releases settings true)))))
(defn- set-authentication
"Calls the setAuthentication method on obj"
[obj {:keys [username password passphrase private-key-file] :as settings}]
(if (or username password private-key-file passphrase)
(.setAuthentication obj (Authentication. username password private-key-file passphrase))
obj))
(defn- set-proxy
[repo {:keys [type host port non-proxy-hosts ]
:or {type "http"}
:as proxy} ]
(if (and repo host port)
(let [prx-sel (doto (DefaultProxySelector.)
(.add (set-authentication (Proxy. type host port nil) proxy)
non-proxy-hosts))
prx (.getProxy prx-sel repo)]
(.setProxy repo prx))
repo))
(defn- make-repository
[[id settings] proxy]
(let [settings-map (if (string? settings)
{:url settings}
settings)]
(doto (RemoteRepository. id
(:type settings-map "default")
(str (:url settings-map)))
(set-policies settings-map)
(set-proxy proxy)
(set-authentication settings-map))))
(defn- group
[group-artifact]
(or (namespace group-artifact) (name group-artifact)))
(defn- coordinate-string
"Produces a coordinate string with a format of
:[:[:]]:>
given a lein-style dependency spec. :extension defaults to jar."
[[group-artifact version & {:keys [classifier extension] :or {extension "jar"}}]]
(->> [(group group-artifact) (name group-artifact) extension classifier version]
(remove nil?)
(interpose \:)
(apply str)))
(defn- exclusion
[[group-artifact & {:as opts}]]
(Exclusion.
(group group-artifact)
(name group-artifact)
(:classifier opts "*")
(:extension opts "*")))
(defn- normalize-exclusion-spec [spec]
(if (symbol? spec)
[spec]
spec))
(defn- dependency
[[group-artifact version & {:keys [scope optional exclusions]
:as opts
:or {scope "compile"
optional false}}
:as dep-spec]]
(Dependency. (DefaultArtifact. (coordinate-string dep-spec))
scope
optional
(map (comp exclusion normalize-exclusion-spec) exclusions)))
(declare dep-spec*)
(defn- exclusion-spec
"Given an Aether Exclusion, returns a lein-style exclusion vector with the
:exclusion in its metadata."
[^Exclusion ex]
(with-meta (-> ex bean dep-spec*) {:exclusion ex}))
(defn- dep-spec
"Given an Aether Dependency, returns a lein-style dependency vector with the
:dependency and its corresponding artifact's :file in its metadata."
[^Dependency dep]
(let [artifact (.getArtifact dep)]
(-> (merge (bean dep) (bean artifact))
dep-spec*
(with-meta {:dependency dep :file (.getFile artifact)}))))
(defn- dep-spec*
"Base function for producing lein-style dependency spec vectors for dependencies
and exclusions."
[{:keys [groupId artifactId version classifier extension scope optional exclusions]
:or {version nil
scope "compile"
optional false
exclusions nil}}]
(let [group-artifact (apply symbol (if (= groupId artifactId)
[artifactId]
[groupId artifactId]))]
(vec (concat [group-artifact]
(when version [version])
(when (and (seq classifier)
(not= "*" classifier))
[:classifier classifier])
(when (and (seq extension)
(not (#{"*" "jar"} extension)))
[:extension extension])
(when optional [:optional true])
(when (not= scope "compile")
[:scope scope])
(when (seq exclusions)
[:exclusions (vec (map exclusion-spec exclusions))])))))
(defn- create-artifact
[files artifact]
(if-let [file (get files artifact)]
(-> (coordinate-string artifact)
DefaultArtifact.
(.setFile (io/file file)))
(throw (IllegalArgumentException. (str "No file provided for artifact " artifact)))))
(defn deploy-artifacts
"Deploy the artifacts kwarg to the repository kwarg.
:files - map from artifact vectors to file paths or java.io.File objects
where the file to be deployed for each artifact is to be found
An artifact vector is e.g.
'[group/artifact \"1.0.0\"] or
'[group/artifact \"1.0.0\" :extension \"pom\"].
All artifacts should have the same version and group and artifact IDs
:repository - {name url} | {name settings}
settings:
:url - URL of the repository
:snapshots - use snapshots versions? (default true)
:releases - use release versions? (default true)
:username - username to log in with
:password - password to log in with
:passphrase - passphrase to log in wth
:private-key-file - private key file to log in with
:update - :daily (default) | :always | :never
:checksum - :fail | :ignore | :warn (default)
:local-repo - path to the local repository (defaults to ~/.m2/repository)
:transfer-listener - same as provided to resolve-dependencies
:proxy - proxy configuration, can be nil, the host scheme and type must match
:host - proxy hostname
:type - http (default) | http | https
:port - proxy port
:non-proxy-hosts - The list of hosts to exclude from proxying, may be null
:username - username to log in with, may be null
:password - password to log in with, may be null
:passphrase - passphrase to log in wth, may be null
:private-key-file - private key file to log in with, may be null"
[& {:keys [files repository local-repo transfer-listener proxy repository-session-fn]}]
(when (empty? files)
(throw (IllegalArgumentException. "Must provide valid :files to deploy-artifacts")))
(when (->> (keys files)
(map (fn [[ga v]] [(if (namespace ga) ga (symbol (str ga) (str ga))) v]))
set
count
(< 1))
(throw (IllegalArgumentException.
(str "Provided artifacts have varying version, group, or artifact IDs: " (keys files)))))
(let [system (repository-system)
session ((or repository-session-fn
repository-session)
{:repository-system system
:local-repo local-repo
:offline? false
:transfer-listener transfer-listener})]
(.deploy system session
(doto (DeployRequest.)
(.setArtifacts (vec (map (partial create-artifact files) (keys files))))
(.setRepository (first (map #(make-repository % proxy) repository)))))))
(defn install-artifacts
"Deploy the file kwarg using the coordinates kwarg to the repository kwarg.
:files - same as with deploy-artifacts
:local-repo - path to the local repository (defaults to ~/.m2/repository)
:transfer-listener - same as provided to resolve-dependencies"
[& {:keys [files local-repo transfer-listener repository-session-fn]}]
(let [system (repository-system)
session ((or repository-session-fn
repository-session)
{:repository-system system
:local-repo local-repo
:offline? false
:transfer-listener transfer-listener})]
(.install system session
(doto (InstallRequest.)
(.setArtifacts (vec (map (partial create-artifact files) (keys files))))))))
(defn- artifacts-for
"Takes a coordinates map, an a map from partial coordinates to "
[coordinates file-map]
(zipmap (map (partial into coordinates) (keys file-map)) (vals file-map)))
(defn- optional-artifact
"Takes a coordinates map, an a map from partial coordinates to "
[artifact-coords path]
(when path {artifact-coords path}))
(defn deploy
"Deploy the jar-file kwarg using the pom-file kwarg and coordinates
kwarg to the repository kwarg.
:coordinates - [group/name \"version\"]
:artifact-map - a map from partial coordinates to file path or File
:jar-file - a file pointing to the jar
:pom-file - a file pointing to the pom
:repository - {name url} | {name settings}
settings:
:url - URL of the repository
:snapshots - use snapshots versions? (default true)
:releases - use release versions? (default true)
:username - username to log in with
:password - password to log in with
:passphrase - passphrase to log in wth
:private-key-file - private key file to log in with
:update - :daily (default) | :always | :never
:checksum - :fail (default) | :ignore | :warn
:local-repo - path to the local repository (defaults to ~/.m2/repository)
:transfer-listener - same as provided to resolve-dependencies
:proxy - proxy configuration, can be nil, the host scheme and type must match
:host - proxy hostname
:type - http (default) | http | https
:port - proxy port
:non-proxy-hosts - The list of hosts to exclude from proxying, may be null
:username - username to log in with, may be null
:password - password to log in with, may be null
:passphrase - passphrase to log in wth, may be null
:private-key-file - private key file to log in with, may be null"
[& {:keys [coordinates artifact-map jar-file pom-file] :as opts}]
(when (empty? coordinates)
(throw
(IllegalArgumentException. "Must provide valid :coordinates to deploy")))
(apply deploy-artifacts
(apply concat (assoc opts
:files (artifacts-for
coordinates
(merge
artifact-map
(optional-artifact [:extension "pom"] pom-file)
(optional-artifact [] jar-file)))))))
(defn install
"Install the artifacts specified by the jar-file or file-map and pom-file
kwargs using the coordinates kwarg.
:coordinates - [group/name \"version\"]
:artifact-map - a map from partial coordinates to file path or File
:jar-file - a file pointing to the jar
:pom-file - a file pointing to the pom
:local-repo - path to the local repository (defaults to ~/.m2/repository)
:transfer-listener - same as provided to resolve-dependencies"
[& {:keys [coordinates artifact-map jar-file pom-file] :as opts}]
(when (empty? coordinates)
(throw
(IllegalArgumentException. "Must provide valid :coordinates to install")))
(apply install-artifacts
(apply concat (assoc opts
:files (artifacts-for
coordinates
(merge
artifact-map
(optional-artifact [:extension "pom"] pom-file)
(optional-artifact [] jar-file)))))))
(defn- dependency-graph
([node]
(reduce (fn [g ^DependencyNode n]
(if-let [dep (.getDependency n)]
(update-in g [(dep-spec dep)]
clojure.set/union
(->> (.getChildren n)
(map #(.getDependency %))
(map dep-spec)
set))
g))
{}
(tree-seq (constantly true)
#(seq (.getChildren %))
node))))
(defn- mirror-selector-fn
"Default mirror selection function. The first argument should be a map
like that described as the :mirrors argument in resolve-dependencies.
The second argument should be a repository spec, also as described in
resolve-dependencies. Will return the mirror spec that matches the
provided repository spec."
[mirrors {:keys [name url snapshots releases]}]
(let [mirrors (filter (fn [[matcher mirror-spec]]
(or
(and (string? matcher) (or (= matcher name) (= matcher url)))
(and (instance? java.util.regex.Pattern matcher)
(or (re-matches matcher name) (re-matches matcher url)))))
mirrors)]
(case (count mirrors)
0 nil
1 (-> mirrors first second)
(if (some nil? (map second mirrors))
;; wildcard override
nil
(throw (IllegalArgumentException.
(str "Multiple mirrors configured to match repository " {name url} ": "
(into {} (map #(update-in % [1] select-keys [:name :url]) mirrors)))))))))
(defn- mirror-selector
"Returns a MirrorSelector that delegates matching of mirrors to given remote repositories
to the provided function. Any returned repository specifications are turned into
RemoteRepository instances, and configured to use the provided proxy."
[mirror-selector-fn proxy]
(reify MirrorSelector
(getMirror [_ repo]
(let [repo-spec {:name (.getId repo)
:url (.getUrl repo)
:snapshots (-> repo (.getPolicy true) .isEnabled)
:releases (-> repo (.getPolicy false) .isEnabled)}
{:keys [name repo-manager content-type] :as mirror-spec}
(mirror-selector-fn repo-spec)]
(when-let [mirror (and mirror-spec (make-repository [name mirror-spec] proxy))]
(-> (.setMirroredRepositories mirror [repo])
(.setRepositoryManager (boolean repo-manager))
(.setContentType (or content-type "default"))))))))
(defn resolve-dependencies*
"Collects dependencies for the coordinates kwarg, using repositories from the
`:repositories` kwarg.
Retrieval of dependencies can be disabled by providing `:retrieve false` as a kwarg.
Returns an instance of either `org.sonatype.aether.collection.CollectResult` if
`:retrieve false` or `org.sonatype.aether.resolution.DependencyResult` if
`:retrieve true` (the default). If you don't want to mess with the Aether
implmeentation classes, then use `resolve-dependencies` instead.
:coordinates - [[group/name \"version\" & settings] ..]
settings:
:extension - the maven extension (type) to require
:classifier - the maven classifier to require
:scope - the maven scope for the dependency (default \"compile\")
:optional - is the dependency optional? (default \"false\")
:exclusions - which sub-dependencies to skip : [group/name & settings]
settings:
:classifier (default \"*\")
:extension (default \"*\")
:repositories - {name url ..} | {name settings ..}
(defaults to {\"central\" \"http://repo1.maven.org/maven2/\"}
settings:
:url - URL of the repository
:snapshots - use snapshots versions? (default true)
:releases - use release versions? (default true)
:username - username to log in with
:password - password to log in with
:passphrase - passphrase to log in wth
:private-key-file - private key file to log in with
:update - :daily (default) | :always | :never
:checksum - :fail (default) | :ignore | :warn
:local-repo - path to the local repository (defaults to ~/.m2/repository)
:offline? - if true, no remote repositories will be contacted
:transfer-listener - the transfer listener that will be notifed of dependency
resolution and deployment events.
Can be:
- nil (the default), i.e. no notification of events
- :stdout, corresponding to a default listener implementation that writes
notifications and progress indicators to stdout, suitable for an
interactive console program
- a function of one argument, which will be called with a map derived from
each event.
- an instance of org.sonatype.aether.transfer.TransferListener
:proxy - proxy configuration, can be nil, the host scheme and type must match
:host - proxy hostname
:type - http (default) | http | https
:port - proxy port
:non-proxy-hosts - The list of hosts to exclude from proxying, may be null
:username - username to log in with, may be null
:password - password to log in with, may be null
:passphrase - passphrase to log in wth, may be null
:private-key-file - private key file to log in with, may be null
:mirrors - {matches settings ..}
matches - a string or regex that will be used to match the mirror to
candidate repositories. Attempts will be made to match the
string/regex to repository names and URLs, with exact string
matches preferred. Wildcard mirrors can be specified with
a match-all regex such as #\".+\". Excluding a repository
from mirroring can be done by mapping a string or regex matching
the repository in question to nil.
settings include these keys, and all those supported by :repositories:
:name - name/id of the mirror
:repo-manager - whether the mirror is a repository manager"
[& {:keys [repositories coordinates files retrieve local-repo
transfer-listener offline? proxy mirrors repository-session-fn]
:or {retrieve true}}]
(let [repositories (or repositories maven-central)
system (repository-system)
mirror-selector-fn (memoize (partial mirror-selector-fn mirrors))
mirror-selector (mirror-selector mirror-selector-fn proxy)
session ((or repository-session-fn
repository-session)
{:repository-system system
:local-repo local-repo
:offline? offline?
:transfer-listener transfer-listener
:mirror-selector mirror-selector})
deps (->> coordinates
(map #(if-let [local-file (get files %)]
(.setArtifact (dependency %)
(-> (dependency %)
.getArtifact
(.setProperties {ArtifactProperties/LOCAL_PATH
(.getPath (io/file local-file))})))
(dependency %)))
vec)
collect-request (doto (CollectRequest. deps
nil
(vec (map #(let [repo (make-repository % proxy)]
(-> session
(.getMirrorSelector)
(.getMirror repo)
(or repo)))
repositories)))
(.setRequestContext "runtime"))]
(if retrieve
(.resolveDependencies system session (DependencyRequest. collect-request nil))
(.collectDependencies system session collect-request))))
(defn resolve-dependencies
"Same as `resolve-dependencies*`, but returns a graph of dependencies; each
dependency's metadata contains the source Aether Dependency object, and
the dependency's :file on disk. Please refer to `resolve-dependencies*` for details
on usage, or use it if you need access to Aether dependency resolution objects."
[& args]
(-> (apply resolve-dependencies* args)
.getRoot
dependency-graph))
(defn dependency-files
"Given a dependency graph obtained from `resolve-dependencies`, returns a seq of
files from the dependencies' metadata."
[graph]
(->> graph keys (map (comp :file meta)) (remove nil?)))
(defn- exclusion= [spec1 spec2]
(let [[dep & opts] (normalize-exclusion-spec spec1)
[sdep & sopts] (normalize-exclusion-spec spec2)
om (apply hash-map opts)
som (apply hash-map sopts)]
(and (= (group dep)
(group sdep))
(= (name dep)
(name sdep))
(= (:extension om "*")
(:extension som "*"))
(= (:classifier om "*")
(:classifier som "*"))
spec2)))
(defn- exclusions-match? [excs sexcs]
(if-let [ex (first excs)]
(if-let [match (some (partial exclusion= ex) sexcs)]
(recur (next excs) (remove #{match} sexcs))
false)
(empty? sexcs)))
(defn within?
"Determines if the first coordinate would be a version in the second
coordinate. The first coordinate is not allowed to contain a
version range."
[[dep version & opts] [sdep sversion & sopts]]
(let [om (apply hash-map opts)
som (apply hash-map sopts)]
(and (= (group dep)
(group sdep))
(= (name dep)
(name sdep))
(= (:extension om "jar")
(:extension som "jar"))
(= (:classifier om)
(:classifier som))
(= (:scope om "compile")
(:scope som "compile"))
(= (:optional om false)
(:optional som false))
(exclusions-match? (:exclusions om) (:exclusions som))
(or (= version sversion)
(if-let [[_ ver] (re-find #"^(.*)-SNAPSHOT$" sversion)]
(re-find (re-pattern (str "^" ver "-\\d+\\.\\d+-\\d+$"))
version)
(let [gsv (GenericVersionScheme.)
vc (.parseVersionConstraint gsv sversion)
v (.parseVersion gsv version)]
(.containsVersion vc v)))))))
(defn dependency-hierarchy
"Returns a dependency hierarchy based on the provided dependency graph
(as returned by `resolve-dependencies`) and the coordinates that should
be the root(s) of the hierarchy. Siblings are sorted alphabetically."
[root-coordinates dep-graph]
(let [root-specs (map (comp dep-spec dependency) root-coordinates)
hierarchy (for [root (filter
#(some (fn [root] (within? % root)) root-specs)
(keys dep-graph))]
[root (dependency-hierarchy (dep-graph root) dep-graph)])]
(when (seq hierarchy)
(into (sorted-map-by #(apply compare (map coordinate-string %&))) hierarchy))))
src/test/ 0000775 0000000 0000000 00000000000 12121704554 0012625 5 ustar 00root root 0000000 0000000 src/test/clojure/ 0000775 0000000 0000000 00000000000 12121704554 0014270 5 ustar 00root root 0000000 0000000 src/test/clojure/cemerick/ 0000775 0000000 0000000 00000000000 12121704554 0016052 5 ustar 00root root 0000000 0000000 src/test/clojure/cemerick/pomegranate/ 0000775 0000000 0000000 00000000000 12121704554 0020354 5 ustar 00root root 0000000 0000000 src/test/clojure/cemerick/pomegranate/aether_test.clj 0000664 0000000 0000000 00000051673 12121704554 0023371 0 ustar 00root root 0000000 0000000 (ns cemerick.pomegranate.aether-test
(:require [cemerick.pomegranate.aether :as aether]
[clojure.java.io :as io])
(:use [clojure.test]))
(deftest dependency-roundtripping
(are [x] (= x (#'aether/dep-spec (#'aether/dependency x)))
'[ring "1.0.0" :optional true]
'[com.cemerick/pomegranate "0.0.1" :classifier "sources"]
'[demo/demo2 "1.0.0" :exclusions [[demo :classifier "jdk5"]]]))
(def tmp-dir (io/file (System/getProperty "java.io.tmpdir") "pomegranate-test-tmp"))
(def tmp-remote-repo-dir (.getAbsolutePath (io/file tmp-dir "remote-repo")))
(def tmp-local-repo-dir (io/file tmp-dir "local-repo"))
(def tmp-local-repo2-dir (io/file tmp-dir "local-repo2"))
(def test-remote-repo {"central" "http://repo1.maven.org/maven2/"})
(def test-repo {"test-repo" "file://test-repo"})
(def tmp-remote-repo {"tmp-remote-repo" (str "file://" tmp-remote-repo-dir)})
(defn delete-recursive
[dir]
(when (.isDirectory dir)
(doseq [file (.listFiles dir)]
(delete-recursive file)))
(.delete dir))
(defn- clear-tmp
[f]
(delete-recursive (io/file tmp-dir)) (f))
(use-fixtures :each clear-tmp)
(defn file-path-eq [file1 file2]
(= (.getAbsolutePath file1)
(.getAbsolutePath file2)))
(deftest live-resolution
(let [deps '[[commons-logging "1.1"]]
graph '{[javax.servlet/servlet-api "2.3"] nil,
[avalon-framework "4.1.3"] nil,
[logkit "1.0.1"] nil,
[log4j "1.2.12"] nil,
[commons-logging "1.1"]
#{[javax.servlet/servlet-api "2.3"] [avalon-framework "4.1.3"]
[logkit "1.0.1"] [log4j "1.2.12"]}}
hierarchy '{[commons-logging "1.1"]
{[avalon-framework "4.1.3"] nil,
[javax.servlet/servlet-api "2.3"] nil,
[log4j "1.2.12"] nil,
[logkit "1.0.1"] nil}}]
(is (= graph (aether/resolve-dependencies :coordinates deps :retrieve false :local-repo tmp-local-repo-dir)))
(is (not (some #(-> % .getName (.endsWith ".jar")) (file-seq tmp-local-repo-dir))))
(doseq [[dep _] (aether/resolve-dependencies :coordinates deps :local-repo tmp-local-repo-dir)]
(is (-> dep meta :file))
(is (-> dep meta :file .exists)))
(is (some #(-> % .getName (.endsWith ".jar")) (file-seq tmp-local-repo-dir)))
(is (= hierarchy (aether/dependency-hierarchy deps graph)))))
(deftest impl-detail-types
(let [args [:coordinates '[[commons-logging "1.1"]] :local-repo tmp-local-repo-dir]]
(is (instance? org.sonatype.aether.resolution.DependencyResult
(apply aether/resolve-dependencies* args)))
(is (instance? org.sonatype.aether.collection.CollectResult
(apply aether/resolve-dependencies* :retrieve false args)))))
(deftest resolve-deps-with-proxy
(let [deps (aether/resolve-dependencies :repositories test-remote-repo
:coordinates '[[javax.servlet/servlet-api "2.5"]]
:proxy {:host "repo1.maven.org" :port 80 :non-proxy-hosts "clojars.org"}
:local-repo tmp-local-repo-dir)]
(is (= 1 (count deps)))
(is (= (.getAbsolutePath (io/file tmp-dir "local-repo" "javax" "servlet" "servlet-api" "2.5" "servlet-api-2.5.jar"))
(.getAbsolutePath (first (aether/dependency-files deps)))))))
(deftest resolve-deps-with-mirror
(let [deps (aether/resolve-dependencies :repositories {"clojars" "http://clojars.org/repo"}
:coordinates '[[javax.servlet/servlet-api "2.5"]]
:mirrors {"clojars" {:url "http://uk.maven.org/maven2"}}
:local-repo tmp-local-repo-dir)]
(is (= 1 (count deps)))
(is (= (.getAbsolutePath (io/file tmp-dir "local-repo" "javax" "servlet" "servlet-api" "2.5" "servlet-api-2.5.jar"))
(.getAbsolutePath (first (aether/dependency-files deps)))))))
(deftest resolve-deps-with-wildcard-mirror
(let [deps (aether/resolve-dependencies :repositories {"clojars" "http://clojars.org/repo"}
:coordinates '[[javax.servlet/servlet-api "2.5"]]
:mirrors {#".+" {:url "http://uk.maven.org/maven2"}}
:local-repo tmp-local-repo-dir)]
(is (= 1 (count deps)))
(is (= (.getAbsolutePath (io/file tmp-dir "local-repo" "javax" "servlet" "servlet-api" "2.5" "servlet-api-2.5.jar"))
(.getAbsolutePath (first (aether/dependency-files deps)))))))
(deftest resolve-deps-with-wildcard-override-mirror
(let [deps (aether/resolve-dependencies :repositories test-remote-repo
:coordinates '[[javax.servlet/servlet-api "2.5"]]
:mirrors {#".+" {:url "http://clojars.org/repo"}
(ffirst test-remote-repo) nil}
:local-repo tmp-local-repo-dir)]
(is (= 1 (count deps)))
(is (= (.getAbsolutePath (io/file tmp-dir "local-repo" "javax" "servlet" "servlet-api" "2.5" "servlet-api-2.5.jar"))
(.getAbsolutePath (first (aether/dependency-files deps)))))))
(deftest resolve-deps
(let [deps (aether/resolve-dependencies :repositories test-repo
:coordinates '[[demo/demo "1.0.0"]]
:local-repo tmp-local-repo-dir)]
(is (= 1 (count deps)))
(is (= (.getAbsolutePath (io/file tmp-dir "local-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar"))
(.getAbsolutePath (first (aether/dependency-files deps)))))))
(deftest resolve-deps-with-deps
(let [deps (aether/resolve-dependencies :repositories test-repo
:coordinates '[[demo/demo2 "1.0.0"]]
:local-repo tmp-local-repo-dir)
files (aether/dependency-files deps)]
(is (= 2 (count files)))
(is (= 1 (count (filter #(file-path-eq % (io/file tmp-dir "local-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar"))
files))))
(is (= 1 (count (filter #(file-path-eq % (io/file tmp-dir "local-repo" "demo" "demo2" "1.0.0" "demo2-1.0.0.jar"))
files))))))
(deftest resolve-unmanaged-dependencies
(let [deps (aether/resolve-dependencies
:repositories {}
:coordinates '[[demo "1.0.0"]]
:files {'[demo "1.0.0"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar")}
:local-repo tmp-local-repo-dir)
files (aether/dependency-files deps)]
(is (= 1 (count files)))
(is (= nil (:file (meta (first files)))))))
(deftest resolve-deps-with-exclusions
(let [deps (aether/resolve-dependencies :repositories test-repo
:coordinates
'[[demo/demo2 "1.0.0" :exclusions [demo/demo]]]
:local-repo tmp-local-repo-dir)]
(is (= 1 (count deps)))
(is (= (.getAbsolutePath (io/file tmp-dir "local-repo" "demo" "demo2" "1.0.0" "demo2-1.0.0.jar"))
(.getAbsolutePath (first (aether/dependency-files deps)))))))
(deftest deploy-jar
(aether/deploy :coordinates '[group/artifact "1.0.0"]
:jar-file (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar")
:repository tmp-remote-repo
:local-repo tmp-local-repo-dir)
(is (= 3 (count (.list (io/file tmp-remote-repo-dir "group" "artifact" "1.0.0"))))))
(deftest deploy-jar-with-pom
(aether/deploy :coordinates '[group/artifact "1.0.0"]
:jar-file (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar")
:pom-file (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.pom")
:repository tmp-remote-repo
:local-repo tmp-local-repo-dir)
(is (= 6 (count (.list (io/file tmp-remote-repo-dir "group" "artifact" "1.0.0"))))))
(deftest deploy-jar-with-artifact-map
(let [repo-file (partial io/file "test-repo" "demo" "demo" "1.0.0")]
(aether/deploy
:coordinates '[group/artifact "1.0.0"]
:artifact-map {[] (repo-file "demo-1.0.0.pom")
[:extension "pom"] (repo-file "demo-1.0.0.pom")}
:repository tmp-remote-repo
:local-repo tmp-local-repo-dir))
(is (= 6 (count (.list (io/file tmp-remote-repo-dir "group" "artifact" "1.0.0"))))))
(deftest install-jar
(aether/install :coordinates '[group/artifact "1.0.0"]
:jar-file (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar")
:pom-file (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.pom")
:local-repo tmp-local-repo-dir)
(is (= 3 (count (.list (io/file tmp-local-repo-dir "group" "artifact" "1.0.0"))))))
(deftest install-jar-with-artifact-map
(let [repo-file (partial io/file "test-repo" "demo" "demo" "1.0.0")]
(aether/install
:coordinates '[group/artifact "1.0.0"]
:artifact-map {[] (repo-file "demo-1.0.0.jar")
[:extension "pom"] (repo-file "demo-1.0.0.pom")}
:local-repo tmp-local-repo-dir))
(is (= 3 (count (.list (io/file tmp-local-repo-dir "group" "artifact" "1.0.0"))))))
(deftest deploy-artifacts
(aether/deploy-artifacts
:artifacts '[[demo "1.0.0"]
[demo "1.0.0" :extension "jar.asc"]
[demo "1.0.0" :extension "pom"]
[demo "1.0.0" :extension "pom.asc"]]
;; note: the .asc files in the test-repo are dummies, but it doesn't matter for this test
:files {'[demo "1.0.0"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar")
'[demo "1.0.0" :extension "jar.asc"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar.asc")
'[demo "1.0.0" :extension "pom"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.pom")
'[demo "1.0.0" :extension "pom.asc"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.pom.asc")}
:repository tmp-remote-repo
:local-repo tmp-local-repo-dir)
(is (= #{"demo-1.0.0.pom.md5"
"demo-1.0.0.pom.sha1"
"demo-1.0.0.pom"
"demo-1.0.0.pom.asc.md5"
"demo-1.0.0.pom.asc.sha1"
"demo-1.0.0.pom.asc"
"demo-1.0.0.jar.md5"
"demo-1.0.0.jar.sha1"
"demo-1.0.0.jar"
"demo-1.0.0.jar.asc.md5"
"demo-1.0.0.jar.asc.sha1"
"demo-1.0.0.jar.asc"}
(set (.list (io/file tmp-remote-repo-dir "demo" "demo" "1.0.0")))))
(is (= '{[demo "1.0.0"] nil}
(aether/resolve-dependencies :repositories tmp-remote-repo
:coordinates
'[[demo "1.0.0"]]
:local-repo tmp-local-repo2-dir)))
(is (= '{[demo "1.0.0" :extension "pom"] nil}
(aether/resolve-dependencies :repositories tmp-remote-repo
:coordinates
'[[demo "1.0.0" :extension "pom"]]
:local-repo tmp-local-repo2-dir)))
(is (= '{[demo "1.0.0" :extension "jar.asc"] nil}
(aether/resolve-dependencies :repositories tmp-remote-repo
:coordinates
'[[demo "1.0.0" :extension "jar.asc"]]
:local-repo tmp-local-repo2-dir)))
(is (= '{[demo "1.0.0" :extension "pom.asc"] nil}
(aether/resolve-dependencies :repositories tmp-remote-repo
:coordinates
'[[demo "1.0.0" :extension "pom.asc"]]
:local-repo tmp-local-repo2-dir))))
(deftest install-artifacts
(aether/install-artifacts
:artifacts '[[demo "1.0.0"]
[demo "1.0.0" :extension "jar.asc"]
[demo "1.0.0" :extension "pom"]
[demo "1.0.0" :extension "pom.asc"]]
;; note: the .asc files in the test-repo are dummies, but it doesn't matter for this test
:files {'[demo "1.0.0"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar")
'[demo "1.0.0" :extension "jar.asc"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar.asc")
'[demo "1.0.0" :extension "pom"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.pom")
'[demo "1.0.0" :extension "pom.asc"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.pom.asc")}
:local-repo tmp-local-repo-dir)
(is (= #{"demo-1.0.0.jar"
"demo-1.0.0.pom"
"demo-1.0.0.jar.asc"
"demo-1.0.0.pom.asc"
"_maven.repositories"}
(set (.list (io/file tmp-local-repo-dir "demo" "demo" "1.0.0"))))))
(deftest deploy-exceptions
(is (thrown-with-msg? IllegalArgumentException #"Provided artifacts have varying"
(aether/deploy-artifacts
:artifacts '[[demo "1.0.0"]
[group/demo "1.0.0" :extension "jar.asc"]]
:files {'[demo "1.0.0"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar")
'[group/demo "1.0.0" :extension "jar.asc"]
(io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar.asc")}
:repository tmp-remote-repo
:local-repo tmp-local-repo-dir)))
(is (thrown-with-msg? IllegalArgumentException #"Provided artifacts have varying version, group, or artifact IDs"
(aether/deploy-artifacts
:artifacts '[[demo "1.0.0"]
[demo/artifact "1.0.0" :extension "jar.asc"]]
:files {'[demo "1.0.0"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar")
'[demo/artifact "1.0.0" :extension "jar.asc"]
(io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar.asc")}
:repository tmp-remote-repo
:local-repo tmp-local-repo-dir)))
(is (thrown-with-msg? IllegalArgumentException #"Provided artifacts have varying version, group, or artifact IDs"
(aether/deploy-artifacts
:artifacts '[[demo "1.0.0"]
[demo "1.1.0" :extension "jar.asc"]]
:files {'[demo "1.0.0"] (io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar")
'[demo "1.1.0" :extension "jar.asc"]
(io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar.asc")}
:repository tmp-remote-repo
:local-repo tmp-local-repo-dir)))
(is (thrown-with-msg? IllegalArgumentException #"Provided artifacts have varying version, group, or artifact IDs"
(aether/deploy-artifacts
:files {'[demo "1.0.0"]
(io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar")
'[demo "1.1.0" :extension "jar.asc"]
(io/file "test-repo" "demo" "demo" "1.0.0" "demo-1.0.0.jar")}
:repository tmp-remote-repo
:local-repo tmp-local-repo-dir))))
(deftest within?-comparisons
(is (aether/within? '[demo "0.0.1"]
'[demo "0.0.1"]))
(is (aether/within? '[demo "0.0.1"]
'[demo/demo "0.0.1"]))
(is (aether/within? '[demo/demo "0.0.1"]
'[demo "0.0.1"]))
(is (aether/within? '[demo "0.0.1"]
'[demo "[0.0.1,2.0.0)"]))
(is (not (aether/within? '[demo "2.0.0"]
'[demo "[0.0.1,2.0.0)"])))
(is (not (aether/within? '[demo "0.0.1"]
'[demo "(0.0.1,2.0.0)"])))
(is (aether/within? '[demo "0.0.1-SNAPSHOT"]
'[demo/demo "0.0.1-SNAPSHOT"]))
(is (aether/within? '[demo "0.0.1-SNAPSHOT"]
'[demo "0.0.1-SNAPSHOT"]))
(is (aether/within? '[demo "0.0.1-20120403.012847-1"]
'[demo "0.0.1-SNAPSHOT"]))
(is (not (aether/within? '[demo "0.0.1-SNAPSHOT"]
'[demo "0.0.1-20120403.012847-10"])))
(is (aether/within? '[demo "0.0.1"]
'[demo "0.0.1" :extension "jar"]))
(is (aether/within? '[demo "0.0.1" :extension "jar"]
'[demo "0.0.1"]))
(is (not (aether/within? '[demo "0.0.1" :extension "pom"]
'[demo "0.0.1"])))
(is (not (aether/within? '[demo "0.0.1"]
'[demo "0.0.1":extension "pom"])))
(is (aether/within? '[demo "0.0.1" :classifier "sources"]
'[demo "0.0.1" :classifier "sources"]))
(is (not (aether/within? '[demo "0.0.1"]
'[demo "0.0.1" :classifier "sources"])))
(is (not (aether/within? '[demo "0.0.1" :classifier "sources"]
'[demo "0.0.1"])))
(is (aether/within? '[demo "0.0.1"]
'[demo "0.0.1" :scope "compile"]))
(is (aether/within? '[demo "0.0.1" :scope "compile"]
'[demo "0.0.1"]))
(is (aether/within? '[demo "0.0.1" :scope "compile"]
'[demo "0.0.1" :scope "compile"]))
(is (not (aether/within? '[demo "0.0.1" :scope "compile"]
'[demo "0.0.1" :scope "test"])))
(is (not (aether/within? '[demo "0.0.1" :scope "test"]
'[demo "0.0.1" :scope "compile"])))
(is (aether/within? '[demo "0.0.1"]
'[demo "0.0.1" :optional false]))
(is (aether/within? '[demo "0.0.1" :optional false]
'[demo "0.0.1"]))
(is (aether/within? '[demo "0.0.1" :optional true]
'[demo "0.0.1" :optional true]))
(is (not (aether/within? '[demo "0.0.1" :optional true]
'[demo "0.0.1"])))
(is (not (aether/within? '[demo "0.0.1"]
'[demo "0.0.1":optional true])))
(is (aether/within? '[demo "0.0.1" :exclusions []]
'[demo "0.0.1"]))
(is (aether/within? '[demo "0.0.1"]
'[demo "0.0.1" :exclusions []]))
(is (aether/within? '[demo "0.0.1" :exclusions [[demo2]]]
'[demo "0.0.1" :exclusions [[demo2]]]))
(is (not (aether/within? '[demo "0.0.1" :exclusions [[demo2]]]
'[demo "0.0.1"])))
(is (not (aether/within? '[demo "0.0.1"]
'[demo "0.0.1" :exclusions [[demo2]]])))
(is (not (aether/within? '[demo "0.0.1" :exclusions [demo2]]
'[demo "0.0.1"])))
(is (not (aether/within? '[demo "0.0.1"]
'[demo "0.0.1" :exclusions [demo2]])))
(is (aether/within? '[demo "0.0.1" :exclusions [[demo2]
[demo3]]]
'[demo "0.0.1" :exclusions [[demo2]
[demo3]]]))
(is (aether/within? '[demo "0.0.1" :exclusions [[demo3]
[demo2]]]
'[demo "0.0.1" :exclusions [[demo2]
[demo3]]]))
(is (not (aether/within? '[demo "0.0.1" :exclusions [[demo2]]]
'[demo "0.0.1" :exclusions [[demo2]
[demo3]]])))
(is (not (aether/within? '[demo "0.0.1" :exclusions [[demo2]
[demo3]]]
'[demo "0.0.1" :exclusions [[demo2]]])))
(is (aether/within? '[demo "0.0.1" :exclusions [[demo2]]]
'[demo "0.0.1" :exclusions [[demo2 :classifier "*"]]]))
(is (aether/within? '[demo "0.0.1" :exclusions [[demo2 :classifier "*"]]]
'[demo "0.0.1" :exclusions [[demo2]]]))
(is (not (aether/within?
'[demo "0.0.1" :exclusions [[demo2 :classifier "*"]]]
'[demo "0.0.1" :exclusions [[demo2 :classifier "sources"]]])))
(is (not (aether/within?
'[demo "0.0.1" :exclusions [[demo2 :classifier "sources"]]]
'[demo "0.0.1" :exclusions [[demo2 :classifier "*"]]])))
(is (aether/within? '[demo "0.0.1" :exclusions [[demo2]]]
'[demo "0.0.1" :exclusions [[demo2 :extension "*"]]]))
(is (aether/within? '[demo "0.0.1" :exclusions [[demo2 :extension "*"]]]
'[demo "0.0.1" :exclusions [[demo2]]]))
(is (not (aether/within?
'[demo "0.0.1" :exclusions [[demo2 :extension "*"]]]
'[demo "0.0.1" :exclusions [[demo2 :extension "jar"]]])))
(is (not (aether/within?
'[demo "0.0.1" :exclusions [[demo2 :extension "jar"]]]
'[demo "0.0.1" :exclusions [[demo2 :extension "*"]]]))))
(deftest dependency-hierarchy-matching
(let [coords '[[demo/demo2 "[0.0.1,2.0.0)"]
[tester "0.1.0-SNAPSHOT"]]
deps (aether/resolve-dependencies
:repositories test-repo
:coordinates coords
:local-repo tmp-local-repo-dir)]
(is (= {['demo/demo2 "1.0.0"] {['demo "1.0.0"] nil}
['tester "0.1.0-20120403.012847-1"] nil}
(aether/dependency-hierarchy coords deps)))))
(comment
"tests needed for:
repository authentication
repository policies
dependency options (scope/optional)
exclusion options (classifier/extension)")
src/test/clojure/cemerick/pomegranate_test.clj 0000664 0000000 0000000 00000001213 12121704554 0022102 0 ustar 00root root 0000000 0000000 (ns cemerick.pomegranate-test
(:require [cemerick.pomegranate :as p]
clojure.java.io)
(:use clojure.test))
(deftest resources
(is (= (first (p/resources "META-INF/MANIFEST.MF"))
(clojure.java.io/resource "META-INF/MANIFEST.MF")))
; the last classloader should be ext, for e.g. $JAVA_HOME/lib/ext/*
(is (->> (p/resources [(last (p/classloader-hierarchy))] "META-INF/MANIFEST.MF")
(map str)
(filter #(.contains % "clojure"))
empty?))
(is (->> (p/resources (butlast (p/classloader-hierarchy)) "META-INF/MANIFEST.MF")
(map str)
(filter #(.contains % "clojure"))
seq))) test-repo/ 0000775 0000000 0000000 00000000000 12121704554 0013001 5 ustar 00root root 0000000 0000000 test-repo/demo/ 0000775 0000000 0000000 00000000000 12121704554 0013725 5 ustar 00root root 0000000 0000000 test-repo/demo/demo/ 0000775 0000000 0000000 00000000000 12121704554 0014651 5 ustar 00root root 0000000 0000000 test-repo/demo/demo/1.0.0/ 0000775 0000000 0000000 00000000000 12121704554 0015305 5 ustar 00root root 0000000 0000000 test-repo/demo/demo/1.0.0/demo-1.0.0.jar 0000664 0000000 0000000 00000000000 12121704554 0017347 0 ustar 00root root 0000000 0000000 test-repo/demo/demo/1.0.0/demo-1.0.0.jar.asc 0000664 0000000 0000000 00000000171 12121704554 0020125 0 ustar 00root root 0000000 0000000 This file here only to test deployment operations; the fact that this isn't a signature file shouldn't cause any issues.
test-repo/demo/demo/1.0.0/demo-1.0.0.jar.md5 0000664 0000000 0000000 00000000040 12121704554 0020037 0 ustar 00root root 0000000 0000000 d41d8cd98f00b204e9800998ecf8427e test-repo/demo/demo/1.0.0/demo-1.0.0.jar.sha1 0000664 0000000 0000000 00000000050 12121704554 0020207 0 ustar 00root root 0000000 0000000 da39a3ee5e6b4b0d3255bfef95601890afd80709 test-repo/demo/demo/1.0.0/demo-1.0.0.pom 0000664 0000000 0000000 00000000704 12121704554 0017401 0 ustar 00root root 0000000 0000000
4.0.0
demo
demo
1.0.0
POM was created from install:install-file
test-repo/demo/demo/1.0.0/demo-1.0.0.pom.asc 0000664 0000000 0000000 00000000171 12121704554 0020144 0 ustar 00root root 0000000 0000000 This file here only to test deployment operations; the fact that this isn't a signature file shouldn't cause any issues.
test-repo/demo/demo/1.0.0/demo-1.0.0.pom.md5 0000664 0000000 0000000 00000000040 12121704554 0020056 0 ustar 00root root 0000000 0000000 5689f1eaf926240885eb9976e5269e5d test-repo/demo/demo/1.0.0/demo-1.0.0.pom.sha1 0000664 0000000 0000000 00000000050 12121704554 0020226 0 ustar 00root root 0000000 0000000 eb4ff44542c69c5271390dad76a96bfd15b4858a test-repo/demo/demo/maven-metadata.xml 0000664 0000000 0000000 00000000440 12121704554 0020255 0 ustar 00root root 0000000 0000000
demo
demo
1.0.0
1.0.0
20120403044818
test-repo/demo/demo/maven-metadata.xml.md5 0000664 0000000 0000000 00000000040 12121704554 0020735 0 ustar 00root root 0000000 0000000 eba3d5db3eae4a0d224883479f0af51a test-repo/demo/demo/maven-metadata.xml.sha1 0000664 0000000 0000000 00000000050 12121704554 0021105 0 ustar 00root root 0000000 0000000 e135fe7f1f28125dc0ed279af92e112af16bcf64 test-repo/demo/demo2/ 0000775 0000000 0000000 00000000000 12121704554 0014733 5 ustar 00root root 0000000 0000000 test-repo/demo/demo2/1.0.0/ 0000775 0000000 0000000 00000000000 12121704554 0015367 5 ustar 00root root 0000000 0000000 test-repo/demo/demo2/1.0.0/demo2-1.0.0.jar 0000664 0000000 0000000 00000000000 12121704554 0017513 0 ustar 00root root 0000000 0000000 test-repo/demo/demo2/1.0.0/demo2-1.0.0.jar.md5 0000664 0000000 0000000 00000000040 12121704554 0020203 0 ustar 00root root 0000000 0000000 d41d8cd98f00b204e9800998ecf8427e test-repo/demo/demo2/1.0.0/demo2-1.0.0.jar.sha1 0000664 0000000 0000000 00000000050 12121704554 0020353 0 ustar 00root root 0000000 0000000 da39a3ee5e6b4b0d3255bfef95601890afd80709 test-repo/demo/demo2/1.0.0/demo2-1.0.0.pom 0000664 0000000 0000000 00000001155 12121704554 0017546 0 ustar 00root root 0000000 0000000
4.0.0
demo
demo2
1.0.0
POM was created from install:install-file
demo
demo
1.0.0
test-repo/demo/demo2/1.0.0/demo2-1.0.0.pom.md5 0000664 0000000 0000000 00000000040 12121704554 0020222 0 ustar 00root root 0000000 0000000 79893caa8e0888232ed9dfe56e396172 test-repo/demo/demo2/1.0.0/demo2-1.0.0.pom.sha1 0000664 0000000 0000000 00000000050 12121704554 0020372 0 ustar 00root root 0000000 0000000 d2a795ccaccf9ade57f7938c390368eeb2e842e7 test-repo/demo/demo2/maven-metadata.xml 0000664 0000000 0000000 00000000441 12121704554 0020340 0 ustar 00root root 0000000 0000000
demo
demo2
1.0.0
1.0.0
20120403045648
test-repo/demo/demo2/maven-metadata.xml.md5 0000664 0000000 0000000 00000000040 12121704554 0021017 0 ustar 00root root 0000000 0000000 8550a6918ed8fe0cf9ff6e08e61c4d8c test-repo/demo/demo2/maven-metadata.xml.sha1 0000664 0000000 0000000 00000000050 12121704554 0021167 0 ustar 00root root 0000000 0000000 c38f519e44c887aa563201af58779379cbee4b18 test-repo/tester/ 0000775 0000000 0000000 00000000000 12121704554 0014307 5 ustar 00root root 0000000 0000000 test-repo/tester/tester/ 0000775 0000000 0000000 00000000000 12121704554 0015615 5 ustar 00root root 0000000 0000000 test-repo/tester/tester/0.1.0-SNAPSHOT/ 0000775 0000000 0000000 00000000000 12121704554 0017506 5 ustar 00root root 0000000 0000000 test-repo/tester/tester/0.1.0-SNAPSHOT/maven-metadata.xml 0000664 0000000 0000000 00000001372 12121704554 0023117 0 ustar 00root root 0000000 0000000
tester
tester
0.1.0-SNAPSHOT
20120403.012847
1
20120403012847
jar
0.1.0-20120403.012847-1
20120403012847
pom
0.1.0-20120403.012847-1
20120403012847
test-repo/tester/tester/0.1.0-SNAPSHOT/maven-metadata.xml.md5 0000664 0000000 0000000 00000000040 12121704554 0023572 0 ustar 00root root 0000000 0000000 32f77845f3013e9010130dd3c5056d35 test-repo/tester/tester/0.1.0-SNAPSHOT/maven-metadata.xml.sha1 0000664 0000000 0000000 00000000050 12121704554 0023742 0 ustar 00root root 0000000 0000000 34f980dd6a45ef4ad423c27315015580f5e8c996 test-repo/tester/tester/0.1.0-SNAPSHOT/tester-0.1.0-20120403.012847-1.jar 0000664 0000000 0000000 00000003223 12121704554 0023737 0 ustar 00root root 0000000 0000000 PK @ META-INF/MANIFEST.MF MLK-.
K-*ϳR03r*)uR(KL/J+r.JM,IMfe楧)err PK39Q R PK @ $ META-INF/maven/tester/tester/pom.xml]o 8.PMjUJhR|a@8iV/x/徖ZiQ*+6K~IBr|뽙l`-n`uE!{'f.kМDWc.9I} ,m()<.봋P31ʇ4 Vԣh b¬όQm",{