doomemacs/core/core-packages.el
Henrik Lissner f3a9d0c03c
Resolve pinned packages based on overridden recipes
And rearrange "Checking out ..." message during 'doom upgrade' to
display package first, then commit (and abbreviated), rather than the
other around. It's easier to read.
2020-01-14 21:57:58 -05:00

334 lines
14 KiB
EmacsLisp

;;; core/core-packages.el -*- lexical-binding: t; -*-
;; Emacs package management is opinionated, and so is Doom. Doom uses `straight'
;; to create a declarative, lazy-loaded and optionally rolling-release package
;; management system. We use `straight' over `package' because the latter is
;; tempermental. ELPA sources suffer downtime occasionally, and often fail at
;; building some packages when GNU Tar is unavailable (e.g. MacOS users start
;; with BSD tar). There are also known gnutls errors in the current stable
;; release of Emacs (26.x) which bork TLS handshakes with ELPA repos (mainly
;; gnu.elpa.org). See https://debbugs.gnu.org/cgi/bugreport.cgi?bug=3434.
;;
;; What's worse, you can only get the latest version of packages through ELPA.
;; In an ecosystem that is constantly changing, this is more frustrating than
;; convenient. Straight (and Doom) can do rolling release, but it is optional
;; (and will eventually be opt-in).
;;
;; ANyhow, interacting with this package management system is done through the
;; bin/doom script included with Doom Emacs. You'll find more about it by
;; running 'doom help' (I highly recommend you add it to your PATH), but here
;; are the highlights:
;;
;; + `bin/doom install`: a wizard that guides you through setting up Doom and
;; your private config for the first time.
;; + `bin/doom refresh`: your go-to command for making sure Doom is in optimal
;; condition. It ensures all unneeded packages are removed, all needed ones
;; are installed, and all metadata associated with them is generated.
;; + `bin/doom upgrade`: upgrades Doom Emacs and your packages to the latest
;; versions. There's also 'bin/doom update' for updating only your packages.
;;
;; How this works is: the system reads packages.el files located in each
;; activated module, your private directory (`doom-private-dir'), and one in
;; `doom-core-dir'. These contain `package!' declarations that tell DOOM what
;; plugins to install and where from.
;;
;; All that said, you can still use package.el's commands, but 'bin/doom
;; refresh' will purge ELPA packages.
(defvar doom-init-packages-p nil
"If non-nil, Doom's package management system has been initialized.")
(defvar doom-packages ()
"A list of enabled packages. Each element is a sublist, whose CAR is the
package's name as a symbol, and whose CDR is the plist supplied to its
`package!' declaration. Set by `doom-initialize-packages'.")
(defvar doom-pinned-packages nil
"An alist mapping package names to commit hashes; both strings.
We avoid straight's lockfiles because we want to pin packages straight from
their `package!' declarations, which is simpler than lockfiles, where version
management would be done in a whole new file that users shouldn't have to deal
with.")
(defvar doom-core-packages '(straight use-package)
"A list of packages that must be installed (and will be auto-installed if
missing) and shouldn't be deleted.")
(defvar doom-core-package-sources
'((org-elpa :local-repo nil)
(melpa
:type git :host github
:repo "melpa/melpa"
:no-build t)
(gnu-elpa-mirror
:type git :host github
:repo "emacs-straight/gnu-elpa-mirror"
:no-build t)
(emacsmirror-mirror
:type git :host github
:repo "emacs-straight/emacsmirror-mirror"
:no-build t))
"A list of recipes for straight's recipe repos.")
(defvar doom-disabled-packages ()
"A list of packages that should be ignored by `use-package!' and `after!'.")
;;
;;; Package managers
;; Ensure that, if we do need package.el, it is configured correctly. You really
;; shouldn't be using it, but it may be convenient for quick package testing.
(setq package-enable-at-startup nil
package-user-dir (concat doom-local-dir "elpa/")
package-gnupghome-dir (expand-file-name "gpg" package-user-dir)
;; I omit Marmalade because its packages are manually submitted rather
;; than pulled, so packages are often out of date with upstream.
package-archives
(let ((proto (if gnutls-verify-error "https" "http")))
`(("gnu" . ,(concat proto "://elpa.gnu.org/packages/"))
("melpa" . ,(concat proto "://melpa.org/packages/"))
("org" . ,(concat proto "://orgmode.org/elpa/")))))
(advice-add #'package--ensure-init-file :override #'ignore)
;; Don't save `package-selected-packages' to `custom-file'
(defadvice! doom--package-inhibit-custom-file-a (&optional value)
:override #'package--save-selected-packages
(if value (setq package-selected-packages value)))
;; Refresh package.el the first time you call `package-install'
(add-transient-hook! 'package-install (package-refresh-contents))
;;; straight
(setq straight-base-dir doom-local-dir
straight-repository-branch "develop"
straight-cache-autoloads nil ; we already do this, and better.
;; Doom doesn't encourage you to modify packages in place. Disabling this
;; makes 'doom refresh' instant (once everything set up), which is much
;; nicer UX than the several seconds modification checks.
straight-check-for-modifications nil
;; We handle package.el ourselves (and a little more comprehensively)
straight-enable-package-integration nil
;; Before switching to straight, `doom-local-dir' would average out at
;; around 100mb with half Doom's modules at ~230 packages. Afterwards, at
;; around 1gb. With shallow cloning, that is reduced to ~400mb. This
;; imposes an issue with packages that require their git history for
;; certain things to work (like magit and org), but we can deal with that
;; when we cross that bridge.
straight-vc-git-default-clone-depth 1
;; Prefix declarations are unneeded bulk added to our autoloads file. Best
;; we just don't have to deal with them at all.
autoload-compute-prefixes nil
;; We handle it ourselves
straight-fix-org nil)
;;
;;; Bootstrapper
(defun doom-initialize-packages (&optional force-p)
"Ensures that Doom's package system and straight.el are initialized.
If FORCE-P is non-nil, do it anyway.
This ensure `doom-packages' is populated, if isn't aren't already. Use this
before any of straight's or Doom's package management's API to ensure all the
necessary package metadata is initialized and available for them."
(unless doom-init-packages-p
(setq force-p t))
(when (or force-p (not (bound-and-true-p package--initialized)))
(doom-log "Initializing package.el")
(require 'package)
(package-initialize))
(when (or force-p (not doom-packages))
(doom-log "Initializing straight")
(setq doom-init-packages-p t)
(unless (fboundp 'straight--reset-caches)
(doom-ensure-straight)
(require 'straight))
(straight--reset-caches)
(setq straight-recipe-repositories nil
straight-recipe-overrides nil)
(mapc #'straight-use-recipes doom-core-package-sources)
(straight-register-package
`(straight :type git :host github
:repo ,(format "%s/straight.el" straight-repository-user)
:files ("straight*.el")
:branch ,straight-repository-branch
:no-byte-compile t))
(mapc #'straight-use-package doom-core-packages)
(doom-log "Initializing doom-packages")
(setq doom-disabled-packages nil
doom-pinned-packages nil
doom-packages (doom-package-list))
(dolist (package doom-packages)
(let ((name (car package)))
(with-plist! (cdr package) (recipe modules disable ignore pin)
(if ignore
(doom-log "Ignoring package %S" name)
(if (not disable)
(with-demoted-errors "Package error: %s"
(when recipe
(straight-override-recipe (cons name recipe)))
(straight-register-package name))
(doom-log "Disabling package %S" name)
(cl-pushnew name doom-disabled-packages)
;; Warn about disabled core packages
(when (cl-find :core modules :key #'car)
(print! (warn "%s\n%s")
(format "You've disabled %S" name)
(indent 2 (concat "This is a core package. Disabling it will cause errors, as Doom assumes\n"
"core packages are always available. Disable their minor-modes or hooks instead.")))))
(when pin
(let ((realname
(if-let* ((recipe (cdr (straight-recipes-retrieve name)))
(repo (straight-vc-local-repo-name recipe)))
repo
(symbol-name name))))
(doom-log "Pinning package %S to %S" realname pin)
(setf (alist-get realname doom-pinned-packages
nil nil #'equal)
pin)))))))))
(defun doom-ensure-straight ()
"Ensure `straight' is installed and was compiled with this version of Emacs."
(defvar bootstrap-version)
(let* (;; Force straight to install into ~/.emacs.d/.local/straight instead of
;; ~/.emacs.d/straight by pretending `doom-local-dir' is our .emacs.d.
(user-emacs-directory straight-base-dir)
(bootstrap-file (doom-path straight-base-dir "straight/repos/straight.el/straight.el"))
(bootstrap-version 5))
(make-directory (doom-path straight-base-dir "straight/build") 'parents)
(unless (featurep 'straight)
(unless (or (require 'straight nil t)
(file-readable-p bootstrap-file))
(with-current-buffer
(url-retrieve-synchronously
(format "https://raw.githubusercontent.com/raxod502/straight.el/%s/install.el"
straight-repository-branch)
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil t))))
;;
;;; Module package macros
(cl-defmacro package!
(name &rest plist &key built-in recipe ignore _pin _disable)
"Declares a package and how to install it (if applicable).
This macro is declarative and does not load nor install packages. It is used to
populate `doom-packages' with metadata about the packages Doom needs to keep
track of.
Only use this macro in a module's packages.el file.
Accepts the following properties:
:recipe RECIPE
Specifies a straight.el recipe to allow you to acquire packages from external
sources. See https://github.com/raxod502/straight.el#the-recipe-format for
details on this recipe.
:disable BOOL
Do not install or update this package AND disable all of its `use-package!'
and `after!' blocks.
:ignore FORM
Do not install this package.
:pin STR|nil
(NOT IMPLEMENTED YET)
Pin this package to commit hash STR. Setting this to nil will unpin this
package if previously pinned.
:built-in BOOL|'prefer
Same as :ignore if the package is a built-in Emacs package. This is more to
inform help commands like `doom/help-packages' that this is a built-in
package. If set to 'prefer, the package will not be installed if it is
already provided by Emacs.
Returns t if package is successfully registered, and nil if it was disabled
elsewhere."
(declare (indent defun))
(when (and recipe (keywordp (car-safe recipe)))
(plist-put! plist :recipe `(quote ,recipe)))
;; :built-in t is basically an alias for :ignore (locate-library NAME)
(when built-in
(when (and (not ignore)
(equal built-in '(quote prefer)))
(setq built-in `(locate-library ,(symbol-name name) nil doom--initial-load-path)))
(plist-delete! plist :built-in)
(plist-put! plist :ignore built-in))
`(let* ((name ',name)
(plist (cdr (assq name doom-packages))))
;; Record what module this declaration was found in
(let ((module-list (plist-get plist :modules))
(module ',(doom-module-from-path)))
(unless (member module module-list)
(plist-put! plist :modules
(append module-list
(list module)
nil))))
;; Merge given plist with pre-existing one
(doplist! ((prop val) (list ,@plist) plist)
(unless (null val)
(plist-put! plist prop val)))
;; Some basic key validation; error if you're not using a valid key
(condition-case e
(when-let (recipe (plist-get plist :recipe))
(cl-destructuring-bind
(&key local-repo _files _flavor
_no-build _no-byte-compile _no-autoloads
_type _repo _host _branch _remote _nonrecursive _fork _depth)
recipe
;; Expand :local-repo from current directory
(when local-repo
(plist-put! plist :recipe
(plist-put recipe :local-repo
(expand-file-name local-repo ,(dir!)))))))
(error
(signal 'doom-package-error
(cons ,(symbol-name name)
(error-message-string e)))))
;; This is the only side-effect of this macro!
(setf (alist-get name doom-packages) plist)
(with-no-warnings
(not (plist-get plist :disable)))))
(defmacro disable-packages! (&rest packages)
"A convenience macro for disabling packages in bulk.
Only use this macro in a module's (or your private) packages.el file."
(macroexp-progn
(cl-loop for p in packages
collect `(package! ,p :disable t))))
(defmacro unpin! (&rest targets)
"Unpin packages in TARGETS.
Elements in TARGETS can be package names (symbols), a list consisting of
(CATEGORY MODULE) where MODULE is optional, or the boolean `t'.
Each package in this list is unpinned, which means its latest version will be
installed next time you run 'doom upgrade'. If you specify a (CATEGORY), all
packages in all modules in that category will be unpinned. If you specify
(CATEGORY MODULE), only packages in that particular module will be unpinned.
Lastly, a value of `t' means unpin all packages."
(dolist (target targets)
(cond
((eq target t)
(setq doom-pinned-packages nil))
((listp target)
(cl-destructuring-bind (category &optional module) target
(dolist (pkg doom-packages)
(when-let (mod (assq category (plist-get (cdr pkg) :modules)))
(and (or (null module)
(eq (cdr mod) module))
(assq-delete-all (car pkg) doom-pinned-packages))))))
((symbolp target)
(assq-delete-all target doom-pinned-packages)))))
(provide 'core-packages)
;;; core-packages.el ends here