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

Last change on this file since 13859 was 13859, checked in by Mark Evenson, 10 years ago

abcl-asdf: return the connector for "https" role-hint.

File size: 13.5 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 references
7
8## Installing Maven
9
10## Current Javadoc for Maven Aether connector
11http://sonatype.github.com/sonatype-aether/apidocs/overview-summary.html
12
13## Incomplete, seemingly often wrong
14https://docs.sonatype.org/display/AETHER/Home
15
16|#
17
18(in-package :abcl-asdf)
19
20(require :abcl-contrib)
21(require :jss)
22
23#|
24Test:
25(resolve-dependencies "org.slf4j" "slf4j-api" "1.6.1")
26
27(resolve-dependencies "org.apache.maven" "maven-aether-provider" "3.0.4")
28|#
29
30(defparameter *maven-verbose* t
31  "Stream to send output from the Maven Aether subsystem to, or NIL to muffle output")
32
33(defvar *mavens* '("/opt/local/bin/mvn3" "mvn3" "mvn" "mvn3.bat" "mvn.bat")
34  "Locations to search for the Maven executable.")
35
36(defun find-mvn () 
37  "Attempt to find a suitable Maven ('mvn') executable on the hosting operating system.
38
39Returns the path of the Maven executable or nil if none are found."
40
41  (let ((m2-home (asdf:getenv "M2_HOME"))
42        (m2 (asdf:getenv "M2"))
43        (mvn-executable (if (find :unix *features*)
44                               "mvn"
45                               "mvn.bat")))
46    (when (and m2-home (probe-file m2-home))
47      (let* ((m2-home (truename m2-home))
48             (mvn-path (merge-pathnames 
49                        (format nil "bin/~A" mvn-executable)
50                        m2-home))
51             (mvn (truename mvn-path)))
52        (if mvn
53            (return-from find-mvn mvn)
54            (warn "M2_HOME was set to '~A' in the process environment but '~A' doesn't exist." 
55                  m2-home mvn-path))))
56    (when (and m2 (probe-file m2))
57      (let* ((m2 (truename m2))
58             (mvn-path (merge-pathnames mvn-executable m2))
59             (mvn (truename mvn-path)))
60        (if mvn
61            (return-from find-mvn mvn)
62            (warn "M2 was set to '~A' in the process environment but '~A' doesn't exist." 
63                  m2 mvn-path))))
64    (let* ((which-cmd 
65            (if (find :unix *features*)
66                "which" 
67                ;; Starting with Windows Server 2003
68                "where.exe"))
69           (which-cmd-p 
70            (handler-case 
71                (sys::run-program which-cmd nil)
72              (t () nil))))
73      (when which-cmd-p
74        (dolist (mvn-path *mavens*)
75          (let ((mvn 
76                 (handler-case 
77                     (truename (read-line (sys::process-output 
78                                           (sys::run-program 
79                                            which-cmd `(,mvn-path))))) 
80                   (end-of-file () nil)
81                   (t (e) 
82                     (format *maven-verbose* 
83                             "~&Failed to find Maven executable '~A' in PATH because~&~A" 
84                             mvn-path e)))))
85            (when mvn
86              (return-from find-mvn mvn))))))))
87
88(defun find-mvn-libs ()
89  (let ((mvn (find-mvn)))
90    (unless mvn
91      (warn "Failed to find Maven3 libraries.")
92      (return-from find-mvn-libs nil))
93    (truename (make-pathname 
94               :defaults (merge-pathnames "../lib/" mvn)
95               :name nil :type nil))))
96
97(defparameter *mvn-libs-directory*
98  nil
99  "Location of 'maven-core-3.<m>.<p>.jar', 'maven-embedder-3.<m>.<p>.jar' etc.")
100
101(defun mvn-version ()
102  "Return the Maven version used by the Aether connector."
103  (let* ((line
104         (read-line (sys::process-output 
105                     (sys::run-program 
106                      (namestring (find-mvn)) '("-version")))))
107         (pattern (#"compile" 
108                   'regex.Pattern
109                   "Apache Maven ([0-9]+)\\.([0-9]+)\\.([0-9]+)"))
110         (matcher (#"matcher" pattern line))
111         (found (#"find" matcher)))
112    (unless found 
113      (return-from mvn-version nil))
114    (mapcar #'parse-integer
115            `(,(#"group" matcher 1)
116              ,(#"group" matcher 2)
117              ,(#"group" matcher 3)))))
118
119(defun ensure-mvn-version ()
120  "Return t if Maven version is 3.0.3 or greater."
121  (let* ((version (mvn-version))
122         (major (first version))
123         (minor (second version))
124         (patch (third version)))
125    (or 
126     (and (>= major 3)
127          (>= minor 1))
128     (and (>= major 3)
129          (>= major 0)
130          (>= patch 3)))))
131
132(defparameter *init* nil)
133
134(defun init (&optional &key (force nil))
135  "Run the initialization strategy to bootstrap a Maven dependency node."
136  (unless (or force *mvn-libs-directory*)
137    (setf *mvn-libs-directory* (find-mvn-libs)))
138  (unless (probe-file *mvn-libs-directory*)
139    (error "You must download maven-3.0.3 or later from http://maven.apache.org/download.html, then set ABCL-ASDF:*MVN-DIRECTORY* appropiately."))
140  (unless (ensure-mvn-version)
141    (error "We need maven-3.0.3 or later."))
142  (add-directory-jars-to-class-path *mvn-libs-directory* nil)
143  (setf *init* t))
144
145(defparameter *http-wagon-implementations*
146  `("org.apache.maven.wagon.providers.http.HttpWagon" ;; introduced as default with maven-3.0.4
147    "org.apache.maven.wagon.providers.http.LightweightHttpWagon")
148  "A list of possible candidate implementations that provide access to http and https resources.
149
150Supposedly configurable with the java.net.protocols (c.f. reference maso2000 in the Manual.")
151
152(defun make-wagon-provider ()
153  "Returns an implementation of the org.sonatype.aether.connector.wagon.WagonProvider contract.
154
155The implementation is specified as Lisp closures.  Currently, it only
156specializes the lookup() method if passed an 'http' role hint."
157  (unless *init* (init))
158  (java:jinterface-implementation 
159   "org.sonatype.aether.connector.wagon.WagonProvider"
160   "lookup"
161   (lambda (role-hint)
162     (cond 
163       ((find role-hint '("http" "https") :test #'string-equal)
164        (some (lambda (provider) (java:jnew provider)) *http-wagon-implementations*))
165       (t
166        (progn 
167          (format *maven-verbose* 
168                  "~&WagonProvider stub passed '~A' as a hint it couldn't satisfy.~%" role-hint)
169           java:+null+))))
170   "release"
171   (lambda (wagon)
172     (declare (ignore wagon)))))
173
174(defun make-repository-system ()
175  (unless *init* (init))
176  (let ((locator 
177         (java:jnew "org.apache.maven.repository.internal.DefaultServiceLocator"))
178        (repository-connector-factory-class 
179         (java:jclass "org.sonatype.aether.spi.connector.RepositoryConnectorFactory"))
180        (wagon-repository-connector-factory-class
181         (java:jclass "org.sonatype.aether.connector.wagon.WagonRepositoryConnectorFactory"))
182        (wagon-provider-class 
183         (java:jclass "org.sonatype.aether.connector.wagon.WagonProvider"))
184        (repository-system-class
185         (java:jclass "org.sonatype.aether.RepositorySystem")))
186    (#"addService" locator
187                   repository-connector-factory-class 
188                   wagon-repository-connector-factory-class)
189    (#"setServices" locator
190                    wagon-provider-class
191                    (java:jnew-array-from-list 
192                     "org.sonatype.aether.connector.wagon.WagonProvider"
193                     (list 
194                      (make-wagon-provider))))
195    (#"getService" locator
196                   repository-system-class)))
197       
198(defun make-session (repository-system)
199  "Construct a new org.sonatype.aether.RepositorySystemSession from REPOSITORY-SYSTEM"
200  (let ((session 
201         (java:jnew (jss:find-java-class "MavenRepositorySystemSession")))
202        (local-repository 
203         (java:jnew (jss:find-java-class "LocalRepository")
204                  (namestring (merge-pathnames ".m2/repository/"
205                                               (user-homedir-pathname))))))
206    (#"setLocalRepositoryManager" 
207     session
208     (#"newLocalRepositoryManager" repository-system local-repository))))
209
210(defparameter *maven-http-proxy* nil
211  "A string containing the URI of an http proxy for Maven to use.")
212
213(defun make-proxy ()
214  "Return an org.sonatype.aether.repository.Proxy instance initialized form *MAVEN-HTTP-PROXY*."
215  (unless *maven-http-proxy*
216    (warn "No proxy specified in *MAVEN-HTTP-PROXY*")
217    (return-from make-proxy nil))
218  (let* ((p (pathname *maven-http-proxy*))
219         (scheme (sys::url-pathname-scheme p))
220         (authority (sys::url-pathname-authority p))
221         (host (if (search ":" authority)
222                   (subseq authority 0 (search ":" authority))
223                   authority))
224         (port (when (search ":" authority)
225                 (parse-integer (subseq authority (1+ (search ":" authority))))))
226         ;; TODO allow specification of authentication
227         (authentication java:+null+))
228    (jss:new 'org.sonatype.aether.repository.Proxy
229             scheme host port authentication)))
230
231(defparameter *repository-system*  nil
232  "The org.sonatype.aether.RepositorySystem used by the Maeven Aether connector.")
233(defun ensure-repository-system ()
234  (unless *repository-system*
235    (setf *repository-system* (make-repository-system)))
236  *repository-system*)
237
238(defparameter *session* nil
239  "Reference to the Maven RepositorySystemSession")
240(defun ensure-session ()
241  "Ensure that the RepositorySystemSession has been created.
242
243If *MAVEN-HTTP-PROXY* is non-nil, parse its value as the http proxy."
244  (unless *session*
245    (ensure-repository-system)
246    (setf *session* (make-session *repository-system*))
247    (#"setRepositoryListener" *session* (make-repository-listener))
248    (when *maven-http-proxy*
249      (let ((proxy (make-proxy)))
250        (#"add" (#"getProxySelector" *session*)
251                proxy 
252                ;; A string specifying non proxy hosts, or null
253                java:+null+))))
254    *session*)
255
256;;; TODO change this to work on artifact strings like log4j:log4j:jar:1.2.16
257(defun resolve-artifact (group-id artifact-id &optional (version "LATEST" versionp))
258  "Directly resolve Maven dependencies for item with GROUP-ID and ARTIFACT-ID at VERSION, ignoring dependencies.
259
260Declared dependencies are not attempted to be located.
261
262If unspecified, the string \"LATEST\" will be used for the VERSION.
263
264Returns the Maven specific string for the artifact "
265  (unless versionp
266    (warn "Using LATEST for unspecified version."))
267  (unless *init* (init))
268  (let* ((artifact-string (format nil "~A:~A:~A" group-id artifact-id version))
269         (artifact 
270          (jss:new "org.sonatype.aether.util.artifact.DefaultArtifact" artifact-string))
271         (artifact-request 
272          (java:jnew "org.sonatype.aether.resolution.ArtifactRequest")))
273    (#"setArtifact" artifact-request artifact)
274    (#"addRepository" artifact-request (ensure-remote-repository))
275    (#"toString" (#"resolveArtifact" (ensure-repository-system) (ensure-session) artifact-request))))
276
277(defun make-remote-repository (id type url) 
278  (jss:new 'aether.repository.RemoteRepository id type url))
279
280(defparameter *maven-remote-repository*  nil
281    "The remote repository used by the Maven Aether embedder.")
282(defun ensure-remote-repository () 
283  (unless *init* (init))
284  (unless *maven-remote-repository*
285    (let ((r (make-remote-repository "central" "default" "http://repo1.maven.org/maven2/")))
286      (when *maven-http-proxy*
287        (#"setProxy" r (make-proxy)))
288      (setf *maven-remote-repository* r)))
289  *maven-remote-repository*)
290
291(defun resolve-dependencies (group-id artifact-id &optional (version "LATEST" versionp))
292  "Dynamically resolve Maven dependencies for item with GROUP-ID and ARTIFACT-ID at VERSION.
293
294All recursive dependencies will be visited before resolution is successful.
295
296If unspecified, the string \"LATEST\" will be used for the VERSION.
297
298Returns a string containing the necessary jvm classpath entries packed
299in Java CLASSPATH representation."
300  (unless *init* (init))
301  (unless versionp
302    (warn "Using LATEST for unspecified version."))
303  (let* ((artifact
304          (java:jnew (jss:find-java-class "aether.util.artifact.DefaultArtifact")
305                     (format nil "~A:~A:~A"
306                             group-id artifact-id version)))
307         (dependency 
308          (java:jnew (jss:find-java-class "aether.graph.Dependency")
309                     artifact "compile"))
310         (collect-request (java:jnew (jss:find-java-class "CollectRequest"))))
311    (#"setRoot" collect-request dependency)
312    (#"addRepository" collect-request (ensure-remote-repository))
313    (let* ((node 
314            (#"getRoot" (#"collectDependencies" (ensure-repository-system) (ensure-session) collect-request)))
315           (dependency-request 
316            (java:jnew (jss:find-java-class "DependencyRequest")
317                       node java:+null+))
318           (nlg 
319            (java:jnew (jss:find-java-class "PreorderNodeListGenerator"))))
320      (#"resolveDependencies" (ensure-repository-system) (ensure-session) dependency-request)
321      (#"accept" node nlg)
322      (#"getClassPath" nlg))))
323
324(defun make-repository-listener ()
325  (flet ((log (e) 
326           (format *maven-verbose* "~&~A~%" (#"toString" e))))
327    (java:jinterface-implementation 
328     "org.sonatype.aether.RepositoryListener"
329     "artifactDeployed" 
330     #'log
331     "artifactDeploying" 
332     #'log
333     "artifactDescriptorInvalid" 
334     #'log
335     "artifactDescriptorMissing" 
336     #'log
337     "artifactDownloaded" 
338     #'log
339     "artifactDownloading" 
340     #'log
341     "artifactInstalled" 
342     #'log
343     "artifactInstalling" 
344     #'log
345     "artifactResolved" 
346     #'log
347     "artifactResolving" 
348     #'log
349     "metadataDeployed" 
350     #'log
351     "metadataDeploying" 
352     #'log
353     "metadataDownloaded" 
354     #'log
355     "metadataDownloading" 
356     #'log
357     "metadataInstalled"
358     #'log
359     "metadataInstalling" 
360     #'log
361     "metadataInvalid" 
362     #'log
363     "metadataResolved" 
364     #'log
365     "metadataResolving"
366     #'log)))
367
368         
Note: See TracBrowser for help on using the repository browser.