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

Last change on this file since 14791 was 14791, checked in by Mark Evenson, 7 years ago

abcl-asdf: fix usage with local repository

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