Screwtape's Notepad

Tricking Rust into following the XDG Base Directory Specification

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.

Actual procedure starts here

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 -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

Things to Worry About

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!