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