(Doom) Emacs as an IDE for ebuilds

Writing ebuilds is kind of an obscure task. It’s a thing that not many people do, and certainly not tech-illiterates and therefore tools that evolve around it are sparse and usually stick to the command line. What I use on a daily basis is pkgdev, pkgcheck, mgorny-dev-scripts and iwdevtools. All of them are text based shell tools, but that makes them perfect for Emacs-integration!

So, I am not the first one to have this idea of course. There is ebuild-mode and all the packages from Akater. What I think is lacking is proper instructions on how to integrate into Emacs and how to use them. Please note that I personlly use Doom Emacs now, so not everything here will work out of the box with vanilla GNU Emacs.

I will put this post also into a page on the Gentoo wiki and add things there.

ebuild-mode

Ebuild mode is the center package of ebuild development on Gentoo. It sits on top of sh mode. You can read its documentation with bzcat /usr/share/info/ebuild-mode.info.bz2.

Installation

Ebuild-mode is available on Gentoos main repository as app-misc/ebuild-mode, so install it like any other package with

emerge -a app-emacs/ebuild-mode

Ebuild mode package brings in several other modes apart from the ebuild-mode core, which are devbook-mode, gentoo-newsitem-mode and glep-mode. I don’t use them, and therefore this article will focus on ebuild-mode itself. Ebuild mode is then installed into /usr/share/emacs/site-lisp/ebuild-mode, so we have to tell emacs to load elisp from there:

