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

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

abcl-asdf: Note use of RESOLVE in tests

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