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

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

Fix the broken require contracts in ABCL-CONTRIB.

CL:REQUIRE now calls PROVIDE with module names when successful (as
opposed to relying in the loaded code to do this explicity).

File size: 15.9 KB
RevLine 
[13844]1;;;; Use the Aether system in a localy installed Maven3 distribution to download
2;;;; and install JVM artifact dependencies.
[13355]3
[13844]4#|
5
[13865]6# Implementation
[13844]7
[14168]8Not necessarily multi-threaded safe, and unclear how much work that
9would be, as it is unknown how the Maven implementation behaves.
[13865]10
[13844]11## Installing Maven
[13865]12http://maven.apache.org/download.html
[13844]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
[13865]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
[13844]24|#
25
[14168]26;;; N.b. evaluated *after* we load the ABCL specific modifications of
27;;;      ASDF in abcl-asdf.lisp
[13901]28
[13354]29(in-package :abcl-asdf)
30
31(require :abcl-contrib)
32(require :jss)
33
[13362]34#|
35Test:
[13367]36(resolve-dependencies "org.slf4j" "slf4j-api" "1.6.1")
[13362]37
[13807]38(resolve-dependencies "org.apache.maven" "maven-aether-provider" "3.0.4")
[13362]39|#
40
[13841]41(defparameter *maven-verbose* t
42  "Stream to send output from the Maven Aether subsystem to, or NIL to muffle output")
43
[14169]44(defparameter *mavens* 
45  (if (find :windows *features*)
46      '("mvn.bat" "mvn3.bat")
47      '("/opt/local/bin/mvn3" "mvn3" "mvn"))
[13367]48  "Locations to search for the Maven executable.")
[13362]49
50(defun find-mvn () 
[13844]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
[14166]55  (let ((m2-home (ext:getenv "M2_HOME"))
56        (m2 (ext:getenv "M2"))
[13845]57        (mvn-executable (if (find :unix *features*)
[13844]58                               "mvn"
[13845]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))))))))
[13362]101
102(defun find-mvn-libs ()
103  (let ((mvn (find-mvn)))
104    (unless mvn
105      (warn "Failed to find Maven3 libraries.")
[13836]106      (return-from find-mvn-libs nil))
[13362]107    (truename (make-pathname 
108               :defaults (merge-pathnames "../lib/" mvn)
109               :name nil :type nil))))
110
111(defparameter *mvn-libs-directory*
112  nil
[13354]113  "Location of 'maven-core-3.<m>.<p>.jar', 'maven-embedder-3.<m>.<p>.jar' etc.")
114
[13362]115(defun mvn-version ()
[13841]116  "Return the Maven version used by the Aether connector."
[13364]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 
[13362]127      (return-from mvn-version nil))
[13364]128    (mapcar #'parse-integer
129            `(,(#"group" matcher 1)
130              ,(#"group" matcher 2)
131              ,(#"group" matcher 3)))))
[13362]132
133(defun ensure-mvn-version ()
134  "Return t if Maven version is 3.0.3 or greater."
[13364]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)
[13907]143          (>= minor 0)
[13901]144          (>= patch 4)))))
[13362]145
146(defparameter *init* nil)
147
[13841]148(defun init (&optional &key (force nil))
[13807]149  "Run the initialization strategy to bootstrap a Maven dependency node."
[13841]150  (unless (or force *mvn-libs-directory*)
[13362]151    (setf *mvn-libs-directory* (find-mvn-libs)))
152  (unless (probe-file *mvn-libs-directory*)
[13901]153    (error "You must download maven-3.0.4 or later from http://maven.apache.org/download.html, then set ABCL-ASDF:*MVN-DIRECTORY* appropiately."))
[13362]154  (unless (ensure-mvn-version)
[13901]155    (error "We need maven-3.0.4 or later."))
[13430]156  (add-directory-jars-to-class-path *mvn-libs-directory* nil)
[13362]157  (setf *init* t))
158
[13804]159(defparameter *http-wagon-implementations*
[13901]160  ;;; maven-3.0.3 reported as not working with all needed functionality
[13807]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.
[13804]164
[14168]165Supposedly configurable with the java.net.protocols (c.f. reference maso2000 in the Manual.)")
[13807]166
[13367]167(defun make-wagon-provider ()
[13807]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."
[13367]172  (unless *init* (init))
173  (java:jinterface-implementation 
174   "org.sonatype.aether.connector.wagon.WagonProvider"
175   "lookup"
176   (lambda (role-hint)
[13859]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+))))
[13367]185   "release"
186   (lambda (wagon)
187     (declare (ignore wagon)))))
188
[13841]189(defun make-repository-system ()
[13362]190  (unless *init* (init))
[13355]191  (let ((locator 
[14168]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"))
[13367]197        (repository-connector-factory-class 
198         (java:jclass "org.sonatype.aether.spi.connector.RepositoryConnectorFactory"))
[13355]199        (repository-system-class
200         (java:jclass "org.sonatype.aether.RepositorySystem")))
[14168]201    (#"setServices" locator
202                    wagon-provider-class
203                   (java:jarray-from-list
204                    (list (make-wagon-provider))))
[13367]205    (#"addService" locator
[14168]206                   repository-connector-factory-class
[13355]207                   wagon-repository-connector-factory-class)
[14168]208    (values (#"getService" locator
209                           repository-system-class)
210            locator)))
[13367]211       
[13841]212(defun make-session (repository-system)
213  "Construct a new org.sonatype.aether.RepositorySystemSession from REPOSITORY-SYSTEM"
[13355]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
[14168]222     (#"newLocalRepositoryManager" repository-system
223                                   local-repository))))
[13354]224
[13836]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 ()
[13860]229  "Return an org.sonatype.aether.repository.Proxy instance initialized from *MAVEN-HTTP-PROXY*."
[13836]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
[13841]246(defparameter *repository-system*  nil
247  "The org.sonatype.aether.RepositorySystem used by the Maeven Aether connector.")
[14168]248(defun ensure-repository-system (&key (force nil))
249  (when (or force (not *repository-system*))
[13841]250    (setf *repository-system* (make-repository-system)))
251  *repository-system*)
252
253(defparameter *session* nil
254  "Reference to the Maven RepositorySystemSession")
[14168]255(defun ensure-session (&key (force nil))
[13836]256  "Ensure that the RepositorySystemSession has been created.
257
258If *MAVEN-HTTP-PROXY* is non-nil, parse its value as the http proxy."
[14168]259  (when (or force (not *session*))
260    (ensure-repository-system :force force)
[13841]261    (setf *session* (make-session *repository-system*))
[13836]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
[13841]271;;; TODO change this to work on artifact strings like log4j:log4j:jar:1.2.16
[13575]272(defun resolve-artifact (group-id artifact-id &optional (version "LATEST" versionp))
[13860]273  "Resolve artifact to location on the local filesystem.
[13575]274
275Declared dependencies are not attempted to be located.
276
277If unspecified, the string \"LATEST\" will be used for the VERSION.
278
[13841]279Returns the Maven specific string for the artifact "
[13575]280  (unless versionp
281    (warn "Using LATEST for unspecified version."))
[13841]282  (unless *init* (init))
283  (let* ((artifact-string (format nil "~A:~A:~A" group-id artifact-id version))
[13367]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)
[13841]289    (#"addRepository" artifact-request (ensure-remote-repository))
[13860]290    (#"toString" (#"getFile" 
291                  (#"getArtifact" (#"resolveArtifact" (ensure-repository-system) 
292                                                      (ensure-session) artifact-request))))))
[13354]293
[13841]294(defun make-remote-repository (id type url) 
295  (jss:new 'aether.repository.RemoteRepository id type url))
[13836]296
[13904]297(defparameter *default-repository* 
298   "http://repo1.maven.org/maven2/")
299
300(defun add-repository (repository)
301  (ensure-remote-repository :repository repository))
302
[13841]303(defparameter *maven-remote-repository*  nil
304    "The remote repository used by the Maven Aether embedder.")
[14168]305(defun ensure-remote-repository (&key 
306                                   (force nil)
307                                   (repository *default-repository* repository-p))
[13841]308  (unless *init* (init))
[14168]309  (when (or force 
310            repository-p 
311            (not *maven-remote-repository*))
[13904]312    (let ((r (make-remote-repository "central" "default" repository)))
[13841]313      (when *maven-http-proxy*
314        (#"setProxy" r (make-proxy)))
315      (setf *maven-remote-repository* r)))
316  *maven-remote-repository*)
317
[13904]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
[13575]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."
[13362]331  (unless *init* (init))
[13575]332  (unless versionp
333    (warn "Using LATEST for unspecified version."))
[13841]334  (let* ((artifact
[13355]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")
[13863]340                     artifact (java:jfield (jss:find-java-class "JavaScopes") "RUNTIME")))
[13355]341         (collect-request (java:jnew (jss:find-java-class "CollectRequest"))))
342    (#"setRoot" collect-request dependency)
[13904]343    (#"addRepository" collect-request 
344                      (if repository-p
345                          (ensure-remote-repository :repository repository)
346                          (ensure-remote-repository)))
[13355]347    (let* ((node 
[13836]348            (#"getRoot" (#"collectDependencies" (ensure-repository-system) (ensure-session) collect-request)))
[13355]349           (dependency-request 
350            (java:jnew (jss:find-java-class "DependencyRequest")
[13362]351                       node java:+null+))
[13355]352           (nlg 
353            (java:jnew (jss:find-java-class "PreorderNodeListGenerator"))))
[13836]354      (#"resolveDependencies" (ensure-repository-system) (ensure-session) dependency-request)
[13355]355      (#"accept" node nlg)
356      (#"getClassPath" nlg))))
357
[13836]358(defun make-repository-listener ()
[13841]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)))
[13354]401
[13367]402         
[13904]403(defmethod resolve ((string string))
[13865]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."
[13862]412  (let ((result (split-string string ":")))
413    (cond 
[13904]414      ((= (length result) 3)
415       (resolve-dependencies (first result) (second result) (third result)))
416      (t
[13862]417       (apply #'resolve-dependencies result)))))
418 
[13904]419#+nil
420(defmethod resolve ((mvn asdf:mvn))
421  (with-slots (asdf::group-id asdf::artifact-id asdf::version)
422      (asdf:ensure-parsed-mvn mvn)
423    (resolve-dependencies (format nil "~A:~A:~A" asdf::group-id asdf::artifact-id asdf::version))))
[14170]424
425;;; Currently the last file listed in ASDF
426(provide 'abcl-asdf)
Note: See TracBrowser for help on using the repository browser.