0.8.2.23
authorDaniel Barlow <dan@telent.net>
Sun, 10 Aug 2003 19:25:02 +0000 (19:25 +0000)
committerDaniel Barlow <dan@telent.net>
Sun, 10 Aug 2003 19:25:02 +0000 (19:25 +0000)
A full and final answer[*] to all the people who keep asking
for slightly tangential software to be added to contrib: to
wit, a contrib module to automate the downloading and
installing of packages that are _not_ part of contrib.

* (require 'asdf-install)
* (asdf-install:install 'xlunit)

or
$ sbcl-asdf-install xlunit

See contrib/asdf-install/README for more details and scary
security-related muttering.

Also added a :INITIAL-FUNCTION to SB-EXECUTABLE:MAKE-EXECUTABLE
so that files don't have to be written such that the
application starts as soon as they're loaded

[*] Ha!

contrib/asdf-install/Makefile [new file with mode: 0644]
contrib/asdf-install/README [new file with mode: 0644]
contrib/asdf-install/asdf-install [new file with mode: 0755]
contrib/asdf-install/asdf-install.asd [new file with mode: 0644]
contrib/asdf-install/defpackage.lisp [new file with mode: 0644]
contrib/asdf-install/installer.lisp [new file with mode: 0644]
contrib/asdf-install/loader.lisp [new file with mode: 0644]
contrib/asdf-module.mk
contrib/sb-executable/sb-executable.lisp
install.sh
version.lisp-expr

diff --git a/contrib/asdf-install/Makefile b/contrib/asdf-install/Makefile
new file mode 100644 (file)
index 0000000..5ec18e7
--- /dev/null
@@ -0,0 +1,10 @@
+SYSTEM=asdf-install
+EXTRA_INSTALL_TARGETS=asdf-install-install
+
+include ../asdf-module.mk
+
+asdf-install-install: asdf-install
+       if test -f $(INSTALL_ROOT)/bin/sbcl-asdf-install ; then \
+        mv $(INSTALL_ROOT)/bin/sbcl-asdf-install $(INSTALL_ROOT)/bin/sbcl-asdf-install.old ; \
+        fi
+       cp asdf-install $(INSTALL_ROOT)/bin/sbcl-asdf-install
diff --git a/contrib/asdf-install/README b/contrib/asdf-install/README
new file mode 100644 (file)
index 0000000..d5928d8
--- /dev/null
@@ -0,0 +1,109 @@
+Downloads and installs an ASDF system or anything else that looks
+convincingly like one, including updating the ASDF:*CENTRAL-REGISTRY*
+symlinks for all the toplevel .asd files it contains.  Please read
+this file before use: in particular: this is an automatic tool that
+downloads and compiles stuff it finds on the 'net.  Please look at the
+SECURITY section and be sure you understand the implications
+
+
+= USAGE
+
+This can be used either from within an SBCL instance:
+
+* (require 'asdf-install)
+* (asdf-install:install 'xlunit) ; for example
+
+or standalone from the shell:
+
+$ sbcl-asdf-install xlunit
+
+Each argument may be -
+
+ - The name of a cliki page.  asdf-install visits that page and finds
+   the download location from the `:(package)' tag - usually rendered
+   as "Download ASDF package from ..."
+
+ - A URL, which is downloaded directly
+
+ - A local tar.gz file, which is installed
+
+
+= SECURITY CONCERNS: READ THIS CAREFULLY
+
+When you invoke asdf-install, you are asking SBCL to download,
+compile, and install software from some random site on the web.  Given
+that it's indirected through a page on CLiki, any malicious third party
+doesn't even need to hack the distribution server to replace the
+package with something else: he can just edit the link.  
+
+For this reason, we encourage package providers to crypto-sign their
+packages (see details at the URL in the PACKAGE CREATION section) and
+users to check the signatures.  asdf-install has three levels of
+automatic signature checking: "on", "off" and "unknown sites", which
+can be set using the configuration variables described in
+CUSTOMIZATION below.  The default is "unknown sites", which will
+expect a GPG signature on all downloads except those from
+presumed-good sites.  The current default presumed-good sites are
+CCLAN nodes, and two web sites run by SBCL maintainers: again, see
+below for customization details
+
+
+= CUSTOMIZATION
+
+If the file $HOME/.asdf-install exists, it is loaded.  This can be
+used to override the default values of exported special variables.
+Presently these are 
+
+*PROXY*         
+   defaults to $http_proxy environment variable
+*CCLAN-MIRROR*        
+   preferred/nearest CCLAN node.  See the list at 
+   http://ww.telent.net/cclan-choose-mirror
+*SBCL-HOME*
+   Set from $SBCL_HOME environment variable.  This should already be 
+   correct for whatever SBCL is running, if it's been installed correctly
+*VERIFY-GPG-SIGNATURES*
+   Verify GPG signatures for the downloaded packages?
+   NIL - no, T - yes, :UNKNOWN-LOCATIONS - only for URLs which aren't in CCLAN
+   and don't begin with one of the prefixes in *SAFE-URL-PREFIXES*
+*LOCATIONS*
+   Possible places in the filesystem to install packages into.  See default
+   value for format
+*SAFE-URL-PREFIXES* 
+   List of locations for which GPG signature checking /won't/ be done when
+   *verify-gpg-signatures* is :unknown-locations
+
+
+= PACKAGE CREATION
+
+If you want to create your own packages that can be installed using this
+loader, see the "Making your package downloadable..." section at
+<http://www.cliki.net/asdf-install> 
+
+
+= HACKERS NOTE
+
+Listen very carefully: I will say this only as often as it appears to
+be necessary to say it.  asdf-install is not a good example of how to
+write a URL parser, HTTP client, or anything else, really.
+Well-written extensible and robust URL parsers, HTTP clients, FTP
+clients, etc would definitely be nice things to have, but it would be
+nicer to have them in CCLAN where anyone can use them - after having
+downloaded them with asdf-install - than in SBCL contrib where they're
+restricted to SBCL users and can only be updated once a month via SBCL
+developers.  This is a bootstrap tool, and as such, will tend to
+resist changes that make it longer or dependent on more other
+packages, unless they also add to its usefulness for bootstrapping.
+
+
+= TODO
+
+a) gpg signature checking would be better if it actually checked against
+a list of "trusted to write Lisp" keys, instead of just "trusted to be
+who they say they are"
+
+e) nice to have: resume half-done downloads instead of starting from scratch
+every time.  but right now we're dealing in fairly small packages, this is not
+an immediate concern
+
+
diff --git a/contrib/asdf-install/asdf-install b/contrib/asdf-install/asdf-install
new file mode 100755 (executable)
index 0000000..db1ac64
Binary files /dev/null and b/contrib/asdf-install/asdf-install differ
diff --git a/contrib/asdf-install/asdf-install.asd b/contrib/asdf-install/asdf-install.asd
new file mode 100644 (file)
index 0000000..cdcec0a
--- /dev/null
@@ -0,0 +1,35 @@
+;;; -*-  Lisp -*-
+
+(defpackage #:asdf-install-system 
+  (:use #:cl #:asdf))
+
+(in-package #:asdf-install-system)
+(require 'sb-executable)
+
+;;; this is appalling misuse of asdf.  please don't treat it as any
+;;; kind of example.  this shouldn't be a compile-op, or if it is, should
+;;; define output-files properly instead oif leaving it be the fasl
+(defclass exe-file (cl-source-file) ())
+(defmethod perform ((o compile-op) (c exe-file))
+  (call-next-method)
+  (sb-executable:make-executable
+   (make-pathname :name "asdf-install"
+                 :type nil
+                 :defaults (component-pathname c))
+   (output-files o c)
+   :initial-function "RUN"))
+
+(defmethod perform ((o load-op) (c exe-file)) nil)
+
+(defsystem asdf-install
+  :depends-on (sb-posix sb-bsd-sockets)
+  :version "0.2"
+  :components ((:file "defpackage")
+              (exe-file "loader")
+              (:file "installer")))
+              
+(defmethod perform :after ((o load-op) (c (eql (find-system :asdf-install))))
+  (provide 'asdf-install))
+
+(defmethod perform ((o test-op) (c (eql (find-system :asdf-install))))
+  t)
diff --git a/contrib/asdf-install/defpackage.lisp b/contrib/asdf-install/defpackage.lisp
new file mode 100644 (file)
index 0000000..1b9636b
--- /dev/null
@@ -0,0 +1,13 @@
+(cl:in-package :cl-user)
+(defpackage :asdf-install
+  (:use "CL" "SB-EXT"  "SB-BSD-SOCKETS")
+  (:export
+   ;; customizable variables
+   #:*proxy* #:*cclan-mirror* #:*sbcl-home*
+   #:*verify-gpg-signatures* #:*locations*
+   #:*safe-url-prefixes*
+   ;; entry point
+   #:install))
+
+(defpackage :asdf-install-customize
+  (:use "CL" "SB-EXT"  "SB-BSD-SOCKETS" "ASDF-INSTALL"))
diff --git a/contrib/asdf-install/installer.lisp b/contrib/asdf-install/installer.lisp
new file mode 100644 (file)
index 0000000..c8a3551
--- /dev/null
@@ -0,0 +1,266 @@
+(in-package :asdf-install)
+
+(defvar *proxy* (posix-getenv "http_proxy"))
+(defvar *cclan-mirror*
+  (or (posix-getenv "CCLAN_MIRROR")
+      "http://ftp.linux.org.uk/pub/lisp/cclan/"))
+    
+(defun directorify (name)
+  ;; input name may or may not have a training #\/, but we know we
+  ;; want a directory
+  (let ((path (pathname name)))
+    (if (pathname-name path)
+       (merge-pathnames
+        (make-pathname :directory `(:relative ,(pathname-name path))
+                       :name "")
+        path)
+       path)))
+
+(defvar *sbcl-home* (directorify (posix-getenv "SBCL_HOME")))
+(defvar *dot-sbcl*
+  (merge-pathnames (make-pathname :directory '(:relative ".sbcl"))
+                  (user-homedir-pathname)))
+
+(defvar *verify-gpg-signatures* :unknown-locations
+  "Should we get detached GPG signatures for the packages and verify them?
+NIL - no, T - yes, :UNKNOWN-LOCATIONS - for any URL which isn't in CCLAN
+and doesn't begin with one of the prefixes in *SAFE-URL-PREFIXES*")
+(defvar *safe-url-prefixes*
+  (list "http://ftp.linux.org.uk/pub/lisp/"
+       "http://files.b9.com/"))
+
+(defun verify-gpg-signatures-p (url)
+  (labels ((prefixp (prefix string)
+            (let ((m (mismatch prefix string)))
+              (or (not m) (>= m (length prefix))))))
+    (case *verify-gpg-signatures*
+      (nil nil)
+      (:unknown-locations
+       (notany
+       (lambda (x) (prefixp x url))
+       (cons *cclan-mirror* *safe-url-prefixes*)))
+      (t t))))
+         
+(defvar *locations*
+  `((,(merge-pathnames "site/" *sbcl-home*)
+     ,(merge-pathnames "site-systems/" *sbcl-home*)
+     "System-wide install")
+    (,(merge-pathnames "site/" *dot-sbcl*)
+     ,(merge-pathnames "systems/" *dot-sbcl*)
+     "Personal installation")))
+
+(let* ((*package* (find-package :asdf-install-customize))
+       (file (probe-file (merge-pathnames
+                         (make-pathname :name ".asdf-install")
+                         (user-homedir-pathname)))))
+  (when file (load file)))
+
+(define-condition download-error (error)
+  ((url :initarg :url :reader download-url)
+   (response :initarg :response :reader download-response))
+  (:report (lambda (c s)
+            (format s "Server responded ~A for GET ~A"
+                    (download-response c) (download-url c)))))
+
+(define-condition signature-error (error)
+  ((cause :initarg :cause :reader signature-error-cause))
+  (:report (lambda (c s)
+            (format s "Cannot verify package signature:  ~A"
+                    (signature-error-cause c)))))
+            
+(defun url-host (url)
+  (assert (string-equal url "http://" :end1 7))
+  (let* ((port-start (position #\: url :start 7))
+        (host-end (min (or (position #\/ url :start 7) (length url))
+                       (or port-start (length url)))))
+    (subseq url 7 host-end)))
+
+(defun url-port (url)
+  (assert (string-equal url "http://" :end1 7))
+  (let ((port-start (position #\: url :start 7)))
+    (if port-start (parse-integer url :start port-start :junk-allowed t) 80)))
+
+(defun url-connection (url)
+  (let ((s (make-instance 'inet-socket :type :stream :protocol :tcp))
+       (host (url-host url))
+       (port (url-port url)))
+    (socket-connect
+     s (car (host-ent-addresses (get-host-by-name (url-host (or *proxy* url)))))
+     (url-port (or  *proxy* url)))
+    (let ((stream (socket-make-stream s :input t :output t :buffering :full)))
+      ;; we are exceedingly unportable about proper line-endings here.
+      ;; Anyone wishing to run this under non-SBCL should take especial care
+      (format stream "GET ~A HTTP/1.0~%Host: ~A~%Cookie: CCLAN-SITE=~A~%~%"
+             url host *cclan-mirror*)
+      (force-output stream)
+      (list
+       (let* ((l (read-line stream))
+             (space (position #\Space l)))
+        (parse-integer l :start (1+ space) :junk-allowed t))
+       (loop for line = (read-line stream nil nil)
+            until (or (null line) (eql (elt line 0) (code-char 13)))
+            collect
+            (let ((colon (position #\: line)))
+              (cons (intern (string-upcase (subseq line 0 colon)) :keyword)
+                    (string-trim (list #\Space (code-char 13))
+                                 (subseq line (1+ colon))))))
+       stream))))
+
+(defun download-files-for-package (package-name-or-url file-name)
+  (let ((url
+        (if (= (mismatch package-name-or-url "http://") 7)
+            package-name-or-url
+            (format nil "http://www.cliki.net/~A?download"
+                    package-name-or-url))))
+    (destructuring-bind (response headers stream)
+       (block got
+         (loop
+          (destructuring-bind (response headers stream) (url-connection url)
+            (unless (member response '(301 302))              
+              (return-from got (list response headers stream)))
+            (close stream)
+            (setf url (cdr (assoc :location headers))))))
+      (if (>= response 400)
+       (error 'download-error :url url :response response))
+      (let ((length (parse-integer
+                    (or (cdr (assoc :content-length headers)) "")
+                    :junk-allowed t)))
+       (format t "Downloading ~A bytes from ~A ..."
+               (if length length "some unknown number of") url)
+       (force-output)
+       (with-open-file (o file-name :direction :output)
+         (if length
+             (let ((buf (make-array length
+                                    :element-type
+                                    (stream-element-type stream)  )))
+               (read-sequence buf stream)
+               (write-sequence buf o)) 
+             (sb-executable:copy-stream stream o))))
+      (close stream)
+      (terpri)
+      ;; seems to have worked.  let's try for a detached gpg signature too
+      (when (verify-gpg-signatures-p url)
+       (verify-gpg-signature url file-name)))))
+
+(defun verify-gpg-signature (url file-name)
+  (destructuring-bind (response headers stream)
+      (url-connection (concatenate 'string url ".asc"))
+    (unwind-protect
+        (if (= response 200)
+            ;; sadly, we can't pass the stream directly to run-program,
+            ;; because (at least in sbcl 0.8) that ignores existing buffered
+            ;; data and only reads new fresh data direct from the file
+            ;; descriptor
+            (let ((data (make-string (parse-integer
+                                      (cdr (assoc :content-length headers))
+                                      :junk-allowed t))))
+              (read-sequence data stream)
+              (let ((ret
+                     (process-exit-code
+                      (sb-ext:run-program "gpg"
+                                          (list "--verify" "-"
+                                                (namestring file-name))
+                                          :output t
+                                          :search t
+                                          :input (make-string-input-stream data)
+                                          :wait t))))
+                (unless (zerop ret)
+                  (error 'signature-error
+                         :cause (make-condition
+                                 'simple-error
+                                 :format-control "GPG returned exit status ~A"
+                                 :format-arguments (list ret))))))
+            (error 'signature-error
+                   :cause
+                   (make-condition
+                    'download-error :url  (concatenate 'string url ".asc")
+                    :response response)))
+      (close stream))))
+       
+    
+
+
+(defun where ()  
+  (format t "Install where?~%")
+  (loop for (source system name) in *locations*
+       for i from 1
+       do (format t "~A) ~A: ~%   System in ~A~%   Files in ~A ~%"
+                  i name system source))
+  (format t " --> ") (force-output)
+  (let ((response (read)))
+    (when (> response 0)
+      (elt *locations* (1- response)))))
+
+(defun install-package (source system packagename)
+  "Returns a list of asdf system names for installed asdf systems"
+  (ensure-directories-exist source )
+    (ensure-directories-exist system )
+  (let* ((tar
+         (with-output-to-string (o)
+           (or
+            (sb-ext:run-program "tar"
+                                (list "-C" (namestring source)
+                                      "-xzvf" (namestring packagename))
+                                :output o
+                                :search t
+                                :wait t)
+            (error "can't untar"))))
+        (dummy (princ tar))
+        (pos-slash (position #\/ tar))
+        (*default-pathname-defaults*
+         (merge-pathnames
+          (make-pathname :directory
+                         `(:relative ,(subseq tar 0 pos-slash)))
+          source)))
+    (loop for asd in (directory
+                     (make-pathname :name :wild :type "asd"))
+         do (let ((target (merge-pathnames
+                           (make-pathname :name (pathname-name asd)
+                                          :type (pathname-type asd))
+                           system)))
+              (when (probe-file target)
+                (sb-posix:unlink target))
+              (sb-posix:symlink asd target))
+         collect (pathname-name asd))))
+
+(defvar *temporary-files*)
+(defun temp-file-name (p)
+  (let* ((pos-slash (position #\/ p :from-end t))
+        (pos-dot (position #\. p :start (or pos-slash 0))))
+    (merge-pathnames
+     (make-pathname
+      :name (subseq p (if pos-slash (1+ pos-slash) 0) pos-dot)
+      :type "asdf-install-tmp"))))
+                    
+
+;; this is the external entry point
+(defun install (&rest packages)
+  (let ((*temporary-files* nil))
+    (unwind-protect
+        (destructuring-bind (source system name) (where)
+          (labels ((one-iter (packages)
+                     (dolist (asd
+                               (loop for p in (mapcar 'string packages)
+                                     unless (probe-file p)
+                                     do (let ((tmp (temp-file-name p)))
+                                          (pushnew tmp *temporary-files*)
+                                          (download-files-for-package p tmp)
+                                          (setf p tmp))
+                                     end
+                                     do (format t "Installing ~A in ~A,~A~%"
+                                                p source system)
+                                     append (install-package source system p)))
+                       (handler-case
+                           (asdf:operate 'asdf:load-op asd)
+                         (asdf:missing-dependency (c)
+                           (format t
+                                   "Downloading package ~A, required by ~A~%"
+                                   (asdf::missing-requires c)
+                                   (asdf:component-name
+                                    (asdf::missing-required-by c)))
+                           (one-iter (list
+                                      (symbol-name
+                                       (asdf::missing-requires c)))))))))
+            (one-iter packages)))
+      (dolist (l *temporary-files*)
+           (when (probe-file l) (delete-file l))))))
diff --git a/contrib/asdf-install/loader.lisp b/contrib/asdf-install/loader.lisp
new file mode 100644 (file)
index 0000000..7d1211f
--- /dev/null
@@ -0,0 +1,15 @@
+
+(in-package :cl-user)
+(eval-when (:compile-toplevel :load-toplevel :execute)
+  (require 'asdf)
+  (asdf:operate 'asdf:load-op 'asdf-install :verbose nil))
+
+(defun run ()
+  (handler-case
+      (apply #'asdf-install:install (cdr *posix-argv*))
+    (error (c)
+      (princ "Install failed due to error:") (terpri)
+      (princ c) (terpri)
+      (quit :unix-status 1))))
+
+;(quit)
\ No newline at end of file
index cdfd9d6..15e5268 100644 (file)
@@ -1,7 +1,7 @@
 CC=gcc
-export CC
+export CC SBCL
 
-all: 
+all: $(EXTRA_ALL_TARGETS)
        $(MAKE) -C ../asdf
        $(SBCL) --eval '(load "../asdf/asdf")' \
          --eval "(setf asdf::*central-registry* '((MERGE-PATHNAMES \"systems/\" (TRUENAME (SB-EXT:POSIX-GETENV \"SBCL_HOME\")))))" \
@@ -14,6 +14,6 @@ test: all
          $(SBCL) --eval '(load "../asdf/asdf")'
 
 
-install:
+install: $(EXTRA_INSTALL_TARGETS)
        tar cf - . | ( cd $(INSTALL_DIR) && tar xpvf - )
        ( cd  $(SBCL_HOME)/systems && ln -fs ../$(SYSTEM)/$(SYSTEM).asd . )
index c424d43..b4ba7ed 100644 (file)
 
 (defvar *exec-header*
   "#!/bin/sh --
-exec sbcl --noinform ~{~A ~}--eval \"(with-open-file (i \\\"$0\\\" :element-type '(unsigned-byte 8)) (loop while (< ret 2) when (= (read-byte i) 10) count 1 into ret) (load i) (quit))\" --end-toplevel-options ${1+\"$@\"}
+exec sbcl --noinform ~{~A ~}--eval \"(with-open-file (i \\\"$0\\\" :element-type '(unsigned-byte 8)) (loop while (< ret 2) when (= (read-byte i) 10) count 1 into ret) (load i) (funcall (quote ~A)) (quit))\" --end-toplevel-options ${1+\"$@\"}
 ")
 
 (defun make-executable (output-file fasls
                        &key (runtime-flags '("--disable-debugger"
                                              "--userinit /dev/null"
-                                             "--sysinit /dev/null")))
+                                             "--sysinit /dev/null"))
+                       initial-function)
   "Write an executable called OUTPUT-FILE which can be run from the shell, by 'linking' together code from FASLS.  Actually works by concatenating them and prepending a #! header"
   (with-open-file (out output-file :direction :output
                       :element-type '(unsigned-byte 8))
     (write-sequence (map 'vector #'char-code
-                        (format nil *exec-header* runtime-flags)) out)
+                        (format nil *exec-header* runtime-flags
+                                (or initial-function 'values))) out)
     (dolist (input-file (if (listp fasls) fasls (list fasls)))
       (with-open-file (in (merge-pathnames input-file
                                           (make-pathname :type "fasl"))
index dee7db4..50c66b6 100644 (file)
@@ -19,7 +19,7 @@ if [ -n "$SBCL_HOME" -a "$INSTALL_ROOT/lib/sbcl" != "$SBCL_HOME" ];then
    exit 1
 fi
 SBCL_HOME=$INSTALL_ROOT/lib/sbcl
-export SBCL_HOME
+export SBCL_HOME INSTALL_ROOT
 ensure_dirs $INSTALL_ROOT $INSTALL_ROOT/bin $INSTALL_ROOT/lib \
     $INSTALL_ROOT/man $INSTALL_ROOT/man/man1 \
     $SBCL_HOME $SBCL_HOME/systems $SBCL_HOME/site-systems
index 30ef385..d49828d 100644 (file)
@@ -17,4 +17,4 @@
 ;;; checkins which aren't released. (And occasionally for internal
 ;;; versions, especially for internal versions off the main CVS
 ;;; branch, it gets hairier, e.g. "0.pre7.14.flaky4.13".)
-"0.8.2.22"
+"0.8.2.23"