Building ruby gem with rust extensions

I’m trying to build a ruby gem (rubyx-py to be exact), but have a hard time figuring out how to build the native extension (as these are made with rust and not with C).

The gem did not have a Cargo.lock file commited, but that was easy to add (though I added both the Cargo.lock file for rustPlatform.importCargoLock and a patch to add this to the folder itself)

My gem config thus looks like:

gems = pkgs.bundlerEnv {
  ruby = pkgs.ruby_3_4;
  name = "bundler-env";
  gemfile = ./Gemfile;
  lockfile = ./Gemfile.lock;
  gemset = ./gemset.nix;
  groups = [ "default" "development" "test" "production" ];
  gemConfig = pkgs.defaultGemConfig // {
    rubyx-py = attrs: {
      buildInputs = [ pkgs.rustc pkgs.cargo pkgs.rustPlatform.cargoSetupHook ];
      dontBuild = false;
      patches = [ ./patches/add-cargo-lock.patch ];

      cargoDeps = pkgs.rustPlatform.importCargoLock { lockFile = ./rubyx-py-Cargo.lock; };
    };
  };
};

At this point I run into the following error when the gem tries to build its native extensions

Running phase: installPhase
@nix { "action": "setPhase", "phase": "installPhase" }
buildFlags: 
WARNING:  You build with buildroot.
  Build root: /
  Bin dir: /nix/store/861nnhwpxfa6vcqnrm0mbvcr8lpwpdfw-ruby3.4-rubyx-py-0.2.1/lib/ruby/gems/3.4.0/bin
  Gem home: /nix/store/861nnhwpxfa6vcqnrm0mbvcr8lpwpdfw-ruby3.4-rubyx-py-0.2.1/lib/ruby/gems/3.4.0
  Plugins dir: /nix/store/861nnhwpxfa6vcqnrm0mbvcr8lpwpdfw-ruby3.4-rubyx-py-0.2.1/lib/ruby/gems/3.4.0/plugins
Building native extensions. This could take a while...
ERROR:  Error installing rubyx-py-0.2.1.gem:
        ERROR: Failed to build gem native extension.

    current directory: /nix/store/861nnhwpxfa6vcqnrm0mbvcr8lpwpdfw-ruby3.4-rubyx-py-0.2.1/lib/ruby/gems/3.4.0/gems/rubyx-py-0.2.1/ext/rubyx
/nix/store/axil30p7f6cva0cll8s6kp6nxv3qvq9y-ruby-3.4.9/bin/ruby extconf.rb
checking for clang... yes
checking for clang++... yes
checking for ar... yes
checking for install_name_tool... yes

current directory: /nix/store/861nnhwpxfa6vcqnrm0mbvcr8lpwpdfw-ruby3.4-rubyx-py-0.2.1/lib/ruby/gems/3.4.0/gems/rubyx-py-0.2.1/ext/rubyx
make DESTDIR\= sitearchdir\=./.gem.20260716-545-2vqvzn sitelibdir\=./.gem.20260716-545-2vqvzn clean

current directory: /nix/store/861nnhwpxfa6vcqnrm0mbvcr8lpwpdfw-ruby3.4-rubyx-py-0.2.1/lib/ruby/gems/3.4.0/gems/rubyx-py-0.2.1/ext/rubyx
make DESTDIR\= sitearchdir\=./.gem.20260716-545-2vqvzn sitelibdir\=./.gem.20260716-545-2vqvzn
generating target/release/librubyx.dylib (release)
cargo rustc  --manifest-path ./Cargo.toml --target-dir target --lib --profile release -- -C linker=clang -L native=/nix/store/axil30p7f6cva0cll8s6kp6nxv3qvq9y-ruby-3.4.9/lib -C link-arg=-Wl,-multiply_defined,suppress -C link-arg=-Wl,-undefined,dynamic_lookup -l pthread
    Updating crates.io index
error: failed to get `crossbeam-channel` as a dependency of package `rubyx v0.1.0 (/nix/store/861nnhwpxfa6vcqnrm0mbvcr8lpwpdfw-ruby3.4-rubyx-py-0.2.1/lib/ruby/gems/3.4.0/gems/rubyx-py-0.2.1/ext/rubyx)`

Caused by:
  download of config.json failed

Caused by:
  failed to download from `https://index.crates.io/config.json`

Caused by:
  [60] SSL peer certificate or SSH remote key was not OK (SSL certificate OpenSSL verify result: unable to get local issuer certificate (20))
make: *** [Makefile:365: target/release/librubyx.dylib] Error 101

make failed, exit code 2

Gem files will remain installed in /nix/store/861nnhwpxfa6vcqnrm0mbvcr8lpwpdfw-ruby3.4-rubyx-py-0.2.1/lib/ruby/gems/3.4.0/gems/rubyx-py-0.2.1 for inspection.
Results logged to /nix/store/861nnhwpxfa6vcqnrm0mbvcr8lpwpdfw-ruby3.4-rubyx-py-0.2.1/lib/ruby/gems/3.4.0/extensions/arm64-darwin-25/3.4.0/rubyx-py-0.2.1/gem_make.out

The instruction for how to build the gem come from rb-sys: rb-sys/gem/lib/rb_sys/mkmf.rb at 94b205cd8425ebdc09d5682f14503b3f3756fa9b · oxidize-rb/rb-sys · GitHub
There are a few options that can be specified there, but I don’t quite see how to configure it to not download the crates, but use the crates as downloaded by nix

Are there any examples of ruby gems with native extensions in rust and how to build them?

Thanks!

I’ve gotten a step further based on the prometheus-client-mmap as used in gitlab.

rubyx-py = attrs: {
  nativeBuildInputs = [
    pkgs.rustc
    pkgs.cargo
    pkgs.rustPlatform.cargoSetupHook
    pkgs.rustPlatform.bindgenHook
  ];
  patches = [ ./patches/add-cargo-lock.patch ];
  dontBuild = false;
  cargoDeps = pkgs.rustPlatform.fetchCargoVendor {
    src = pkgs.stdenv.mkDerivation {
      inherit (pkgs.buildRubyGem { inherit (attrs) gemName version source; })
        name
        src
        unpackPhase
        nativeBuildInputs
        ;
      dontBuilt = true;
      installPhase = ''
        cp -R ext/rubyx $out
        cp ${./rubyx-py-Cargo.lock} $out/Cargo.lock
      '';
    };
    hash = "sha256-M1rJIC6jPHyo3EjGQ9mXmf/X+zp+N1bpH01+o2zKaWc=";
  };

  disallowedReferences = [
    pkgs.rustc.unwrapped
  ];
  preInstall = ''
    export CARGO_HOME="$PWD/../.cargo/"
  '';

  postInstall = ''
    find $out -type f -name .rustc_info.json -delete
  '';
};

Though this still needs to add both the Cargo.lock directly and through a patch. Would be nice if I’d only need one of those

Hey, I’m only guessing. Would it be idiomatic to build the crate using rustPlatform and then use it in the gem derivation?

I’m not sure - I think it could be an option, but:

  • AFAIK pkgs.rustPlaform exposes the different hooks so that it’s easy to use this in cross-language projects
  • rb-sys generates a makefile with a bunch of options, that I’d need to mirror. I’m a bit worried that it could become tricky to mirror this and keep up with updates in that gem