gnu-emacs-sources
[Top][All Lists]
Advanced

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

package.el - simple package manager (alpha)


From: Tom Tromey
Subject: package.el - simple package manager (alpha)
Date: 10 Mar 2007 12:10:15 -0700
User-agent: Gnus/5.09 (Gnus v5.9.0) Emacs/21.4

This is package.el, a simple package manager for Emacs.

This package manager understands version numbers and dependencies
between packages.  It separates package installation and activation
and can cope with an inconsistent installation.  It can download and
install packages from an online package archive (for the time being
hosted on my web site); when downloading a package it will also
download all of that package's dependencies.

One idea behind the design is that, in my years of using Emacs I've
noticed that if I install some package, and later it is incorporated
into Emacs itself, I will often not notice and continue running an
out-of-date version, sometimes for years.  This package manager tries
to improve upon this by only activating the newest version of any
given package

E.g., if Emacs itself used package.el to activate major sub-packages
like ERC, then end users could install a new ERC and, later, when
upgrading Emacs itself, their private copy would no longer be used.

package.el requires the 'url' package, so if you are running Emacs 21,
you will need to find that.

A package is a tar file with a single "metadata" file,
"something-pkg.el", which invokes "define-package".  package.el will
generate autoloads, byte-compile the emacs lisp code, and (if a 'dir'
file exists) update the info path.

