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

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

abcl-asdf: Patch to fix obvious mistakes (stas).

Fix logic for working around a missing Maven.

ABCL-ASDF:RESOLVE had a broken conditional.

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