#!/bin/sh ":"; # -*- mode: emacs-lisp; lexical-binding: t; -*- ":"; exec emacs --script "$0" "$@" (require 'format-spec) (require 'package) (require 'subr-x) (defvar epm-dir nil "Overrides `package-user-dir'.") (defvar epm-verbose nil) (defun epm-nonempty-p (s) (and s (not (string-empty-p s)))) (defun epm-init () (unless (epm-nonempty-p epm-dir) (setq epm-dir (getenv "EPMDIR"))) (when (epm-nonempty-p epm-dir) (let* ((fmt-expanded (format-spec epm-dir `((?e . ,emacs-version)))) (dir (directory-file-name (expand-file-name fmt-expanded)))) ;; `package-user-dir' ~/.emacs.d/elpa by default ;; `package-directory-list' does not include it (setq package-user-dir dir))) (package-initialize)) (defun epm-library-unavailable-p (lib) (unless (locate-library lib) lib)) (defun epm-missing (libs) ;; TODO consider `require' catching load errors (delq nil (mapcar #'epm-library-unavailable-p libs))) (defun epm-cmd-help (_cmd _args) "List commands." (princ "Usage: epm [--dbg|--debug-on-error] [--epm-dir] COMMAND ARGS... CLI tool to install ELPA packages. Any Emacs option may be specified, e.g. --quck,-Q or --directory,-L DIR --dbg, --debug-on-error Enable `debug-on-error' --epm-dir DIR Set `package-user-dir'. \"%e\" is replaced by `emacs-version'. Alternatively EPMDIR environment may be specified. \n") (pcase-dolist (`(,name . ,func) epm-commands) (princ (concat name "\n")) (princ (replace-regexp-in-string "\\`\\|\n" "\\1 " (documentation func) 'fixedcase nil)) (princ "\n\n") )) (defun epm-cmd-missing (_ libs) "Report not installed libraries and exit with non-zero code." (let ((missing (epm-missing libs))) (when missing (princ (mapconcat #'identity missing " ")) (princ "\n") (kill-emacs 1)))) (defun epm-cmd-install (_ libs) "Install packages from LIBS that are not available yet" ;; TODO force option or update command (let ((missing (epm-missing libs))) (when missing (package-refresh-contents) (make-directory package-user-dir 'parents)) (dolist (pkg missing) (package-install (intern pkg))))) (defun epm-cmd-report (_ libs) "Report paths of available libraries" (princ (format "package-user-dir: %s\n" package-user-dir)) ;; (princ (format "load-path: %s\n" load-path)) (dolist (name libs) ;; (version-to-list version) (princ (format "%-20s %s " name (if (package-installed-p (intern name)) "package " " "))) (princ (locate-library name)) (princ "\n"))) (defvar epm-commands '(("help" . epm-cmd-help) ("install" . epm-cmd-install) ("missing" . epm-cmd-missing) ("report" . epm-cmd-report))) ;; Perhaps there is a way to use `command-switch-alist'. (defun epm-args-parse (arg-list) (let ((parse-opts t) unprocessed cmd) (while (and arg-list (or parse-opts (not cmd))) (pcase (pop arg-list) ("--" (setq parse-opts nil)) ((and (guard parse-opts) ;; otherwise processed after script exit (or "-L" (pred (lambda (x) (string-prefix-p x "--directory"))))) (push (expand-file-name (command-line-normalize-file-name (pop arg-list))) load-path)) ((and (guard parse-opts) "--epm-dir") (setq epm-dir (pop arg-list))) ((and (guard parse-opts) (or "--dbg" "--debug-on-error")) ;; -d is handled as --display, --debug as --debug-init (setq debug-on-error t)) ((and (guard (not cmd)) (pred (string-match-p "\\`[^-]")) arg) (push arg cmd) (unless parse-opts (push "--" cmd))) (arg (if cmd (push arg cmd) (push arg unprocessed))))) (cons (nreverse cmd) (nreverse unprocessed)))) (pcase-let ((`(,cmd . ,unprocessed) (epm-args-parse command-line-args-left))) (unless (setq command-line-args-left unprocessed) (epm-init) (let ((func (cdr (assoc (car cmd) epm-commands)))) (if func (funcall func (car cmd) (cdr cmd)) (error "Unknown command %s" (car cmd))))))