I've packaged a couple useful programs and put them in the archive, so
you can try package.el very simply.  For instance, you can play the
cool "bubbles" game by evaluating:

   (package-install 'bubbles)
   (bubbles)

This is an alpha release.  There is still a lot to do: more
documentation, a nice UI for package management, and perhaps some
tools for managing ELPA (the package archive).  Also I would like to
add support for single-file packages, where the needed metadata is
incorporated in the package's sole .el file.

I'm looking for feedback of all kinds -- on the code, the approach,
etc.

In an ideal world I would eventually like to see this incorporated
into Emacs and used for major packages; and of course also I'd like to
see code on the Emacs wiki, or source posts to this group, take the
form of packages -- no more tweaking my load-path and adding autoloads
to my .emacs, but instead a single keystroke in gnus to install a cool
hack.

Tom

;;; package.el - Simple package system for Emacs

;; Copyright (C) 2007 Tom Tromey <address@hidden>

;; This file is not (yet) part of GNU Emacs.
;; However, it is distributed under the same license.

;; GNU Emacs 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.

;; GNU Emacs 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., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.

;;; Commentary:

;; The idea is to be able to download packages and install them.
;; Packages are versioned and have versioned dependencies.
;; Furthermore, this supports built-in packages which may or may not
;; be newer than user-specified packages.  This makes it possible to
;; upgrade Emacs and automatically disable packages which have moved
;; from external to core.  (Note though that we don't currently
;; register any of these, so this feature does not actually work.)

;; This code supports a single package repository, ELPA.  All packages
;; must be registered there.

;; A package is described by its name and version.  The distribution
;; format is simply a tar file, named "NAME-VERSION.tar".  The tar
;; file must unpack into a directory named after the package and
;; version: "NAME-VERSION".  It must contain a file named
;; "PACKAGE-pkg.el" which consists of a call to define-package.  It
;; may also contain a "dir" file and the info files it references.

;; The downloader will download all dependent packages.  It will also
;; byte-compile the package's lisp at install time.

;; At activation time we will set up the load-path and the info path,
;; and we will load the package's autoloads.  If a package's
;; dependencies are not available, we will not activate that package.

;;; To do:

;; - Make it possible to complete on package names when installing.
;;   Must download & cache a descriptor file...
;; - Use hierarchical layout.  PKG/etc PKG/lisp PKG/info
;;   ... except maybe lisp?
;; - Allow multiple versions on the server...?
;; - Don't install a package which will invalidate dependencies overall
;; - Allow something like (or (>= emacs 21.0) (>= xemacs 21.5))
;; - Allow optional package dependencies
;;   then if we require 'bbdb', bbdb-specific lisp in lisp/bbdb
;;   and just don't compile to add to load path ...?
;; - Have a list of archive URLs.  Allow ssh URLs?  Does url interface
;;   with tramp?


;; (require 'info) ??
;; info-initialize


(defconst package-archive-base "http://tromey.com/elpa/";
  "Base URL for the package archive.
Ordinarily you should not need to edit this.
The default points to ELPA, the Emacs Lisp Package Archive.")

;; Prime the cache.
(defvar package-archive-contents
  '((bubbles . [(0 2) nil nil "Bubbles (same game) puzzle" nil])
    (newsticker [(1 10) nil nil "Headline news ticker" nil])
    )
  "A representation of the contents of the ELPA archive.
This is an alist mapping package names (symbols) to package
descriptor vectors.")

(defvar package-user-dir (expand-file-name "~/.emacs.d/elpa")
  "Name of the directory where the user's packages are stored.")

(defvar package-directory-list
  (list (file-name-as-directory package-user-dir)
        "/usr/share/emacs/site-lisp/elpa/")
  "List of directories to search for packages.")

(defvar package-alist
  `((emacs . [(,emacs-major-version ,emacs-minor-version) nil nil
              "GNU Emacs"]))
  "Alist of all packages available for activation.
Maps the package name to a vector [VERSION REQS BODY].")

(defvar package-activated-list '(emacs)
  "List of the names of all activated packages.")

(defvar package-obsolete-list nil
  "List of cells representing obsolete packages.
Like package-alist, but not an alist as the keys are not unique.")

(defun package-version-split (string)
  "Split a package string into a version list."
  (mapcar 'string-to-int (split-string string "[.]")))

(defun package-version-join (l)
  "Turn a list of version numbers into a version string."
  (mapconcat 'int-to-string l "."))

(defun package-version-compare (v1 v2 fun)
  "Compare two version lists according to FUN.
FUN can be <, <=, =, >, or >=."
  (while (and v1 v2 (= (car v1) (car v2)))
    (setq v1 (cdr v1))
    (setq v2 (cdr v2)))
  (setq v1 (if v1 (car v1) 0))
  (setq v2 (if v2 (car v2) 0))
  (funcall fun v1 v2))

(defun package-strip-version (dirname)
  "Strip the version from a combined package name and version.
E.g., if given \"quux-23.0\", will return \"quux\""
  (if (string-match "^\\(.*\\)-[0-9]+\\([.][0-9]+\\)*$" dirname)
      (match-string 1 dirname)))

(defun package-load-descriptor (dir package)
  "Load the description file for a package.
Return nil if the package could not be found."
  (let ((pkg-dir (concat (file-name-as-directory dir) package "/")))
    (if (file-directory-p pkg-dir)
        (load (concat pkg-dir (package-strip-version package) "-pkg") nil t))))

(defun package-load-all-descriptors ()
  "Load descriptors of all packages.
Uses `package-directory-list' to find packages."
  (mapc (lambda (dir)
          (if (file-directory-p dir)
              (mapc (lambda (name)
                      (package-load-descriptor dir name))
                    (directory-files dir nil "^[^.]"))))
        package-directory-list))

(defun package-do-activate (package pkg-vec)
  (let* ((activate-body (aref pkg-vec 2))
         (pkg-name (symbol-name package))
         (pkg-ver-str (package-version-join (aref pkg-vec 0)))
         (dir-list package-directory-list)
         (pkg-dir))
    (while dir-list
      (let ((subdir (concat (car dir-list) pkg-name "-" pkg-ver-str "/")))
        (when (file-directory-p subdir)
          (setq pkg-dir subdir)
          (setq dir-list nil))))
    (unless pkg-dir
      (error "Internal error: could not find directory for %s-%s"
             pkg-name pkg-ver-str))
    (if (file-exists-p (concat pkg-dir "dir"))
        (progn
          ;; FIXME: not the friendliest, but simple.
          (require 'info)
          (info-initialize)
          (setq Info-directory-list (cons pkg-dir Info-directory-list))))
    (setq load-path (cons pkg-dir load-path))
    ;; Load the autoloads and activate the package.
    (load (concat pkg-dir (symbol-name package) "-autoloads")
          nil t)
    (eval `(progn ,@activate-body))
    (setq package-activated-list
          (cons package package-activated-list))
    ;; Don't return nil.
    t))

;; FIXME: return a reason instead?
(defun package-activate (package version)
  "Try to activate a package.
Return nil if the package could not be activated.
Recursively activates all dependencies of the named package."
  (or (memq package package-activated-list)
      (let* ((pkg-desc (assq package package-alist))
             (this-version (aref (cdr pkg-desc) 0))
             (req-list (aref (cdr pkg-desc) 1))
             (keep-going (package-version-compare this-version version '>=)))
        (while (and req-list keep-going)
          (or (package-activate (car (car req-list))
                                (car (cdr (car req-list))))
              (setq keep-going nil))
          (setq req-list (cdr req-list)))
        (if keep-going
            (package-do-activate package (cdr pkg-desc))))))

;; (define-package "emacs" "21.4.1" "GNU Emacs core package.")
;; (define-package "erc" "5.1" "ERC - irc client" '((emacs "21.0")))
(defun define-package (name-str version-string
                            &optional docstring
                            &optional requirements
                            &rest body)
  "Define a new package.
NAME is the name of the package, a string.
VERSION-STRING is the version of the package, a dotted sequence
of integers.
DOCSTRING is the optional description.
REQUIREMENTS is a list of requirements on other packages.
Each requirement is of the form (OTHER-PACKAGE \"VERSION\").
BODY is a form to be evaluated when the package is activated.
Note that the package's autoloads are automatically processed
on activation; so BODY should not usually be needed."
  (let* ((name (intern name-str))
         (pkg-desc (assq name package-alist))
         (new-version (package-version-split version-string))
         (new-pkg-desc
          (cons name
                (vector new-version
                        (if requirements
                            (mapcar
                             (lambda (elt)
                               (list (car elt)
                                     (package-version-split (car (cdr elt)))))
                             requirements))
                        body
                        docstring))))
    ;; Only redefine a package if the redefinition is newer.
    (if (or (not pkg-desc)
            (package-version-compare new-version (aref (cdr pkg-desc) 0) '>))
        (progn
          (when pkg-desc
            ;; Remove old package and declare it obsolete.
            (setq package-alist (delq pkg-desc package-alist))
            (setq package-obsolete-list (cons pkg-desc package-obsolete-list)))
          ;; Add package to the alist.
          (setq package-alist (cons new-pkg-desc package-alist)))
      ;; The package is born obsolete.
      (setq package-obsolete-list (cons new-pkg-desc package-obsolete-list)))))

(defun package-generate-autoloads (name pkg-dir)
  (let* ((auto-name (concat name "-autoloads.el"))
         (ignore-name (concat name "-pkg.el"))
         (generated-autoload-file (concat pkg-dir auto-name))
         (version-control 'never))
    ;; In Emacs 22 'update-autoloads-from-directories' does not seem
    ;; to be autoloaded...
    (require 'autoload)
    (update-autoloads-from-directories pkg-dir)))

(defun package-unpack (name version)
  (let ((pkg-dir (concat (file-name-as-directory package-user-dir)
                         (symbol-name name) "-" version "/")))
    ;; Be careful!!
    (make-directory package-user-dir t)
    (if (file-directory-p pkg-dir)
        (mapc (lambda (file) nil) ; 'delete-file
              (directory-files pkg-dir t "^[^.]")))
    (let ((default-directory (file-name-as-directory package-user-dir)))
      ;; FIXME check the result?
      ;; FIXME: use tar-untar-buffer
      (call-process-region (point) (point-max) "tar" nil '(nil nil) nil
                           "xf" "-")
      (package-generate-autoloads (symbol-name name) pkg-dir)
      ;; do we need to set load-path here?
      (byte-recompile-directory pkg-dir 0 t))))

(defun package-download (name version)
  (let ((tar-buffer (url-retrieve-synchronously
                     (concat package-archive-base
                             (symbol-name name) "-" version ".tar"))))
    (save-excursion
      (set-buffer tar-buffer)
      (goto-char (point-min))
      (re-search-forward "^$" nil 'move)
      (forward-char)
      (package-unpack name version)
      (kill-buffer tar-buffer))))

(defun package-installed-p (package version)
  (let ((pkg-desc (assq package package-alist)))
    (and pkg-desc
         (package-version-compare version (aref (cdr pkg-desc) 0) '>=))))

(defun package-compute-transaction (result requirements)
  (while requirements
    (let* ((elt (car requirements))
           (next-pkg (car elt))
           (next-version (car (cdr elt))))
      (unless (package-installed-p next-pkg next-version)
        (let ((pkg-desc (assq next-pkg package-archive-contents)))
          (unless pkg-desc
            (error "Package '%s' not available for installation."
                   (symbol-name next-pkg)))
          (unless (package-version-compare (aref (cdr pkg-desc) 0)
                                           next-version
                                           '>=)
            (error
             "Need package '%s' with version %s, but only %s is available."
             (symbol-name next-pkg) (package-version-join next-version)
             (package-version-join (aref (cdr pkg-desc) 0))))
          ;; Only add to the transaction if we don't already have it.
          (unless (memq next-pkg result)
            (setq result (cons next-pkg result)))
          (setq result
                (package-compute-transaction result
                                             (aref (cdr pkg-desc) 1))))))
    (setq requirements (cdr requirements)))
  result)

(defun package-install (name)
  "Install the package named NAME."
  (interactive "M")
  (let ((pkg-desc (assq name package-archive-contents)))
    (unless pkg-desc
      (error "Package '%s' not available for installation."
             (symbol-name name)))
    (let ((transaction
           (package-compute-transaction (list name) (aref (cdr pkg-desc) 1))))
      (mapc (lambda (elt)
              (let ((v-string (package-version-join
                               (aref (cdr (assq elt package-archive-contents))
                                     0))))
                (package-download elt v-string)))
            transaction)))
  ;; Try to activate it.
  (package-initialize))

(defun package-refresh-contents ()
  "Download the ELPA archive description if needed.
Invoking this will ensure that Emacs knows about the latest versions
of all packages.  This will let Emacs make them available for
download."
  (interactive)
  (let ((buffer (url-retrieve-synchronously
                 (concat package-archive-base "archive-contents.el"))))
    (save-excursion
      (set-buffer buffer)
      (goto-char (point-min))
      (re-search-forward "^$" nil 'move)
      (forward-char)
      (delete-region (point-min) (point))
      (setq buffer-file-name (concat (file-name-as-directory package-user-dir)
                                     "archive-contents.el"))
      (let ((version-control 'never))
        (save-buffer))
      (kill-buffer buffer))))

(defun package-initialize ()
  "Load all packages and activate as many as possible."
  (package-load-all-descriptors)
  ;; Read the contents of the package archive, if available.
  (load (concat (file-name-as-directory package-user-dir)
                "archive-contents")
        t)
  ;; Try to activate all our packages.
  (mapc (lambda (elt)
          (package-activate (car elt) (aref (cdr elt) 0)))
        package-alist))





reply via email to

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