source: branches/1.1.x/contrib/abcl-asdf/maven-embedder.lisp

Last change on this file was 14373, checked in by Mark Evenson, 12 years ago

Backport r14364 | mevenson | 2013-01-31 11:02:29 +0100 (Thu, 31 Jan 2013) | 9 lines

ASDF systems using the MVN component now load again.

log4j.asd example corrected as well.

FIND-MVN now emits a warning if it cannot actually find a Maven exectuable.

Thanks to Milos Negovanovic for identifying the problem with a patch.

Fixes #299.

File size: 16.4 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
7
8Not necessarily multi-threaded safe, and unclear how much work that
9would be, as it is unknown how the Maven implementation behaves.
10
11## Installing Maven
12http://maven.apache.org/download.html
13
14## Current Javadoc for Maven Aether connector
15http://sonatype.github.com/sonatype-aether/apidocs/overview-summary.html
16
17## Incomplete, seemingly often wrong
18https://docs.sonatype.org/display/AETHER/Home
19
20Note that this is not an implementation of Maven per se, but the use
21of the Maven Aether connector infrastructure.  Among other things, this means
22that the Maven specific "~/.m2/settings.xml" file is NOT parsed for settings.
23
24|#
25
26;;; N.b. evaluated *after* we load the ABCL specific modifications of
27;;;      ASDF in abcl-asdf.lisp
28
29(in-package :abcl-asdf)
30
31(require :abcl-contrib)
32(require :jss)
33
34#|
35Test:
36(resolve-dependencies "org.slf4j" "slf4j-api" "1.6.1")
37
38(resolve-dependencies "org.apache.maven" "maven-aether-provider" "3.0.4")
39|#
40
41(defparameter *maven-verbose* t
42  "Stream to send output from the Maven Aether subsystem to, or NIL to muffle output")
43
44(defparameter *mavens* 
45  (if (find :windows *features*)
46      '("mvn.bat" "mvn3.bat")
47      '("/opt/local/bin/mvn3" "mvn3" "mvn"))
48  "Locations to search for the Maven executable.")
49
50(defun find-mvn () 
51  "Attempt to find a suitable Maven ('mvn') executable on the hosting operating system.
52
53Returns the path of the Maven executable or nil if none are found.
54
55Emits warnings if not able to find a suitable executable."
56
57  (let ((m2-home (ext:getenv "M2_HOME"))
58        (m2 (ext:getenv "M2"))
59        (mvn-executable (if (find :unix *features*)
60                               "mvn"
61                               "mvn.bat")))
62    (when (and m2-home (probe-file m2-home))
63      (let* ((m2-home (truename m2-home))
64             (mvn-path (merge-pathnames 
65                        (format nil "bin/~A" mvn-executable)
66                        m2-home))
67             (mvn (truename mvn-path)))
68        (if mvn
69            (return-from find-mvn mvn)
70            (warn "M2_HOME was set to '~A' in the process environment but '~A' doesn't exist." 
71                  m2-home mvn-path))))
72    (when (and m2 (probe-file m2))
73      (let* ((m2 (truename m2))
74             (mvn-path (merge-pathnames mvn-executable m2))
75             (mvn (truename mvn-path)))
76        (if mvn
77            (return-from find-mvn mvn)
78            (warn "M2 was set to '~A' in the process environment but '~A' doesn't exist." 
79                  m2 mvn-path))))
80    (let* ((which-cmd 
81            (if (find :unix *features*)
82                "which" 
83                ;; Starting with Windows Server 2003
84                "where.exe"))
85           (which-cmd-p 
86            (handler-case 
87                (sys::run-program which-cmd nil)
88              (t () nil))))
89      (when which-cmd-p
90        (dolist (mvn-path *mavens*)
91          (let ((mvn 
92                 (handler-case 
93                     (truename (read-line (sys::process-output 
94                                           (sys::run-program 
95                                            which-cmd `(,mvn-path))))) 
96                   (end-of-file () nil)
97                   (t (e) 
98                     (format *maven-verbose* 
99                             "~&Failed to find Maven executable '~A' in PATH because~&~A" 
100                             mvn-path e)))))
101            (when mvn
102              (return-from find-mvn mvn)))))))
103  (warn "Unable to locate Maven executable."))
104
105(defun find-mvn-libs ()
106  (let ((mvn (find-mvn)))
107    (unless mvn
108      (warn "Failed to find Maven3 libraries.")
109      (return-from find-mvn-libs nil))
110    (truename (make-pathname 
111               :defaults (merge-pathnames "../lib/" mvn)
112               :name nil :type nil))))
113
114(defparameter *mvn-libs-directory*
115  nil
116  "Location of 'maven-core-3.<m>.<p>.jar', 'maven-embedder-3.<m>.<p>.jar' etc.")
117
118(defun mvn-version ()
119  "Return the Maven version used by the Aether connector."
120  (let ((stream (sys::process-output
121                 (sys::run-program (truename (find-mvn)) '("-version"))))
122        (pattern (#"compile"
123                  'regex.Pattern
124                  "Apache Maven ([0-9]+)\\.([0-9]+)\\.([0-9]+)")))
125    (do ((line (read-line stream nil :eof) 
126              (read-line stream nil :eof)))
127        ((or (not line) (eq line :eof)) nil)
128      (let ((matcher (#"matcher" pattern line)))
129        (when (#"find" matcher)
130          (return-from mvn-version
131            (handler-case 
132                (mapcar #'parse-integer 
133                        `(,(#"group" matcher 1) 
134                           ,(#"group" matcher 2) 
135                           ,(#"group" matcher 3)))
136              (t (e) 
137                (error "Failed to parse Maven version from ~A because~&~A." line e)))))))))
138
139(defun ensure-mvn-version ()
140  "Return t if Maven version is 3.0.3 or greater."
141  (let* ((version (mvn-version))
142         (major (first version))
143         (minor (second version))
144         (patch (third version)))
145    (or 
146     (and (>= major 3)
147          (>= minor 1))
148     (and (>= major 3)
149          (>= minor 0)
150          (>= patch 3)))))
151
152(defparameter *init* nil)
153
154(defun init (&optional &key (force nil))
155 "Run the initialization strategy to bootstrap a Maven dependency node."
156 (unless (or force *mvn-libs-directory*)
157   (setf *mvn-libs-directory* (find-mvn-libs)))
158  (unless (and *mvn-libs-directory*
159               (probe-file *mvn-libs-directory*))
160   (error "Please obtain and install maven-3.0.4 locally from http://maven.apache.org/download.html, then set ABCL-ASDF:*MVN-DIRECTORY* appropiately."))
161 (unless (ensure-mvn-version)
162   (error "We need maven-3.0.3 or later."))  (add-directory-jars-to-class-path *mvn-libs-directory* nil)
163  (setf *init* t))
164
165(defun find-http-wagon ()
166  "Find an implementation of the object that provides access to http and https resources.
167
168Supposedly configurable with the java.net.protocols (c.f. reference
169maso2000 in the Manual.)"
170  (handler-case 
171      ;; maven-3.0.4
172      (java:jnew "org.apache.maven.wagon.providers.http.HttpWagon") 
173    (error () 
174      ;; maven-3.0.3 reported as not working with all needed functionality
175      (java:jnew  "org.apache.maven.wagon.providers.http.LightweightHttpWagon"))))
176
177(defun make-wagon-provider ()
178  "Returns an implementation of the org.sonatype.aether.connector.wagon.WagonProvider contract.
179
180The implementation is specified as Lisp closures.  Currently, it only
181specializes the lookup() method if passed an 'http' role hint."
182  (unless *init* (init))
183  (java:jinterface-implementation 
184   "org.sonatype.aether.connector.wagon.WagonProvider"
185   "lookup"
186   (lambda (role-hint)
187     (cond 
188       ((find role-hint '("http" "https") :test #'string-equal)
189        (find-http-wagon))
190       (t
191        (progn 
192          (format *maven-verbose* 
193                  "~&WagonProvider stub passed '~A' as a hint it couldn't satisfy.~%" role-hint)
194           java:+null+))))
195   "release"
196   (lambda (wagon)
197     (declare (ignore wagon)))))
198
199(defun find-service-locator ()
200  (handler-case 
201      (java:jnew "org.apache.maven.repository.internal.MavenServiceLocator") ;; maven-3.0.4
202    (error () 
203      (java:jnew "org.apache.maven.repository.internal.DefaultServiceLocator"))))
204
205(defun make-repository-system ()
206  (unless *init* (init))
207  (let ((locator 
208         (find-service-locator))
209        (wagon-provider-class 
210         (java:jclass "org.sonatype.aether.connector.wagon.WagonProvider"))
211        (wagon-repository-connector-factory-class
212         (java:jclass "org.sonatype.aether.connector.wagon.WagonRepositoryConnectorFactory"))
213        (repository-connector-factory-class 
214         (java:jclass "org.sonatype.aether.spi.connector.RepositoryConnectorFactory"))
215        (repository-system-class
216         (java:jclass "org.sonatype.aether.RepositorySystem")))
217    (#"setServices" locator
218                    wagon-provider-class
219                   (java:jarray-from-list
220                    (list (make-wagon-provider))))
221    (#"addService" locator
222                   repository-connector-factory-class
223                   wagon-repository-connector-factory-class)
224    (values (#"getService" locator
225                           repository-system-class)
226            locator)))
227       
228(defun make-session (repository-system)
229  "Construct a new org.sonatype.aether.RepositorySystemSession from REPOSITORY-SYSTEM"
230  (let ((session 
231         (java:jnew (jss:find-java-class "MavenRepositorySystemSession")))
232        (local-repository 
233         (java:jnew (jss:find-java-class "LocalRepository")
234                  (namestring (merge-pathnames ".m2/repository/"
235                                               (user-homedir-pathname))))))
236    (#"setLocalRepositoryManager" 
237     session
238     (#"newLocalRepositoryManager" repository-system
239                                   local-repository))))
240
241(defparameter *maven-http-proxy* nil
242  "A string containing the URI of an http proxy for Maven to use.")
243
244(defun make-proxy ()
245  "Return an org.sonatype.aether.repository.Proxy instance initialized from *MAVEN-HTTP-PROXY*."
246  (unless *maven-http-proxy*
247    (warn "No proxy specified in *MAVEN-HTTP-PROXY*")
248    (return-from make-proxy nil))
249  (let* ((p (pathname *maven-http-proxy*))
250         (scheme (sys::url-pathname-scheme p))
251         (authority (sys::url-pathname-authority p))
252         (host (if (search ":" authority)
253                   (subseq authority 0 (search ":" authority))
254                   authority))
255         (port (when (search ":" authority)
256                 (parse-integer (subseq authority (1+ (search ":" authority))))))
257         ;; TODO allow specification of authentication
258         (authentication java:+null+))
259    (jss:new 'org.sonatype.aether.repository.Proxy
260             scheme host port authentication)))
261
262(defparameter *repository-system*  nil
263  "The org.sonatype.aether.RepositorySystem used by the Maeven Aether connector.")
264(defun ensure-repository-system (&key (force nil))
265  (when (or force (not *repository-system*))
266    (setf *repository-system* (make-repository-system)))
267  *repository-system*)
268
269(defparameter *session* nil
270  "Reference to the Maven RepositorySystemSession")
271(defun ensure-session (&key (force nil))
272  "Ensure that the RepositorySystemSession has been created.
273
274If *MAVEN-HTTP-PROXY* is non-nil, parse its value as the http proxy."
275  (when (or force (not *session*))
276    (ensure-repository-system :force force)
277    (setf *session* (make-session *repository-system*))
278    (#"setRepositoryListener" *session* (make-repository-listener))
279    (when *maven-http-proxy*
280      (let ((proxy (make-proxy)))
281        (#"add" (#"getProxySelector" *session*)
282                proxy 
283                ;; A string specifying non proxy hosts, or null
284                java:+null+))))
285    *session*)
286
287;;; TODO change this to work on artifact strings like log4j:log4j:jar:1.2.16
288(defun resolve-artifact (group-id artifact-id &optional (version "LATEST" versionp))
289  "Resolve artifact to location on the local filesystem.
290
291Declared dependencies are not attempted to be located.
292
293If unspecified, the string \"LATEST\" will be used for the VERSION.
294
295Returns the Maven specific string for the artifact "
296  (unless versionp
297    (warn "Using LATEST for unspecified version."))
298  (unless *init* (init))
299  (let* ((artifact-string (format nil "~A:~A:~A" group-id artifact-id version))
300         (artifact 
301          (jss:new "org.sonatype.aether.util.artifact.DefaultArtifact" artifact-string))
302         (artifact-request 
303          (java:jnew "org.sonatype.aether.resolution.ArtifactRequest")))
304    (#"setArtifact" artifact-request artifact)
305    (#"addRepository" artifact-request (ensure-remote-repository))
306    (#"toString" (#"getFile" 
307                  (#"getArtifact" (#"resolveArtifact" (ensure-repository-system) 
308                                                      (ensure-session) artifact-request))))))
309
310(defun make-remote-repository (id type url) 
311  (jss:new 'aether.repository.RemoteRepository id type url))
312
313(defparameter *default-repository* 
314   "http://repo1.maven.org/maven2/")
315
316(defun add-repository (repository)
317  (ensure-remote-repository :repository repository))
318
319(defparameter *maven-remote-repository*  nil
320    "The remote repository used by the Maven Aether embedder.")
321(defun ensure-remote-repository (&key 
322                                   (force nil)
323                                   (repository *default-repository* repository-p))
324  (unless *init* (init))
325  (when (or force 
326            repository-p 
327            (not *maven-remote-repository*))
328    (let ((r (make-remote-repository "central" "default" repository)))
329      (when *maven-http-proxy*
330        (#"setProxy" r (make-proxy)))
331      (setf *maven-remote-repository* r)))
332  *maven-remote-repository*)
333
334(defun resolve-dependencies (group-id artifact-id 
335                             &optional  ;;; XXX Uggh.  Move to keywords when we get the moxie.
336                             (version "LATEST" versionp)
337                             (repository *maven-remote-repository* repository-p))
338  "Dynamically resolve Maven dependencies for item with GROUP-ID and ARTIFACT-ID
339optionally with a VERSION and a REPOSITORY.  Users of the function are advised
340
341All recursive dependencies will be visited before resolution is successful.
342
343If unspecified, the string \"LATEST\" will be used for the VERSION.
344
345Returns a string containing the necessary jvm classpath entries packed
346in Java CLASSPATH representation."
347  (unless *init* (init))
348  (unless versionp
349    (warn "Using LATEST for unspecified version."))
350  (let* ((artifact
351          (java:jnew (jss:find-java-class "aether.util.artifact.DefaultArtifact")
352                     (format nil "~A:~A:~A"
353                             group-id artifact-id version)))
354         (dependency 
355          (java:jnew (jss:find-java-class "aether.graph.Dependency")
356                     artifact (java:jfield (jss:find-java-class "JavaScopes") "RUNTIME")))
357         (collect-request (java:jnew (jss:find-java-class "CollectRequest"))))
358    (#"setRoot" collect-request dependency)
359    (#"addRepository" collect-request 
360                      (if repository-p
361                          (ensure-remote-repository :repository repository)
362                          (ensure-remote-repository)))
363    (let* ((node 
364            (#"getRoot" (#"collectDependencies" (ensure-repository-system) (ensure-session) collect-request)))
365           (dependency-request 
366            (java:jnew (jss:find-java-class "DependencyRequest")
367                       node java:+null+))
368           (nlg 
369            (java:jnew (jss:find-java-class "PreorderNodeListGenerator"))))
370      (#"resolveDependencies" (ensure-repository-system) (ensure-session) dependency-request)
371      (#"accept" node nlg)
372      (#"getClassPath" nlg))))
373
374(defun make-repository-listener ()
375  (flet ((log (e) 
376           (format *maven-verbose* "~&~A~%" (#"toString" e))))
377    (java:jinterface-implementation 
378     "org.sonatype.aether.RepositoryListener"
379     "artifactDeployed" 
380     #'log
381     "artifactDeploying" 
382     #'log
383     "artifactDescriptorInvalid" 
384     #'log
385     "artifactDescriptorMissing" 
386     #'log
387     "artifactDownloaded" 
388     #'log
389     "artifactDownloading" 
390     #'log
391     "artifactInstalled" 
392     #'log
393     "artifactInstalling" 
394     #'log
395     "artifactResolved" 
396     #'log
397     "artifactResolving" 
398     #'log
399     "metadataDeployed" 
400     #'log
401     "metadataDeploying" 
402     #'log
403     "metadataDownloaded" 
404     #'log
405     "metadataDownloading" 
406     #'log
407     "metadataInstalled"
408     #'log
409     "metadataInstalling" 
410     #'log
411     "metadataInvalid" 
412     #'log
413     "metadataResolved" 
414     #'log
415     "metadataResolving"
416     #'log)))
417
418         
419(defmethod resolve ((string string))
420  "Resolve a colon separated GROUP-ID:ARTIFACT-ID[:VERSION] reference to a Maven artifact.
421
422Examples of artifact references: \"log4j:log4j:1.2.14\" for
423'log4j-1.2.14.jar'.  Resolving \"log4j:log4j\" would return the latest
424version of the artifact known to the distributed Maven pom.xml graph.
425
426Returns a string containing the necessary classpath entries for this
427artifact and all of its transitive dependencies."
428  (let ((result (split-string string ":")))
429    (cond 
430      ((= (length result) 3)
431       (resolve-dependencies 
432        (first result) (second result) (third result)))
433      ((string= string "com.sun.jna:jna")
434       (warn "Replacing request for no longer available com.sun.jna:jna with net.java.dev.jna:jna")
435       (resolve-dependencies "net.java.dev.jna" "jna" "LATEST"))
436      (t
437       (apply #'resolve-dependencies result)))))
438 
439;;; Currently the last file listed in ASDF
440(provide 'abcl-asdf)
Note: See TracBrowser for help on using the repository browser.