Rust is a fancy new programming language with many good reasons to recommend it, but one of the things I like most is the tooling built around it: Cargo is one of the best language-specific package managers available for any language, while rustup is a friendly installer that will give you the latest versions of the compiler, Cargo, etc. while making it easy to keep them up-to-date.
Unfortunately, while these tools are designed to be easy to use, they’re
also designed to be cross-platform, which makes life easy and pleasant
for people who work exclusively on Rust across many platforms, but less so
for people who work with many tools on one platform: it’s another set of
configuration files and cache directories to clean up, another entry on
$PATH
, and so forth.
While there is a Rust RFC suggesting that Cargo (and hence, rustup) follow platform-specific conventions out of the box, apparently there’s some technical issues that make it difficult in practice. Also, it’s not immediately obvious what the platform conventions of POSIX are: the POSIX standard has never mandated any specific conventions for storing per-user information, and Cargo and rustup don’t do anything wildly uncommon.
While I appreciate that a universal convention might be difficult to
identify and implement, most of the sofware I personally use on a daily
basis follows very well-defined conventions. Specifically, the XDG Base
Directory Specification which says that per-user configuration
should go in ~/.config/appname
and caches of re-generatable or
re-downloadable data should go into ~/.cache/appname
. The base directory
spec has a fairly straight-forward extension in file-hierarchy,
which adds ~/.local/bin
for per-user binaries that should be available
from the shell. Since I’ve already learned those conventions and adapted
them into my workflow, I want to make Cargo and rustup follow them
without having to modify the software myself, and convince upstream to
adopt my changes.
If you already have Cargo or rustup installed, maybe you can migrate your old configuration but to keep things simple let’s just move them out of the way:
mkdir ~/old-rust-bits
mv ~/.cargo ~/.rustup ~/.multirust ~/old-rust-bits
Create the directories where we want Cargo and rustup to actually store their stuff:
mkdir -p .local/bin
mkdir -p .cache/cargo
mkdir -p .cache/rustup/downloads
mkdir -p .cache/rustup/tmp
mkdir -p .cache/rustup/toolchains
mkdir -p .cache/rustup/update-hashes
mkdir -p .config/rustup
Create the old directories that Cargo and rustup look in, and fill them with symlinks to the corresponding XDG-compliant locations:
mkdir -p .cargo
mkdir -p .rustup
ln -sfn .rustup .multirust
ln -sfn ../.local/bin .cargo/bin
ln -sfn ../.cache/cargo .cargo/registry
ln -sfn ../.cache/rustup/downloads .rustup/downloads
ln -sfn ../.cache/rustup/tmp .rustup/tmp
ln -sfn ../.cache/rustup/toolchains .rustup/toolchains
ln -sfn ../.cache/rustup/update-hashes .rustup/update-hashes
ln -sfn ../.config/rustup/settings.toml .rustup/settings.toml
Now you should be able to install rustup. Note that we pass
--no-modify-path
to the installer, because the whole premise of this
operation is that you want rust installed in a location your environment
already knows about, right?
curl https://sh.rustup.rs -sSf | sh -s -- --no-modify-path
Once that’s done, test that you can run all the shiny new tools:
rustup --version
cargo --version
rustc --version
You might even want to install something, to verify Cargo will put things into the right places:
cargo install ripgrep
I created the above list of symlinks by running rustup and cargo in an isolated environment, looking at what files and directories they created, and deciding which XDG base-directory they each belonged to. If future versions of Cargo or rustup change their directory layout, or if the current versions create other files or directories in circumstances I haven’t exercised, these instructions will need updating.
Properly following the XDG Base Directory Specification requires
you to respect environment variables like $XDG_CONFIG_HOME
and
$XDG_CONFIG_DIRS
, which this example does not do. I don’t think it’s
possible to do that without modifying the source-code to these tools,
though.
EDIT: It has been pointed out that if you make rustup
install things into a shared directory like ~/.local/bin
, you can
never run rustup self uninstall
, or it will blow away things it did
not install. Be careful!