Spacemacs for GNU Radio
I love Spacemacs. It’s by far the most awesome editor I’ve ever used. Strictly speaking, it’s Emacs, but considering their modifications, Emacs is more the run-time environment.
Spacemacs bundles Emacs plugins to so called layers that come with a sane and consistent configuration including mnemonic key bindings. A central layer is Evil, it brings Vim’s modal editing to Emacs. With Evil, large parts of Spacemacs just feel like Vim, but with the extensiblity of Lisp under the hood. (I used Vim over the last, maybe, 15 years and was super happy. But what I loved is the editing concept, not the binary.)
I tried Emacs several times in the past, but never got the hang of it. With Spacemacs, it’s really easy to get started since it provides a complete, nicely configured environment by default. For me, it was much easier and much more motivating to start from there than to start from scratch. Spacemacs is really worth giving it a try. Let alone because of Org mode, a fantastic todo list and outlining tool.
GNU Radio Configuration
When I don’t have to write stuff, I spent most of my time working with GNU Radio. This post will be a walk-through of my configurations to make Spacemacs a nice environment to work on GNU Radio.
Layers
As I already mentioned, Spacemacs bundles plugins in layers that serve a specific purpose. I have lots of them installed, but for C++ development the most essential are:
dotspacemacs-configuration-layers
'(helm
auto-completion
(c-c++ :variables
c-c++-enable-clang-support t
c-c++-default-mode-for-headers 'c++-mode)
cscope
git
(syntax-checking :variables syntax-checking-enable-by-default nil)
(version-control :variables
version-control-diff-tool 'diff-hl
version-control-global-margin t))
Projectile Other File
Projectile is a plugin that brings the notion of projects to Emacs.
It will look for a .git
folder (or any other version control folder) in the parent directories and, if one is found, use that as the project root.
One feature of projectile is to open other files. The corresponding header to a C++ file, for example. We just have to tell projectile, how alternate files look.
(with-eval-after-load "projectile"
(push '("cc" "h") projectile-other-file-alist)
(push '("c" "h") projectile-other-file-alist)
(push '("h" "cc" "c") projectile-other-file-alist))
From there on, we can use ",ga"
to jump to alternate files in the same buffer or ",gA"
to open it in a new window.
Auto Completion
To be honest, I don’t use auto completion very often, but it’s certainly nice to have. To enable good auto completion, the tool has to know about the build configuration (defines, include directories, …). AFAIK, the highest quality completions are provided by clang complete.
The trick is to use a compile wrapper (available here) that keeps track of the configuration and writes it to .clang-complete
files.
Using the compile wrapper and ccache, GNU Radio is build with something like:
CXX="cc_args.py /usr/lib/ccache/clang++" cmake .. -DCMAKE_INSTALL_PREFIX=~/usr/gnuradio-next
Unfortunately, GNU Radio doesn’t support that kind of compile wrappers at the moment.
It can, however, easily be added by changing the top-level CMakeLists.txt
.
string(STRIP "${CMAKE_C_COMPILER_ARG1}" CMAKE_C_COMPILER_ARG1)
execute_process(COMMAND ${CMAKE_C_COMPILER} ${CMAKE_C_COMPILER_ARG1} "--version"
OUTPUT_VARIABLE cmake_c_compiler_version)
string(STRIP "${CMAKE_CXX_COMPILER_ARG1}" CMAKE_CXX_COMPILER_ARG1)
execute_process(COMMAND ${CMAKE_CXX_COMPILER} ${CMAKE_CXX_COMPILER_ARG1} --version
OUTPUT_VARIABLE cmake_cxx_compiler_version)
Another option would be to adapt the wrapper script and hardcode the compiler.
After compilation, completion information is split into multiple .clang-complete
files in the build directory.
Then, all we have to do is sort and filter all options to prepare a combined one.
find . | ag .clang_comp | xargs cat | sort | uniq > ../.clang_complete
This file is detected automatically and used for completion.
Clang Format
If you ever worked with GNU Radio’s sources, you might have gone through some headaches. The formatting was (or is) a mess. Some files use multiple indentation and formatting styles, which has the potential to drive me crazy.
Clang format, is a really nice tool to quickly format buffers. I usually format all open buffers. Fortunately, Spacemacs supports clang format. However, it doesn’t setup key bindings for it. The C/C++ mode had many unused slots, so I added them there.
(spacemacs/set-leader-keys-for-major-mode 'c-mode
"f" 'clang-format-buffer
"F" 'clang-format-region
(spacemacs/set-leader-keys-for-major-mode 'c++-mode
"f" 'clang-format-buffer
"F" 'clang-format-region))
Note, clang format is much more powerful then normal indentation rules.
It parses the whole file and can also put braces on the correct line, for example.
The trick is to have a good configuration file that tells clang-format, how exactly you want your code to look like.
Ideally, GNU Radio would commit itself to a .clang-format
file.
Indentation
Emacs allows very fine granular adjustments of how code should be indented. I use something like this in my OOT projects.
(c-add-style "basti"
'((c-basic-offset . 4)
(indent-tabs-mode . t)
(c-offsets-alist
(arglist-intro . ++)
(arglist-cont . ++)
(arglist-cont-nonempty . ++)
(defun-open . 0)
(topmost-intro . 0)
(topmost-intro-cont . 0)
(namespace-open . 0)
(namespace-close . 0)
(label . 0)
(innamespace . 0))))
I enable it by default for all C/C++ buffers.
(push '(c-mode . "basti") c-default-style)
(push '(c++-mode . "basti") c-default-style)
In a C/C++ buffer, the indentation style can be changed with “C-c .
“.
If you want to fix the indentation of a buffer or region, you can mark it and press “=
“, but I would use clang format instead.
Tags
Everybody loves tags. Basically, there are two alternatives to create them – cscope and exuberant-ctags. I recommend to use the latter, as it is more capable. But using both also doesn’t hurt. Tags can be created in the project root with
find . \( -name "*.h" -o -name "*.cc" -o -name "*.c" \) -not -iwholename './build*/*' > cscope.files
cscope -b -q
ctags -e -L cscope.files -f tags
First, it selects all files that are not in the build dir and, then, runs cscope and ctags on them.
I have the script in my path and run it with projectile in the project root, i.e., in any GNU Radio buffer, I can just do “SPC p !
“ to run a shell command in the project root and then type the script name.
The trick is to have each open buffer pointing to the right individual tags file.
I didn’t find a nice way for that yet.
At the moment, I search for a tags
file in the parent directory.
In addition to that, if the file is somewhere under src/gr-
, it’s considered to be a GNU Radio OOT module and also loads the GNU Radio tags.
(defun basti/find-tags-file ()
(labels
((find-tags-file-r (path)
(let* ((parent (file-name-directory path))
(possible-tags-file (concat parent "tags")))
(cond
((file-exists-p possible-tags-file) (throw 'found-it possible-tags-file))
((string= "/tags" possible-tags-file) (error "no tags file found"))
(t (find-tags-file-r (directory-file-name parent)))))))
(if (buffer-file-name)
(catch 'found-it
(find-tags-file-r (buffer-file-name)))
(error "buffer is not visiting a file"))))
(defun basti/c++-mode-hook ()
(let (tags
(ltags (basti/find-tags-file)))
(cond ((string-match ".*/src/gr-.*" (expand-file-name (buffer-file-name)))
(add-to-list 'tags (expand-file-name "~/src/gnuradio/tags"))))
(if ltags
(add-to-list 'tags ltags))
(make-local-variable 'tags-file-name)
(make-local-variable 'tags-table-list)
(setq tags-add-tables nil
tags-file-name nil
tags-table-list tags)))
(add-hook 'c++-mode-hook 'basti/c++-mode-hook)
(setq projectile-tags-file-name "tags")
Maybe, the better approach would be to have all tags in one file, i.e., add the GNU Radio tags to the tags file in the OOT module.
Anyhow, the most important part is to make tags-table-list
and tags-file-name
buffer local, otherwise the tags are defined by the most recently loaded file, which is total non-sense.
Helm Build
A feature that I use rarely is to trigger the build from within Spacemacs. It is, of course possible, we just have to specify our build directory.
(setq-default helm-make-build-dir "build")
(put 'helm-make-build-dir 'safe-local-variable 'stringp)
The second line allows to overwrite the variable in a per project configuration file.
I, for example, have various build directories for the different branches (build-next
, build-master
, and build-maint
).
Search for directory local variables for information how to set them.