sorting the wheat from the chaff

EMACS Rust linter suddenly chocking

§ Part I: a little surprise

I'd spend a fair amount of time to setup EMACS for Rust development.

All of a sudden, I've recently realized I could not run cargo build easily anymore, I often had this message:

$ cargo build
    Blocking waiting for file lock on build directory

and there I had to wait for long (like, a full minute!).

Ooook, let's spend some quality-time debugging *groan*

So, where do we start? The message, of course. Here I see someone is experiencing the same symptoms with another setup. The message lingo basically says there are concurrent tasks attempting to compile sources. Uhm ... and who is the other ghost compiling process?

I'll start doing some tests randomly saving a buffer in EMACS after or before a fresh build. After a while I see that simply opening a file in a buffer is triggering a chain reaction of cargo test processes:

/home/me/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo test --no-run --bin my_rust_project --message-format=json
/home/me/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo test --no-run --lib --message-format=json

What in the world is triggering a tests run!?

§ Part II: the facepalm

I comment out all my customization to EMACS and run on bare Prelude.

Still tests running when I open a file.

Damn, ok let's patiently comment all the Prelude modules and see which one is triggering this. Turns out that any modules I have enabled triggers this behaviour, so there must be a common module above all.

Fast-forward: the culprit is the prelude-programming module, namely this piece of code:

(if (fboundp 'global-flycheck-mode)
    (global-flycheck-mode +1)
  (add-hook 'prog-mode-hook 'flycheck-mode))

What does Flycheck know about Rust? A syntax checker that triggers tests? *sigh*

So after some more research, I see that Flycheck does know Jack about Rust and in a way I didn't expect: a flycheck-rust-check-tests config parameter.

This parameter has been added many moons ago (in 2014) when the project was not even on GitHub so I don't have a diff or an issue to refer to. Let's have a look at the flycheck.el elisp code:

(flycheck-def-option-var flycheck-rust-check-tests t (rust-cargo rust)
  "Whether to check test code in Rust.

For the `rust' checker: When non-nil, `rustc' is passed the
`--test' flag, which will check any code marked with the
`#[cfg(test)]' attribute and any functions marked with
`#[test]'. Otherwise, `rustc' is not passed `--test' and test
code will not be checked.  Skipping `--test' is necessary when
using `#![no_std]', because compiling the test runner requires

For the `rust-cargo' checker: When non-nil, calls `cargo test
--no-run' instead of `cargo check'."
  :type 'boolean
  :safe #'booleanp
  :package-version '("flycheck" . "0.19"))

If flycheck-rust-check-tests is set to nil and cargo is installed, flycheck will execute cargo test --no-run instead of cargo check. Let's do this and add a line to the Rust prelude module (prelude-rust.el) trying to mute that parameter:

(setq flycheck-rust-check-tests nil)

and ... the cargo test little devils are not spawned anymore.

§ Part III: the unanswered questions

The saying goes that if you reproduce a bug, you're halfway to its resolution. I'll add that fixing the bug takes you to a 90%; but only understanding the cause of a behaviour unlocks the real 100% achievement.

So what triggered such a cargo test frenzy? Has it ever always been there, just unnoticed?

The most likely answer is that Flycheck run cargo test and I manually run cargo build from the command line. Each of these two commands invalidates the compiled cache, so what happened is something like this:

  • Open file in buffer (cargo test triggered, slow run unnoticed)
  • code-code-code
  • Save buffer (cargo test triggered: fast run)
  • from CLI run cargo build to run my application (slow run)
  • /me wtf?!

and then:

  • code-code-code
  • Save buffer (cargo test triggered: slow run unnoticed)
  • from CLI run cargo build (message warning about concurrent build)
  • /me wtf?! again

Running several times in a row only one of these two commands doesn't invalidate the build cache, so the issue doesn't happen.

Setting to nil that variable made Flycheck switch from cargo test to cargo check to get errors produced the following benefits:

  • cargo check is the recommended way to get compilation warning/errors and in some scenario should speed things up
  • informed me to not run cargo build unless I really need to
  • does not run cargo test to get syntax/lint errors, which was awkward and confusing in the first place
  • BUG: there's an old outstanding bug, due to cargo check metadata caching: on --lib cargo projects (not --bin) it only shows compiler warnings once after a rebuild, see issue

Flycheck command before:

cargo test --no-run --lib --message-format=json

and after:

cargo check --lib --message-format=json

Finally, let's ensure this won't happen again - let's save this in my custom EMACS config:

M-x flycheck-rust-check-tests

set the value to nil, then save to ~/.emacs.d/personal/custom.el. This improve upon the previous solution as now I don't need to customize prelude-rust.el anymore.

Second step, disable rust from the list of checkers:

M-x flycheck-disabled-checkers

Now, my custom.el has two new configs:

 '(flycheck-disabled-checkers (quote (rust)))
 '(flycheck-rust-check-tests nil)

Closing thoughts: an outstanding issue in the Rust world is how to speed compiling times up, but that's nothing we can do here, eventually we will get there.

Oh, one last comment: I hate elisp so much that I find a perverse pleasure in understanding how it works.

Comments closed for this article