Set the root directory of a project in Emacs with dir-locals.el
by Matthias Puech
Hi, and welcome to my blog. I’ll try to write here about the stuff I do, use or discover during my work as a Ph.D in Computer Science. There will hopefully be a bit of science (proof theory), some code (functional programming), some tips and tricks for developers.
For a gentle start, I’d like to share a nice little trick I put together for Emacs. I am developing in a big project (Coq) with multiple subdirectories, which compilation is managed by GNU make. When I open a file in a subdirectory and then hit M-x compile, make complains because I am now in the subdirectory and not at the root of the project anymore. So I took the habit for hitting very fast M-x cd .. every time I open a new file in a subdirectory. Annoying…
Now, emacs 23 has a very cool new feature called directory variables: put a file named .dir-locals.el on any directory D; whenever emacs opens a file in a (sub-)subdirectory of D, variables set in this file get set in your new buffer. Exactly what you were doing with the local variables, the -*- var: value -*- lines, but for a directory and all its contents. The syntax of this special file looks something like:
((ocaml-mode . ((tab-width . 4) (indent-tabs-mode . t)))
We could set the variable default-directory which is the one in which compilation will happen directly here:
((nil . ((default-directory . "/home/puech/Code/coq")))
But that wouldn’t be very portable right? We need default-directory to point exactly where this very file is. One option is to set it to (locate-dominating-file buffer-file-name ".dir-locals.el"): this evaluates to the first file named .dir-locals.el descending from the current directory. The problem is that this last expression needs to be evaluated before being set to default-directory. We’re going to trick emacs into thinking it sets a variable when in fact it is evaluating a piece of elisp by setting the eval name:
((nil . ((eval . (setq default-directory (locate-dominating-file buffer-file-name ".dir-locals.el") )))))
That’s it! Put this code in your .dir-locals.el at the root of your project, and you’ll never have to change directory before compiling again. This directory variable feature is a cheap way to get a notion of project into emacs! Of course you can use it for many other purposes. What can you think of?
I GOT BITTEN.
I used the Coq project and suddenly found that when I tried to open a file in Emacs, the directory was the wrong directory!
1) .dir-locals.el is a new feature, not widely know.
2) It is a hidden file.
3) It doesn’t have to be in a directory to affect Emacs – it can be in any parent directory.
So, this is unfamiliar feature that is hidden and indirect. If you do something wrong, you users don’t know why things are going wrong and certainly not how to fix them.
The bug in this code is “default-directory”. That variable is used by a lot of Emacs features, including “open a file” (find-file).
You are completely right.
Several Coq devs have reported the annoyance of this trick also affecting find-file. I don’t know of a variable setting the working directory just for compilation though (“default-compile-directory”?)…
About your initial problem, I do use “M-x compile” with “make -C ../..” the first time I compile. Then, I stick to “M-x recompile”. Maybe too simple?
Yes… What about switching then to a file in the root directory? You M-x recompile and end up in ../.. which is not you project anymore. Plus make -C doesn’t let emacs know where to find for source files to highlight errors. That’s why I looked for another solution
‘M-x recompile’ replace you automatically in the directory you first did ‘M-x compile’. So, it works in that case too. Again, maybe I miss some difficulties.
I’m not really sure I get where your comment comes from. I’ve been (ab)using recompile since Emacs22 and have never faced issues you’re describing. Basically, no, it doesn’t do “make -C”, and yes, it does let emacs know where to find errors locations by parsing the “make [X]: entering directory ” messages. Moreover, for remote compilation scenario, i.e. when editing & compiling over ssh, your trick may fail, since .dir-locals aren’t looked up for remote files as far as I know, while recompile would still work.
[...] Stole most of this from a blog. [...]