bug#81091: package-selected-packages not updated after init
Mark Diekhans via "Bug reports for GNU Emacs, the Swiss army knife of text editors" <[email protected]> Fri, 31 Jul 2026 06:13:31 -0700
| Newsgroups | gmane.emacs.bugs |
|---|---|
| Message-ID | <[email protected]> |
I am encountering this problem as well. Below is the Claude
Code analysis and suggest bug report. I confirm the findings.
-----------
I hit this on Emacs 30.2 (macOS/MacPorts), and I think the bug is worse
than the subject line suggests: it does not merely fail to record new
state, it silently destroys an existing, non-empty
`package-selected-packages' that was already saved in custom-file.
Still present on master as of 698776764d8 (2026-07-31),
lisp/emacs-lisp/package.el:1853.
Sequence, as observed:
1. `use-package' with `:ensure t' installs a missing package during
init. `package-install' calls `package--save-selected-packages'
with the new list. `after-init-time' is nil, so it sets the
variable correctly and defers the save by adding itself to
`after-init-hook'.
2. startup.el sets `after-init-time' (startup.el:1566) *before* running
`after-init-hook' (startup.el:1570).
3. The hook calls `package--save-selected-packages' with no arguments.
VALUE is nil, but `after-init-time' is now non-nil, so the
`(or value after-init-time)' guard passes and the function executes
(setq package-selected-packages (sort nil #'string<)) => nil,
then `customize-save-variable' writes
'(package-selected-packages nil)
into custom-file, replacing the 77 entries that were there.
The comment on that guard says "It is valid to set it to nil, for
example when the last package is uninstalled. But it shouldn't be done
at init time, to avoid overwriting configurations that haven't yet been
loaded." The argument-less hook call defeats exactly that intent,
because by the time the hook runs `after-init-time' is already set.
The consequence that makes this more than cosmetic: on the next
invocation, `package-autoremove' sees an empty selection list, so
`package--removable-packages' returns *every* installed package. In my
case it prompted
Packages to delete: 89 (async ess flycheck ghostel gptel helm ...), proceed? (y or n)
i.e. one keystroke away from deleting the entire installation. I
answered n, which is how I noticed the custom-file had been rewritten.
Recipe from a clean state. A nonexistent package is enough, because the
save happens before the download transaction in `package-install' (it
will still refresh the archives first, so this does contact the network):
$ mkdir -p /tmp/ed
$ cat > /tmp/ed/init.el <<'EOF'
(setq custom-file "/tmp/ed/custom.el")
(load custom-file)
(condition-case nil (package-install 'zzz-no-such-package) (error nil))
EOF
$ cat > /tmp/ed/custom.el <<'EOF'
(custom-set-variables '(package-selected-packages '(aaa bbb ccc)))
EOF
$ emacs --init-directory=/tmp/ed
;; then look at /tmp/ed/custom.el:
;; '(package-selected-packages nil)
Cosimo's patch in this report fixes it; I would like to see it applied.
Separating the hook function from the value-taking function is the right
shape, since the whole problem is that one function is serving as both a
setter and a zero-argument hook.
As a stopgap for anyone else who lands here, this advice prevents the
data loss without touching package.el:
(advice-add 'package--save-selected-packages :around
(lambda (orig &optional value)
(if (and (null value) package-selected-packages)
(message "ignoring attempt to clear package-selected-packages")
(funcall orig value))))
In GNU Emacs 30.2, built on macOS.