doomemacs/core/autoload/packages.el
Henrik Lissner 51d3b1b424
💥 revise advice naming convention (1/2)
This is first of three big naming convention updates that have been a
long time coming. With 2.1 on the horizon, all the breaking updates will
batched together in preparation for the long haul.

In this commit, we do away with the asterix to communicate that a
function is an advice function, and we replace it with the '-a' suffix.
e.g.

  doom*shut-up -> doom-shut-up-a
  doom*recenter -> doom-recenter-a
  +evil*static-reindent -> +evil--static-reindent-a

The rationale behind this change is:

1. Elisp's own formatting/indenting tools would occasionally struggle
   with | and * (particularly pp and cl-prettyprint). They have no
   problem with / and :, fortunately.
2. External syntax highlighters (like pygmentize, discord markdown or
   github markdown) struggle with it, sometimes refusing to highlight
   code beyond these symbols.
3. * and | are less expressive than - and -- in communicating the
   intended visibility, versatility and stability of a function.
4. It complicated the regexps we must use to search for them.
5. They were arbitrary and over-complicated to begin with, decided
   on haphazardly way back when Doom was simply "my private config".

Anyhow, like how predicate functions have the -p suffix, we'll adopt the
-a suffix for advice functions, -h for hook functions and -fn for
variable functions.

Other noteable changes:
- Replaces advice-{add,remove}! macro with new def-advice!
  macro. The old pair weren't as useful. The new def-advice! saves on a
  lot of space.
- Removed "stage" assertions to make sure you were using the right
  macros in the right place. Turned out to not be necessary, we'll
  employ better checks later.
2019-07-22 02:27:45 +02:00

625 lines
24 KiB
EmacsLisp

