maven-embedder.lisp 21.9 KB
Newer Older
1 2 3 4 5
;;;; Use the Aether system in a localy installed Maven3 distribution to download
;;;; and install JVM artifact dependencies.

#|

6 7
# Implementation 

8 9
Not necessarily multi-threaded safe, and unclear how much work that
would be, as it is unknown how the Maven implementation behaves.
10 11

## Installing Maven
12
http://maven.apache.org/download.html
13 14 15 16 17 18 19

## Current Javadoc for Maven Aether connector
http://sonatype.github.com/sonatype-aether/apidocs/overview-summary.html 

## Incomplete, seemingly often wrong
https://docs.sonatype.org/display/AETHER/Home 

20 21 22 23
Note that this is not an implementation of Maven per se, but the use
of the Maven Aether connector infrastructure.  Among other things, this means
that the Maven specific "~/.m2/settings.xml" file is NOT parsed for settings.

24
|#
25

26 27
;;; N.b. evaluated *after* we load the ABCL specific modifications of
;;;      ASDF in abcl-asdf.lisp
28

29 30 31 32 33
(in-package :abcl-asdf)

(require :abcl-contrib)
(require :jss)

34 35
#| 
Test:
36 37 38 39 40
(abcl-asdf:resolve "org.slf4j:slf4j-api:1.6.1")

(abcl-asdf:resolve "org.apache.maven:maven-aether-provider:3.0.4")

(abcl-asdf:resolve "com.google.gwt:gwt-user")
41 42 43

|#

44 45 46
(defparameter *maven-verbose* t
  "Stream to send output from the Maven Aether subsystem to, or NIL to muffle output")

