June 20, 2023
I'm working on building NeetoCI, which is a CI/CD solution. While building pre-compiled Ruby binaries we ran into some challenges. This blog post explores the problems we faced and how we solved them.
Pre-compiled Ruby binaries are distribution-ready versions of Ruby that include optimized features for specific systems. These Ruby binaries save time by eliminating the need to compile Ruby source code manually. Pre-compiled Ruby binaries help users quickly deploy applications that use different versions of Ruby on multiple machines.
RVM (Ruby Version Manager) is widely used for managing Ruby installations on Unix-like systems. RVM provides customized pre-compiled Ruby binaries tailored for various CPU architectures. These binaries offer additional features like readline support and SSL/TLS support. You can find them at RVM binaries.
NeetoCI must execute user code in a containerized environment. A Ruby environment is essential for running Ruby on Rails applications. However, relying on the system's Ruby version is impractical since it may differ from the user's required version. Although rbenv or rvm can be used to install the necessary Ruby version, this approach could be slow. To save time, we chose to leverage pre-compiled Ruby binaries.
As a CI/CD system, NeetoCI must ensure that all versions of Ruby that an application requires are always available. Hence we decided to build our binaries instead of relying on binaries provided by RVM. Also, this would allow us to do more system-specific optimizations to the Ruby binary at build time.
We built a Ruby binary following the official documentation . We were able to execute it on our local development machines. But the same binary ran into an error in our CI/CD environment.
$ bundle config path vendor/bundle
./ruby: bad interpreter: No such file or directory
To debug the issue, we initially focused on $PATH
. However, even after
resolving the $PATH
issues, the problem persisted. We conducted a thorough
investigation to identify the root cause. Unfortunately, not much was written on
the Internet about this error. There was no mention of it in the official
Ruby documentation.
As the next step, we decided to download the binary for version 3.2.2 from RVM. While examining the configuration file, we noticed that the following arguments were used with the configure command during the Ruby binary build process:
configure_args="'--prefix=/usr/share/rvm/rubies/ruby-3.2.2' '--enable-load-relative' '--sysconfdir=/etc' '--disable-install-doc' '--enable-shared'"
Here are the explanations of the configuration arguments:
--prefix=/usr/share/rvm/rubies/ruby-3.2.2
: This specifies the directory
where the Ruby binaries, libraries and other files will be kept after the
installation is done.
--enable-load-relative
: This specifies that Ruby can load relative paths
for dynamically linked libraries. It allows the usage of relative paths
instead of absolute paths when loading shared libraries. This feature can be
beneficial in specific deployment scenarios.
--sysconfdir=/etc
: This argument sets the directory where Ruby's system
configuration files will be installed. In this case, it specifies the /etc
directory as the location for these files.
--disable-install-doc
: When this option is enabled, the installation of
documentation files during the build process is disabled. This can help speed
up the build process and save disk space, especially if you do not require
the documentation files.
--enable-shared
: Enabling this option allows the building of shared
libraries for Ruby. Shared libraries enable Ruby to dynamically link and load
specific functionality at runtime, leading to potential performance
improvements and reduced memory usage.
In simpler terms, when the --enable-load-relative
flag is enabled, the
compiled Ruby binary can search for shared libraries in its own directory using
the $ORIGIN
variable.
When I built the binary on the docker registry then the passed --prefix
was
something like /usr/share/neetoci
. When the binary is built then binary had
/usr/share/neetoci
hard coded at various places. When we download this binary
and use in CI then in the CI environment Ruby is looking for
/user/share/neetoci
to load dependencies.
By enabling --enable-load-relative
flag while building the binary Ruby will
not use the hard coded value. Rather Ruby will use $ORIGIN
variable and will
search for the dependencies in directory mentioned in $ORIGIN
.
This is particularly helpful when the Ruby binary is relocated to a different
directory or system. By using relative paths with $ORIGIN
, the binary can find
its shared libraries regardless of its new location. Without this flag, shared
libraries are loaded using absolute paths, which can cause issues if the binary
is moved to a different location and cannot locate its shared libraries.
In our specific use case, where we create and download binaries in separate
containers, we encountered an error due to the absolute paths. To overcome this,
we enabled the --enable-load-relative
flag. This allowed the binary to find
its shared libraries successfully, and it worked as expected in our CI/CD
environment.
If this blog was helpful, check out our full blog archive.