О, use-package!

Это пакет, который буквально изменил всё. Если раньше init.el заполняли кодом в императивном стиле, то с появлением use-package очень многие пользователи Emacs стали описывать свои настройки в декларативном стиле.

Пакет оказал настолько большое влияние на управление настройками Emacs, что начиная с Emacs 29 use-package стал встроенным (builtin), т. е. уже не требует установки вручную (хотя его всё ещё можно установить или обновить из других источников).

На сайте документации GNU есть целый раздел, посвящённый use-package, и вроде бы всё хорошо, но... Я бы тогда не написал эту статью.

Неправильное использование :init и :config

Первая ошибка, которую делают пользователи use-package — путают :init и :config. Ссылку на документацию я дал выше, можете почитать в оригинале. Здесь же расскажу своими словами.

В блоке :init нужно добавлять код, который выполняется до загрузки пакета.

Допустим, у вас есть такой фрагмент кода:

(use-package anzu
  :init
  (global-anzu-mode 1))

Этот код эквивалентен такому коду в императивном стиле:

(global-anzu-mode 1)
(require 'anzu)

Как видите, функция из модуля вызывается до его загрузки. Надо ли говорить, что этот код не будет работать?

Правильное решение — размещение вызовов функций пакета внутри блока :config:

(use-package anzu
  :config
  (global-anzu-mode 1))

Злоупотребление use-package-always-ensure

В use-package есть такая встроенная функциональность, как автоматическая установка неустановленных пакетов. Можно включить глобальную настройку use-package-always-ensure, и тогда use-package будет пытаться установить все неустановленные пакеты автоматически:

(require 'use-package)
(custom-set-variables '(use-package-always-ensure t))

Всё хорошо, если вы используете Emacs только на локальном компьютере или одних и тех же типовых окружениях: дома Ubuntu 20.04 LTS и на работе на всех машинах тоже Ubuntu 20.04 LTS. В этом случае вы будете очень редко сталкиваться с тем, что use-package не может установить пакет из-за того, что автор пакета повысил требования к версии Emacs.

В этом случае каждое выполнение вашего init.el будет завершаться ошибкой установки пакетов.

Допустим, пакет markdown-mode.el с некоторых пор стал требовать Emacs 27.1 или более новый.

Наивное и неработающее решение (напоминаю, use-package-always-ensure t) c :if, :when или :unless:

(use-package ace-window
  :when (or ((> emacs-major-version 27)
           (and (= emacs-major-version 27)
                (>= emacs-minor-version 1)))))

Проблема в том, что весь код настройки пакета выполняется после его установки, и :if процессу установки никак не мешает:

The :if, :when, and :unless keywords predicates the loading and initialization of packages. They all accept one argument, an Emacs Lisp form that is evaluated at run-time.

:requires вам тоже не поможет. Код в этом блоке тоже выполняется после установки пакета.

Может, тогда не будем присваивать use-package-always-ensure значение t, а просто напишем в блоке :ensure нужное выражение? Давайте попробуем!

Исходные условия:

  • Emacs 29.3.

  • use-package v2.4.5.

Добавим в init.el такой код:

(use-package all-the-icons
  :ensure (> emacs-major-version 27))

Запускаем init.el с помощью eval-buffer... Ой!

⛔ Error (use-package): Failed to parse package all-the-icons: use-package: :ensure wants an optional package name (an unquoted symbol name), or (<symbol> :pin <string>)

Что же делать? Очевидно, проверять версию Emacs и другие требования до вызова use-package. Если у вас много пакетов, которые не работают в определённых окружениях, целесообразно написать функцию типа такой:

;; Возвращает t, если версия Emacs больше или равна указанной.
(defun emacs-version-not-less-than (major minor)
  "True when Emacs version is not less than MAJOR and MINOR versions."
  (or
    (> emacs-major-version major)
    (and (= emacs-major-version major)
      (>= emacs-minor-version minor))))

Использовать её нужно так:

(when (emacs-version-not-less-than 27 1)
  (use-package buffer-env
    :ensure t
    :pin "gnu"
    :defer t
    :after (files)
    :hook ((
             hack-local-variables
             comint-mode
             ) . buffer-env-update)))

(when (emacs-version-not-less-than 28 1)
  (use-package denote
    :pin "gnu"
    :ensure t
    :custom
    (denote-directory "~/Документы/Notes/" "Каталог для хранения заметок.")))

Пакет buffer-env будет установлен только если версия Emacs выше или равна 27.1, а пакет denote — 28.1.

Комментарии (3)


  1. semenInRussia
    03.07.2024 21:46

    У use-package кстати хороший конкурент в лице leaf.el

    Как мне кажется leaf более чистый чем use-package, а ещё есть много интересных встроенных keywords

    Ещё он лучше справляется с проблемой, которая описана в этой статье. Я лично его использую


  1. unreal_undead2
    03.07.2024 21:46

    Интересные штуки - но хороши, когда только входишь в Emacs, а вот если конфиг вырос за долгие годы и последние лет десять к нему особо не притрагивался, то как то лениво пробовать ) У меня ещё со времён 19.34 per-package (хотя тогда это были просто сторонние el файлы) конфигурация вынесена в отдельные файлики, в .emacs цикл, загружающий их при наличии пакета.


  1. jsirex
    03.07.2024 21:46

    (use-package anzu :init (global-anzu-mode 1))

    Такой код будет работать. А вот с :config - ещё вопрос. Но вообще система инициализации в emacs настолько сложна, что, перерыв все ихсодники, прочитав package.el, cus-* и т.п. всё равно не понимаешь как инициализировать его правильно. setq vs setopt и т.п. большниство конфигов даже с use-package неправильны... тут целая статья нужна. но что-то я отвлёкся.

    Почему будет работать: в пакетах есть autoloads. функция в 99% будет объявлена как autoload (если пакет написан нормально) и require отработает ДО вызова функции.

    Почему с :config может не заработать: этот код по-сути эквивалент `(with-eval-after-load 'anzu (global-anzu-mode 1)). Т.е. пока кто-нибудь не дёрнет autoload функцию из пакета или не вызовет сам пакет global anzu mode не будет.

    Но, иногда пакеты умеют делать трюк с progn+autoload (как magit), чтобы автоматом включить глобальный режим. ну или через custom vars (там ещё интереснее).

    А ещё в use-package есть ключевое слово :mode