emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

install.el


From: Stefan Monnier
Subject: install.el
Date: Sat, 14 Jun 2003 16:45:50 -0400

I think it should be easier for users to install external packages.
The current way to do it is
- download the file
- look at it (or at its README file if it's a tarball)
- follow the instructions
But it's not clear where to put the file, and some instructions assume
you already know about the .emacs file and have already setup your
load-path to point somewhere where you put your downloaded packages.

So I started to write install.el which is a package that offer the function
`install-file'.   It takes a file (a ginel elisp file or a tarball) and
somehow installs it at a useful place, compiles the elisp file(s)
and activates the package by adding some relevant commands in your .emacs.

Its current status is closer to proof-of-concept than to fool-proof,
but I'd like to install it in CVS.

Currently it can install any single-elisp-file (but will generally
not activate it, since it uses autoloads and most single-elisp-files
don't have autoload cookies) and several tarballs such as X-Symbol,
Gnus, AucTeX, sml-mode, bbdb, ...

For tarballs it does a good deal of guess work, unless the package
follows some conventions.


        Stefan


;;; install.el --- Package to ease installation of Elisp packages

;; Copyright (C) 2001, 2003  Stefan Monnier

;; Author: Stefan Monnier <address@hidden>

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to
;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;;; Commentary:

;; This little package is meant to ease up the task of installing
;; third party ELisp packages.  I.e. it takes care of placing it in
;; an appropriate location, finds out what code is necessary to get
;; the package activated, sets up your .emacs file to activate the
;; package, and byte-compiles the package.

;; It should work on both single-file packages and tarballs.

;; On tarball packages, it does a bit of guess work to figure out
;; where are which files and how to use them.  This is bound to
;; fail sometimes.

;; Tested on:
;; + ProofGeneral
;; + sml-mode
;; + AUCTeX   (missed the info page)
;; + X-Symbol (as an XEmacs package)
;; + Gnus     (but doesn't install the info doc :-( )
;; + BBDB     (misses the main info page)
;; - WhizzyTeX (needs to hack the perl script and stuff :-( )
;; ? ECB
;; ? JDEE
;; ? preview-latex
;; ? VM
;; ? mmm-mode
;; ? Semantic

;; The on-disk structure is as follows:
;; - there are two area: the `home' and the `site' each with their
;;   respective directory (~/lib/emacs and /usr/share/emacs/site-lisp)
;;   and file (.emacs and site-start).
;; - There is a distinction between installing and activating.
;;   Installing only places the files on disk, whereas activating sets up
;;   autoloads and friends.
;; - Activation is done on a directory by directory basis.  Each directory
;;   has an `autoloads' file.  Loading it activates the package(s)
;;   in directory.
;; - Single-file packages are placed together in the toplevel directory
;;   whereas tarball-packages are placed in their own subdirectory (so they
;;   can be activated independently).

;;; Todo:

;; - don't ask whether to activate site-wide packages installed in home.
;; - Create Info from Texinfo when needed.
;; - Try harder to find Info files such as doc/auctex.
;; - UI to (un)install and (de)activate packages, get a list, ...
;; - If a single-file package lacks ;;;###autoload, try to add them
;;   based on the Commentary section or something.
;; - don't leave out reams of `autoloads~' backup files.

;;; Code:

(require 'em-glob)

(defgroup install nil
  "Elisp package installation")

(defmacro install-filter (list exp)
  (declare (debug t))
  `(let ((res nil))
     (dolist (x ,list (nreverse res))
       (if ,exp (push x res)))))

(defcustom install-site-file
  (or
   (locate-file (or site-run-file "site-start") load-path load-suffixes)
   (let ((lp (mapcar 'abbreviate-file-name load-path)))
     ;; Prefer non-user directories.
     (setq lp (or (install-filter lp (not (string-match "\\`~/" x))) lp))
     ;; Prefer site-lisp directories.
     (setq lp (or (install-filter lp (string-match "/site-lisp\\'" x)) lp))
     ;; Prefer shorter directory names (i.e. parents rather than subdirs).
     (setq lp (sort lp (lambda (d1 d2) (< (length d1) (length d2)))))
     ;; 
     (expand-file-name (concat (or site-run-file "site-start") ".el") (car 
lp))))
  "Site-wide customization file."
  :type 'file)

(defcustom install-site-dir (file-name-directory install-site-file)
  "Directory where site-wide packages should be installed."
  :type 'directory)

(defcustom install-home-file (or user-init-file "~/.emacs")
  "Main customization file into which Install should place `load' commands."
  :type 'file)

(defcustom install-home-dir
  ;; FIXME: We should be careful never to choose one of Emacs's own
  ;; directories, even if the user installed Emacs in his home dir.
  (let ((lp (mapcar 'abbreviate-file-name load-path)))
    ;; Only consider writable directories.
    (setq lp (install-filter lp (file-writable-p x)))
    ;; Only consider user directories.
    (setq lp (install-filter lp (string-match "\\`~/" x)))
    ;; Prefer shorter directory names (i.e. parents rather than subdirs).
    (setq lp (sort lp (lambda (d1 d2) (< (length d1) (length d2)))))
    ;; Default to ~/lib/emacs.
    (if (or (null lp)
            ;; If it's a subdir of lib/emacs, use lib/emacs.  This can happen
            ;; because Install does not automatically add lib/emacs to the
            ;; load-path if it only installs tar packages underneath.
            (string-match "\\`~/lib/emacs/" (car lp)))
        "~/lib/emacs/"
      (car lp)))
  "Directory into which elisp packages should be placed by Install."
  :type 'directory)

(defcustom install-autoload-file "autoloads"
  "Name of autoload files used by Install.")

(defcustom install-compress-source-files nil ;; ".gz"
  "If non-nil, Install will try to compress file.")

;;

(defun install-get-dir ()
  "Return the directory into which to install packages."
  (or (and (file-writable-p install-site-dir)
           (y-or-n-p "Install site-wide? ")
           install-site-dir)
      (progn
        (unless (file-writable-p install-home-dir)
          (setq install-home-dir
                (let ((default-directory install-home-dir))
                  (read-directory-name "Directory to install into: ")))
          (unless (file-directory-p install-home-dir)
            (make-directory install-home-dir t)))
        install-home-dir)))

(defun install-get-file ()
  "Return the file into which to activate packages."
  (or (and (file-writable-p install-site-file)
           (y-or-n-p "Activate site-wide? ")
           install-site-file)
      install-home-file))

(defmacro install-with-file (file &rest body)
  (declare (debug t) (indent 1))
  `(let ((install-with-existing-file (find-buffer-visiting ,file)))
     (with-current-buffer (or install-with-existing-file
                              (find-file-noselect ,file))
       (prog1 (save-current-buffer ,@body)
         (unless install-with-existing-file
           (kill-buffer (current-buffer)))))))

;;;###autoload
(defun install-file (file)
  (interactive "fFile to install: ")
  (with-current-buffer (find-file-noselect file)
    (install-buffer)))


;;;###autoload
(defun install-buffer ()
  "Install the current elisp buffer as a package.
The package is install in `install-home-dir', autoloads are added
to the `install-autoload-file' in that directory and the
`install-custom-file' is then updated to load these autoloads."
  (interactive)
  (cond
   ((derived-mode-p 'tar-mode) (install-tar-buffer))
   ((not (derived-mode-p 'emacs-lisp-mode))
    (error "I only know how to install tar.gz and elisp files."))
   (t
    (let* ((install-dir (install-get-dir))
           (package (file-name-nondirectory buffer-file-name))
           (file (expand-file-name package install-dir))
           (autoload (expand-file-name install-autoload-file install-dir)))
      (when (and install-compress-source-files
                 (string-match "\\.el\\'" file))
        (setq file (concat file (if (stringp install-compress-source-files)
                                    install-compress-source-files ".gz"))))
      ;; Install the elisp file.
      (write-region (point-min) (point-max) file)
      ;; Extract the autoloads into a separate file.
      (install-update-autoloads autoload)
      ;; Activate.
      (install-activate autoload)
      ;; Finally, byte compile.  In the present case (a single-file package),
      ;; this could be done before activation.
      (byte-compile-file file)))))

(defun install-tar-buffer ()
  "Like `install-buffer' but for a tar package rather than single file."
  (let* ((name (file-name-nondirectory buffer-file-name))
         ;; Strip off ".tar.gz", ".tar", ".tgz", ".tar.Z", ...
         (name (if (string-match "\\.[tT][^.]+\\(\\.[^.]+\\)?\\'" name)
                   (substring name 0 (match-beginning 0)) name))
         (install-dir (install-get-dir))
         (default-directory (expand-file-name name install-dir)))
    ;; Install the files.
    ;; FIXME: check what `tar-untar-buffer' does with symlinks and stuff.
    ;; FIXME: the dir might already exist.
    (make-directory default-directory)
    (tar-untar-buffer)
    (let ((files (directory-files default-directory
                                  nil "\\`\\([^.]\\|\\.[^.]\\|\\.\\..\\)" t)))
      ;; If the tar file already had everything under a single directory,
      ;; remove the redundant level of directory.
      (when (and (= (length files) 1) (file-directory-p (car files)))
        (let* ((f (car files))
               ;; Keep the longest name of the two, assuming that the
               ;; difference is that the longer one has a version number.
               (final (if (> (length name) (length f)) name f))
               (temp (if (= (length name) (length f)) (concat f ".tmp") f)))
          ;; FIXME: the dir might already exist.
          (rename-file f (expand-file-name temp install-dir))
          (setq default-directory install-dir)
          (delete-directory name)
          ;; FIXME: the dir might already exist.
          (unless (equal final temp) (rename-file temp final))
          (setq name final)
          (setq default-directory (expand-file-name final)))))
    ;; Extract the autoloads.
    (install-setup-tree)
    ;; Activate the package.
    (install-activate (expand-file-name (install-get-activation-file)))
    ;; Finally, byte-compile the files.
    (install-byte-compile-dir)))

(defun install-dirs-of-files (files)
  "Return a list of subdirs containing elisp files."
  (let ((dirs nil)
        (ignore (regexp-opt
                 (cons
                  ;; Ignore contrib directories because they tend to contain
                  ;; either less-debugged code, or packages that might
                  ;; already be installed and can thus interfere.
                  "contrib/"
                  (let ((exts nil))
                    (dolist (ext completion-ignored-extensions exts)
                      (if (eq (aref ext (1- (length ext))) ?/)
                          (push ext exts))))))))
    ;; Collect the dirs that hold elisp files.
    (dolist (file files dirs)
      (let ((dir (file-name-directory file)))
        (unless (or (member dir dirs)
                    (and dir (string-match ignore dir)))
          (push dir dirs))))))

(defun install-find-elisp-dirs ()
  "Return a list of subdirs containing elisp files."
  (install-dirs-of-files (install-glob "**/*.el")))

(defun install-byte-compile-dir ()
  "Byte compile all elisp files under the current directory."
  (let ((load-path (append (mapcar (lambda (dir)
                                     (if dir
                                         (expand-file-name dir)
                                       default-directory))
                                   (install-find-elisp-dirs))
                           load-path)))
    (byte-recompile-directory default-directory 0)))

(defun install-glob (pattern)
  (let ((res (eshell-extended-glob pattern)))
    (if (listp res) res)))

(defun install-get-activation-file ()
  "Return the file to load to activate the package.
This is usually \"./autoloads\", but it can also be \"lisp/foo-site.el\"."
  (if (file-exists-p install-autoload-file)
      install-autoload-file
    (or (car (install-glob (concat "**/" install-autoload-file)))
        (car (install-glob "**/auto-autoloads.el"))
        (car (install-glob "**/*-site.el")))))

(defun install-setup-tree ()
  (eshell-glob-initialize)
  ;; Look for elisp files.
  (let ((dirs (install-find-elisp-dirs))
        (autoload-files nil)
        (toplevel nil))
    ;; Prepare each elisp subdir and collect info along the way.
    (dolist (dir dirs)
      (let ((default-directory (expand-file-name (or dir default-directory))))
        ;; Remove *.elc files, in case they were not compiled for our version.
        (mapc 'delete-file (install-glob "*.elc"))
        ;; Extract autoloads.
        (let ((sites (or (install-glob "auto-autoloads.el")
                         (install-glob "*-site.el"))))
          (if (= 1 (length sites))
              ;; Some packages come with a <pkg>-site.el file instead
              ;; of using autoloads.  In that case, just load that file.
              (push (concat dir (car sites)) autoload-files)
            ;; Otherwise.  Make an autoloads file and load it.
            ;; FIXME: Don't make hundreds of autoload files.
            (let ((exists (file-exists-p install-autoload-file)))
              (if (not (install-update-autoloads install-autoload-file))
                  ;; Don't stupidly add empty autoloads files.
                  (unless exists (delete-file install-autoload-file))
                (push (concat dir install-autoload-file) autoload-files)))))))
    (mapc 'install-ensure-autoloads-file autoload-files)
    ;; Setup the toplevel activation file.
    (if (and (= 1 (length autoload-files))
             (equal (car autoload-files) (install-get-activation-file)))
        (setq toplevel (car autoload-files))
      (setq toplevel install-autoload-file)
      (dolist (file autoload-files)
        (unless (equal file toplevel)
          (install-activate
           `(expand-file-name
             ,(file-relative-name
               (expand-file-name file)
               (file-name-directory (expand-file-name toplevel)))
             (file-name-directory load-file-name))
           toplevel))))
    ;; Make up an info/dir file if necessary and register the info dirs.
    (let ((info-dirs (install-make-info)))
      (when info-dirs
        (with-current-buffer (find-file-noselect toplevel)
          (unless (derived-mode-p 'emacs-lisp-mode) (emacs-lisp-mode))
          (goto-char (point-min))
          (unless (re-search-forward "(add-to-list[ 
\t\n]+'Info-default-directory-list" nil t)
            (forward-comment (point-max))
            (while (re-search-backward "^" nil t))
            (unless (bolp) (newline))
            (let ((top-dir (file-name-directory (expand-file-name toplevel))))
              (dolist (dir info-dirs)
                (setq dir (expand-file-name (or dir default-directory)))
                (if (equal dir top-dir)
                    (insert "(add-to-list 'Info-default-directory-list 
(file-name-directory load-file-name))\n")
                  (let ((text (pp-to-string (file-relative-name dir top-dir))))
                    (if (string-match "\n\\'" text)
                        (setq text (substring text 0 -1)))
                    (insert "(add-to-list 'Info-default-directory-list\n"
                            "             (expand-file-name " text
                            " (file-name-directory load-file-name)))\n")))))
            (save-buffer)))))))

(defun install-ensure-autoloads-file (file)
  "Make sure that the autoload file FILE exists and if not create it."
  (with-current-buffer (find-file-noselect file)
    (unless (derived-mode-p 'emacs-lisp-mode) (emacs-lisp-mode))
    (when buffer-read-only
      (set-file-modes buffer-file-name
                      (logior ?\200 (file-modes buffer-file-name)))
      (toggle-read-only))
    (goto-char (point-min))
    ;; Insert a little boiler plate if there's nothing yet.
    (when (eobp)
      (insert ";;; " (file-name-nondirectory file)
              " --- automatically extracted autoloads\n"
              ";;\n"
              ";;; Code:\n\n"
              "\n;; Local Variables:\n"
              ";; version-control: never\n"
              ";; no-byte-compile: t\n"
              ";; no-update-autoloads: t\n"
              ";; End:\n"
              ";;; " (file-name-nondirectory file)
              " ends here\n")
     (goto-char (point-min)))
    ;; Make sure it will setup the load path properly.
    (unless (re-search-forward "\\<load-file-name\\>" nil t)
      (forward-comment (point-max))
      (while (re-search-backward "^" nil t))
      (unless (bolp) (newline))
      (unless (eq (char-before (1- (point))) ?\n) (newline))
      (insert ";; Tell Emacs to look for elisp files in this directory."
              ;; Add some sort of signature.
              "  -- Install\n")
      (insert "(add-to-list 'load-path
              (or (file-name-directory load-file-name) (car load-path)))\n\n")
      (save-buffer)))
  file)

(defvar generated-autoload-file)

(defun install-update-autoloads (autoload)
  "Update file AUTOLOAD.  This will create the file if necessary.
Returns non-nil if there is anything autoloaded into it."
  (setq autoload (expand-file-name autoload))
  (let ((generated-autoload-file
         (install-ensure-autoloads-file autoload)))
    ;; (update-file-autoloads file)
    (update-directory-autoloads (file-name-directory autoload)))
  ;; Make sure the file sets up the load-path appropriately.
  (with-current-buffer (find-file-noselect autoload)
    (unless (derived-mode-p 'emacs-lisp-mode) (emacs-lisp-mode))
    (re-search-forward "^" nil t) ;Find the first autoload entry.
    (forward-comment (point-max))
    (not (eobp))))

(defun install-activate (autoload &optional into)
  "Update INTO to make sure it loads AUTOLOAD.
AUTOLOAD can be an expression.
If it is a string, this also loads it into the currently running Emacs.
If provided, INTO specifies the file which should load AUTOLOAD.
The default is to use `install-get-file'."
  (when (stringp autoload)
    (setq autoload (abbreviate-file-name autoload))
    (load autoload))
  (with-current-buffer (find-file-noselect (or into (install-get-file)))
    (unless (derived-mode-p 'emacs-lisp-mode) (emacs-lisp-mode))
    (save-excursion
      (let ((text (pp-to-string autoload)))
        (if (string-match "\n\\'" text)
            (setq text (substring text 0 -1)))
        (goto-char (point-min))
        (unless (re-search-forward (regexp-quote text) nil t)
          (goto-char (point-min))
          (forward-comment (point-max))
          (while (re-search-backward "^" nil t))
          (unless (bolp) (newline))
          ;; Pass `install' as argument to load: this both makes Emacs
          ;; ignore the load if the file is missing and is used as a marker
          ;; indicating that this load statement was introduced by us.
          (insert "(load " text " 'install)\n")
          (save-buffer))))))

;;;###autoload
(defun install-list-packages ()
  "Show the installed packages."
  (interactive)
  (dired (install-get-dir)))

;; Info files and DIR files.
;; Some of this should probably be moved to info.el.

(defconst install-info-dir "-*- Text -*-\n\n\
File: dir       Node: Top       This is the top of the INFO tree\
\n\n* Menu:\n\n"
  "Text content of a barebones empty `info/dir' file.")

(defun install-find-info-files ()
  (let ((files (or (install-glob "**/*.info*")
                   (install-glob "**/info/*")))
        (tmp nil))
    (dolist (f files)
      (unless (or (member f '("dir" "localdir"))
                  (and (string-match "-[0-9]+" f)
                       (member (replace-match "" t t f) files))
                  (not (string-match 
"\\.info\\>\\|\\(\\`\\|/\\)[^.]+\\(\\'\\|\\.\\(gz\\|Z\\)\\)" f)))
        (push f tmp)))
    tmp))

(defun install-make-info ()
  "Make an info/dir file if necessary and return the info directories."
  ;; FIXME: This should create the info files from the Texinfo files
  ;; if necessary !!
  ;; Problems to do that:
  ;; - detect when necessary. E.g. BBDB comes with an info page for
  ;;   the bbdb-filters stuff, but the main bbdb doc is in texinfo.
  ;; - figure out how to makeinfo the thing.  E.g. AucTeX comes with
  ;;   a whole bunch of Texinfo files and it's really not clear which
  ;;   is the right one.
  ;; - The info file might be there, but not found.  E.e. AucTeX has its
  ;;   page in doc/auctex.
  (let* ((files (install-find-info-files))
         (dirs (install-dirs-of-files files))
         (dir-files nil))
    ;; Remove files that were in ignored directories.
    (dolist (file files)
      (unless (member (file-name-directory file) dirs)
        (setq files (delq file files))))
    ;; Check that there's something to do.
    (when files
      (assert dirs)
      (dolist (dir dirs)
        (if (file-exists-p (expand-file-name "dir" dir))
            (push (expand-file-name "dir" dir) dir-files)))
      (unless dir-files
        ;; Pick the dir closest to the toplevel to put the main dir file.
        (setq dirs (sort dirs (lambda (s1 s2) (< (length s1) (length s2)))))
        (with-current-buffer (find-file-noselect (expand-file-name "dir" (car 
dirs)))
          (assert (= (point-min) (point-max)))
          (insert install-info-dir)
          (narrow-to-region (point) (point-max))
          (dolist (file files)
            (let ((section "Miscellaneous")
                  (entry nil))
              (install-with-file file
                (goto-char (point-min))
                (if (not (re-search-forward
                          (concat "^START-INFO-DIR-ENTRY\n"
                                  "\\([* \t].*\n\\)+"
                                  "END-INFO-DIR-ENTRY$") nil t))
                    ;; No entry in the file, let's build a default one.
                    (let ((base (file-name-nondirectory
                                 (file-name-sans-extension file))))
                      (setq entry (concat "* " (upcase base)
                                          ": (" base ").\n")))
                  (setq entry (match-string 1))
                  (goto-char (point-min))
                  (when (re-search-forward
                         "^INFO-DIR-SECTION[ \t]+\\(.*[^ \t\n]\\)" nil t)
                    (setq section (match-string 1)))))
              (goto-char (point-min))
              (unless (search-forward entry nil t)
                (unless (re-search-forward (concat "^" (regexp-quote section) 
"[ \t]*\n") nil 'move)
                  (unless (bobp) (newline))
                  (insert section) (newline))
                (insert entry))))
          (save-buffer)
          (kill-buffer (current-buffer))))
      dirs)))
              
              

(provide 'install)
;;; install.el ends here





reply via email to

[Prev in Thread] Current Thread [Next in Thread]