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

Last change on this file since 14574 was 14574, checked in by Mark Evenson, 9 years ago

abcl-asdf/1.3.0: updated to work with maven-3.1.0.

Big thanks to Anton Vodonosov for the initial patch.

The code in maven-embedder.lisp should now work for all versions of
Maven greater than 3.0.3.

Added MAKE-ARTIFACT and MAKE-ARTIFACT-REQUEST for creating the
correponding Aether objects.

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