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

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

Promote ABCL-ASDF:ENSURE-MVN-VERSION as primary API that Maven can be executed.

Returns the found (MAJOR MINOR PATCH) as the second value.

Adding support for test framework (use :RT).

File size: 16.7 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' role hint."
188  (unless *init* (init))
189  (java:jinterface-implementation 
190   "org.sonatype.aether.connector.wagon.WagonProvider"
191   "lookup"
192   (lambda (role-hint)
193     (cond 
194       ((find role-hint '("http" "https") :test #'string-equal)
195        (find-http-wagon))
196       (t
197        (progn 
198          (format *maven-verbose* 
199                  "~&WagonProvider stub passed '~A' as a hint it couldn't satisfy.~%" role-hint)
200           java:+null+))))
201   "release"
202   (lambda (wagon)
203     (declare (ignore wagon)))))
204
205(defun find-service-locator ()
206  (handler-case 
207      (java:jnew "org.apache.maven.repository.internal.MavenServiceLocator") ;; maven-3.0.4
208    (error () 
209      (java:jnew "org.apache.maven.repository.internal.DefaultServiceLocator"))))
210
211(defun make-repository-system ()
212  (unless *init* (init))
213  (let ((locator 
214         (find-service-locator))
215        (wagon-provider-class 
216         (java:jclass "org.sonatype.aether.connector.wagon.WagonProvider"))
217        (wagon-repository-connector-factory-class
218         (java:jclass "org.sonatype.aether.connector.wagon.WagonRepositoryConnectorFactory"))
219        (repository-connector-factory-class 
220         (java:jclass "org.sonatype.aether.spi.connector.RepositoryConnectorFactory"))
221        (repository-system-class
222         (java:jclass "org.sonatype.aether.RepositorySystem")))
223    (#"setServices" locator
224                    wagon-provider-class
225                   (java:jarray-from-list
226                    (list (make-wagon-provider))))
227    (#"addService" locator
228                   repository-connector-factory-class
229                   wagon-repository-connector-factory-class)
230    (values (#"getService" locator
231                           repository-system-class)
232            locator)))
233       
234(defun make-session (repository-system)
235  "Construct a new org.sonatype.aether.RepositorySystemSession from REPOSITORY-SYSTEM"
236  (let ((session 
237         (java:jnew (jss:find-java-class "MavenRepositorySystemSession")))
238        (local-repository 
239         (java:jnew (jss:find-java-class "LocalRepository")
240                  (namestring (merge-pathnames ".m2/repository/"
241                                               (user-homedir-pathname))))))
242    (#"setLocalRepositoryManager" 
243     session
244     (#"newLocalRepositoryManager" repository-system
245                                   local-repository))))
246
247(defparameter *maven-http-proxy* nil
248  "A string containing the URI of an http proxy for Maven to use.")
249
250(defun make-proxy ()
251  "Return an org.sonatype.aether.repository.Proxy instance initialized from *MAVEN-HTTP-PROXY*."
252  (unless *maven-http-proxy*
253    (warn "No proxy specified in *MAVEN-HTTP-PROXY*")
254    (return-from make-proxy nil))
255  (let* ((p (pathname *maven-http-proxy*))
256         (scheme (sys::url-pathname-scheme p))
257         (authority (sys::url-pathname-authority p))
258         (host (if (search ":" authority)
259                   (subseq authority 0 (search ":" authority))
260                   authority))
261         (port (when (search ":" authority)
262                 (parse-integer (subseq authority (1+ (search ":" authority))))))
263         ;; TODO allow specification of authentication
264         (authentication java:+null+))
265    (jss:new 'org.sonatype.aether.repository.Proxy
266             scheme host port authentication)))
267
268(defparameter *repository-system*  nil
269  "The org.sonatype.aether.RepositorySystem used by the Maeven Aether connector.")
270(defun ensure-repository-system (&key (force nil))
271  (when (or force (not *repository-system*))
272    (setf *repository-system* (make-repository-system)))
273  *repository-system*)
274
275(defparameter *session* nil
276  "Reference to the Maven RepositorySystemSession")
277(defun ensure-session (&key (force nil))
278  "Ensure that the RepositorySystemSession has been created.
279
280If *MAVEN-HTTP-PROXY* is non-nil, parse its value as the http proxy."
281  (when (or force (not *session*))
282    (ensure-repository-system :force force)
283    (setf *session* (make-session *repository-system*))
284    (#"setRepositoryListener" *session* (make-repository-listener))
285    (when *maven-http-proxy*
286      (let ((proxy (make-proxy)))
287        (#"add" (#"getProxySelector" *session*)
288                proxy 
289                ;; A string specifying non proxy hosts, or null
290                java:+null+))))
291    *session*)
292
293;;; TODO change this to work on artifact strings like log4j:log4j:jar:1.2.16
294(defun resolve-artifact (group-id artifact-id &optional (version "LATEST" versionp))
295  "Resolve artifact to location on the local filesystem.
296
297Declared dependencies are not attempted to be located.
298
299If unspecified, the string \"LATEST\" will be used for the VERSION.
300
301Returns the Maven specific string for the artifact "
302  (unless versionp
303    (warn "Using LATEST for unspecified version."))
304  (unless *init* (init))
305  (let* ((artifact-string (format nil "~A:~A:~A" group-id artifact-id version))
306         (artifact 
307          (jss:new "org.sonatype.aether.util.artifact.DefaultArtifact" artifact-string))
308         (artifact-request 
309          (java:jnew "org.sonatype.aether.resolution.ArtifactRequest")))
310    (#"setArtifact" artifact-request artifact)
311    (#"addRepository" artifact-request (ensure-remote-repository))
312    (#"toString" (#"getFile" 
313                  (#"getArtifact" (#"resolveArtifact" (ensure-repository-system) 
314                                                      (ensure-session) artifact-request))))))
315
316(defun make-remote-repository (id type url) 
317  (jss:new 'aether.repository.RemoteRepository id type url))
318
319(defparameter *default-repository* 
320   "http://repo1.maven.org/maven2/")
321
322(defun add-repository (repository)
323  (ensure-remote-repository :repository repository))
324
325(defparameter *maven-remote-repository*  nil
326    "The remote repository used by the Maven Aether embedder.")
327(defun ensure-remote-repository (&key 
328                                   (force nil)
329                                   (repository *default-repository* repository-p))
330  (unless *init* (init))
331  (when (or force 
332            repository-p 
333            (not *maven-remote-repository*))
334    (let ((r (make-remote-repository "central" "default" repository)))
335      (when *maven-http-proxy*
336        (#"setProxy" r (make-proxy)))
337      (setf *maven-remote-repository* r)))
338  *maven-remote-repository*)
339
340(defun resolve-dependencies (group-id artifact-id 
341                             &optional  ;;; XXX Uggh.  Move to keywords when we get the moxie.
342                             (version "LATEST" versionp)
343                             (repository *maven-remote-repository* repository-p))
344  "Dynamically resolve Maven dependencies for item with GROUP-ID and ARTIFACT-ID
345optionally with a VERSION and a REPOSITORY.  Users of the function are advised
346
347All recursive dependencies will be visited before resolution is successful.
348
349If unspecified, the string \"LATEST\" will be used for the VERSION.
350
351Returns a string containing the necessary jvm classpath entries packed
352in Java CLASSPATH representation."
353  (unless *init* (init))
354  (unless versionp
355    (warn "Using LATEST for unspecified version."))
356  (let* ((artifact
357          (java:jnew (jss:find-java-class "aether.util.artifact.DefaultArtifact")
358                     (format nil "~A:~A:~A"
359                             group-id artifact-id version)))
360         (dependency 
361          (java:jnew (jss:find-java-class "aether.graph.Dependency")
362                     artifact (java:jfield (jss:find-java-class "JavaScopes") "RUNTIME")))
363         (collect-request (java:jnew (jss:find-java-class "CollectRequest"))))
364    (#"setRoot" collect-request dependency)
365    (#"addRepository" collect-request 
366                      (if repository-p
367                          (ensure-remote-repository :repository repository)
368                          (ensure-remote-repository)))
369    (let* ((node 
370            (#"getRoot" (#"collectDependencies" (ensure-repository-system) (ensure-session) collect-request)))
371           (dependency-request 
372            (java:jnew (jss:find-java-class "DependencyRequest")
373                       node java:+null+))
374           (nlg 
375            (java:jnew (jss:find-java-class "PreorderNodeListGenerator"))))
376      (#"resolveDependencies" (ensure-repository-system) (ensure-session) dependency-request)
377      (#"accept" node nlg)
378      (#"getClassPath" nlg))))
379
380(defun make-repository-listener ()
381  (flet ((log (e) 
382           (format *maven-verbose* "~&~A~%" (#"toString" e))))
383    (java:jinterface-implementation 
384     "org.sonatype.aether.RepositoryListener"
385     "artifactDeployed" 
386     #'log
387     "artifactDeploying" 
388     #'log
389     "artifactDescriptorInvalid" 
390     #'log
391     "artifactDescriptorMissing" 
392     #'log
393     "artifactDownloaded" 
394     #'log
395     "artifactDownloading" 
396     #'log
397     "artifactInstalled" 
398     #'log
399     "artifactInstalling" 
400     #'log
401     "artifactResolved" 
402     #'log
403     "artifactResolving" 
404     #'log
405     "metadataDeployed" 
406     #'log
407     "metadataDeploying" 
408     #'log
409     "metadataDownloaded" 
410     #'log
411     "metadataDownloading" 
412     #'log
413     "metadataInstalled"
414     #'log
415     "metadataInstalling" 
416     #'log
417     "metadataInvalid" 
418     #'log
419     "metadataResolved" 
420     #'log
421     "metadataResolving"
422     #'log)))
423
424         
425(defmethod resolve ((string string))
426  "Resolve a colon separated GROUP-ID:ARTIFACT-ID[:VERSION] reference to a Maven artifact.
427
428Examples of artifact references: \"log4j:log4j:1.2.14\" for
429'log4j-1.2.14.jar'.  Resolving \"log4j:log4j\" would return the latest
430version of the artifact known to the distributed Maven pom.xml graph.
431
432Returns a string containing the necessary classpath entries for this
433artifact and all of its transitive dependencies."
434  (let ((result (split-string string ":")))
435    (cond 
436      ((= (length result) 3)
437       (resolve-dependencies 
438        (first result) (second result) (third result)))
439      ((string= string "com.sun.jna:jna")
440       (warn "Replacing request for no longer available com.sun.jna:jna with net.java.dev.jna:jna")
441       (resolve-dependencies "net.java.dev.jna" "jna" "LATEST"))
442      (t
443       (apply #'resolve-dependencies result)))))
444 
445;;; Currently the last file listed in ASDF
446(provide 'abcl-asdf)
Note: See TracBrowser for help on using the repository browser.