(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

Schievels software archeology 3 – wcd

app-misc/wcd is a utility program that should make changing directories on the command line more efficient. It was written by Erwin Waterlander in 1996 for HP-UX and was ported to many different operating systems, like DOS, Windows (95 to 10), MacOSX and, of course, Linux.

Because wcd indexes the filesystem on first run, and therefore can find the directories very fast. But what I also like is that this is coming from a time where mouse support was not really a big thing, so it has keyboard centric usage and is therefore very past to use.

When you type something like wcd org you get a list of files:

a /home/pascal/.emacs.d/.local/cache/org
b /home/pascal/.emacs.d/.local/straight/build-28.1/evil-collection/modes/org
c /home/pascal/.emacs.d/.local/straight/build-28.1/org
d /home/pascal/.emacs.d/.local/straight/repos/evil-collection/modes/org
e /home/pascal/.emacs.d/.local/straight/repos/org
f /home/pascal/.emacs.d/modules/lang/org
g /home/pascal/.emacs.old/.local/cache/org
h /home/pascal/.emacs.old/.local/straight/build-28.1/evil-collection/modes/org
i /home/pascal/.emacs.old/.local/straight/build-28.1/org
j /home/pascal/.emacs.old/.local/straight/repos/evil-collection/modes/org
k /home/pascal/.emacs.old/.local/straight/repos/org
l /home/pascal/.emacs.old/modules/lang/org
m /home/pascal/nextcloud/Uni/Theoretische Informatik/JFLAP7.1/org
n /home/pascal/nextcloud/org
o /home/pascal/org
                                                                                                                                                                                   w=up x=down ?=help  Page 1/1
Perfect match for 15 directories.
Please choose one (<Enter> to abort):

Typing a letter now will change the working directory to the given path.

On top of that wcd offers a very nice file browser for the terminal. I used to use lf for that (and only that, I know lf can do much more, but for I all the file operations I just stick to the normal shell commands), which is also a good program, but the default vim-like behaviour make things in wcd even a tad better for me. You activate the file browser with wcd -g and something like this will come up:

                        ├─ org2blog ─┬─<htmlize>─── .git ─┬─ hooks
                        │            │                    ├─ info
                        │            │                    ├─ logs ─── refs ─┬─ heads
                        │            │                    │                 └─ remotes ─── origin
                        │            │                    ├─ objects ─┬─ info
                        │            │                    │           └─ pack
                        │            │                    └─ refs ─┬─ heads
                        │            │                             ├─ remotes ─── origin
                        │            │                             └─ tags
                        │            ├─ hydra ─┬─ .git ─┬─ hooks
                        │            │         │        ├─ info
                        │            │         │        ├─ logs ─── refs ─┬─ heads
                        │            │         │        │                 └─ remotes ─── origin
                        │            │         │        ├─ objects ─┬─ info
                        │            │         │        │           └─ pack
                        │            │         │        └─ refs ─┬─ heads
                        │            │         │                 ├─ remotes ─── origin
                        │            │         │                 └─ tags
                        │            │         ├─ .github
                        │            │         ├─ doc
                        │            │         └─ targets
                        │            ├─ metaweblog ─┬─ .git ─┬─ hooks
                        │            │              │        ├─ info
                        │            │              │        ├─ logs ─── refs ─┬─ heads
                        │            │              │        │                 └─ remotes ─── origin
                        │            │              │        ├─ objects ─┬─ info
                        │            │              │        │           └─ pack
                        │            │              │        └─ refs ─┬─ heads
                        │            │              │                 ├─ remotes ─── origin
                        │            │              │                 └─ tags
                        │            │              ├─ .github ─── ISSUE_TEMPLATE
                        │            │              ├─ ci ─── vale ─── styles ─── Vocab ─── Base
                        │            │              ├─ docs
                        │            │              ├─ images
                        │            │              ├─ svg
                        │            │              └─ tests
                        │            ├─ org2blog ─┬─ .git ─┬─ hooks
                        │            │            │        ├─ info
                        │            │            │        ├─ logs ─── refs ─┬─ heads
                        │            │            │        │                 └─ remotes ─── origin

Now, you can go around the directory tree with arrow keys or HJKL. Additionally you can search similar to vim by typing /. To go to the next occurence of the searched term, type CTRL-n. Pressing enter will exit the tree view and change the working dir of the shell to the highlighted directory.

Schievels software archeology 2 – Levee

app-editors/levee is a vi clone by David Parsons. It is from a time where vi was still closed source. Last release it saw was in 2019, it has a Github repository and also a Webpage. I do not know why but levee is also called ’Captain Video’ by David.

Well, there is not that much to say about this technically other than this is a vi clone. It does what you expect from vi: Going around with HJKL and trapping users that do not know about :q!. What struck me is that this thing is really ancient, it is from 1981 or so and was originally written in some dialect of Pascal called USCD Pascal and was ported to C in the 80s. Yet it is still maintained actively by David, or Orc on GitHub. It is also really tiny, David writes about 54k on 64bit OSX. I am considering using this on an STM32 as an editor running on the display of my keyboard project. if I get this to compile for ATmega.

The changelog of version 4.0 reads fix a 39 year old bug where changing the last line in a file wouldn’t refresh properly.

David also has some other interesting projects, including his own version of autoconf, so check out his homepage.

Schievels software archeology 1 – Boxes

A series for ancient software pearls

I recently started with fixing bugs for clang16 in Gentoos packages. By default, Clang-16 will not allow implicit integer return types, implicit function declarations etc.

Eventually GCC-14 will follow, and instead of just changing the CFLAGS to make those errors into warnings again, we try to write patches for the packages. This is because this is not only cosmetic, implicit functions can lead to runtime failures on some architectures while the program runs fine on other architectures.

This is quite a task, here you can read up about it. Currently Gentoo has more than 500 open bugs because of this.

But this post should not be mainly about fixing clang16 bugs. When fixing those bugs I mainly aim for the packages in Gentoo that have no maintainer because these are the ones that are fixed the least likely so I feel my help is needed the most here. Many of those packages are abandoned by upstream as well, last release was 10+ years ago. But every now and then I come across an ancient software pearl which has a long standing history, is still maintained or does really interesting things. Things that are done in a different (supposedly more user friendly) way today, which makes using those packages and looking at them interesting as well.

Therefore I would like to start as small series here, where I present an ancient program in every post when I come across such a pearl.

Boxes

First software pearl I came across was app-misc/boxes by Thomas Jensen, they also have a GitHub repository and Webpage. This thing was founded some time around 1999 and saw it’s last release on Sep. 22, 2022!

What it does is generating ascii-art boxes, hence the name, around a given string. Like this:

/**************************/
/* Different all twisty a */
/* of in maze are you,    */
/* passages little.       */
/**************************/

or this:

          ,
      /\^/`\
     | \/   |
     | |    |               SPRING IS IN THE AIR!              jgs
     \ \    /                                                _ _
      '\\//'                                               _{ ' }_
        ||                      joan stark                { `.!.` }
        ||                  <spunk1111@juno.com>          ',_/Y\_,'
        ||  ,                                               {_,_}
    |\  ||  |\                                                |
    | | ||  | |              ASCII ART GALLERY:             (\|  /)
    | | || / /      <http://www.geocities.com/SoHo/7373/>    \| //
     \ \||/ /                                                 |//
      `\\//`   \\   \./    \\ /     //    \\./   \\   //   \\ |/ /
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

(The geocities link is sadly dead)
Boxes has a wide selection of ascii-art boxes to choose from. Now, the more artsy ones are nice, I have a nick for ascii-art, but not very useful to me. But the other ones are very good to split C-code into sections.
Boxes is especially great because it generates the box around an already existing string. So you don’t need to have the box in mind when writing comments, you can put them in afterwards. Secondly boxes not only generates those boxes, it also removes botched boxes and puts new ones around the string again. Say you have a nice box and you edit the text inside. The box will become broken. You can remove the broken box with boxes -r and put a new one in afterwards. This is a great feature, since removing such boxes manually is kind of tedious.

You can also integrate this into the editor of your choice. They have a dedicated page to that. There is also a boxes-modes for Emacs, of course, but I have not checked it out yet. I am quite happy with the two functions of adding a box and removing it again.