source: trunk/abcl/contrib/abcl-asdf/maven-embedder.lisp @ 13846

Last change on this file since 13846 was 13846, checked in by Mark Evenson, 10 years ago

abcl-asdf: fix typo in previous commit.

Or maybe I shouldn't attempt to fix code (not (sober)).

File size: 13.3 KB
Line 
1;;;; Use the Aether system in a localy installed Maven3 distribution to download
2;;;; and install JVM artifact dependencies.
3
4#|
5
6# Implementation references
7
8## Installing Maven
9
10## Current Javadoc for Maven Aether connector
11http://sonatype.github.com/sonatype-aether/apidocs/overview-summary.html
12
13## Incomplete, seemingly often wrong
14https://docs.sonatype.org/display/AETHER/Home
15
16|#
17
18(in-package :abcl-asdf)
19
20(require :abcl-contrib)
21(require :jss)
22
23#|
24Test:
25(resolve-dependencies "org.slf4j" "slf4j-api" "1.6.1")
26
27(resolve-dependencies "org.apache.maven" "maven-aether-provider" "3.0.4")
28|#
29
30(defparameter *maven-verbose* t
31  "Stream to send output from the Maven Aether subsystem to, or NIL to muffle output")
32
33(defvar *mavens* '("/opt/local/bin/mvn3" "mvn3" "mvn" "mvn3.bat" "mvn.bat")
34  "Locations to search for the Maven executable.")
35
36(defun find-mvn () 
37  "Attempt to find a suitable Maven ('mvn') executable on the hosting operating system.
38
39Returns the path of the Maven executable or nil if none are found."
40
41  (let ((m2-home (asdf:getenv "M2_HOME"))
42        (m2 (asdf:getenv "M2"))
43        (mvn-executable (if (find :unix *features*)
44                               "mvn"
45                               "mvn.bat")))
46    (when (and m2-home (probe-file m2-home))
47      (let* ((m2-home (truename m2-home))
48             (mvn-path (merge-pathnames 
49                        (format nil "bin/~A" mvn-executable)
50                        m2-home))
51             (mvn (truename mvn-path)))
52        (if mvn
53            (return-from find-mvn mvn)
54            (warn "M2_HOME was set to '~A' in the process environment but '~A' doesn't exist." 
55                  m2-home mvn-path))))
56    (when (and m2 (probe-file m2))
57      (let* ((m2 (truename m2))
58             (mvn-path (merge-pathnames mvn-executable m2))
59             (mvn (truename mvn-path)))
60        (if mvn
61            (return-from find-mvn mvn)
62            (warn "M2 was set to '~A' in the process environment but '~A' doesn't exist." 
63                  m2 mvn-path))))
64    (let* ((which-cmd 
65            (if (find :unix *features*)
66                "which" 
67                ;; Starting with Windows Server 2003
68                "where.exe"))
69           (which-cmd-p 
70            (handler-case 
71                (sys::run-program which-cmd nil)
72              (t () nil))))
73      (when which-cmd-p
74        (dolist (mvn-path *mavens*)
75          (let ((mvn 
76                 (handler-case 
77                     (truename (read-line (sys::process-output 
78                                           (sys::run-program 
79                                            which-cmd `(,mvn-path))))) 
80                   (end-of-file () nil)
81                   (t (e) 
82                     (format *maven-verbose* 
83                             "~&Failed to find Maven executable '~A' in PATH because~&~A" 
84                             mvn-path e)))))
85            (when mvn
86              (return-from find-mvn mvn))))))))
87
88(defun find-mvn-libs ()
89  (let ((mvn (find-mvn)))
90    (unless mvn
91      (warn "Failed to find Maven3 libraries.")
92      (return-from find-mvn-libs nil))
93    (truename (make-pathname 
94               :defaults (merge-pathnames "../lib/" mvn)
95               :name nil :type nil))))
96
97(defparameter *mvn-libs-directory*
98  nil
99  "Location of 'maven-core-3.<m>.<p>.jar', 'maven-embedder-3.<m>.<p>.jar' etc.")
100
101(defun mvn-version ()
102  "Return the Maven version used by the Aether connector."
103  (let* ((line
104         (read-line (sys::process-output 
105                     (sys::run-program 
106                      (namestring (find-mvn)) '("-version")))))
107         (pattern (#"compile" 
108                   'regex.Pattern
109                   "Apache Maven ([0-9]+)\\.([0-9]+)\\.([0-9]+)"))
110         (matcher (#"matcher" pattern line))
111         (found (#"find" matcher)))
112    (unless found 
113      (return-from mvn-version nil))
114    (mapcar #'parse-integer
115            `(,(#"group" matcher 1)
116              ,(#"group" matcher 2)
117              ,(#"group" matcher 3)))))
118
119(defun ensure-mvn-version ()
120  "Return t if Maven version is 3.0.3 or greater."
121  (let* ((version (mvn-version))
122         (major (first version))
123         (minor (second version))
124         (patch (third version)))
125    (or 
126     (and (>= major 3)
127          (>= minor 1))
128     (and (>= major 3)
129          (>= major 0)
130          (>= patch 3)))))
131
132(defparameter *init* nil)
133
134(defun init (&optional &key (force nil))
135  "Run the initialization strategy to bootstrap a Maven dependency node."
136  (unless (or force *mvn-libs-directory*)
137    (setf *mvn-libs-directory* (find-mvn-libs)))
138  (unless (probe-file *mvn-libs-directory*)
139    (error "You must download maven-3.0.3 or later from http://maven.apache.org/download.html, then set ABCL-ASDF:*MVN-DIRECTORY* appropiately."))
140  (unless (ensure-mvn-version)
141    (error "We need maven-3.0.3 or later."))
142  (add-directory-jars-to-class-path *mvn-libs-directory* nil)
143  (setf *init* t))
144
145(defparameter *http-wagon-implementations*
146  `("org.apache.maven.wagon.providers.http.HttpWagon" ;; introduced as default with maven-3.0.4
147    "org.apache.maven.wagon.providers.http.LightweightHttpWagon")
148  "A list of possible candidate implementations that provide access to http and https resources.
149
150Supposedly configurable with the java.net.protocols (c.f. reference maso2000 in the Manual.")
151
152(defun make-wagon-provider ()
153  "Returns an implementation of the org.sonatype.aether.connector.wagon.WagonProvider contract.
154
155The implementation is specified as Lisp closures.  Currently, it only
156specializes the lookup() method if passed an 'http' role hint."
157  (unless *init* (init))
158  (java:jinterface-implementation 
159   "org.sonatype.aether.connector.wagon.WagonProvider"
160   "lookup"
161   (lambda (role-hint)
162     (if (string-equal "http" role-hint)
163         (some (lambda (provider) (java:jnew provider)) *http-wagon-implementations*)
164       java:+null+))
165   "release"
166   (lambda (wagon)
167     (declare (ignore wagon)))))
168
169(defun make-repository-system ()
170  (unless *init* (init))
171  (let ((locator 
172         (java:jnew "org.apache.maven.repository.internal.DefaultServiceLocator"))
173        (repository-connector-factory-class 
174         (java:jclass "org.sonatype.aether.spi.connector.RepositoryConnectorFactory"))
175        (wagon-repository-connector-factory-class
176         (java:jclass "org.sonatype.aether.connector.wagon.WagonRepositoryConnectorFactory"))
177        (wagon-provider-class 
178         (java:jclass "org.sonatype.aether.connector.wagon.WagonProvider"))
179        (repository-system-class
180         (java:jclass "org.sonatype.aether.RepositorySystem")))
181    (#"addService" locator
182                   repository-connector-factory-class 
183                   wagon-repository-connector-factory-class)
184    (#"setServices" locator
185                    wagon-provider-class
186                    (java:jnew-array-from-list 
187                     "org.sonatype.aether.connector.wagon.WagonProvider"
188                     (list 
189                      (make-wagon-provider))))
190    (#"getService" locator
191                   repository-system-class)))
192       
193(defun make-session (repository-system)
194  "Construct a new org.sonatype.aether.RepositorySystemSession from REPOSITORY-SYSTEM"
195  (let ((session 
196         (java:jnew (jss:find-java-class "MavenRepositorySystemSession")))
197        (local-repository 
198         (java:jnew (jss:find-java-class "LocalRepository")
199                  (namestring (merge-pathnames ".m2/repository/"
200                                               (user-homedir-pathname))))))
201    (#"setLocalRepositoryManager" 
202     session
203     (#"newLocalRepositoryManager" repository-system local-repository))))
204
205(defparameter *maven-http-proxy* nil
206  "A string containing the URI of an http proxy for Maven to use.")
207
208(defun make-proxy ()
209  "Return an org.sonatype.aether.repository.Proxy instance initialized form *MAVEN-HTTP-PROXY*."
210  (unless *maven-http-proxy*
211    (warn "No proxy specified in *MAVEN-HTTP-PROXY*")
212    (return-from make-proxy nil))
213  (let* ((p (pathname *maven-http-proxy*))
214         (scheme (sys::url-pathname-scheme p))
215         (authority (sys::url-pathname-authority p))
216         (host (if (search ":" authority)
217                   (subseq authority 0 (search ":" authority))
218                   authority))
219         (port (when (search ":" authority)
220                 (parse-integer (subseq authority (1+ (search ":" authority))))))
221         ;; TODO allow specification of authentication
222         (authentication java:+null+))
223    (jss:new 'org.sonatype.aether.repository.Proxy
224             scheme host port authentication)))
225
226(defparameter *repository-system*  nil
227  "The org.sonatype.aether.RepositorySystem used by the Maeven Aether connector.")
228(defun ensure-repository-system ()
229  (unless *repository-system*
230    (setf *repository-system* (make-repository-system)))
231  *repository-system*)
232
233(defparameter *session* nil
234  "Reference to the Maven RepositorySystemSession")
235(defun ensure-session ()
236  "Ensure that the RepositorySystemSession has been created.
237
238If *MAVEN-HTTP-PROXY* is non-nil, parse its value as the http proxy."
239  (unless *session*
240    (ensure-repository-system)
241    (setf *session* (make-session *repository-system*))
242    (#"setRepositoryListener" *session* (make-repository-listener))
243    (when *maven-http-proxy*
244      (let ((proxy (make-proxy)))
245        (#"add" (#"getProxySelector" *session*)
246                proxy 
247                ;; A string specifying non proxy hosts, or null
248                java:+null+))))
249    *session*)
250
251;;; TODO change this to work on artifact strings like log4j:log4j:jar:1.2.16
252(defun resolve-artifact (group-id artifact-id &optional (version "LATEST" versionp))
253  "Directly resolve Maven dependencies for item with GROUP-ID and ARTIFACT-ID at VERSION, ignoring dependencies.
254
255Declared dependencies are not attempted to be located.
256
257If unspecified, the string \"LATEST\" will be used for the VERSION.
258
259Returns the Maven specific string for the artifact "
260  (unless versionp
261    (warn "Using LATEST for unspecified version."))
262  (unless *init* (init))
263  (let* ((artifact-string (format nil "~A:~A:~A" group-id artifact-id version))
264         (artifact 
265          (jss:new "org.sonatype.aether.util.artifact.DefaultArtifact" artifact-string))
266         (artifact-request 
267          (java:jnew "org.sonatype.aether.resolution.ArtifactRequest")))
268    (#"setArtifact" artifact-request artifact)
269    (#"addRepository" artifact-request (ensure-remote-repository))
270    (#"toString" (#"resolveArtifact" (ensure-repository-system) (ensure-session) artifact-request))))
271
272(defun make-remote-repository (id type url) 
273  (jss:new 'aether.repository.RemoteRepository id type url))
274
275(defparameter *maven-remote-repository*  nil
276    "The remote repository used by the Maven Aether embedder.")
277(defun ensure-remote-repository () 
278  (unless *init* (init))
279  (unless *maven-remote-repository*
280    (let ((r (make-remote-repository "central" "default" "http://repo1.maven.org/maven2/")))
281      (when *maven-http-proxy*
282        (#"setProxy" r (make-proxy)))
283      (setf *maven-remote-repository* r)))
284  *maven-remote-repository*)
285
286(defun resolve-dependencies (group-id artifact-id &optional (version "LATEST" versionp))
287  "Dynamically resolve Maven dependencies for item with GROUP-ID and ARTIFACT-ID at VERSION.
288
289All recursive dependencies will be visited before resolution is successful.
290
291If unspecified, the string \"LATEST\" will be used for the VERSION.
292
293Returns a string containing the necessary jvm classpath entries packed
294in Java CLASSPATH representation."
295  (unless *init* (init))
296  (unless versionp
297    (warn "Using LATEST for unspecified version."))
298  (let* ((artifact
299          (java:jnew (jss:find-java-class "aether.util.artifact.DefaultArtifact")
300                     (format nil "~A:~A:~A"
301                             group-id artifact-id version)))
302         (dependency 
303          (java:jnew (jss:find-java-class "aether.graph.Dependency")
304                     artifact "compile"))
305         (collect-request (java:jnew (jss:find-java-class "CollectRequest"))))
306    (#"setRoot" collect-request dependency)
307    (#"addRepository" collect-request (ensure-remote-repository))
308    (let* ((node 
309            (#"getRoot" (#"collectDependencies" (ensure-repository-system) (ensure-session) collect-request)))
310           (dependency-request 
311            (java:jnew (jss:find-java-class "DependencyRequest")
312                       node java:+null+))
313           (nlg 
314            (java:jnew (jss:find-java-class "PreorderNodeListGenerator"))))
315      (#"resolveDependencies" (ensure-repository-system) (ensure-session) dependency-request)
316      (#"accept" node nlg)
317      (#"getClassPath" nlg))))
318
319(defun make-repository-listener ()
320  (flet ((log (e) 
321           (format *maven-verbose* "~&~A~%" (#"toString" e))))
322    (java:jinterface-implementation 
323     "org.sonatype.aether.RepositoryListener"
324     "artifactDeployed" 
325     #'log
326     "artifactDeploying" 
327     #'log
328     "artifactDescriptorInvalid" 
329     #'log
330     "artifactDescriptorMissing" 
331     #'log
332     "artifactDownloaded" 
333     #'log
334     "artifactDownloading" 
335     #'log
336     "artifactInstalled" 
337     #'log
338     "artifactInstalling" 
339     #'log
340     "artifactResolved" 
341     #'log
342     "artifactResolving" 
343     #'log
344     "metadataDeployed" 
345     #'log
346     "metadataDeploying" 
347     #'log
348     "metadataDownloaded" 
349     #'log
350     "metadataDownloading" 
351     #'log
352     "metadataInstalled"
353     #'log
354     "metadataInstalling" 
355     #'log
356     "metadataInvalid" 
357     #'log
358     "metadataResolved" 
359     #'log
360     "metadataResolving"
361     #'log)))
362
363         
364
Note: See TracBrowser for help on using the repository browser.