47 48 49 50
(defparameter *mavens* 
  (if (find :windows *features*)
      '("mvn.bat" "mvn3.bat")
      '("/opt/local/bin/mvn3" "mvn3" "mvn"))
51
  "Locations to search for the Maven executable.")
52 53

(defun find-mvn () 
54 55
  "Attempt to find a suitable Maven ('mvn') executable on the hosting operating system.

56 57
Returns the path of the Maven executable or nil if none are found.

58 59
Returns the version of Maven found as the second value.

60
Emits warnings if not able to find a suitable executable."
61

62 63
  (let ((m2-home (ext:getenv "M2_HOME"))
        (m2 (ext:getenv "M2"))
64
        (mvn-executable (if (find :unix *features*)
65 66
                            "mvn"
                            "mvn.bat")))
67 68 69 70 71 72 73
    (when (and m2-home (probe-file m2-home))
      (let* ((m2-home (truename m2-home))
             (mvn-path (merge-pathnames 
                        (format nil "bin/~A" mvn-executable)
                        m2-home))
             (mvn (truename mvn-path)))
        (if mvn
74 75
            (values (return-from find-mvn mvn)
                    (ensure-mvn-version))
76 77 78 79 80 81 82
            (warn "M2_HOME was set to '~A' in the process environment but '~A' doesn't exist." 
                  m2-home mvn-path))))
    (when (and m2 (probe-file m2))
      (let* ((m2 (truename m2))
             (mvn-path (merge-pathnames mvn-executable m2))
             (mvn (truename mvn-path)))
        (if mvn
83 84
            (values (return-from find-mvn mvn)
                    (ensure-mvn-version))
85 86 87 88 89 90 91 92 93
            (warn "M2 was set to '~A' in the process environment but '~A' doesn't exist." 
                  m2 mvn-path))))
    (let* ((which-cmd 
            (if (find :unix *features*)
                "which" 
                ;; Starting with Windows Server 2003
                "where.exe"))
           (which-cmd-p 
            (handler-case 
94
                (sys:run-program which-cmd nil)
95 96 97 98 99
              (t () nil))))
      (when which-cmd-p
        (dolist (mvn-path *mavens*)
          (let ((mvn 
                 (handler-case 
100 101
                     (truename (read-line (sys:process-output 
                                           (sys:run-program 
102 103 104 105 106 107 108
                                            which-cmd `(,mvn-path))))) 
                   (end-of-file () nil)
                   (t (e) 
                     (format *maven-verbose* 
                             "~&Failed to find Maven executable '~A' in PATH because~&~A" 
                             mvn-path e)))))
            (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 114 115 116 117 118 119 120
  (unless (find-mvn)
    (warn "Failed to find Maven executable to determine Aether library location."))
  (some 
   (lambda (d)
     (when (directory (merge-pathnames "maven-core-*.jar" d))
       (truename d)))
   (list (make-pathname :defaults (merge-pathnames "../lib/" (find-mvn))
                        :name nil :type nil)
121 122 123
         ;; library location for homebrew maven package on OS X
         (make-pathname :defaults (merge-pathnames "../libexec/lib/" (find-mvn))
                        :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 128
(defparameter *mvn-libs-directory*
  nil
129 130
  "Location of 'maven-core-3.<m>.<p>.jar', 'maven-embedder-3.<m>.<p>.jar' etc.")

131
(defun mvn-version ()
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
  "Return the Maven version used by the Aether connector located by
  FIND-MVN as a list of (MAJOR MINOR PATHLEVEL) integers.

Signals a simple-error with additional information if this attempt fails."
  (handler-case 
      (let* ((process (sys:run-program (truename (find-mvn)) '("-version")))
             (output (sys:process-output process))
             (pattern (#"compile"
                       'regex.Pattern
                       "Apache Maven ([0-9]+)\\.([0-9]+)\\.([0-9]+)"))
             lines)
        (do ((line (read-line output nil :eof) 
                   (read-line output nil :eof)))
            ((or (not line) (eq line :eof)) nil)
          (push line lines)
          (let ((matcher (#"matcher" pattern line)))
            (when (#"find" matcher)
              (return-from mvn-version
150 151 152
                (mapcar #'parse-integer 
                        `(,(#"group" matcher 1) 
                           ,(#"group" matcher 2) 
153 154 155 156 157 158 159 160 161 162 163
                           ,(#"group" matcher 3)))))))
        (when lines 
          (signal "No parseable Maven version found in ~{~&  ~A~}" (nreverse lines)))
        (let ((error (sys:process-error process)))
          (do ((line (read-line error nil :eof) 
                     (read-line error nil :eof)))
              ((or (not line) (eq line :eof)) nil)
            (push line lines)
            (signal "Invocation of Maven returned the error ~{~&  ~A~}" (nreverse lines)))))
    (t (e) 
      (error "Failed to determine Maven version: ~A." e))))
164 165 166

(defun ensure-mvn-version ()
  "Return t if Maven version is 3.0.3 or greater."
167 168 169 170
  (let* ((version (mvn-version))
         (major (first version))
         (minor (second version))
         (patch (third version)))
171 172 173 174 175 176 177 178
    (values
     (or 
      (and (>= major 3)
           (>= minor 1))
      (and (>= major 3)
           (>= minor 0)
           (>= patch 3)))
     (list major minor patch))))
179 180 181

(defparameter *init* nil)

182
(defun init (&optional &key (force nil))
183
  "Run the initialization strategy to bootstrap a Maven dependency node.
184 185 186 187

Set *MVN-LIBS-DIRECTORY* to an explicit value before running this
function in order to bypass the dynamic introspection of the location
of the mvn executable with an explicit value."
188 189 190
  (when force
    (setf *session* nil
          *repository-system* nil))
191 192
  (unless (or force *mvn-libs-directory*)
    (setf *mvn-libs-directory* (find-mvn-libs)))
193 194
  (unless (and *mvn-libs-directory*
               (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 201
(defun find-http-wagon ()
  "Find an implementation of the object that provides access to http and https resources.
202

203 204 205 206 207 208 209 210
Supposedly configurable with the java.net.protocols (c.f. reference
maso2000 in the Manual.)"
  (handler-case 
      ;; maven-3.0.4
      (java:jnew "org.apache.maven.wagon.providers.http.HttpWagon") 
    (error () 
      ;; maven-3.0.3 reported as not working with all needed functionality
      (java:jnew  "org.apache.maven.wagon.providers.http.LightweightHttpWagon"))))
211

212
(defun make-wagon-provider ()
213 214 215
  "Returns an implementation of the org.sonatype.aether.connector.wagon.WagonProvider contract.

The implementation is specified as Lisp closures.  Currently, it only
216 217
specializes the lookup() method if passed an 'http' or an 'https' role
hint."
218 219
  (unless *init* (init))
  (java:jinterface-implementation 
220 221
   (#"getName" 
    (or
222 223
     (ignore-errors  ;; Maven 3.2.5+
       (jss:find-java-class 'aether.transport.wagon.WagonProvider))
224 225 226
     (ignore-errors  ;; Maven 3.1.0+
       (jss:find-java-class 'aether.connector.wagon.WagonProvider))
     (ignore-errors  ;; Maven 3.0.x
227
       (jss:find-java-class 'org.sonatype.aether.connector.wagon.WagonProvider))))
228 229
   "lookup"
   (lambda (role-hint)
230 231
     (cond 
       ((find role-hint '("http" "https") :test #'string-equal)
232
        (find-http-wagon))
233 234 235 236
       (t
        (progn 
          (format *maven-verbose* 
                  "~&WagonProvider stub passed '~A' as a hint it couldn't satisfy.~%" role-hint)
237
          java:+null+))))
238 239 240 241
   "release"
   (lambda (wagon)
     (declare (ignore wagon)))))

242
(defun find-service-locator ()
243 244 245 246
  (or 
   (ignore-errors 
     (#"newServiceLocator" 'org.apache.maven.repository.internal.MavenRepositorySystemUtils)) ;; maven-3.1.0
   (ignore-errors
247
     (java:jnew "org.apache.maven.repository.internal.MavenServiceLocator")) ;; maven-3.0.4
248 249 250 251
   (ignore-errors
     (java:jnew "org.apache.maven.repository.internal.DefaultServiceLocator"))
   (ignore-errors  ;; maven-3.1.0 using org.eclipse.aether...
     (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 260
          (ignore-errors ;; Maven-3.3.x
            (jss:find-java-class 'connector.transport.TransporterFactory))
261 262
          (ignore-errors ;; Maven-3.2.5
            (jss:find-java-class 'org.eclipse.aether.transport.wagon.WagonProvider))
263 264
          (ignore-errors  ;; Maven-3.1.x 
            (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 269 270
         (or 
          (ignore-errors 
            (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 279 280
            (jss:find-java-class 'org.eclipse.aether.spi.connector.RepositoryConnectorFactory))
          (ignore-errors
            (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 285 286
         (or
          (ignore-errors 
            (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 293 294 295 296 297 298 299 300 301 302

    (if (equal wagon-provider-class (ignore-errors (jss:find-java-class 'TransporterFactory)))
        ;;; Maven-3.3.3
        (let ((wagon-transporter-factory (jss:new 'WagonTransporterFactory)))
          (#"setWagonProvider" wagon-transporter-factory (make-wagon-provider))
          (#"setServices" locator
                          wagon-provider-class
                          (java:jarray-from-list (list wagon-transporter-factory))))
        (#"setServices" locator
                        wagon-provider-class
                        (java:jarray-from-list
                         (list (make-wagon-provider)))))
303 304 305 306 307 308
    (#"addService" locator
                   repository-connector-factory-class
                   wagon-repository-connector-factory-class)
    (values (#"getService" locator
                           repository-system-class)
            locator)))
309

310
(defun make-session (repository-system)
311 312
  "Construct a new aether.RepositorySystemSession from the specified REPOSITORY-SYSTEM."
  (let ((session
313 314 315
         (or 
          (ignore-errors (#"newSession" 'org.apache.maven.repository.internal.MavenRepositorySystemUtils))
          (ignore-errors (java:jnew (jss:find-java-class "MavenRepositorySystemSession")))))
316 317
        (local-repository 
         (java:jnew (jss:find-java-class "LocalRepository")
318 319
                    (namestring (merge-pathnames ".m2/repository/"
                                                 (user-homedir-pathname))))))
320 321
    (#"setLocalRepositoryManager" 
     session
322 323
     (or 
      (ignore-errors      ;; maven-3.1.0
324 325
        (#"newLocalRepositoryManager" 
         repository-system session local-repository))
326
      (ignore-errors 
327 328
        (#"newLocalRepositoryManager" 
         repository-system local-repository))))))
329

330 331 332 333
(defparameter *maven-http-proxy* nil
  "A string containing the URI of an http proxy for Maven to use.")

(defun make-proxy ()
334
  "Return an aether.repository.Proxy instance initialized from *MAVEN-HTTP-PROXY*."
335 336 337 338 339 340 341 342 343 344 345 346 347
  (unless *maven-http-proxy*
    (warn "No proxy specified in *MAVEN-HTTP-PROXY*")
    (return-from make-proxy nil))
  (let* ((p (pathname *maven-http-proxy*))
         (scheme (sys::url-pathname-scheme p))
         (authority (sys::url-pathname-authority p))
         (host (if (search ":" authority)
                   (subseq authority 0 (search ":" authority))
                   authority))
         (port (when (search ":" authority)
                 (parse-integer (subseq authority (1+ (search ":" authority))))))
         ;; TODO allow specification of authentication
         (authentication java:+null+))
348 349 350
    (or 
     (ignore-errors
       (jss:new 'org.eclipse.aether.repository.Proxy
351
                scheme host port authentication))
352 353
     (ignore-errors
       (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 359
(defun ensure-repository-system (&key (force nil))
  (when (or force (not *repository-system*))
360 361 362 363 364
    (setf *repository-system* (make-repository-system)))
  *repository-system*)

(defparameter *session* nil
  "Reference to the Maven RepositorySystemSession")
365
(defun ensure-session (&key (force nil))
366 367 368
  "Ensure that the RepositorySystemSession has been created.

If *MAVEN-HTTP-PROXY* is non-nil, parse its value as the http proxy."
369 370
  (when (or force (not *session*))
    (ensure-repository-system :force force)
371
    (setf *session* (make-session *repository-system*))
372 373 374 375 376 377 378
    (#"setRepositoryListener" *session* (make-repository-listener))
    (when *maven-http-proxy*
      (let ((proxy (make-proxy)))
        (#"add" (#"getProxySelector" *session*)
                proxy 
                ;; A string specifying non proxy hosts, or null
                java:+null+))))
379
  *session*)
380

381 382
(defun make-artifact (artifact-string)
  "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 390 391 392 393 394 395 396

(defun make-artifact-request () 
  "Construct a new aether.resolution.ArtifactRequest."
  (or 
   (ignore-errors
     (java:jnew (jss:find-java-class 'aether.resolution.ArtifactRequest)))
   (ignore-errors
     (java:jnew "org.sonatype.aether.resolution.ArtifactRequest"))))

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 401 402 403 404

Declared dependencies are not attempted to be located.

If unspecified, the string \"LATEST\" will be used for the VERSION.

405
Returns the Maven specific string for the artifact "
406 407
  (unless versionp
    (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 418 419
    (#"toString" (#"getFile" 
                  (#"getArtifact" (#"resolveArtifact" (ensure-repository-system) 
                                                      (ensure-session) artifact-request))))))
420

421
(defun make-remote-repository (id type url) 
422 423 424 425 426
  (or 
   (ignore-errors 
     (#"build" (jss:new "org.eclipse.aether.repository.RemoteRepository$Builder" id type url)))
   (ignore-errors
     (jss:new 'aether.repository.RemoteRepository id type url))))
427

428
(defparameter *default-repository* 
429
  "http://repo1.maven.org/maven2/")
430 431 432 433

(defun add-repository (repository)
  (ensure-remote-repository :repository repository))

434
(defparameter *maven-remote-repository*  nil
435
  "The remote repository used by the Maven Aether embedder.")
436 437 438
(defun ensure-remote-repository (&key 
                                   (force nil)
                                   (repository *default-repository* repository-p))
439
  (unless *init* (init))
440 441 442
  (when (or force  
            repository-p 
            (not *maven-remote-repository*))
443
    (let ((r (make-remote-repository "central" "default" repository)))
444 445 446 447
      (when *maven-http-proxy*
        (#"setProxy" r (make-proxy)))
      (setf *maven-remote-repository* r)))
  *maven-remote-repository*)
448

449

450
(defun resolve-dependencies (group-id artifact-id 
451
                             &key
452 453
                               (version "LATEST" versionp)
                               (repository *maven-remote-repository* repository-p))
454
  "Dynamically resolve Maven dependencies for item with GROUP-ID and ARTIFACT-ID 
455
optionally with a VERSION and a REPOSITORY.  
456 457 458 459 460 461 462

All recursive dependencies will be visited before resolution is successful.

If unspecified, the string \"LATEST\" will be used for the VERSION.

Returns a string containing the necessary jvm classpath entries packed
in Java CLASSPATH representation."
463
  (unless *init* (init))
464 465
  (unless versionp
    (warn "Using LATEST for unspecified version."))
466
  (let* ((coords 
467 468 469
          (format nil "~A:~A:~A" group-id artifact-id (if versionp version "LATEST")))
         (artifact 
          (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 474
         (collect-request (java:jnew (jss:find-java-class "CollectRequest"))))
    (#"setRoot" collect-request dependency)
475 476 477 478 479 480
     ;; Don't call addRepository if we explicitly specify a NIL repository
    (unless (and repository-p (not repository))
      (#"addRepository" collect-request 
                        (if repository-p
                            (ensure-remote-repository :repository repository)
                            (ensure-remote-repository))))
481
    (let* ((node 
482
            (#"getRoot" (#"collectDependencies" (ensure-repository-system) (ensure-session) collect-request)))
483 484 485
           (dependency-request
            ;;; pre Maven-3.3.x
            #+nil
486
            (java:jnew (jss:find-java-class "DependencyRequest")
487 488
                       node java:+null+)
            (jss:new 'DependencyRequest))
489 490
           (nlg 
            (java:jnew (jss:find-java-class "PreorderNodeListGenerator"))))
491
      (#"setRoot" dependency-request node)
492
      (#"resolveDependencies" (ensure-repository-system) (ensure-session) dependency-request)
493 494
      (#"accept" node nlg)
      (#"getClassPath" nlg))))
495

496
(defun make-repository-listener ()
497 498 499
  (flet ((log (e) 
           (format *maven-verbose* "~&~A~%" (#"toString" e))))
    (java:jinterface-implementation 
500
     (#"getName" (jss:find-java-class 'aether.RepositoryListener))
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
     "artifactDeployed" 
     #'log
     "artifactDeploying" 
     #'log
     "artifactDescriptorInvalid" 
     #'log
     "artifactDescriptorMissing" 
     #'log
     "artifactDownloaded" 
     #'log
     "artifactDownloading" 
     #'log
     "artifactInstalled" 
     #'log
     "artifactInstalling" 
     #'log
     "artifactResolved" 
     #'log
     "artifactResolving" 
     #'log
     "metadataDeployed" 
     #'log
     "metadataDeploying" 
     #'log
     "metadataDownloaded" 
     #'log
     "metadataDownloading" 
     #'log
     "metadataInstalled"
     #'log
     "metadataInstalling" 
     #'log
     "metadataInvalid" 
     #'log
     "metadataResolved" 
     #'log
     "metadataResolving"
     #'log)))
539

540

541
(defmethod resolve ((string string))
542 543 544 545 546 547 548 549
  "Resolve a colon separated GROUP-ID:ARTIFACT-ID[:VERSION] reference to a Maven artifact.

Examples of artifact references: \"log4j:log4j:1.2.14\" for
'log4j-1.2.14.jar'.  Resolving \"log4j:log4j\" would return the latest
version of the artifact known to the distributed Maven pom.xml graph.

Returns a string containing the necessary classpath entries for this
artifact and all of its transitive dependencies."
550 551
  (let ((result (split-string string ":")))
    (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 559 560
      ((= (length result) 2)
       (resolve-dependencies
        (first result) (second result)))
561
      (t 
562 563 564 565 566 567 568 569
       (destructuring-bind (group-id artifact-id &optional version repository)
           (split-string string "/")
         (setf result 
               (apply #'resolve-dependencies group-id artifact-id
                      (append (when version
                                `(:version ,version))
                              (when repository
                                `(:repository ,repository))))))))))
570

571 572
;;; Currently the last file listed in ASDF
(provide 'abcl-asdf)