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:

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.