Developing Rust in Guix FHS Container

2024-12-08

While Guix has packaged many Rust packages, developing code in Rust is not a easy story: inevitably you'll fall into the dependency hell. I decide to take a easier step, by setting up a Rust development environment inside a guix-shell FHS container.

Set up the container

This is demonstrated in John Kehayias: The Filesystem Hierarchy Standard Comes to Guix Containers(Kehayias 2023). Although the example is a little bit outdated, the idea is still the same.

The minimal input I can get is

(specifications->manifest
 (list "bash"
       "coreutils"
       "curl"
       "grep"
       "nss-certs"
       "gcc-toolchain"
       "pkg-config"
       "git"
       "which"
       "zlib"))

Then enter the container. I put all my rust projects under $HOME/workspace/rust, and for this setup I'll create 2 sub-directories under that (rustup and cargo) to save the files for Rustup and Cargo respectively.

guix shell --network --container --no-cwd --emulate-fhs \
     -m $HOME/workspace/rust/manifest.scm \
     --share=$HOME/workspace/rust/rustup=$HOME/.rustup \
     --share=$HOME/workspace/rust/cargo=$HOME/.cargo \
     --preserve='^RUSTUP.+' --preserve=TERM

Then just install Rustup with the official script (remember to source .cargo/env), and add the needed components

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

rustup component add rust-src
rustup component add rust-analyzer

Try build a project

Clone a project into rust-home, and build it with cargo build to see if everything works.

Wrap the tools inside Container

There are several challenges to overcome:

.cargo/env needs to be sourced after we enter the container environment
Rustup generate .profile inside the container, which is for login shell only. Besides, /bin/sh, which is used in container shell, does not load any bash configs.
We cannot use the rust tools inside the FHS container directly from the host
For example, if I understand correctly there is nothing --search-paths can help us here, because the rust tools rely on the FHS structure inside the container, and on the host there is no way we can provide such a FHS environment. If you try this way, your-favorite-shell will likely yield "cannot find the file /path/to/cargo" etc, which is a misleading error message and I believe it is actually due to the broken dynamic library path.
We want the rust tools be available on a project-by-project basis
That is, it is still preferable these tools are "populated" with the help of Direnv or something similar.

Wrapper Scripts

I experimented a little bit and the best solution I can come up with my Guix-fu is by using wrapper scripts. For example, put the following under $HOME/workspace/rust/scripts, named cargo

#!/usr/bin/env bash
set -ex

CMDS="cargo $@"

exec guix shell --network --container --emulate-fhs \
     -m $HOME/workspace/rust/manifest.scm \
     --share=$HOME/workspace/rust/rustup=$HOME/.rustup \
     --share=$HOME/workspace/rust/cargo=$HOME/.cargo \
     --expose=$HOME/workspace/rust/.profile=$HOME/.profile \
     --preserve='^RUSTUP.+' --preserve=TERM \
     -- \
     bash -l -c "$CMDS"

Now say if you have a project under $HOME/workspace/rust/hello, run ../scripts/cargo clippy inside hello will correctly invoke Clippy. Hooray!

You can then go on adding scripts for other tools, by just replacing the tool executable inside CMD variable.

Use the tools inside Emacs

Instead of writing long path towards the scripts in the Emacs config or adding them to the global PATH, we can use Direnv to only set the PATH when we are actually inside the project.

I'm using envrc.el (and inheritenv) to utilize Direnv, but .envrc will be the same for other setups:

PATH_add scripts

Put this inside $HOME/workspace/rust, and now when editing the Rust code in projects under this directory, we can use tools directly. For example, Eglot should be able to start rust-analyzer.

Fix rustic-cargo-build

Note that despite --container, we are not passing --no-cwd, so if we invoke the above code right inside each project root, it should work out of the box. However, if we invoke them under some sub-directory, only the path leads to this sub-directory will be presented inside the container, i.e all other files all gone, which very likely includes Cargo.toml.

Rustic for example invokes things directly with the buffer-file's absolute path, and cargo locate-project will thus fail with errors.

Thus, by using project-execute-extended-command (need Emacs 30) or a similar implementation in order version, i.e

(let ((default-directory (project-root (project-current t))))
  (rustic-cargo-build))

Now rustic-cargo-build should work … Okay, actually not.

Somehow envrc-mode is not enabled in the *rust-compilation* buffer. If I understand correctly, it is because:

  • it try to use cargo before its major mode is turned on,
  • which of course failed because envrc-mode is only turned on in the major mode change hook,
  • thus the major mode proper initialization is aborted

we need the following extra hack:

(inheritenv-add-advice #'rustic-compilation)

Now if we retry the rustic-cargo-build with the project root default directory (from non-project-root files of course), it should work.

Happy hacking!

Reference

Kehayias, John. 2023. “The Filesystem Hierarchy Standard Comes to Guix Containers.” January 6, 2023. https://guix.gnu.org/blog/2023/the-filesystem-hierarchy-standard-comes-to-guix-containers/.