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

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

abcl-asdf: restore the ability to use maven-3.0.3

Warn when interpreting alias for "com.sun.jna:jna".

Closes #268.

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