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