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!