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

Last change on this file since 14233 was 14233, checked in by Mark Evenson, 8 years ago

Re #262 in abcl-asdf: use first matching line if the first line doesn't work out.

Further work needed as something more meaningful should be returned
other than "The value NIL is not of type REAL" when abcl-asdf can't
parse the Maven version string.

Initial patch by Christoph.

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