;;; core/autoload/packages.el -*- lexical-binding: t; -*-
(require 'core-packages)
(load! "cache") ; in case autoloads haven't been generated yet
(defun doom--packages-choose (prompt)
(let ((table (cl-loop for pkg in package-alist
unless (doom-package-built-in-p (cdr pkg))
collect (cons (package-desc-full-name (cdr pkg))
(cdr pkg)))))
(cdr (assoc (completing-read prompt
(mapcar #'car table)
nil t)
table))))
(defun doom--refresh-pkg-cache ()
"Clear the cache for `doom-refresh-packages-maybe'."
(setq doom--refreshed-p nil)
(doom-cache-set 'last-pkg-refresh nil))
;;;###autoload
(defun doom-refresh-packages-maybe (&optional force-p)
"Refresh ELPA packages, if it hasn't been refreshed recently."
(when force-p
(doom--refresh-pkg-cache))
(unless (or (doom-cache-get 'last-pkg-refresh)
doom--refreshed-p)
(condition-case e
(progn
(message "Refreshing package archives")
(package-refresh-contents)
(doom-cache-set 'last-pkg-refresh t 1200))
((debug error)
(doom--refresh-pkg-cache)
(signal 'doom-error e)))))
;;
;;; Package metadata
;;;###autoload
(defun doom-package-plist (package)
"Returns PACKAGE's `package!' recipe from `doom-packages'."
(cdr (assq package doom-packages)))
;;;###autoload
(defun doom-package-desc (package)
"Returns PACKAGE's desc struct from `package-alist'."
(cadr (assq (or (car (doom-package-prop package :recipe))
package)
package-alist)))
;;;###autoload
(defun doom-package-true-name (package)
"Return PACKAGE's true name.
It is possible for quelpa packages to be given a psuedonym (the first argument
of `package!'). Its real name is the car of package's :recipe. e.g.
(package! X :recipe (Y :fetcher github :repo \"abc/def\"))
X's real name is Y."
(let ((sym (car (doom-package-prop package :recipe))))
(or (and (symbolp sym)
(not (keywordp sym))
sym)
package)))
;;;###autoload
(defun doom-package-psuedo-name (package)
"TODO"
(or (cl-loop for (package . plist) in doom-packages
for recipe-name = (car (plist-get plist :recipe))
if (eq recipe-name package)
return recipe-name)
package))
;;;###autoload
(defun doom-package-backend (package &optional noerror)
"Return backend that PACKAGE was installed with.
Can either be elpa, quelpa or emacs (built-in). Throws an error if NOERROR is
nil and the package isn't installed.
See `doom-package-recipe-backend' to get the backend PACKAGE is registered with
\(as opposed to what it is was installed with)."
(cl-check-type package symbol)
(let ((package-truename (doom-package-true-name package)))
(cond ((assq package-truename quelpa-cache) 'quelpa)
((assq package-truename package-alist) 'elpa)
((doom-package-built-in-p package) 'emacs)
((not noerror) (error "%s package is not installed" package)))))
;;;###autoload
(defun doom-package-recipe-backend (package &optional noerror)
"Return backend that PACKAGE is registered with.
See `doom-package-backend' to get backend for currently installed package."
(cl-check-type package symbol)
(cond ((not (doom-package-registered-p package))
(unless noerror
(error "%s package is not registered" package)))
((let ((builtin (eval (doom-package-prop package :built-in) t)))
(or (and (eq builtin 'prefer)
(locate-library (symbol-name package) nil doom-site-load-path))
(eq builtin 't)))
'emacs)
((doom-package-prop package :recipe)
'quelpa)
('elpa)))
;;;###autoload
(defun doom-package-prop (package prop &optional nil-value)
"Return PROPerty in PACKAGE's plist.
Otherwise returns NIL-VALUE if package isn't registered or PROP doesn't
exist/isn't specified."
(cl-check-type package symbol)
(cl-check-type prop keyword)
(if-let (plist (doom-package-plist package))
(if (plist-member plist prop)
(plist-get plist prop)
nil-value)
nil-value))
;;
;;; Predicate functions
;;;###autoload
(defun doom-package-built-in-p (package)
"Return non-nil if PACKAGE (a symbol) is built-in."
(unless (doom-package-installed-p package)
(or (package-built-in-p (doom-package-true-name package))
(locate-library (symbol-name package) nil doom-site-load-path))))
;;;###autoload
(defun doom-package-installed-p (package)
"Return non-nil if PACKAGE (a symbol) is installed."
(when-let (desc (doom-package-desc package))
(and (package-installed-p desc)
(file-directory-p (package-desc-dir desc)))))
;;;###autoload
(defun doom-package-registered-p (package)
"Return non-nil if PACKAGE (a symbol) has been registered with `package!'.
Excludes packages that have a non-nil :built-in property."
(let ((package (or (cl-loop for (pkg . plist) in doom-packages
for newname = (car (plist-get plist :recipe))
if (and (symbolp newname)
(eq newname package))
return pkg)
package)))
(when-let (plist (doom-package-plist package))
(not (eval (plist-get plist :ignore))))))
;;;###autoload
(defun doom-package-private-p (package)
"Return non-nil if PACKAGE was installed by the user's private config."
(doom-package-prop package :private))
;;;###autoload
(defun doom-package-protected-p (package)
"Return non-nil if PACKAGE is protected.
A protected package cannot be deleted and will be auto-installed if missing."
(memq (doom-package-true-name package) doom-core-packages))
;;;###autoload
(defun doom-package-core-p (package)
"Return non-nil if PACKAGE is a core Doom package."
(or (doom-package-protected-p package)
(assq :core (doom-package-prop package :modules))))
;;;###autoload
(defun doom-package-different-backend-p (package)
"Return t if a PACKAGE (a symbol) has a new backend than what it was installed
with. Returns nil otherwise, or if package isn't installed."
(cl-check-type package symbol)
(and (doom-package-installed-p package)
(not (doom-get-depending-on package)) ; not a dependency
(not (eq (doom-package-backend package 'noerror)
(doom-package-recipe-backend package 'noerror)))))
;;;###autoload
(defun doom-package-different-recipe-p (name)
"Return t if a package named NAME (a symbol) has a different recipe than it
was installed with."
(cl-check-type name symbol)
(when (doom-package-installed-p name)
(let ((package-truename (doom-package-true-name name)))
(when-let* ((quelpa-recipe (assq package-truename quelpa-cache))
(doom-recipe (assq package-truename doom-packages)))
(not (equal (cdr quelpa-recipe)
(cdr (plist-get (cdr doom-recipe) :recipe))))))))
(defvar quelpa-upgrade-p)
;;;###autoload
(defun doom-package-outdated-p (name)
"Determine whether NAME (a symbol) is outdated or not.
If outdated, returns a list, whose car is NAME, and cdr the current version list
and latest version list of the package."
(cl-check-type name symbol)
(when-let (desc (doom-package-desc name))
(let* ((old-version (package-desc-version desc))
(new-version
(pcase (doom-package-backend name)
(`quelpa
(let ((recipe (doom-package-prop name :recipe))
(dir (expand-file-name (symbol-name name) quelpa-build-dir))
(inhibit-message (not doom-debug-mode))
(quelpa-upgrade-p t))
(if-let (ver (quelpa-checkout recipe dir))
(version-to-list ver)
old-version)))
(`elpa
(let ((desc (cadr (assq name package-archive-contents))))
(when (package-desc-p desc)
(package-desc-version desc)))))))
(unless (and (listp old-version) (listp new-version))
(error "Couldn't get version for %s" name))
(when (version-list-< old-version new-version)
(list name old-version new-version)))))
;;
;;; Package list getters
;;;###autoload
(cl-defun doom-find-packages (&key (installed 'any)
(private 'any)
(disabled 'any)
(pinned 'any)
(ignored 'any)
(core 'any)
_changed
backend
deps)
"Retrieves a list of primary packages (i.e. non-dependencies). Each element is
a cons cell, whose car is the package symbol and whose cdr is the quelpa recipe
(if any).
You can build a filtering criteria using one or more of the following
properties:
:backend 'quelpa|'elpa|'emacs|'any
Include packages installed through 'quelpa, 'elpa or 'emacs. 'any is the
wildcard.
:installed BOOL|'any
t = only include installed packages
nil = exclude installed packages
:private BOOL|'any
t = only include user-installed packages
nil = exclude user-installed packages
:core BOOL|'any
t = only include Doom core packages
nil = exclude Doom core packages
:disabled BOOL|'any
t = only include disabled packages
nil = exclude disabled packages
:ignored BOOL|'any
t = only include ignored packages
nil = exclude ignored packages
:pinned BOOL|ARCHIVE
Only return packages that are pinned (t), not pinned (nil) or pinned to a
specific archive (stringp)
:deps BOOL
Includes the package's dependencies (t) or not (nil).
Warning: this function is expensive, as it re-evaluates your all packages.el
files."
(delete-dups
(cl-loop for (sym . plist) in doom-packages
if (and (or (not backend)
(eq (doom-package-backend sym 'noerror) backend))
(or (eq ignored 'any)
(let* ((form (plist-get plist :ignore))
(value (eval form)))
(if ignored value (not value))))
(or (eq disabled 'any)
(if disabled
(plist-get plist :disable)
(not (plist-get plist :disable))))
(or (eq installed 'any)
(if installed
(doom-package-installed-p sym)
(not (doom-package-installed-p sym))))
(or (eq private 'any)
(let ((modules (plist-get plist :modules)))
(if private
(assq :private modules)
(not (assq :private modules)))))
(or (eq core 'any)
(let ((modules (plist-get plist :modules)))
(if core
(assq :core modules)
(not (assq :core modules)))))
(or (eq pinned 'any)
(cond ((eq pinned 't)
(plist-get plist :pin))
((null pinned)
(not (plist-get plist :pin)))
((equal (plist-get plist :pin) pinned)))))
collect (cons sym plist)
and if (and deps (not (doom-package-built-in-p sym)))
nconc
(cl-loop for pkg in (doom-get-dependencies-for sym 'recursive 'noerror)
if (or (eq installed 'any)
(if installed
(doom-package-installed-p pkg)
(not (doom-package-installed-p pkg))))
collect (cons pkg (cdr (assq pkg doom-packages)))))))
(defun doom--read-module-packages-file (file &optional raw noerror)
(with-temp-buffer ; prevent buffer-local settings from propagating
(condition-case e
(if (not raw)
(load file noerror t t)
(when (file-readable-p file)
(insert-file-contents file)
(while (re-search-forward "(package! " nil t)
(save-excursion
(goto-char (match-beginning 0))
(cl-destructuring-bind (name . plist) (cdr (sexp-at-point))
(push (cons name
(plist-put plist :modules
(cond ((file-in-directory-p file doom-private-dir)
'((:private)))
((file-in-directory-p file doom-core-dir)
'((:core)))
((doom-module-from-path file)))))
doom-packages))))))
((debug error)
(signal 'doom-package-error
(list (or (doom-module-from-path file)
'(:private . packages))
e))))))
;;;###autoload
(defun doom-package-list (&optional all-p)
"Retrieve a list of explicitly declared packages from enabled modules.
This excludes core packages listed in `doom-core-packages'.
If ALL-P, gather packages unconditionally across all modules, including disabled
ones."
(let ((noninteractive t)
(doom-modules (doom-modules))
doom-packages
doom-disabled-packages
package-pinned-packages)
(doom--read-module-packages-file (expand-file-name "packages.el" doom-core-dir) all-p)
(let ((private-packages (expand-file-name "packages.el" doom-private-dir)))
(unless all-p
;; We load the private packages file twice to ensure disabled packages
;; are seen ASAP, and a second time to ensure privately overridden
;; packages are properly overwritten.
(doom--read-module-packages-file private-packages nil t))
(if all-p
(mapc #'doom--read-module-packages-file
(doom-files-in doom-modules-dir
:depth 2
:full t
:match "/packages\\.el$"
:sort nil))
(cl-loop for key being the hash-keys of doom-modules
for path = (doom-module-path (car key) (cdr key) "packages.el")
for doom--current-module = key
do (doom--read-module-packages-file path nil t)))
(doom--read-module-packages-file private-packages all-p t))
(append (cl-loop for package in doom-core-packages
collect (list package :modules '((:core internal))))
(nreverse doom-packages))))
;;;###autoload
(defun doom-get-package-alist ()
"Returns a list of all desired packages, their dependencies and their desc
objects, in the order of their `package! blocks.'"
(cl-remove-duplicates
(cl-loop for name in (mapcar #'car doom-packages)
if (assq name package-alist)
nconc (cl-loop for dep in (package--get-deps name)
if (assq dep package-alist)
collect (cons dep (cadr it)))
and collect (cons name (cadr it)))
:key #'car
:from-end t))
;;;###autoload
(defun doom-get-depending-on (name &optional noerror)
"Return a list of packages that depend on the package named NAME."
(cl-check-type name symbol)
(setq name (or (car (doom-package-prop name :recipe)) name))
(unless (doom-package-built-in-p name)
(if-let (desc (cadr (assq name package-alist)))
(mapcar #'package-desc-name (package--used-elsewhere-p desc nil t))
(unless noerror
(error "Couldn't find %s, is it installed?" name)))))
;;;###autoload
(defun doom-get-dependencies-for (name &optional recursive noerror)
"Return a list of dependencies for a package."
(cl-check-type name symbol)
;; can't get dependencies for built-in packages
(unless (doom-package-built-in-p name)
(if-let (desc (doom-package-desc name))
(let* ((deps (mapcar #'car (package-desc-reqs desc)))
(deps (cl-remove-if #'doom-package-built-in-p deps)))
(if recursive
(nconc deps (mapcan (lambda (dep) (doom-get-dependencies-for dep t t))
deps))
deps))
(unless noerror
(error "Couldn't find %s, is it installed?" name)))))
;;;###autoload
(defun doom-get-outdated-packages (&optional include-frozen-p)
"Return a list of packages that are out of date. Each element is a list,
containing (PACKAGE-SYMBOL OLD-VERSION-LIST NEW-VERSION-LIST).
If INCLUDE-FROZEN-P is non-nil, check frozen packages as well.
Used by `doom-packages-update'."
(doom-refresh-packages-maybe doom-debug-mode)
(cl-loop for package in (mapcar #'car package-alist)
when (and (or (not (eval (doom-package-prop package :freeze)))
include-frozen-p)
(not (eval (doom-package-prop package :ignore)))
(not (doom-package-different-backend-p package))
(doom-package-outdated-p package))
collect it))
;;;###autoload
(defun doom-get-orphaned-packages ()
"Return a list of symbols representing packages that are no longer needed or
depended on.
Used by `doom-packages-autoremove'."
(let ((package-selected-packages
(mapcar #'car (doom-find-packages :ignored nil :disabled nil))))
(append (cl-remove-if #'doom-package-registered-p (package--removable-packages))
(cl-loop for pkg in package-selected-packages
if (and (doom-package-different-backend-p pkg)
(not (doom-package-built-in-p pkg)))
collect pkg))))
;;;###autoload
(defun doom-get-missing-packages ()
"Return a list of requested packages that aren't installed or built-in, but
are enabled (with a `package!' directive). Each element is a list whose CAR is
the package symbol, and whose CDR is a plist taken from that package's
`package!' declaration.
Used by `doom-packages-install'."
(cl-loop for (name . plist)
in (doom-find-packages :ignored nil
:disabled nil
:deps t)
if (and (equal (plist-get plist :pin)
(ignore-errors
(package-desc-archive
(cadr (assq name package-alist)))))
(or (not (doom-package-installed-p name))
(doom-package-different-backend-p name)
(doom-package-different-recipe-p name)))
collect (cons name plist)))
;;
;; Main functions
(defun doom--delete-package-files (name-or-desc)
(let ((pkg-build-dir
(if (package-desc-p name-or-desc)
(package-desc-dir name-or-desc)
(expand-file-name (symbol-name name-or-desc) quelpa-build-dir))))
(when (file-directory-p pkg-build-dir)
(delete-directory pkg-build-dir t))))
;;;###autoload
(defun doom-install-package (name &optional plist)
"Installs package NAME with optional quelpa RECIPE (see `quelpa-recipe' for an
example; the package name can be omitted)."
(cl-check-type name symbol)
(when (and (doom-package-installed-p name)
(not (doom-package-built-in-p name)))
(if (or (doom-package-different-backend-p name)
(doom-package-different-recipe-p name))
(doom-delete-package name t)
(user-error "%s is already installed" name)))
(let* ((inhibit-message (not doom-debug-mode))
(plist (or plist (doom-package-plist name))))
(if-let (recipe (plist-get plist :recipe))
(condition-case e
(let (quelpa-upgrade-p)
(quelpa recipe))
((debug error)
(doom--delete-package-files name)
(signal (car e) (cdr e))))
(package-install name))
(if (not (doom-package-installed-p name))
(doom--delete-package-files name)
(add-to-list 'package-selected-packages name nil 'eq)
(setf (alist-get name doom-packages) plist)
name)))
;;;###autoload
(defun doom-update-package (name &optional force-p)
"Updates package NAME (a symbol) if it is out of date, using quelpa or
package.el as appropriate."
(cl-check-type name symbol)
(unless (doom-package-installed-p name)
(error "%s isn't installed" name))
(when (doom-package-different-backend-p name)
(user-error "%s's backend has changed and must be uninstalled first" name))
(when (or force-p (doom-package-outdated-p name))
(let ((inhibit-message (not doom-debug-mode))
(desc (doom-package-desc name)))
(pcase (doom-package-backend name)
(`quelpa
(let ((name (doom-package-true-name name)))
(condition-case e
(let ((quelpa-upgrade-p t))
(quelpa (assq name quelpa-cache)))
((debug error)
(doom--delete-package-files name)
(signal (car e) (cdr e))))))
(`elpa
(let* ((archive (cadr (assq name package-archive-contents)))
(packages
(if (package-desc-p archive)
(package-compute-transaction (list archive) (package-desc-reqs archive))
(package-compute-transaction () (list (list archive))))))
(package-download-transaction packages))))
(unless (doom-package-outdated-p name)
(doom--delete-package-files desc)
t))))
;;;###autoload
(defun doom-delete-package (name &optional force-p)
"Uninstalls package NAME if it exists, and clears it from `quelpa-cache'."
(cl-check-type name symbol)
(unless (doom-package-installed-p name)
(user-error "%s isn't installed" name))
(let ((inhibit-message (not doom-debug-mode))
(name (doom-package-true-name name)))
(when-let (spec (assq name quelpa-cache))
(delq! spec quelpa-cache)
(quelpa-save-cache))
(package-delete (doom-package-desc name) force-p)
(doom--delete-package-files name)
(not (doom-package-installed-p name))))
;;
;; Interactive commands
;;;###autoload
(defun doom/reload-packages ()
"Reload `doom-packages', `package' and `quelpa'."
(interactive)
(message "Reloading packages")
(doom-initialize-packages t)
(message "Reloading packages...DONE"))
;;;###autoload
(defun doom/update-package (pkg)
"Prompts the user with a list of outdated packages and updates the selected
package. Use this interactively. Use `doom-update-package' for direct
calls."
(declare (interactive-only t))
(interactive
(let* ((packages (doom-get-outdated-packages))
(selection (if packages
(completing-read "Update package: "
(mapcar #'car packages)
nil t)
(user-error "All packages are up to date")))
(name (car (assoc (intern selection) package-alist))))
(unless name
(user-error "'%s' is already up-to-date" selection))
(list (assq name packages))))
(cl-destructuring-bind (package old-version new-version) pkg
(if-let (desc (doom-package-outdated-p package))
(let ((old-v-str (package-version-join old-version))
(new-v-str (package-version-join new-version)))
(if (y-or-n-p (format "%s will be updated from %s to %s. Update?"
package old-v-str new-v-str))
(message "%s %s (%s => %s)"
(if (doom-update-package package t) "Updated" "Failed to update")
package old-v-str new-v-str)
(message "Aborted")))
(message "%s is up-to-date" package))))
;;
;; Advice
;;;###autoload
(defun doom*package-delete (desc &rest _)
"Update `quelpa-cache' upon a successful `package-delete'."
(let ((name (package-desc-name desc)))
(unless (doom-package-installed-p name)
(when-let (spec (assq name quelpa-cache))
(setq quelpa-cache (delq spec quelpa-cache))
(quelpa-save-cache)
(doom--delete-package-files name)))))
;;
;; Make package.el cooperate with Doom
;; Updates QUELPA after deleting a package
;;;###autoload
(advice-add #'package-delete :after #'doom*package-delete)
;; Replace with Doom variants
;;;###autoload
(advice-add #'package-autoremove :override #'doom//autoremove)
;;;###autoload
(advice-add #'package-install-selected-packages :override #'doom//install)