Sunday, October 07, 2007

Erlang compilation with Emacs

Since I switched to a Mac as my primary development platform I am using aquamacs for all my my coding needs. It is a fast native app, looks good, is highly customizable and together with distel it integrates very well with Erlang. Just one thing was giving me a headache:

Compiling from within Emacs based on an Emakefile

I tried some script by Alexey Lebedeff recently discussed on the Erlang mailing list, but at first try I did not manage to adapt the script to fit my needs. The situation got worse when I started to use non-Erlang source files such as templates and lexer / parser grammars which all get compiled to beam files and need to be reloaded when compilation was successful. After digging a bit into Emacs Lisp I came up with the following approach:

By pressing a function key (F13 in my case) emacs invokes via RPC at the Erlang application a custom compile command which in case of an Erlang source file switches to the directory of the Emakefile, runs a make:all([load]). and switches back to the original directory of the currently edited file. If the source file is a non-Erlang, than custom code gets called to perform all the necessary steps until beam file reloading.

There are two implications which need to be considered before adapting this approach:
  • An application must contain specific code which can be called by emacs and that code must be able to determine the location of the Emakefile.
  • The approach can't be used for initial compilation, because emacs calls a function from an application module, which must be compiled already.
And of course you must have distel installed. Below a code snippet showing the Emacs Lisp code, which goes into emacs.el (or Preferences.el in case of aquamacs):

(defun my-erlang-compile ()
(interactive)
(save-some-buffers (not compilation-ask-about-save) nil)
(save-excursion
(let ((thisdir default-directory))
(setq src-file-name buffer-file-name)
(with-current-buffer (get-buffer-create "*erl-output*")
(setq default-directory thisdir)
(erase-buffer)
(compilation-mode)
(toggle-read-only nil)
(setq compilation-current-error nil)
(display-buffer (current-buffer))
(erl-spawn
(erl-send-rpc (erl-target-node)
'distel
'eval_expression
(list (format "myapp_make:all(%S)." src-file-name)))
(erl-receive ()
((['rex ['ok string]]
(insert string))
(['rex ['error reason]]
(insert reason))
(other
(message "Unexpected: %S" other)))))))))

(add-hook 'erlang-mode-hook 'my-erlang-mode-hook)
(defun my-erlang-mode-hook ()
;; when starting an Erlang shell in Emacs, default in the node name
(setq inferior-erlang-machine-options '("-sname" "emacs"))

(define-key erlang-mode-map [f13]
(lambda () (interactive)
(progn
(my-erlang-compile))))
)

;; just to illustrate how to use custom erlang compilation
;; from within other modes
(add-hook 'foo-helper-mode-hook
(lambda ()
(define-key foo-helper-mode-map [f13]
(lambda () (interactive)
(progn
(my-erlang-compile))))))

With the elisp snippet from above, the corresponding Erlang application must contain a module myapp_make which implements the function all/1 :

all(Path) ->
case filename:extension(Path) of
".erl" ->
{ok, OldDir} = file:get_cwd(),
ok = file:set_cwd(my_find_emakefile_dir()),
make:all([load]),
ok = file:set_cwd(OldDir);
Ext ->
maybe_my_custom_stuff
end.

No comments: