Building a statically linked rustfmt binary that is portable irrespective of rust installation

Hi there!

I'm trying to build a portable rustfmt binary that will allow me to format rust code without needing to install rust on the machine. The usecase I have in mind is specifically formatting on our CI machines where we do not have rust installed.

We could install rustup on the machine and use it to install rust -- but that is a decent chunk of time that would be spent downloading and setting up rust just to run the formatter. A stand-alone binary could be a fraction of the size and thus allow us to be lighter and faster.

For example the 1.6.0 release from GH releases is just 5mb for macos.

We can see that this binary is wired to look for librustc:

$ otool -L ~/Downloads/rustfmt_macos-x86_64_v1.6.0/rustfmt
~/Downloads/rustfmt_macos-x86_64_v1.6.0/rustfmt:
        @rpath/librustc_driver-f2b62a1142f55552.dylib (compatibility version 0.0.0, current version 0.0.0)
        @rpath/libstd-46630039a22cca11.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.0.0)
        /usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11)
        /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1300.36.0)

Which means that running it without a rust install crashes out:

$ ~/Downloads/rustfmt_macos-x86_64_v1.6.0/rustfmt
dyld[73925]: Library not loaded: @rpath/librustc_driver-f2b62a1142f55552.dylib
  Referenced from: <D1A7C532-F20D-3F3C-9EC8-7223956328D4> ~/Downloads/rustfmt_macos-x86_64_v1.6.0/rustfmt
  Reason: tried: '/usr/local/lib/librustc_driver-f2b62a1142f55552.dylib' (no such file), '/usr/lib/librustc_driver-f2b62a1142f55552.dylib' (no such file, not in dyld cache)

The exact same thing occurs when using the binary bundled with the rust language installer tarballs (eg https://static.rust-lang.org/dist/rust-1.78.0-aarch64-apple-darwin.tar.gz).

Is there any way that I can create a portable binary here that can be run without a rust install?
I'd be more than happy if we had to create this binary ourselves -- I just wasn't sure if it was even possible based on my poking around and rough understanding of how the builds worked.

Thanks!

2 Likes

I think you can download and install the rustfmt component directly. for example. download the channel manifest first, parse and extract the url of the rustfmt component from the manifest, and then download the rustfmt package.

see Release Channel Layout - Rust Forge for details.

here's some examples:

  • manifest of latest stable release: https://static.rust-lang.org/dist/channel-rust-stable.toml
  • manifest with specific version: https://static.rust-lang.org/dist/channel-rust-1.81.0.toml
  • the relevant section in the manifest (note the pkg is named rustfmt-preview):
    [pkg.rustfmt-preview.target.x86_64-unknown-linux-musl]
    available = true
    url = "https://static.rust-lang.org/dist/2024-09-05/rustfmt-1.81.0-x86_64-unknown-linux-musl.tar.gz"
    hash = "105587dab3b687ccbcf5b8c109e493666de9cc0a0804999710776b236743e63c"
    xz_url = "https://static.rust-lang.org/dist/2024-09-05/rustfmt-1.81.0-x86_64-unknown-linux-musl.tar.xz"
    xz_hash = "b64f1850cf3ae3096e77a2fe1e79368824b7b27636731e88c08723b6a69107da"
    

Sadly this is just an isolated download of the rustfmt binary, but that binary is still dynamically linked to the installed rust libs:

$ otool -L ~/Downloads/rustfmt-1.81.0-aarch64-apple-darwin/rustfmt-preview/bin/rustfmt
~/Downloads/rustfmt-1.81.0-aarch64-apple-darwin/rustfmt-preview/bin/rustfmt:
	@rpath/librustc_driver-e28c98d18becddb2.dylib (compatibility version 0.0.0, current version 0.0.0)
	@rpath/libstd-0f9bda72675979e4.dylib (compatibility version 0.0.0, current version 0.0.0)
	/usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
	/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.11)
	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1500.65.0)

So you still need to separately download and include the rust libs.

so rustfmt depends on certain internal rustc components. in that case, I don't know how (if possible) to build a completely statically linked binary. as far as I know, the rustc-dev components only contains shared libraries (along the necessary rmeta files) to be linked. maybe it's possible if you build a customized toolchain yourself, but I don't know much about this subject, you'll have to do your own research. the rustc-dev-guide should be a good starting point.

For now, at least, I've solved this by bundling rustfmt for our dependency management system along with the rust libs and including an executable which passes the dynamic link variable with the lib path.

VERSION="1.78.0"
ARCHITECTURES=(
  "aarch64-apple-darwin"
  "x86_64-apple-darwin"
  "x86_64-unknown-linux-gnu"
)

for arch in "${ARCHITECTURES[@]}"; do
  RUST_NAME="rust-$VERSION-$arch"
  LANGUAGE_TARBALL="$RUST_NAME.tar.gz"
  RUSTFMT_TARBALL="rustfmt.tar.gz"

  curl -L -O "https://static.rust-lang.org/dist/$LANGUAGE_TARBALL"

  # extract rust
  mkdir -p ./rustlang_extract
  tar -xvzf "$LANGUAGE_TARBALL" -C ./rustlang_extract

  # repackage rust with just rustfmt
  mkdir -p ./rustfmt/bin
  cp "./rustlang_extract/$RUST_NAME/rustfmt-preview/bin/rustfmt" ./rustfmt/bin
  cp -r "./rustlang_extract/$RUST_NAME/rustc/lib/" ./rustfmt/lib
  rm -rf "./rustfmt/lib/rustlib"

  # rustfmt (as with all rust binaries) dynamically link to the rust libs -- so we need to wrap
  # the binaries with a shell scripts so that we can set the link path
  if [[ $arch == *darwin ]]; then
    LD_ENV_VAR_NAME="DYLD_FALLBACK_LIBRARY_PATH"
  else
    LD_ENV_VAR_NAME="LD_LIBRARY_PATH"
  fi
  cat > ./rustfmt/rustfmt <<EOL
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
$LD_ENV_VAR_NAME="\$SCRIPT_DIR/lib:\${$LD_ENV_VAR_NAME:-}" exec "\$SCRIPT_DIR/bin/rustfmt" "\$@"
EOL
  chmod +x ./rustfmt/rustfmt

  tar -czvf "$RUSTFMT_TARBALL" -C ./rustfmt .

  # <<< upload to our dep management cache here >>>
done

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.