'update-alternatives' scripts

One common way for linux systems to support multiple implementations or versions, e.g., allowing 'vi' to point to either 'vim' or 'nvi', is the 'alternatives' framework. It is managed by 'update-alternatives'.

This is usually handled by distro packagers, and of course it's possible that the rust ecosystem already has its own solution but it wasn't sufficiently triggered when building a downloaded project, but I thought this would be a good place to start and let you push it to the packagers.

The script I used to get my 'ninja' build working follows. I know that the Debian ecosystem has an integrated solution with dh_installalternatives. This is better for .deb files since it can work with /etc/apt/preferences.d etc, but this script is a good starting point for the rest of us.

With this 'rustc' and 'cargo' act as expected. If you want to use a different version by default it's easy to call sudo update-alternatives --config rust (or cargo) and then select one of the options listed.

!/bin/sh

VERS=1.81

# apt-get install rustc-$VERS clang-18 lld-18 llvm-18 wasi-libc cargo-$VERS

/usr/bin/update-alternatives --force \
    --install /usr/bin/rust                        rust          /usr/lib/rust-$VERS  100 \
    --slave   /usr/bin/rustc                       rustc         /usr/bin/rustc-$VERS \
    --slave   /usr/bin/rustdoc                     rustdoc       /usr/bin/rustdoc-$VERS \
    --slave   /usr/bin/rust-clang                  rust-clang    /usr/bin/rust-$VERS-clang \
    --slave   /usr/bin/rust-lld                    rust-lld      /usr/bin/rust-$VERS-lld \
    --slave   /usr/bin/rust-llvm-dwp               rust-llvm-dwp /usr/bin/rust-$VERS-llvm-dwp \
    --slave   /usr/share/man/man1/rustc.1.gz       rustc.1.gz    /usr/share/man/man1/rustc-$VERS.1.gz \
    --slave   /usr/share/man/man1/rustdoc.1.gz     rustdoc.1.gz  /usr/share/man/man1/rustdoc-$VERS.1.gz

/usr/bin/update-alternatives --force \
    --install /usr/bin/cargo                                   cargo                        /usr/bin/cargo-$VERS  100  \
    --slave   /usr/share/man/man1/cargo-add.1.gz               cargo-add.1.gz               /usr/share/man/man1/cargo-$VERS-add.1.gz \
    --slave   /usr/share/man/man1/cargo-bench.1.gz             cargo-bench.1.gz             /usr/share/man/man1/cargo-$VERS-bench.1.gz \
    --slave   /usr/share/man/man1/cargo-build.1.gz             cargo-build.1.gz             /usr/share/man/man1/cargo-$VERS-build.1.gz \
    --slave   /usr/share/man/man1/cargo-check.1.gz             cargo-check.1.gz             /usr/share/man/man1/cargo-$VERS-check.1.gz \
    --slave   /usr/share/man/man1/cargo-clean.1.gz             cargo-clean.1.gz             /usr/share/man/man1/cargo-$VERS-clean.1.gz \
    --slave   /usr/share/man/man1/cargo-doc.1.gz               cargo-doc.1.gz               /usr/share/man/man1/cargo-$VERS-doc.1.gz \
    --slave   /usr/share/man/man1/cargo-fetch.1.gz             cargo-fetch.1.gz             /usr/share/man/man1/cargo-$VERS-fetch.1.gz \
    --slave   /usr/share/man/man1/cargo-fix.1.gz               cargo-fix.1.gz               /usr/share/man/man1/cargo-$VERS-fix.1.gz \
    --slave   /usr/share/man/man1/cargo-generate-lockfile.1.gz cargo-generate-lockfile.1.gz /usr/share/man/man1/cargo-$VERS-generate-lockfile.1.gz \
    --slave   /usr/share/man/man1/cargo-help.1.gz              cargo-help.1.gz              /usr/share/man/man1/cargo-$VERS-help.1.gz \
    --slave   /usr/share/man/man1/cargo-init.1.gz              cargo-init.1.gz              /usr/share/man/man1/cargo-$VERS-init.1.gz \
    --slave   /usr/share/man/man1/cargo-install.1.gz           cargo-install.1.gz           /usr/share/man/man1/cargo-$VERS-install.1.gz \
    --slave   /usr/share/man/man1/cargo-locate-project.1.gz    cargo-locate-project.1.gz    /usr/share/man/man1/cargo-$VERS-locate-project.1.gz \
    --slave   /usr/share/man/man1/cargo-login.1.gz             cargo-login.1.gz             /usr/share/man/man1/cargo-$VERS-login.1.gz \
    --slave   /usr/share/man/man1/cargo-logout.1.gz            cargo-logout.1.gz            /usr/share/man/man1/cargo-$VERS-logout.1.gz \
    --slave   /usr/share/man/man1/cargo-metadata.1.gz          cargo-metadata.1.gz          /usr/share/man/man1/cargo-$VERS-metadata.1.gz \
    --slave   /usr/share/man/man1/cargo-new.1.gz               cargo-new.1.gz               /usr/share/man/man1/cargo-$VERS-new.1.gz \
    --slave   /usr/share/man/man1/cargo-owner.1.gz             cargo-owner.1.gz             /usr/share/man/man1/cargo-$VERS-owner.1.gz \
    --slave   /usr/share/man/man1/cargo-package.1.gz           cargo-package.1.gz           /usr/share/man/man1/cargo-$VERS-package.1.gz \
    --slave   /usr/share/man/man1/cargo-pkgid.1.gz             cargo-pkgid.1.gz             /usr/share/man/man1/cargo-$VERS-pkgid.1.gz \
    --slave   /usr/share/man/man1/cargo-publish.1.gz           cargo-publish.1.gz           /usr/share/man/man1/cargo-$VERS-publish.1.gz \
    --slave   /usr/share/man/man1/cargo-remove.1.gz            cargo-remove.1.gz            /usr/share/man/man1/cargo-$VERS-remove.1.gz \
    --slave   /usr/share/man/man1/cargo-report.1.gz            cargo-report.1.gz            /usr/share/man/man1/cargo-$VERS-report.1.gz \
    --slave   /usr/share/man/man1/cargo-run.1.gz               cargo-run.1.gz               /usr/share/man/man1/cargo-$VERS-run.1.gz \
    --slave   /usr/share/man/man1/cargo-rustc.1.gz             cargo-rustc.1.gz             /usr/share/man/man1/cargo-$VERS-rustc.1.gz \
    --slave   /usr/share/man/man1/cargo-rustdoc.1.gz           cargo-rustdoc.1.gz           /usr/share/man/man1/cargo-$VERS-rustdoc.1.gz \
    --slave   /usr/share/man/man1/cargo-search.1.gz            cargo-search.1.gz            /usr/share/man/man1/cargo-$VERS-search.1.gz \
    --slave   /usr/share/man/man1/cargo-test.1.gz              cargo-test.1.gz              /usr/share/man/man1/cargo-$VERS-test.1.gz \
    --slave   /usr/share/man/man1/cargo-tree.1.gz              cargo-tree.1.gz              /usr/share/man/man1/cargo-$VERS-tree.1.gz \
    --slave   /usr/share/man/man1/cargo-uninstall.1.gz         cargo-uninstall.1.gz         /usr/share/man/man1/cargo-$VERS-uninstall.1.gz \
    --slave   /usr/share/man/man1/cargo-update.1.gz            cargo-update.1.gz            /usr/share/man/man1/cargo-$VERS-update.1.gz \
    --slave   /usr/share/man/man1/cargo-vendor.1.gz            cargo-vendor.1.gz            /usr/share/man/man1/cargo-$VERS-vendor.1.gz \
    --slave   /usr/share/man/man1/cargo-verify-project.1.gz    cargo-verify-project.1.gz    /usr/share/man/man1/cargo-$VERS-verify-project.1.gz \
    --slave   /usr/share/man/man1/cargo-version.1.gz           cargo-version.1.gz           /usr/share/man/man1/cargo-$VERS-version.1.gz \
    --slave   /usr/share/man/man1/cargo-yank.1.gz              cargo-yank.1.gz              /usr/share/man/man1/cargo-$VERS-yank.1.gz \
    --slave   /usr/share/man/man1/cargo.1.gz                   cargo.1.gz                   /usr/share/man/man1/cargo-$VERS.1.gz \
    --slave   /usr/share/bash-completion/completions/cargo     completions-cargo            /usr/share/bash-completion/completions/cargo-$VERS

Ah - it looks like 'rustup' does the same thing... but it would still be a good idea to support 'update-alternatives' unless there's compelling reasons to force people to use 'rustup' instead.

I have a specific concern - automation tools like 'ansible'. I've discovered that there's not a lot of overlap between the devops and developers when you get beyond the surface layer. The former will know about 'update-alternatives' (since it's widely used) while the latter is unlikely to remember to mention it to the former. Plus it's a real PITA to run anything that's not in a proper module.

Well... maybe a small PITA. It's definitely possible but it makes you do a lot of extra work vs. a proper module where you don't have to worry about where the application is located, if all dependencies are installed, how to interpret the results, etc.

Ah - I just discovered Ubuntu's 'rustc-#' packages do not recommend, much less require, the 'rustup' package. That's one of the first things I check when installing an unfamiliar ecosystem - what does it recommend and why?

That's an easy fix for the packagers but I still think the 'update-alternatives' approach should also be considered.

rustup is, itself, a sort of package manager, dedicated to downloading, installing, and selecting Rust toolchains. Therefore, distros generally won’t use it because they don't want another package manager getting involved — but most Rust developers use rustup, and distro-packaged Rust toolchains are mostly useful for distro packages depending on them.

Some important features of rustup, which I don't expect you can get all of out of update-alternatives, are that

  • it selects the toolchain based on
    • individual project configuration (“compile me with exactly nightly-2025-01-01”)
    • command-line options (cargo +nightly --do-some-unstable-thing … is actually rustup providing the + option)
  • it can install any version, old or new, stable or unstable
  • it can install cross-compilation targets

Thanks. I'm just trying to think of a solution for the situation I just faced - I found an interesting package (mission-center), wanted to build it locally, and while it mentioned needing the "rust ecosystem" it didn't go into any detail into how that's done. It wasn't hard to figure out 'rustc' and 'cargo' but the build configuration tool wasn't smart enough to determine that I had a single version installed.

Adding the 'alternatives' was sufficient to let me build the downloaded project. I could have used symlinks but I knew that 'alternatives' was more flexible in the long run. E.g., after I needed to install a different version of rustc/cargo.

Most users will never want to rebuild projects locally, and people who want to make serious contributions will already know how to set up the ecosystem.

But then there's people like me who are looking at a peripheral issue - creating an 'apparmor' profile for the app. Apparmor has tools that can create the profile from syslog entries but it looks like it barfs with snaps (and flatpacks?) - or it just requires a configuration flag I missed. Hence wanting to build the app locally so the log messages would have the expected content.

That all makes sense. Please don't take me as saying that you’re necessarily doing something wrong — just giving context on

  • why there might not already be very much interest in a well-known “update-alternatives script” for Rust, and
  • why it would not necessarily be sufficient to build particular projects.

I’m curious what exactly you saw.

For projects not using pinned Rust versions, it should usually be entirely sufficient to have a sufficiently new version of stable Rust on your $PATH. Projects that need nightly Rust will usually outright fail to build, or fail to execute a cargo +nightly command.

What happened in your case?