639fcdce72a64fdbd1a4cefbfaacd0bcf619e90c
[sbcl.git] / contrib / asdf-install / installer.lisp
1 (in-package :asdf-install)
2
3 (defvar *proxy* (posix-getenv "http_proxy"))
4 (defvar *cclan-mirror*
5   (or (posix-getenv "CCLAN_MIRROR")
6       "http://ftp.linux.org.uk/pub/lisp/cclan/"))
7
8 (defun directorify (name)
9   ;; input name may or may not have a training #\/, but we know we
10   ;; want a directory
11   (let ((path (pathname name)))
12     (if (pathname-name path)
13         (merge-pathnames
14          (make-pathname :directory `(:relative ,(pathname-name path))
15                         :name "")
16          path)
17         path)))
18
19 (defvar *sbcl-home* (directorify (posix-getenv "SBCL_HOME")))
20 (defvar *dot-sbcl*
21   (merge-pathnames (make-pathname :directory '(:relative ".sbcl"))
22                    (user-homedir-pathname)))
23
24 (defparameter *trusted-uids* nil)
25
26
27 (defun verify-gpg-signatures-p (url)
28   (labels ((prefixp (prefix string)
29              (let ((m (mismatch prefix string)))
30                (or (not m) (>= m (length prefix))))))
31     (case *verify-gpg-signatures*
32       (nil nil)
33       (:unknown-locations
34        (notany
35         (lambda (x) (prefixp x url))
36         (cons *cclan-mirror* *safe-url-prefixes*)))
37       (t t))))
38           
39 (defvar *locations*
40   `((,(merge-pathnames "site/" *sbcl-home*)
41      ,(merge-pathnames "site-systems/" *sbcl-home*)
42      "System-wide install")
43     (,(merge-pathnames "site/" *dot-sbcl*)
44      ,(merge-pathnames "systems/" *dot-sbcl*)
45      "Personal installation")))
46
47 (let* ((*package* (find-package :asdf-install-customize))
48        (file (probe-file (merge-pathnames
49                           (make-pathname :name ".asdf-install")
50                           (user-homedir-pathname)))))
51   (when file (load file)))
52
53 (define-condition download-error (error)
54   ((url :initarg :url :reader download-url)
55    (response :initarg :response :reader download-response))
56   (:report (lambda (c s)
57              (format s "Server responded ~A for GET ~A"
58                      (download-response c) (download-url c)))))
59
60 (define-condition signature-error (error)
61   ((cause :initarg :cause :reader signature-error-cause))
62   (:report (lambda (c s)
63              (format s "Cannot verify package signature:  ~A"
64                      (signature-error-cause c)))))
65
66 (define-condition gpg-error (error)
67   ((message :initarg :message :reader gpg-error-message))
68   (:report (lambda (c s)
69              (format t "GPG failed with error status:~%~S"
70                      (gpg-error-message c)))))
71
72 (define-condition no-signature (gpg-error) ())
73 (define-condition key-not-found (gpg-error)
74   ((key-id :initarg :key-id :reader key-id))
75   (:report (lambda (c s)
76              (format s "No key found for key id 0x~A.  Try some command like ~%  gpg  --recv-keys 0x~A"
77                      (key-id c) (key-id c)))))
78
79 (define-condition key-not-trusted (gpg-error)
80   ((key-id :initarg :key-id :reader key-id)
81    (key-user-name :initarg :key-user-name :reader key-user-name))
82   (:report (lambda (c s)
83              (format s "GPG warns that the key id 0x~A (~A) is not fully trusted"
84                      (key-id c) (key-user-name c)))))
85 (define-condition author-not-trusted (gpg-error)
86   ((key-id :initarg :key-id :reader key-id)
87    (key-user-name :initarg :key-user-name :reader key-user-name))
88   (:report (lambda (c s)
89              (format s "~A (key id ~A) is not on your package supplier list"
90                      (key-user-name c) (key-id c)))))
91   
92 (defun url-host (url)
93   (assert (string-equal url "http://" :end1 7))
94   (let* ((port-start (position #\: url :start 7))
95          (host-end (min (or (position #\/ url :start 7) (length url))
96                         (or port-start (length url)))))
97     (subseq url 7 host-end)))
98
99 (defun url-port (url)
100   (assert (string-equal url "http://" :end1 7))
101   (let ((port-start (position #\: url :start 7)))
102     (if port-start (parse-integer url :start port-start :junk-allowed t) 80)))
103
104 (defun url-connection (url)
105   (let ((s (make-instance 'inet-socket :type :stream :protocol :tcp))
106         (host (url-host url))
107         (port (url-port url)))
108     (declare (ignore port))
109     (socket-connect
110      s (car (host-ent-addresses (get-host-by-name (url-host (or *proxy* url)))))
111      (url-port (or  *proxy* url)))
112     (let ((stream (socket-make-stream s :input t :output t :buffering :full)))
113       ;; we are exceedingly unportable about proper line-endings here.
114       ;; Anyone wishing to run this under non-SBCL should take especial care
115       (format stream "GET ~A HTTP/1.0~%Host: ~A~%Cookie: CCLAN-SITE=~A~%~%"
116               url host *cclan-mirror*)
117       (force-output stream)
118       (list
119        (let* ((l (read-line stream))
120               (space (position #\Space l)))
121          (parse-integer l :start (1+ space) :junk-allowed t))
122        (loop for line = (read-line stream nil nil)
123              until (or (null line) (eql (elt line 0) (code-char 13)))
124              collect
125              (let ((colon (position #\: line)))
126                (cons (intern (string-upcase (subseq line 0 colon)) :keyword)
127                      (string-trim (list #\Space (code-char 13))
128                                   (subseq line (1+ colon))))))
129        stream))))
130
131 (defun download-files-for-package (package-name-or-url file-name)
132   (let ((url
133          (if (= (mismatch package-name-or-url "http://") 7)
134              package-name-or-url
135              (format nil "http://www.cliki.net/~A?download"
136                      package-name-or-url))))
137     (destructuring-bind (response headers stream)
138         (block got
139           (loop
140            (destructuring-bind (response headers stream) (url-connection url)
141              (unless (member response '(301 302))              
142                (return-from got (list response headers stream)))
143              (close stream)
144              (setf url (cdr (assoc :location headers))))))
145       (if (>= response 400)
146         (error 'download-error :url url :response response))
147       (let ((length (parse-integer
148                      (or (cdr (assoc :content-length headers)) "")
149                      :junk-allowed t)))
150         (format t "Downloading ~A bytes from ~A ..."
151                 (if length length "some unknown number of") url)
152         (force-output)
153         (with-open-file (o file-name :direction :output)
154           (if length
155               (let ((buf (make-array length
156                                      :element-type
157                                      (stream-element-type stream)  )))
158                 (read-sequence buf stream)
159                 (write-sequence buf o)) 
160               (sb-executable:copy-stream stream o))))
161       (close stream)
162       (terpri)
163       (restart-case 
164           (verify-gpg-signature/url url file-name)
165         (skip-gpg-check (&rest rest)
166           :report "Don't ckeck GPG signature for this package"
167           nil)))))
168
169 (defun read-until-eof (stream)
170   (with-output-to-string (o)
171     (sb-executable:copy-stream stream o)))
172   
173 (defun verify-gpg-signature/string (string file-name)
174   (let* ((proc
175           (sb-ext:run-program
176            "gpg"
177            (list
178             "--status-fd" "1" "--verify" "-"
179             (namestring file-name))
180            :output :stream :error :stream :search t
181            :input (make-string-input-stream string) :wait t))
182          (ret (process-exit-code proc))
183          (err (read-until-eof (process-error proc)))
184          tags)
185     (loop for l = (read-line (process-output proc) nil nil)
186           while l
187           when (> (mismatch l "[GNUPG:]") 6)
188           do (destructuring-bind (_ tag &rest data) (asdf::split l)
189                (pushnew (cons (intern tag :keyword)
190                               data) tags)))
191     ;; test for obvious key/sig problems
192     (let ((errsig (assoc :errsig tags)))
193       (and errsig (error 'key-not-found :key-id (second errsig) :gpg-err err)))
194     (let ((badsig (assoc :badsig tags)))
195       (and badsig (error 'key-not-found :key-id (second badsig) :gpg-err err)))
196     (let* ((good (assoc :goodsig tags))
197            (id (second good))
198            (name (format nil "~{~A~^ ~}" (nthcdr 2 good))))
199       ;; good signature, but perhaps not trusted
200       (unless (or (assoc :trust_ultimate tags)
201                   (assoc :trust_fully tags))
202         (cerror "Install the package anyway"
203                 'key-not-trusted
204                 :key-user-name name
205                 :key-id id :gpg-err err))
206       (loop
207        (when
208            (restart-case
209                (or (assoc id *trusted-uids* :test #'equal)
210                    (error 'author-not-trusted
211                           :key-user-name name
212                           :key-id id :gpg-err nil))
213              (add-key (&rest rest)
214                :report "Add to package supplier list"
215                (pushnew (list id name) *trusted-uids*)))
216          (return))))))
217
218
219 (defun verify-gpg-signature/url (url file-name)
220   (destructuring-bind (response headers stream)
221       (url-connection (concatenate 'string url ".asc"))
222     (unwind-protect
223          (if (= response 200)
224              (let ((data (make-string (parse-integer
225                                        (cdr (assoc :content-length headers))
226                                        :junk-allowed t))))
227                (read-sequence data stream)
228                (verify-gpg-signature/string data file-name))
229              (error 'download-error :url  (concatenate 'string url ".asc")
230                     :response response))
231       (close stream))))
232
233 (defun where ()  
234   (format t "Install where?~%")
235   (loop for (source system name) in *locations*
236         for i from 1
237         do (format t "~A) ~A: ~%   System in ~A~%   Files in ~A ~%"
238                    i name system source))
239   (format t " --> ") (force-output)
240   (let ((response (read)))
241     (when (> response 0)
242       (elt *locations* (1- response)))))
243
244 (defun install-package (source system packagename)
245   "Returns a list of asdf system names for installed asdf systems"
246   (ensure-directories-exist source )
247     (ensure-directories-exist system )
248   (let* ((tar
249           (with-output-to-string (o)
250             (or
251              (sb-ext:run-program "tar"
252                                  (list "-C" (namestring source)
253                                        "-xzvf" (namestring packagename))
254                                  :output o
255                                  :search t
256                                  :wait t)
257              (error "can't untar"))))
258          (dummy (princ tar))
259          (pos-slash (position #\/ tar))
260          (*default-pathname-defaults*
261           (merge-pathnames
262            (make-pathname :directory
263                           `(:relative ,(subseq tar 0 pos-slash)))
264            source)))
265     (declare (ignore dummy))
266     (loop for asd in (directory
267                       (make-pathname :name :wild :type "asd"))
268           do (let ((target (merge-pathnames
269                             (make-pathname :name (pathname-name asd)
270                                            :type (pathname-type asd))
271                             system)))
272                (when (probe-file target)
273                  (sb-posix:unlink target))
274                (sb-posix:symlink asd target))
275           collect (pathname-name asd))))
276
277 (defvar *temporary-files*)
278 (defun temp-file-name (p)
279   (let* ((pos-slash (position #\/ p :from-end t))
280          (pos-dot (position #\. p :start (or pos-slash 0))))
281     (merge-pathnames
282      (make-pathname
283       :name (subseq p (if pos-slash (1+ pos-slash) 0) pos-dot)
284       :type "asdf-install-tmp"))))
285                      
286
287 ;; this is the external entry point
288 (defun install (&rest packages)
289   (let ((*temporary-files* nil)
290         (*trusted-uids*
291          (let ((p (merge-pathnames "trusted-uids.lisp" *dot-sbcl*)))
292            (when (probe-file p)
293              (with-open-file (f p) (read f))))))
294     (unwind-protect
295          (destructuring-bind (source system name) (where)
296            (labels ((one-iter (packages)
297                       (dolist (asd
298                                 (loop for p in (mapcar 'string packages)
299                                       unless (probe-file p)
300                                       do (let ((tmp (temp-file-name p)))
301                                            (pushnew tmp *temporary-files*)
302                                            (download-files-for-package p tmp)
303                                            (setf p tmp))
304                                       end
305                                       do (format t "Installing ~A in ~A,~A~%"
306                                                  p source system)
307                                       append (install-package source system p)))
308                         (handler-bind
309                             ((asdf:missing-dependency
310                               (lambda (c) 
311                                 (format t
312                                         "Downloading package ~A, required by ~A~%"
313                                         (asdf::missing-requires c)
314                                         (asdf:component-name
315                                          (asdf::missing-required-by c)))
316                                 (one-iter (list
317                                            (symbol-name
318                                             (asdf::missing-requires c))))
319                                 (invoke-restart 'retry))))
320                           (loop
321                            (multiple-value-bind (ret restart-p)
322                                (with-simple-restart
323                                    (retry "Retry installation")
324                                  (asdf:operate 'asdf:load-op asd))
325                              (unless restart-p (return))))))))
326              (one-iter packages)))
327       (let ((p (merge-pathnames "trusted-uids.lisp" *dot-sbcl*)))
328         (with-open-file (out p :direction :output)
329           (with-standard-io-syntax
330             (prin1 *trusted-uids* out))))
331       (dolist (l *temporary-files*)
332         (when (probe-file l) (delete-file l))))))