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
inherit FROM 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 compatible Makefile from an extconf.rb, users can just use RCD normally by specifying the RCD_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
I maintain a project called
rb-systhat builds on top of therake-compilerecosystem, 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-dockin therb-sysproject (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.
inherit
FROM larskanis/rake-compiler-dock-mri-$PLAT:$VERSION.rb_sysgem knows how to generate a compatibleMakefilefrom anextconf.rb, users can just use RCD normally by specifying theRCD_IMAGE=rbsys/$RUBY_PLATFORM:$RB_SYS_VERSIONenvironment variable.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-dockdoesn't ensure thatbundle installis run for eachRUBY_CC_VERSION. This means that any Rakefile that usesbundler/setupwill fail withBundler::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:
rvm 3.1 && bundle install && rvm 3.0 && bundle install ...bundler/setupor extra gems in your Rakefile.IMO, neither options is ideal. I think it would be nice if RCD could run
bundle installfor eachRUBY_CC_VERSIONbefore 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,LDand the likeThis 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,LDand other environment variables are set correctly.In the
rb-sysdockerfile, I've hardcoded sane defaults for the Rust ecosystem like so:IIRC,
rake-compiler-dockdoes not do this automatically setCC/CXXfor you. I wonder if it should?Mounting cache directories
By default,
rake-compiler-dockmounts the./tmpdirectory so things are cached nicely. For Rust, I would also like to be able to cache the./targetdirectory. 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?Different compilers
This is probably biased for the Rust world, but it would be amazing Ruby were built using
clang+lldfor every platform (ideally, the same version ofclangfor 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_HOSTdoesn't work either since directories cannot be mounted from the host. The only solution I've really found isssh'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-sysdocker stuff as well, if interested.❤️ Ian