You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

26 KiB

weight title date draft summary categories tags
4 Mi configuración de Emacs con Elpaca 2024-12-03T16:22:18+0100 false Configuración de Emacs con el gestor de paquetes Elpaca y otras notas sueltas
notes
emacs
elpaca
use-package

{{< admonition type=danger title="ARTÍCULO SIN TERMINAR" open=true >}}

Este artículo NO ESTÁ TERMINADO, pero yo creo que ya es útil. En cuanto termine la migración de la configuración a Elpaca añadiré un enlace a la misma. Puedes mandarme sugerencias si te apetece.

{{< /admonition >}}

Vamos a ver como configurar Emacs (lo estoy probando en versiones 29, 30 y 31) con el gestor de paquetes Elpaca.

Gestión de paquetes en Emacs

package.el

package.el es el gestor de paquetes por defecto en Emacs. Los paquetes de package.el son ficheros comprimidos estructurados de manera que se puedan descomprimir en el directorio de paquetes de tu Emacs simplificando la instalación.

package.el funciona perfectamente y es la alternativa adecuada si quieres una gestión de paquetes tan sencilla como sea posible. Sólo sería recomendable usar una alternativa si te preocupa alguno de los siguientes puntos:

  • Quieres instalar paquetes para Emacs desde fuentes específicas, como desde el repo del desarrollador por ejemplo
  • Quieres instalar versiones específicas de los paquetes
  • Quieres desarrollar tus propios paquetes, o quieres contribuir al desarrollo de algún paquete o quieres simplemente trastear con el código de paquetes instalados

use-package

use-package no es un gestor de paquetes, es una macro que facilita muchísimo la especificación de paquetes a instalar desde los ficheros de configuración de Emacs. use-package no se encarga de instalar los paquetes, sólo nos da una sintáxis muy potente y sencilla para declarar los paquetes a instalar y sus configuraciones. La instalación propiamente dicha siempre la hará el gestor de paquetes que utilices.

La mayoría (sino todos) los gestores de paquetes para Emacs son compatibles con use-package. De hecho este paquete es tan útil y popular que ya viene incluido como built-in desde la versión 29 de Emacs.

Merece mucho la pena leerse con atención toda la documentación de use-package en esta web hay una explicación detallada de cada palabra clave.

Una característica curiosa (al menos para mi) de use-package es que podemos invocarlo con una feature de Emacs, y no solo con un paquete. Es bastante habitual invocarlo con la feature emacs, veamos un ejemplo:

  (use-package emacs
    :ensure nil
    :init
    (prefer-coding-system 'utf-8))

Esta notación, con la feature emacs, se usa en los ficheros de configuración para especificar opciones comunes. Pero también se puede invocar con cualquiera de las features disponibles (comprueba el valor de la variable features en tu Emacs), por ejemplo:

(use-package paren
  :straight (:type built-in)   ;; Esta es notación de straight, podríamos haber usado :ensure nil
  :config
  (show-paren-mode t)
  :custom
  (show-paren-delay 0))

Tampoco es difícil crear una feature al vuelo, aquí configuramos las fuentes de nuestro editor, creando la feature slv-fonts:

  (use-package slv-font
    :no-require
    :hook (after-init . setup-fonts)
    :preface
    (defun font-installed-p (font-name)
      "Check if a font with FONT-NAME is available."
      (find-font (font-spec :name font-name)))
    (defun setup-fonts ()
      (cond ((font-installed-p "Hack Nerd Font")
             (set-face-attribute 'default nil :font "Hack Nerd Font" :height 158))
            ((font-installed-p "Iosevka Nerd Font")
             (set-face-attribute 'default nil :font "Iosevka Nerd Font" :height 158)))
      (when (font-installed-p "Iosevka Nerd Font")
        (set-face-attribute 'variable-pitch nil :font "Iosevka Nerd Font" :height 158)))
    (provide 'slv-font))

Hay otras formas de conseguir lo mismo en el fichero de configuración, pero se hace así unas veces para disponer de todas las facilidades que nos da use-package (por ejemplo sintáxis simplificada para asociar atajos de teclado, hooks, etc.), y otras simplemente por mantener toda la configuración organizada en bloques de use-package con una sintáxis coherente en todo el fichero.

Alternativas a package.el (straight.el)

Tenemos muchas alternativas al gestor de paquetes por defecto de Emacs. Hay una excelente comparativa de gestores de paquetes en la documentación de straight.el

Yo empecé usando package.el (como todos los que usamos Emacs, supongo) y me pasé a straight.el para tener más facilidad al especificar los orígenes de los paquetes y poder curiosear en el código de los mismos. Además facilita bastante reproducir la configuración en distintos ordenadores (que pueden tener distintas versiones de Emacs y de sistema operativo). La principal diferencia al pasar a trabajar con straight.el es que los paquetes se instalan mediante "recetas" (puedes consultar la receta de un paquete con straight-get-recipe). Con straight.el los paquetes se gestionan clonando el código fuente con git e instalando el paquete, desde donde especique la correspondiente receta. Es fácil escribir nuestras propias recetas en el mismo fichero de configuración si queremos descargar paquetes de repos especiales o desde nuestros propios repos.

La pega que le veo a straight es que no da ninguna facilidad al usuario para explorar los paquetes disponibles. Yo incluso mantenía la configuración de package, pese a no usarlo, para poder visitar la lista de paquetes.

La razón inicial para probar elpaca fue precisamente que facilita de varias maneras explorar los paquetes disponibles. Es mucho más interactivo que straight. Y si a eso añadimos que es significativamente más rápido, tiene todas las papeletas para ser el sucesor de straight en el futuro.

Elpaca

¿Qué tiene de especial Elpaca?

Como comentaba elpaca.el es el descendiente directo de straight.el y varios desarrolladores del segundo forman parte del equipo de desarrollo de elpaca.el.

Elpaca tiene las siguientes características:

  • Instalación asíncrona y en paralelo de los paquetes. Gracias a esto la instalación de paquetes es sorprendentemente rápida
  • Al contrario que straight.el que no incluye herramientas interactivas para explorar el espacio de paquetes, Elpaca si tiene un elpaca-manager con muchas facilidades para estudiar los paquetes disponibles, probarlos, instalarlos, actualizarlos, etc. etc.
  • Permite la descarga de paquetes desde distintos orígenes incluyendo los repos de desarrollo
  • Incluye miles de paquetes "de serie" (MELPA, NonGNU/GNU ELPA, Org/org-contrib)
  • Facilita a los usuarios la creación de sus propios ELPA (Emacs Lisp Package Archive)

Al haber desarrolladores comunes en straight.el y elpaca, hay ciertas similitudes entre ambos gestores de paquetes, así que pasar de straight a elpaca es incluso más fácil. Pero ojo, tampoco es un drama pasar de package a elpaca si ya estás usando use-package.

Instalación

Para instalar elpaca.el es imprescindible:

  • Tener instalado un Emacs con versión 27.1 o posterior
  • Tener instalado git

La instalación de elpaca de forma análoga a la de straight se hace mediante un código bootstrap que añadimos a nuestro fichero init.el:

  1. En primer lugar en el fichero early-init.el inhibimos la carga de package.el conectamos

    ;; Disable package.el in favor of elpaca.el
    (setq package-enable-at-startup nil)
    
  2. Y añadimos a nuestro fichero init.el el código bootstrap de elpaca, en el momento de escribir esto sería la versión 0.8. Este código es mejor que lo copies del github de elpaca para que esté actualizado:

    (defvar elpaca-installer-version 0.8)
    (defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
    (defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
    (defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory))
    (defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
                                  :ref nil :depth 1
                                  :files (:defaults "elpaca-test.el" (:exclude "extensions"))
                                  :build (:not elpaca--activate-package)))
    (let* ((repo  (expand-file-name "elpaca/" elpaca-repos-directory))
           (build (expand-file-name "elpaca/" elpaca-builds-directory))
           (order (cdr elpaca-order))
           (default-directory repo))
      (add-to-list 'load-path (if (file-exists-p build) build repo))
      (unless (file-exists-p repo)
        (make-directory repo t)
        (when (< emacs-major-version 28) (require 'subr-x))
        (condition-case-unless-debug err
            (if-let* ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
                      ((zerop (apply #'call-process `("git" nil ,buffer t "clone"
                                                      ,@(when-let* ((depth (plist-get order :depth)))
                                                          (list (format "--depth=%d" depth) "--no-single-branch"))
                                                      ,(plist-get order :repo) ,repo))))
                      ((zerop (call-process "git" nil buffer t "checkout"
                                            (or (plist-get order :ref) "--"))))
                      (emacs (concat invocation-directory invocation-name))
                      ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
                                            "--eval" "(byte-recompile-directory \".\" 0 'force)")))
                      ((require 'elpaca))
                      ((elpaca-generate-autoloads "elpaca" repo)))
                (progn (message "%s" (buffer-string)) (kill-buffer buffer))
              (error "%s" (with-current-buffer buffer (buffer-string))))
          ((error) (warn "%s" err) (delete-directory repo 'recursive))))
      (unless (require 'elpaca-autoloads nil t)
        (require 'elpaca)
        (elpaca-generate-autoloads "elpaca" repo)
        (load "./elpaca-autoloads")))
    (add-hook 'after-init-hook #'elpaca-process-queues)
    (elpaca `(,@elpaca-order))
    

Mi configuración personal (peculiaridades)

Aquí tengo que hacer un inciso para explicar como es mi configuración personal, que tiene algunas peculiaridades, algunas probablemente poco comunes:

  • Mis ficheros de configuración están en el directorio ~/.config/emacs, es decir ese directorio es mi user-emacs-directory Emacs reconoce ese directorio (especificado en el estandar POSIX) (aunque Emacs 29 en Termux no me lo ha reconocido, me vi obligado a definir un alias)
  • Uso el paquete no-littering para mantener mi user-emacs-directory limpio y almacenar exclusivamente los ficheros de configuración. El resto de ficheros los dejo en el directorio ~/.cache/emacs
  • Casi toda mi configuración está guardada en un fichero de tipo org-mode que se carga desde init.el

Con esas condiciones mi fichero init.el tiene los siguientes contenidos:

;;; init.el --- The Emacs init file for Emacs 29, ELPACA version -*- lexical-binding: t -*-
;;
;;; Commentary:
;; This is the main configuration file for Emacs.
;; In this file:
;; - Set no-littering etc and var locations
;; - Set elpaca as default install method for use-package
;; - Set eln-cache location for no-littering
;; - Set the bootstratp for elpaca.el
;; - Install the latest release of org
;; - Load no-littering
;; - Set elpaca for no-littering
;; - Add elpaca-use-package-mode
;; - Load the file ~/.config/emacs/slv-config.org
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Code:
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Set alternative locations for no-littering
(defvar no-littering-etc-directory (expand-file-name "~/.cache/emacs/etc"))
(defvar no-littering-var-directory (expand-file-name "~/.cache/emacs/var"))
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Uncomment next line for use-package statistics
;; (defvar use-package-compute-statistics t)
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Set eln-cache dir **WORKS ONLY FOR EMACS 29 AND ABOVE**
(when (boundp 'native-comp-eln-load-path)
  (startup-redirect-eln-cache (expand-file-name "eln-cache" no-littering-var-directory)))
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Needed to avoid elpaca warning
(setq elpaca-core-date '(20240623)) ;; set to the build date of Emacs
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Define elpaca bootstrap
(defvar elpaca-installer-version 0.8)
;; (defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-directory (expand-file-name "elpaca/" no-littering-var-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-repos-directory (expand-file-name "repos/" elpaca-directory))
(defvar elpaca-order '(elpaca :repo "https://github.com/progfolio/elpaca.git"
                              :ref nil :depth 1
                              :files (:defaults "elpaca-test.el" (:exclude "extensions"))
                              :build (:not elpaca--activate-package)))
(let* ((repo  (expand-file-name "elpaca/" elpaca-repos-directory))
       (build (expand-file-name "elpaca/" elpaca-builds-directory))
       (order (cdr elpaca-order))
       (default-directory repo))
  (add-to-list 'load-path (if (file-exists-p build) build repo))
  (unless (file-exists-p repo)
    (make-directory repo t)
    (when (< emacs-major-version 28) (require 'subr-x))
    (condition-case-unless-debug err
        (if-let ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
                 ((zerop (apply #'call-process `("git" nil ,buffer t "clone"
                                                 ,@(when-let ((depth (plist-get order :depth)))
                                                     (list (format "--depth=%d" depth) "--no-single-branch"))
                                                 ,(plist-get order :repo) ,repo))))
                 ((zerop (call-process "git" nil buffer t "checkout"
                                       (or (plist-get order :ref) "--"))))
                 (emacs (concat invocation-directory invocation-name))
                 ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
                                       "--eval" "(byte-recompile-directory \".\" 0 'force)")))
                 ((require 'elpaca))
                 ((elpaca-generate-autoloads "elpaca" repo)))
            (progn (message "%s" (buffer-string)) (kill-buffer buffer))
          (error "%s" (with-current-buffer buffer (buffer-string))))
      ((error) (warn "%s" err) (delete-directory repo 'recursive))))
  (unless (require 'elpaca-autoloads nil t)
    (require 'elpaca)
    (elpaca-generate-autoloads "elpaca" repo)
    (load "./elpaca-autoloads")))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Install elpaca use-package support
(elpaca elpaca-use-package
  ;; Enable use-package :ensure support for Elpaca.
  (elpaca-use-package-mode))
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Install org-mode
(elpaca org)
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Keep emacs clean!
;; Use no-littering to automatically set common paths to the new user-emacs-directory
(use-package no-littering
  :ensure t
  :init
  ;; set paths for no-littering etc and var directories
  ;; instead of this paths, you could use
  ;; (setq user-emacs-directory (expand-file-name "~/.cache/emacs"))
  (setq no-littering-etc-directory (expand-file-name "~/.cache/emacs/etc")
        no-littering-var-directory (expand-file-name "~/.cache/emacs/var")
      )
  :config
  ;; set sensible defaults for backups
  (no-littering-theme-backups)
  ;; set paths for url-history-file and custom-file
  (setq url-history-file (no-littering-expand-etc-file-name "url/history")
        custom-file (no-littering-expand-etc-file-name "custom.el"))
  )
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; delight and which-key aux packages needed for configuration loading
(use-package delight
  :ensure t)
(use-package which-key
  :ensure t
  :delight
  :config
  (which-key-mode)
  (setq which-key-idle-delay 0.3)
  (setq which-key-dont-use-unicode nil)
  (setq which-key-separator " → " )
  (setq which-key-ellipsis "…")
  (add-to-list 'which-key-replacement-alist '(("TAB" . nil) . ("↹" . nil)))
  (add-to-list 'which-key-replacement-alist '(("RET" . nil) . ("⏎" . nil)))
  (add-to-list 'which-key-replacement-alist '(("DEL" . nil) . ("⇤" . nil)))
  (add-to-list 'which-key-replacement-alist '(("SPC" . nil) . ("␣" . nil)))
  (add-to-list 'which-key-replacement-alist '(("<left>" . nil) . ("←")))
  (add-to-list 'which-key-replacement-alist '(("<right>" . nil) . ("→"))))
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Wait to every queued elpaca order to finish
(elpaca-wait)
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Load slv-config org file.
(let ((config-el (expand-file-name "slv-config.el" user-emacs-directory))
      (config-org (expand-file-name "slv-config.org" user-emacs-directory)))
  (if (and (file-exists-p config-el)
           (file-exists-p config-org)
           (time-less-p (file-attribute-modification-time (file-attributes config-org))
                        (file-attribute-modification-time (file-attributes config-el))))
      (load-file config-el)
    (if (file-exists-p config-org)
        (org-babel-load-file config-org)
      (error "No file `%s' found!! No configuration loaded!!" config-org))))
;;
(provide 'init)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; init.el ends here

Funcionamiento asíncrono de elpaca

Sea cual sea el modo en que instales elpaca.el (me refiero a las dos formas que he descrito arriba, con y sin no-littering); hay que fijarse que elpaca instala los paquetes de forma asíncrona. Esta es una característica distintiva de elpaca y hace que cambien algunas cosas:

  • Emacs cargaría el fichero init.el, y en virtud del código bootstrap que hemos añadido, instalaría elpaca si es que no lo ve ya instalado
  • Tal y como especifica la línea (add-hook 'after-init-hook #'elpaca-process-queues), casí al final del código bootstrap, elpaca procedería a instalar y cargar los paquetes especificados después de cargar el fichero init.el o, en el caso de mi configuración particular, después de cargar los dos ficheros init.el y slv_config.el

{{< admonition type=warning title="Adelantar la carga de paquetes" open=true >}}

Si queremos adelantar la carga de paquetes, es decir, forzar la instalación y la carga de los paquetes antes de terminar la carga de toda la configuración de Emacs podemos usar (elpaca-wait)

De hecho en mi configuración particular, quiero tener cargadas la compatibilidad de elpaca con use-package, quiero tener org actualizado y quiero tener no-littering instalado, antes de cargar mi fichero slv_config.org; también me interesa tener delight disponible ya que lo aplico en varias secciónes use-package de mi configuración. Y por último para la versión 29 de Emacs quiero tener which-key disponible para definir mapas de teclado personalizados (en versiones posteriores de Emacs which-key ya es un built-in). Así que tengo que invocar a (elpaca-wait) para adelantar la instalación de esos paquetes.

Esa invocación hace que se procesen todas las actividades encoladas en elpaca, los paquetes especificados a posteriori volverán a dejarse en cola y se instalarán y cargarán al final de la ejecución de la configuración de Emacs (o en la siguiente invocación de (elpaca-wait) pero no es buena idea invocarlo muchas veces).

{{< /admonition >}}

La carga asíncrona de paquetes parece funcionar de maravilla con elpaca, la instalación de paquetes es rapidísima y los desarrolladores afirman que es gracias a la asincronía.

Con todo lo explicado pensé que merecía la pena encapsular toda mi configuración en bloques use-package pensando que quizás se podría acelerar la carga de la configuración pero no funciona así, lo único que use-package delega en elpaca son las instalacione de paquetes. Podemos ver como se expande la macro ejecutando pp-macroexpand-last-sexp y comprobaremos que solo se invoca a elpaca cuando hay que instalar un paquete.

Trabajando con elpaca

La mejor presentación de las facilidades que ofrece elpaca es este video de No Wayman. Pero de todas formas algunas pistas, aunque no son en absoluto exhaustivas:

Comando Descripción
elpaca-info Nos da la información de un paquete, incluyendo entre otras cosas la receta de instalación
elpaca-log Nos permite ver el log de elpaca
elpaca-recipe Nos devuelve la receta para instalar un paquete
elpaca-try
elpaca-manager Este es el centro de operaciones de elpaca, nos permite hacer todo tipo de operaciones con los paquetes (ver tabla siguiente)

Comandos disponibles dentro de elpaca-manager:

atajo Comando Descripción
s elpaca-ui-search Especificar cadena de búsqueda, con prefijo: editar la búsqueda actual. Permite especificar criterios para cada columna, separados por |, p.ej. #unique !#installed theme | dark | 2024
i elpaca-ui-install Marca un paquete para instalación, estas instalaciones sólo valen para la sesión actual no persisten entre sesiones Emacs
f elpaca-fetch Baja del repo los últimos cambios del código (git fetch). También podemos usar elpaca-fetch-all
m elpaca-merge Actualiza el código local con los últimos cambios (git merge). También podemos usar elpaca-merge-all
p elpaca-update Combina las operaciones fetch y merge. También podemos usar elpaca-update-all pero no lo recomiendan
d elpaca-delete Marca un paquete para borrado
x elpaca-ui-execute-marks Ejecuta las acciones marcadas
gl elpaca-log Muestra el log de elpaca
gi elpaca-search-installed Listar paquetes instalados
ga elpaca-search-marked Listar paquetes marcados
go elpaca-search-orphaned Listar paquetes huérfanos
gt elpaca-search-tried Listar paquetes probados
gr elpaca-search-refresh Refrescar listado (resultado de la búsqueda)
v elpaca-ui-visit Visitar el directorio del repo local del paquete. Con prefijo, visitar el directorio local del Build del paquete
b elpaca-ui-browse-package Visitar con el navegador la página web del paquete

Problemas encontrados

  • Como ya hemos comentado hay que tener cuidado con cargar anticipadamente todos los paquetes que Emacs necesite para cargar la configuración sin errores. Yo al final opté por mover la carga de paquetes necesarios al fichero init.el antes de la llamada a (elpaca-wait) en la linea 131 del listado de init.el.
  • He tenido problemas con Emacs 29 por que elpaca no podía determinar la variable elpaca-core-date así que la he definido yo en mi fichero init.el para que coincida con la fecha de la release de Emacs 29 (ver linea 35 del listado arriba)