(add-to-list 'load-path "/usr/share/emacs/site-lisp/ebuild-mode")

Put that into the config. There are several different ways how emacs loads configs. For Doom this is usually ~/.doom.d/config.el.

Configuration

To make emacs start ebuild-mode every time an .ebuild-File is opened, we should add this to the config as well:

(add-to-list 'load-path "/usr/share/emacs/site-lisp/ebuild-mode")
(autoload 'ebuild-mode "ebuild-mode"
  "Major mode for Portage .ebuild and .eclass files." t)
(autoload 'ebuild-repo-mode "ebuild-mode"
  "Minor mode for files in an ebuild repository." t)
(autoload 'ebuild-repo-mode-maybe-enable "ebuild-mode")
(autoload 'devbook-mode "devbook-mode"
  "Major mode for editing the Gentoo Devmanual." t)
(autoload 'gentoo-newsitem-mode "gentoo-newsitem-mode"
  "Major mode for Gentoo GLEP 42 news items." t)
(autoload 'glep-mode "glep-mode"
  "Major mode for Gentoo Linux Enhancement Proposals." t)
(add-to-list 'auto-mode-alist '("\\.\\(ebuild\\|eclass\\)\\'" . ebuild-mode))
(add-to-list 'auto-mode-alist '("*.ebuild" . ebuild-mode))
(add-to-list 'auto-mode-alist '("/devmanual.*\\.xml\\'" . devbook-mode))
(add-to-list 'auto-mode-alist
       '("/[0-9]\\{4\\}-[01][0-9]-[0-3][0-9]-.+\\.[a-z]\\{2\\}\\.txt\\'"
               . gentoo-newsitem-mode))
(add-to-list 'auto-mode-alist '("/glep.*\\.rst\\'" . glep-mode))
(add-to-list 'auto-mode-alist
       '("/\\(package\\.\\(mask\\|unmask\\|use\\|env\
\\|license\\|properties\\|accept_\\(keywords\\|restrict\\)\\)\
\\|\\(package\\.\\)?use.\\(stable\\.\\)?\\(force\\|mask\\)\\)\\'"
               . conf-space-mode))
(add-to-list 'auto-mode-alist
       '("/make\\.\\(conf\\|}defaults\\)\\'" . conf-unix-mode))
(add-to-list 'interpreter-mode-alist '("openrc-run" . sh-mode))
(add-to-list 'interpreter-mode-alist '("runscript" . sh-mode))
(add-hook 'find-file-hook #'ebuild-repo-mode-maybe-enable)
(modify-coding-system-alist 'file "\\.\\(ebuild\\|eclass\\)\\'" 'utf-8)

Keybinding

By default ebuild mode uses conventional emacs keybindings, you can look them up by pressing SPC h m while being in ebuild-mode.

key             binding
---             -------
C-c C-b         ebuild-mode-all-keywords-unstable
C-c C-e         ebuild-run-command
C-c C-k         ebuild-mode-keyword
C-c C-n         ebuild-mode-insert-skeleton
C-c C-y         ebuild-mode-ekeyword
C-c -           ebuild-mode-insert-tag-line

I remapped them so that they are more coherent with Doom Emacs:

(map! :localleader
      :map ebuild-mode-map
      "r" #'ebuild-run-command                ;; run a provided phase of the currently open ebuild
      "k" #'ebuild-mode-keyword               ;; change status of a single keyword e.g. from unstable to stable
      "s" #'ebuild-mode-insert-skeleton       ;; insert a skeleton of an ebuild to work from
      "u" #'ebuild-mode-all-keywords-unstable ;; mark all keywords unstable (~)
      "e" #'ebuild-mode-ekeyword              ;; run ekeyword on the current ebuild
      "t" #'ebuild-mode-insert-tag-line)      ;; inselt a tag with our name as the author

For the tag line ebuild mode uses the variables user-full-name and user-email-address. They are present in the default config of Doom Emacs.

ebuild-run-mode

Ebuild run mode provides the functionality to jump directly from an error in the output of ebuild-run-command to the location of the code snippet that produced the error.

Installation

ebuild-run-mode is available in akaters ebuild repository. So activate that using

eselect repository enable akater # if using eselect repository, or run the corresponding layman command
emaint sync -r akater
emerge -a app-emacs/ebuild-run-mode # then install ebuild-run-mode

Ebuild-run-mode is installed into /usr/share/emacs/site-lisp/ebuild-run-mode, so we need to tell emacs to load elisp from that path:

(add-to-list 'load-path "/usr/share/emacs/site-lisp/ebuild-run-mode")

Ebuild-run-mode sits on top of ebuild-mode, so only run that after ebuild-mode is loaded:

(eval-after-load 'ebuild-mode `(setq ebuild-log-buffer-mode 'ebuild-run-mode))

Now, when an ebuild was run using ebuild-run-command a buffer with the output will pop up. When there is an error, place the point (the cursor) on it and press return (bound to compile-goto-error) to jump to the line that produces the error in the packages code.

Little helpers

Put that in config.el/ init.el (for Doom that is ~/.doom.d/config.el).

Tabs instead of spaces in ebuild-mode

By convention ebuilds use Tabs, not spaces, for indentation. This is important, because pkgcheck will get upset when we use spaces or indentation. I usually use spaces in shell-Scripts, and since ebuilds are shell-scripts (for emacs at least they are), emacs would insert spaces when I press tab all the time. So I also put this into the config:

(after! ebuild-mode
  (setq indent-tabs-mode t))

Tag line everywhere

Because this is really useful even outside of ebuilds (in patches for example) I changed the ebuild-mode-insert-tag-line a little and bound it to a key that is always available instead of binding it to a key in ebuild-mode-map.

(defun ebuilds/insert-tag-line ()
  "Insert a tag line with the user's name, e-mail address and date.
Format is \"# Larry The Cow <larry@gentoo.org> (2019-07-01)\"."
  (interactive)
  (beginning-of-line)
  (insert (format "%s%s <%s> (%s)\n"
                  (if comment-start (concat comment-start " ") "")
                  ebuild-mode-full-name ebuild-mode-mail-address
                  (format-time-string "%F" nil t))))

This makes sure that a comment sign is only put into that tagline when a comment sign is known. Before when the line was inserted into a .patch-file, it would put nil at the start, because .patch-files do not have comment signs. (instead only lines with a space in front are not comments.) You could also use user-full-name and user-mail-address here instead of their ebuild-mode counterparts. For me those are the same so it does not really matter.

I bound that to SPC T.

(map! :leader
        :desc "Insert my tagline"           "T" #'ebuilds/insert-tag-line)#+end_src

Environment variables

To test ebuilds we often need to set environment variables like USE and CC for example. To set them in Emacs we use M-x setenv. To make things a little easier we can define little functions to set sets of environment variables and bind them to keys in ebuild-mode-map or call them with M-x. E.g. to change the compiler when testing Clang16 bugs I made this:

(defun ebuild-mode-set-CC-clang ()
    (interactive)
    (setenv "CC" "clang")
    (setenv "CXX" "clang++"))

(defun ebuild-mode-set-CC-gcc ()
    (interactive)
    (setenv "CC" "gcc")
    (setenv "CXX" "g++"))

Call scrub-patch from emacs

According to the Gentoo Dev Manual patches should meet certain QA conditions as well. Therefore we have a neat little program called scrub-patch which is part of app-portage/iwdevtools, so we should install that first using

emerge -a app-portage/iwdevtools

This thing scrubs the patch thoroughly and all we have to do is insert a short description what this patch is for, references to bugs and our tag. I wrote a function for Emacs to call scrub-patch on the current buffer or on the marked file(s) in dired, so we don’t have to go to the shell that often. In dired we mark files with m then call M-x ebuilds/scrub-patch. Or, if we have a patch-file open in the currently open buffer, just M-x ebuilds/scrub-patch.

(defun ebuilds/scrub-patch (&optional @fname)
"Call scrub-patch on marked file in dired or on current file.
 Needs app-portage/iwdevtools.
 Got this from xah lee, modified a bit
 URL `http://xahlee.info/emacs/emacs/emacs_dired_open_file_in_ext_apps.html'"
  (interactive)
  (let* (
         ($file-list
          (if @fname
              (progn (list @fname))
            (if (string-equal major-mode "dired-mode")
                (dired-get-marked-files)
              (list (buffer-file-name)))))
         ($do-it-p (if (<= (length $file-list) 5)
                       t
                     (y-or-n-p "Scrub more than 5 files? "))))
    (when $do-it-p
        (mapc
         (lambda ($fpath)
           (shell-command
            (concat "scrub-patch -c -i " (shell-quote-argument $fpath))))  $file-list)
        (when (not (string-equal major-mode "dired-mode"))
            (revert-buffer)))))

Go to build directory of the currently open ebuild

Sometimes I run into the problem that I want to go to the $WORKDIR of a build. Normally we would either need to type that long path to it (/var/tmp/portage...) or we could run the ebuild and produce an error somehow, than use ebuild-run-mode from above to go from that build error to the file. But what I the build has no error or we just want to run the prepare phase to begin writing patches for the package. Therefore I wrote this little function. When it is called with an ebuild in the current buffer, it will open helm-find-file (or ivy or vertico or whatever is used) in the $WORKDIR (/var/tmp/portage/$CATEGORY/$PACKAGENAME-VERSION/work) of that ebuild. Of course at least the unpack-phase must have ran before, otherwise there is no workdir. Be aware that this one only works in Doom, because we use the Doom specific function (doom/find-file-in-other-project) but if your Emacs has a similar function, tinker around with that function a bit. Also it is assumed that portages tempdir is /var/tmp/portage.

(defun ebuilds/goto-work-dir ()
"In a buffer with an open .ebuild file, go to the work dir of the build"
  (interactive)
  (let* '(currentpath (nreverse (split-string (buffer-file-name) "\/")))
    (if (string-match-p (regexp-quote ".ebuild") (car currentpath))
        (doom/find-file-in-other-project (concat
                                          "/var/tmp/portage/"
                                          (nth 2 currentpath)
                                          "/"
                                          (substring (car currentpath) 0 -7)))
      (print "Current file does not look like an ebuild"))))

I use that so often, I bound it to a key in ebuild mode:

(map! :localleader
      :map ebuild-mode-map
      "w" #'ebuilds/goto-work-dir)      ;; go to work dir of ebuild

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert