How Package.el Works with Use Package

I suppose the below could be framed simply as a complaint that use-package does not update package-selected-packages. Fair enough.

I've recently moved from Straight to package.el for package management (long story), and have been reading-up on how package.el actually works, I took some notes and figured I'd share. I'm surprised that so few configs I see on GitHub take into account the subtleties of how package.el works, particularly in how it interacts with Use Package.

Unsurprisingly, all of this is well documented in (info "(elisp) Startup Summary") and (info "(emacs) Packages") so I'm not writing anything groundbreaking here, just summarizing available information.

Emacs activates all installed packages before reading the user-init-file unless you've set package-enable-at-startup to nil in the early init file. I'm sure this activation does many things but the most important (to me) is that it creates autoloads for all your packages. That way you can bind a key to a package's command, or run it via M-x, without actually loading the package first.

If you want to restrict exactly which installed packages are activated at startup you can customize the package-load-list, but you have to do it before your packages are activated.

When you interactively install a package (via M-x package-install or via the Package Menu) Emacs adds that package to package-selected-packages in your custom-file, which is supposed to be the canonical list of packages you'd like installed. package.el includes commands for installing all your "selected" packages, and removing any installed package that is not "selected".

Use Package does not update package-selected-packages, nor does it have the ability to delete installed packages. This means every package you've ever installed via Use Package's :ensure function is activated when Emacs starts up, even if you've removed all traces of it from your init file. And to remove the packages from your system you'll have to manually compare what's installed against what's in your init file.

If you've disabled package-enable-at-startup in your early init file and use Straight (or something similar) to install packages, you won't have this problem as Straight is designed to never load anything not declared in your init file, regardless of if it is installed or not.

What I learned…

If you're using an alternative to package.el such as Straight, don't forget to place (setq package-enable-at-startup nil) in your early init file.

If you're using Use Package with package.el, and you want to manage your packages via your init file and not via interactive customization, you should really consider manually maintaining package-selected-packages somewhere in your init file, and abstaining from Use Package's :ensure feature. Think of Use Package strictly as a way to configure packages, not installing them. This will allow you to take advantage of package.el's package management features.

The following is everything you need to get your packages installed and configured using package.el + Use Package:

(require 'package)

;; (package-initialize) is unnecessary since I have not disabled
;; package-enable-at-startup in early-init.

;; Add MELPA to package sources (optional)
(add-to-list 'package-archives '("melpa" . "") t)

;; Prefer GNU over MELPA (optional)
(setq package-archive-priorities '(("gnu" . 20)("melpa" . 10)))

(setq package-selected-packages

;; Install packages with (package-install-selected-packages)
;; Remove packages with (package-autoremove)
;; If you want to automate that, maybe add them to your 'emacs-startup-hook'?

  (require 'use-package))

(use-package orderless
  (completion-styles '(orderless))
  (completion-category-defaults nil)
  (completion-category-overrides '((file (styles . (partial-completion))))))

(use-package vertico
  :init (vertico-mode))

To install a new package, simply add it your package-selected-packages list and call (package-install-selected-packages).

Hope this is useful to someone.

Discussion on r/emacs.