Emacs Package Archive Statistics

I use cask for managing the dependencies of my Emacs configuration. Whenever I opened my Cask file, I wondered if I really was using all the sources I had defined:

(source gnu)
(source marmalade)
(source melpa)
(source melpa-stable)
(source org)

It seemed quite strange that we have so many package repositories in the Emacs world and I’m not even using all of them. I find this state less than ideal, much as Jorgen Schäfer details. My ideal package repository would be once that works with VCS releases, mostly because it’s a much simpler process to work with than having to sign up to yet another website just to upload a package, then ensure it’s kept up-to-date on every release.

As such, I prefer the concepts behing MELPA and MELPA Stable to those of Marmalade. GNU ELPA doesn’t appear to allow any submissions and org is specific to org-mode. I’ve also noticed that many packages I find and use are on github and so work with the MELPA system. However, I don’t like MELPA’s versioning: it just gets the latest code and puts the build date in the version, meaning that packages could break at any time.

So, ideally I would use MELPA Stable as much as possible and reduce my usage of Marmalade and MELPA. GNU ELPA doesn’t appear to have many packages, but I wasn’t sure if I was using any. I couldn’t see the information listed in the *Packages* buffer, so I decided to try to figure out how to generate some usage statistics.

I found how to get a list of installed packages, but that just gives a list:

(ace-jump-mode ag auto-compile auto-indent-mode autopair ...)

I needed to get more information about those packages. I looked at where list-packages gets that information from. It seems that package-archive-contents is a list of cons cells:

(org-plus-contrib .
				  [(20140714)
				  nil "Outline-based notes management and organizer" tar "org"])

Then created a function to loop over the contents of package-activated-list, retrieving the corresponding contents of package-archive-contents:

(defun package-list-installed ()
  (loop for pkg in package-activated-list
        collect (assq pkg package-archive-contents)))

This generates a list of arrays from package-archive-contents. There are some helper functions in package.el such as package-desc-kind. package-desc-archive was exactly what I needed. I happened to be using a pretest version of Emacs at the time and didn’t know that it’s not in 24.3, so I just made sure it was defined:

(if (not (fboundp #'package-desc-archive))
    (defsubst package-desc-archive (desc)
      (aref desc (1- (length desc)))))

Weirdly, some of the arrays (seemingly the ones from the org archive) had a different length, but the repository/archive was always the last element, which is why I used (1- (length )) and not a constant, like the other package-desc-* functions.

To generate a list of statistics, I just needed to loop over the installed packages from package-list-installed and update a count for each archive:

(defun package-archive-stats ()
  (let ((archives (makehash))
        (assoc '()))
    (dolist (arc package-archives)
      (puthash (car arc) 0 archives))
    (maphash (lambda (k v)
               (setq assoc (cons (cons k v) assoc)))
             (dolist (pkg (-filter #'identity (package-list-installed)) archives)
               (let ((pkg-arc (package-desc-archive (cdr pkg))))
                 (incf (gethash pkg-arc archives)))))
    assoc))

Running this gives a list of cons cells:

(("gnu" . 0)
 ("org" . 1)
 ("melpa-stable" . 2)
 ("melpa" . 106)
 ("marmalade" . 1))

I wrapped it in an interactive function so that I could check the numbers quickly:

(defun package-show-archive-stats ()
  (interactive)
  (message "%s" (package-archive-stats)))

With that, I removed (source gnu) from my Cask file. Now I had another question. What package was installed from marmalade? In the lisp fashion, I created yet another function:

(defun package-show-installed-from-archive (archive)
  (interactive (list (helm-comp-read "Archive: " (mapcar #'car package-archives)
                                      :must-match t)))
  (let ((from-arc (mapcar #'car
                          (--filter (equalp (package-desc-archive (cdr it)) archive)
                                    (package-list-installed)))))
    (if (called-interactively-p)
        (message "%s" from-arc)
      from-arc)))

(Non-helm users can replace helm-comp-read with ido-completing-read or similar)

Running this with the argument "marmalade" gives:

(php-extras)

I checked on MELPA Stable and MELPA, but it’s not available there. Given that I use php-extras quite a bit at work, I can’t remove marmalade just yet. However, as it’s a git repository, it should be easy for me to create a recipe for MELPA. Then I can remove marmalade from my cask configuration. Hooray for simplification!

Hopefully, packaging in Emacs will become simpler in the future. There are some interesting things in 24.4 like pinning packages to a repository, which would allow MELPA Stable to be used even when MELPA defines the same package with a higher “version”.

Tags: