DEV Community

Andrea Ricchi
Andrea Ricchi

Posted on AI-assisted

cxvm: per-project GCC and Clang versions on Linux

I work on embedded Linux. A lot of that work means building against a vendor BSP, and the cross toolchain in those BSPs is usually several GCC majors behind whatever my distro ships. Some builds want a host GCC with the same major version as that cross compiler. My laptop has GCC 15.

The usual answers are to build GCC manually into /opt and note somewhere which project needs it, or use update-alternatives. The second one needs root and changes /usr/bin/gcc for every project on the machine at once.

Flutter developers have FVM for this problem, Node has nvm, Python has pyenv. I wanted the same thing for native compilers, so I wrote cxvm. Version 0.1.0 is out.

What it does

cxvm keeps any number of GCC and Clang versions side by side under ~/.local/share/cxvm, and each project says which one it uses in a small file you commit:

cxvm install gcc@13      # GCC 13.4.0, into ~/.local/share/cxvm
cd my-project
cxvm use gcc@13          # pins gcc@13.4.0 in .cxvmrc, generates .cxvm/
cmake -S . -B build/gcc-13.4.0 -DCMAKE_TOOLCHAIN_FILE=.cxvm/toolchain.cmake
cmake --build build/gcc-13.4.0
Enter fullscreen mode Exit fullscreen mode

cxvm use writes .cxvmrc and generates a .cxvm/ folder, which it adds to .gitignore. A colleague who clones the project runs cxvm install with no arguments and gets the same compiler.

If your project doesn't use CMake:

cxvm exec -- make        # PATH, CC and CXX set for one command
eval "$(cxvm env)"       # the same for the current shell; works with direnv too
Enter fullscreen mode Exit fullscreen mode

cxvm current tells you which compiler is active and why: an environment variable, the nearest .cxvmrc walking up from the current directory, a global default, or the system cc.

Design choices

Pins are exact

Even when you type gcc@13, .cxvmrc stores gcc@13.4.0. A pin that floats to whatever 13.x is newest isn't a pin.

Point, don't wrap

Many version managers put shims on your PATH that decide at runtime which binary to call. cxvm doesn't. The generated .cxvm/toolchain.cmake and .cxvm/env name the compilers by their real paths in the store, and .cxvm/current is a symlink there for editors and for people.

That makes the tool easy to stop using. If cxvm isn't on PATH, or you uninstall it, configured builds keep working, because nothing in them refers to cxvm.

libstdc++ through rpath, not LD_LIBRARY_PATH

This is the problem that bites everyone who installs a newer GCC by hand. You build with GCC 14 on a host whose system libstdc++ comes from GCC 13, run the program, and get:

version `GLIBCXX_3.4.32' not found
Enter fullscreen mode Exit fullscreen mode

The CMake toolchain file adds an rpath to the pinned GCC's lib64, so the program finds the right libstdc++.so.6 on its own. cxvm deliberately never sets LD_LIBRARY_PATH. It leaks into every child process and hides the problem until you run the binary somewhere else.

Clang with the libstdc++ you choose

Clang doesn't ship a C++ standard library on Linux; it picks the newest GCC installation it finds on the host. An old Clang paired with a much newer libstdc++ often fails inside the standard headers. cxvm lets you pick the GCC:

cxvm install clang@19
cxvm use clang@19 --gcc gcc@13
Enter fullscreen mode Exit fullscreen mode

Under the hood, that's --gcc-install-dir, or --gcc-toolchain for Clang older than 16.

Verified, atomic installs

The sha256 of every archive cxvm can download is compiled into the binary, together with a record of where each checksum came from. It checks each download against the stored checksum, unpacks it into a staging directory, smoke-tests it with a hello-world, and only then moves it into the store. An interrupted download resumes, and a half-unpacked toolchain never shows up as installed.

Where the compilers come from

  • Clang: LLVM's official release builds. Every release since 19.1.0, and the newest patch release of each major from 11 to 18.
  • GCC: the xPack builds, 8.5.0 and 11.2.0 to 15.2.0.
  • GCC from source: cxvm install gcc@16 --from-source downloads the GNU tarball and its GMP, MPFR, MPC and ISL, and builds it into the store. The catalogue covers every release since 7.1.0 plus the last of the 4.8, 4.9, 5 and 6 series. I've built 10.5.0 and 16.2.0 so far; the older ones are untested.
  • Your own: cxvm link /opt/gcc-11 registers a compiler you already have, as gcc@11.4.0-local for example.

How it compares

  • update-alternatives switches the system compiler between versions your distro packages. It needs root and affects everyone. On the other hand, distro packages get security updates; cxvm's toolchains don't.
  • xPack with xpm installs per-project toolchains from a package.json and needs Node.js. cxvm uses the same GCC builds, adds LLVM's Clang and source builds, and generates the CMake and editor files.
  • pixi manages a whole conda-forge environment, libraries included, on Linux, macOS and Windows. If you need the libraries too, use pixi. cxvm builds against your host's C library and leaves everything else alone.
  • Nix pins every tool and library exactly, at the cost of a /nix store and its own way of building. cxvm is a much smaller step.

Limitations

It's 0.1.0, and the limits are real. It runs on x86_64 Linux only and manages native compilers, not cross toolchains. The release list is compiled into the binary, so a new GCC release needs a new cxvm release (or CXVM_CATALOG pointing to a catalogue you generate). Some upstream builds have host quirks: LLVM 13.0.1 and 18.1.8 need libtinfo.so.5, which Ubuntu 24.04 dropped, and GCC 10 from source needs --configure-arg=--disable-libsanitizer on glibc 2.42 and later. The README FAQ lists the workarounds.

How I built it

cxvm is written in Rust and ships as one static musl binary. I wrote it with a lot of help from Claude Code. The design decisions were mine, and I tested it on real hosts myself: Ubuntu 26.04 and an Ubuntu 24.04 container for the older-glibc cases.

Try it

curl -fsSL https://github.com/AndreaRicchi/cxvm/releases/latest/download/install.sh | sh
Enter fullscreen mode Exit fullscreen mode

The script checks the sha256, puts cxvm in ~/.local/bin, and doesn't touch your shell config. cargo install cxvm --locked works too.

The code is at https://github.com/AndreaRicchi/cxvm, under MIT or Apache-2.0. I'd like to hear where it breaks: other distros, old GCC releases from source, workflows it doesn't cover. Issues are open.

Top comments (0)