maven-embedder.lisp 19.7 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
(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 42 43
(defparameter *maven-verbose* t
  "Stream to send output from the Maven Aether subsystem to, or NIL to muffle output")

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

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

53 54
Returns the path of the Maven executable or nil if none are found.

55 56
Returns the version of Maven found as the second value.

57
Emits warnings if not able to find a suitable executable."
58

59 60
  (let ((m2-home (ext:getenv "M2_HOME"))
        (m2 (ext:getenv "M2"))
61
        (mvn-executable (if (find :unix *features*)
62
                               "mvn"
63 64 65 66 67 68 69 70
                               "mvn.bat")))
    (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
71 72
            (values (return-from find-mvn mvn)
                    (ensure-mvn-version))
73 74 75 76 77 78 79
            (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
80 81
            (values (return-from find-mvn mvn)
                    (ensure-mvn-version))
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
            (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 
                (sys::run-program which-cmd nil)
              (t () nil))))
      (when which-cmd-p
        (dolist (mvn-path *mavens*)
          (let ((mvn 
                 (handler-case 
                     (truename (read-line (sys::process-output 
                                           (sys::run-program 
                                            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
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 111 112 113 114 115 116 117
  (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)
118
         #p"/usr/local/share/java/maven3/lib/" ;; FreeBSD ports
119 120
         #p"/usr/local/maven/lib/"))) ;; OpenBSD location suggested by Timo Myyrä
                                  
121 122
(defparameter *mvn-libs-directory*
  nil
123 124
  "Location of 'maven-core-3.<m>.<p>.jar', 'maven-embedder-3.<m>.<p>.jar' etc.")

125
(defun mvn-version ()
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
  "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
144 145 146
                (mapcar #'parse-integer 
                        `(,(#"group" matcher 1) 
                           ,(#"group" matcher 2) 
147 148 149 150 151 152 153 154 155 156 157
                           ,(#"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))))
158 159 160

(defun ensure-mvn-version ()
  "Return t if Maven version is 3.0.3 or greater."
161 162 163 164
  (let* ((version (mvn-version))
         (major (first version))
         (minor (second version))
         (patch (third version)))
165 166 167 168 169 170 171 172
    (values
     (or 
      (and (>= major 3)
           (>= minor 1))
      (and (>= major 3)
           (>= minor 0)
           (>= patch 3)))
     (list major minor patch))))
173 174 175

(defparameter *init* nil)

176
(defun init (&optional &key (force nil))
177 178 179 180 181
 "Run the initialization strategy to bootstrap a Maven dependency node.

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."
182 183 184 185
 (unless (or force *mvn-libs-directory*)
   (setf *mvn-libs-directory* (find-mvn-libs)))
  (unless (and *mvn-libs-directory*
               (probe-file *mvn-libs-directory*))
186
   (error "Please obtain and install maven-3.0.4 or lates 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."))
187
 (unless (ensure-mvn-version)
188
   (error "We need maven-3.0.4 or later."))  (add-directory-jars-to-class-path *mvn-libs-directory* nil)
189
  (setf *init* t))
190

191 192
(defun find-http-wagon ()
  "Find an implementation of the object that provides access to http and https resources.
193

194 195 196 197 198 199 200 201
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"))))
202

203
(defun make-wagon-provider ()
204 205 206
  "Returns an implementation of the org.sonatype.aether.connector.wagon.WagonProvider contract.

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

231
(defun find-service-locator ()
232 233 234 235 236 237 238 239 240
  (or 
   (ignore-errors 
     (#"newServiceLocator" 'org.apache.maven.repository.internal.MavenRepositorySystemUtils)) ;; maven-3.1.0
   (ignore-errors
      (java:jnew "org.apache.maven.repository.internal.MavenServiceLocator")) ;; maven-3.0.4
   (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))))
241

242
(defun make-repository-system ()
243
  (unless *init* (init))
244
  (let ((locator 
245
         (find-service-locator))
246
        (wagon-provider-class 
247 248 249 250 251
	 (or 
	  (ignore-errors 
	    (java:jclass "org.sonatype.aether.connector.wagon.WagonProvider"))
	  (ignore-errors  ;; Maven-3.1.x 
	    (jss:find-java-class 'aether.connector.wagon.WagonProvider))))
252
        (wagon-repository-connector-factory-class
253 254 255 256 257
	 (or 
	  (ignore-errors 
	    (java:jclass "org.sonatype.aether.connector.wagon.WagonRepositoryConnectorFactory"))
	  (ignore-errors 
	    (jss:find-java-class 'aether.connector.wagon.WagonRepositoryConnectorFactory))))
258
        (repository-connector-factory-class 
259 260 261 262 263
	 (or 
	  (ignore-errors 
	    (java:jclass "org.sonatype.aether.spi.connector.RepositoryConnectorFactory"))
	  (ignore-errors
	    (jss:find-java-class 'aether.spi.connector.RepositoryConnectorFactory))))
264
        (repository-system-class
265 266 267 268 269
	 (or
	  (ignore-errors
	    (java:jclass "org.sonatype.aether.RepositorySystem"))
	  (ignore-errors 
	    (jss:find-java-class 'aether.RepositorySystem)))))
270 271
    (#"setServices" locator
                    wagon-provider-class
272 273 274 275 276 277 278 279
                   (java:jarray-from-list
                    (list (make-wagon-provider))))
    (#"addService" locator
                   repository-connector-factory-class
                   wagon-repository-connector-factory-class)
    (values (#"getService" locator
                           repository-system-class)
            locator)))
280
        
281
(defun make-session (repository-system)
282 283 284 285 286
  "Construct a new aether.RepositorySystemSession from the specified REPOSITORY-SYSTEM."
  (let ((session
	 (or 
	  (ignore-errors (#"newSession" 'org.apache.maven.repository.internal.MavenRepositorySystemUtils))
	  (ignore-errors (java:jnew (jss:find-java-class "MavenRepositorySystemSession")))))
287 288 289 290 291 292
        (local-repository 
         (java:jnew (jss:find-java-class "LocalRepository")
                  (namestring (merge-pathnames ".m2/repository/"
                                               (user-homedir-pathname))))))
    (#"setLocalRepositoryManager" 
     session
293 294 295 296 297 298 299
     (or 
      (ignore-errors      ;; maven-3.1.0
	(#"newLocalRepositoryManager" 
	 repository-system session local-repository))
      (ignore-errors 
	(#"newLocalRepositoryManager" 
	 repository-system local-repository))))))
300

301 302 303 304
(defparameter *maven-http-proxy* nil
  "A string containing the URI of an http proxy for Maven to use.")

(defun make-proxy ()
305
  "Return an aether.repository.Proxy instance initialized from *MAVEN-HTTP-PROXY*."
306 307 308 309 310 311 312 313 314 315 316 317 318
  (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+))
319 320 321 322 323 324 325
    (or 
     (ignore-errors
       (jss:new 'org.eclipse.aether.repository.Proxy
		scheme host port authentication))
     (ignore-errors
       (jss:new 'org.sonatype.aether.repository.Proxy
		scheme host port authentication)))))
326

327
(defparameter *repository-system*  nil
328
  "The aether.RepositorySystem used by the Maeven Aether connector.")
329 330
(defun ensure-repository-system (&key (force nil))
  (when (or force (not *repository-system*))
331 332 333 334 335
    (setf *repository-system* (make-repository-system)))
  *repository-system*)

(defparameter *session* nil
  "Reference to the Maven RepositorySystemSession")
336
(defun ensure-session (&key (force nil))
337 338 339
  "Ensure that the RepositorySystemSession has been created.

If *MAVEN-HTTP-PROXY* is non-nil, parse its value as the http proxy."
340 341
  (when (or force (not *session*))
    (ensure-repository-system :force force)
342
    (setf *session* (make-session *repository-system*))
343 344 345 346 347 348 349 350 351
    (#"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+))))
    *session*)

352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
(defun make-artifact (artifact-string)
  "Return an instance of aether.artifact.DefaultArtifact initialized from ARTIFACT-STRING." 
  (or 
   (ignore-errors
     (jss:new "org.sonatype.aether.util.artifact.DefaultArtifact" artifact-string))
   (ignore-errors
     (jss:new 'aether.artifact.DefaultArtifact artifact-string))))

(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"))))

368
;;; TODO change this to work on artifact strings like log4j:log4j:jar:1.2.16
369
(defun resolve-artifact (group-id artifact-id &optional (version "LATEST" versionp))
370
  "Resolve artifact to location on the local filesystem.
371 372 373 374 375

Declared dependencies are not attempted to be located.

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

376
Returns the Maven specific string for the artifact "
377 378
  (unless versionp
    (warn "Using LATEST for unspecified version."))
379
  (unless *init* (init))
380 381
  (let* ((artifact-string 
	  (format nil "~A:~A:~A" group-id artifact-id version))
382
         (artifact 
383
	  (make-artifact artifact-string))
384
         (artifact-request 
385
	  (make-artifact-request)))
386
    (#"setArtifact" artifact-request artifact)
387
    (#"addRepository" artifact-request (ensure-remote-repository))
388 389 390
    (#"toString" (#"getFile" 
                  (#"getArtifact" (#"resolveArtifact" (ensure-repository-system) 
                                                      (ensure-session) artifact-request))))))
391

392
(defun make-remote-repository (id type url) 
393 394 395 396 397
  (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))))
398

399 400 401 402 403 404
(defparameter *default-repository* 
   "http://repo1.maven.org/maven2/")

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

405 406
(defparameter *maven-remote-repository*  nil
    "The remote repository used by the Maven Aether embedder.")
407 408 409
(defun ensure-remote-repository (&key 
                                   (force nil)
                                   (repository *default-repository* repository-p))
410
  (unless *init* (init))
411 412 413
  (when (or force  
            repository-p 
            (not *maven-remote-repository*))
414
    (let ((r (make-remote-repository "central" "default" repository)))
415 416 417 418
      (when *maven-http-proxy*
        (#"setProxy" r (make-proxy)))
      (setf *maven-remote-repository* r)))
  *maven-remote-repository*)
419

420

421 422 423 424 425 426
(defun resolve-dependencies (group-id artifact-id 
                             &optional  ;;; XXX Uggh.  Move to keywords when we get the moxie.
                             (version "LATEST" versionp)
                             (repository *maven-remote-repository* repository-p))
  "Dynamically resolve Maven dependencies for item with GROUP-ID and ARTIFACT-ID 
optionally with a VERSION and a REPOSITORY.  Users of the function are advised 
427 428 429 430 431 432 433

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."
434
  (unless *init* (init))
435 436
  (unless versionp
    (warn "Using LATEST for unspecified version."))
437 438 439 440
  (let* ((coords 
	  (format nil "~A:~A:~A" group-id artifact-id (if versionp version "LATEST")))
	 (artifact 
	  (make-artifact coords))
441
         (dependency 
442 443
          (java:jnew (jss:find-java-class 'aether.graph.Dependency)
		     artifact (java:jfield (jss:find-java-class "JavaScopes") "RUNTIME")))
444 445
         (collect-request (java:jnew (jss:find-java-class "CollectRequest"))))
    (#"setRoot" collect-request dependency)
446 447 448 449
    (#"addRepository" collect-request 
                      (if repository-p
                          (ensure-remote-repository :repository repository)
                          (ensure-remote-repository)))
450
    (let* ((node 
451
            (#"getRoot" (#"collectDependencies" (ensure-repository-system) (ensure-session) collect-request)))
452 453
           (dependency-request 
            (java:jnew (jss:find-java-class "DependencyRequest")
454
                       node java:+null+))
455 456
           (nlg 
            (java:jnew (jss:find-java-class "PreorderNodeListGenerator"))))
457
      (#"resolveDependencies" (ensure-repository-system) (ensure-session) dependency-request)
458 459
      (#"accept" node nlg)
      (#"getClassPath" nlg))))
460

461
(defun make-repository-listener ()
462 463 464
  (flet ((log (e) 
           (format *maven-verbose* "~&~A~%" (#"toString" e))))
    (java:jinterface-implementation 
465
     (#"getName" (jss:find-java-class 'aether.RepositoryListener))
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
     "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)))
504

505
         
506
(defmethod resolve ((string string))
507 508 509 510 511 512 513 514
  "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."
515 516
  (let ((result (split-string string ":")))
    (cond 
517
      ((= (length result) 3)
518 519
       (resolve-dependencies 
        (first result) (second result) (third result)))
520
      ((string= string "com.sun.jna:jna")
521 522
       (warn "Replacing request for no longer available com.sun.jna:jna with net.java.dev.jna:jna")
       (resolve-dependencies "net.java.dev.jna" "jna" "LATEST"))
523 524 525
      ((= (length result) 2)
       (resolve-dependencies
        (first result) (second result)))
526 527 528
      (t 
       (setf result 
             (apply #'resolve-dependencies (split-string string "/")))))))
529
  
530 531
;;; Currently the last file listed in ASDF
(provide 'abcl-asdf)