Emacs config without use-package - an experiment
Motivation
For this one I got inspired by someone else. A little while ago I saw a YouTube stream by System Crafters called Do we really need use-package in Emacs?.
Since starting out with Emacs roughly 2 years ago, I have been using use-package
. Probably because most of the resources that helped me to learn used it. I never questioned it. To be quite frank, I never had a reason to do. For me it does what it promises.
The use-package macro allows you to isolate package configuration in your .emacs file in a way that is both performance-oriented and, well, tidy.
The only reason to even think of an Emacs configuration without use-package
is just the age old question: How hard can it be? In other words, I see an idea and I want to try it out.
Ok, there is an added benefit for me in that I get to learn more about the innards of Emacs. And it is fun.
Let’s get to it then…
The process
The main tool is the function emacs-lisp-macroexpand
. This expands any block with use-package
in my configuration into emacs-lisp
code with Emacs built-in functions, command & co. This is not 100% but with some doc-digging even I managed to do some cleanup.
The effective changes can be found in these commits:
- e477258c - Add custom package installation settings
- 94aeb4eb - Remove ensure keyword from use-package blocks
- 5d130ee2 - Remove use-package blocks
Installing packages
First things first thought. I use the keyword :ensure
quite a lot to make sure, that needed packages are installed. I needed to find a solution for that.
I decided that I wanted to install all the packages at once if these are not already installed. With some search-fu and a little lift-fu I now use the following snippets.
- early-init.el:
(defvar timu-package-list
'(package-1
package-2
...
...)
"List of packages to be installed for the Emacs config to work as configured")
- init.el:
;;; setup package installation
;; credit: https://github.com/bbatsov/prelude
(defun timu/packages-installed-p ()
"Check if all packages in `timu-package-list' are installed."
(cl-every #'package-installed-p timu-package-list))
(defun timu/require-package (package)
"Install PACKAGE unless already installed."
(unless (memq package timu-package-list)
(add-to-list 'timu-package-list package))
(unless (package-installed-p package)
(package-install package)))
(defun timu/require-packages (packages)
"Ensure PACKAGES are installed.
Missing packages are installed automatically."
(mapc #'timu/require-package packages))
(defun timu/install-packages ()
"Install all packages listed in `timu-package-list'."
(unless (timu/packages-installed-p)
;; check for new packages (package versions)
(message "%s" "Reloading packages DB...")
(package-refresh-contents)
(message "%s" " done.")
;; install the missing packages
(timu/require-packages timu-package-list)))
;; run package installation
(timu/install-packages)
swapping use-package for require
Let us use my Dired config for illustration here. The following block …
(use-package dired
:custom
(dired-recursive-copies 'always)
(dired-isearch-filenames 'dwim)
(dired-listing-switches "-Ahlp")
(dired-dwim-target t)
:hook
(dired-mode . hl-line-mode)
(dired-mode . dired-hide-details-mode)
(dired-mode . diredfl-mode)
:init
(require 'dired-x))
… got translated into this.
(require 'dired-x)
(require 'dired)
(setq dired-recursive-copies 'always)
(setq dired-isearch-filenames 'dwim)
(setq dired-listing-switches "-Ahlp")
(setq dired-dwim-target t)
(add-hook 'dired-mode-hook 'hl-line-mode)
(add-hook 'dired-mode-hook 'dired-hide-details-mode)
(add-hook 'dired-mode-hook 'diredfl-mode)
Using setq
instead of the expressions with the :custom
keyword might be not entirely correct here. I just approached it like it was a :config
keyword. It does seem to work for me. Rewriting hooks was reasonably straight forward though. To handle the :init
keyword I just placed a require expression before requiring Dired.
Missing in the above example is the :after
keyword, which I use with diredfl
. This diff shows the changes quite well.
-(use-package diredfl
- :after (dired async))
+(with-eval-after-load 'async
+ (with-eval-after-load 'dired
+ (require 'diredfl)))
Next was the rewriting of configs with the :bind
keyword. As illustrated in the following diff.
-(use-package flyspell-correct
- :after flyspell
- :bind
- (:map flyspell-mode-map ("C-ƒ" . flyspell-correct-wrapper)))
+(with-eval-after-load 'flyspell
+ (require 'flyspell-correct))
+
+(define-key flyspell-mode-map (kbd "C-ƒ") 'flyspell-correct-wrapper)
The next example deals with the translation of blocks with the a :mode
keyword.
-(use-package csv-mode
- :mode
- ("\\.csv\\'" . csv-mode))
+(require 'csv-mode)
+(add-to-list 'auto-mode-alist '("\\.csv\\'" . csv-mode))
With these examples, I think I have covered how I went about changing my config to remove all the use-packages
blocks. The challenge was really how repetitive it was going through the code. Plus of course making sure that the parens
remained balanced.
Conclusion
To answer my own question from the beginning, not hard at all. Well it was tedious at times, but not really hard. Most of the concepts for replacements were already present in my config. Like the use of the functions add-hook
and add-to-list
or the macro with-eval-after-load
. Plus the documentation – online and inside Emacs itself – is fantastic.
Little bonus, my emacs-init-time
seems to be faster. Take this with a pinch of salt though. My slow init time with use-package
is most likely due to my lack of chops.