Description
I maintain a project called rb-sys
that builds on top of the rake-compiler
ecosystem, so building native extensions with Rust would have the same workflow as a C extension. I wanted make it easy to precompile Rust gems for multiple platforms in a repeatable and maintaintainable way. Naturally, I turned to RCD.
After chatting with @flavorjones a bit, he nudged me to document my experience using rake-compiler-dock
in the rb-sys
project (thanks Mike!).
Overall, I'm really happy with how everything has turned out. RCD is such an important gem for Ruby, and I'm grateful for all of the great work that has been put into it.
(ps: I apologize in advance for the massive brain dump, but I knew it was the only way I could document this stuff!)
How I used RCD to compile Rust extensions
Here's a brief overview of things I did to make Rust extensions work with RCD. It's kind of a brain dump, but I hope it's useful.
- I created a bunch of platform-specific dockerfiles which all
inheritFROM larskanis/rake-compiler-dock-mri-$PLAT:$VERSION
. - Each dockerfile installs the Rust toolchain and any other dependencies.
- There's a CI job that builds the docker images and pushes them to dockerhub after each release.
- Since the
rb_sys
gem knows how to generate a compatibleMakefile
from anextconf.rb
, users can just use RCD normally by specifying theRCD_IMAGE=rbsys/$RUBY_PLATFORM:$RB_SYS_VERSION
environment variable. - I also made a GitHub Action to make this process a bit easier.
Stumbling blocks
I ran into a few issues while trying to get this to work. I'll try to document them here. Some of them are Rust specific, so take those as you will.
Bundler and rake-compiler-dock
(user experience)
rake-compiler-dock
doesn't ensure that bundle install
is run for each RUBY_CC_VERSION
. This means that any Rakefile that uses bundler/setup
will fail with Bundler::GemNotFound
(example here). For users of RCD, this is not an intuitive fix, as it's not obvious what the problem is. I've seen a few people run into this feel "actually a bit stuck" (their words).
Essentially, the manual fix is to do something like:
- Do the bundle yourself:
rvm 3.1 && bundle install && rvm 3.0 && bundle install ...
- Be very cautious and make sure to not include
bundler/setup
or extra gems in your Rakefile.
IMO, neither options is ideal. I think it would be nice if RCD could run bundle install
for each RUBY_CC_VERSION
before invoking the commands. This would be an easy win and make RCD much more user friendly!
OS differences
Now this is a more general issue with the cross-platform ecosystem, and not RCD specific. However, I think it's worth mentioning.
Depending on which platform you are compiling for, you may have an entirely different OS / package-manager. Some are redhat, some are debian, and the versions seem to be inconsistent. This poses a challenge when installing dependencies. Typically, it's best practice to compile any gem native libs yourself (e.g. with miniportile
), but for certain things (like LLVM, CMake, etc) this is not feasible. This puts us in a situation where were have differing versions of these deps depending on the platform. Adds some complexity to the build process.
CC
, CXX
, AR
, LD
and the like
This one is not a huge deal, but it has tripped me up a few times. When compiling a third party library, you often need to make sure that the CC
, CXX
, AR
, LD
and other environment variables are set correctly.
In the rb-sys
dockerfile, I've hardcoded sane defaults for the Rust ecosystem like so:
ENV CC_arm_unknown_linux_gnueabihf="arm-linux-gnueabihf-gcc" \
CXX_arm_unknown_linux_gnueabihf="arm-linux-gnueabihf-g++" \
AR_arm_unknown_linux_gnueabihf="arm-linux-gnueabihf-ar"
IIRC, rake-compiler-dock
does not do this automatically set CC
/CXX
for you. I wonder if it should?
Mounting cache directories
By default, rake-compiler-dock
mounts the ./tmp
directory so things are cached nicely. For Rust, I would also like to be able to cache the ./target
directory. I finagled this once, but I don't remember how. I think it's possible, but it's not obvious how to do it. I wonder if RCD should support some type of configuration file to make this type of thing easier?
# .rake-compiler-dock.yml ????
global:
extra_mounts: ["./target:/wherever/the/gem/is/target"]
env:
BAR: baz
x86_64-linux:
image: larskanis/my-custom-mri-x86_64-linux:latest
env:
FOO: bar
Different compilers
This is probably biased for the Rust world, but it would be amazing Ruby were built using clang
+ lld
for every platform (ideally, the same version of clang
for each). This would fix so many headaches and edge cases.
Docker Woes
Again, not an RCD problem, but a more general grief... Docker for Mac is almost un-useably slow for me on M1 (and bug ridden). The slow feedback cycle is extra-painful when debugging build issues.
Using a remote DOCKER_HOST
doesn't work either since directories cannot be mounted from the host. The only solution I've really found is ssh
'ing into another machine. :/
I'm curious if anyone else has run into this, and if they know a better way?
Testing
Testing precompiled gems is a bit of a pain. To solve this, I've created a useful monstrosity to make this a bit easier. It's not ideal, but I'm able to run an entire test suite against a precompiled gem, which is nice.
[Here's the Gist][gist] if you're curious. Maybe we could extract some of this into something proper? It would be nice to have a golden path for testing precompiled gems, because as of now it's a bit of a lone-wolf situation.
Summary
Thanks to RCD, we have a way to reliably build cross-platform gems with Rust. Although there are a couple stumbling blocks for new users, hopefully we can collaborate to make it easier.
PS: Would love to integrate the rb-sys
docker stuff as well, if interested.
❤️ Ian