Validating a Native Ruby Gem on Ruby 4.0.1

Validating a Native Ruby Gem on Ruby 4.0.1
Validating a Native Ruby Gem on Ruby 4.0.1

February 5, 2026

Notes from the ruby-libgd 0.2.4 release

With the release of Ruby 4.0, native extensions deserve a bit more attention than usual. Unlike pure-Ruby gems, C extensions depend not only on Ruby’s public API, but also on how headers, build tools, and packaging are wired together.

For the ruby-libgd 0.2.4 release, I wanted to explicitly validate compatibility with Ruby 4.0.1 before claiming support. This article documents what that process actually looked like, the issues encountered, and how they were resolved — without patching Ruby or adding hacks to the gem itself.


Context

ruby-libgd is a native C extension that provides Ruby bindings for the GD Graphics Library. It compiles a shared object via extconf.rb and links against libgd.

Article content

Because Ruby 4 introduces changes in packaging and standard library layout, I chose to validate compatibility using Docker + CI, starting from a clean environment.

The goal was simple and strict:

  • The extension must compile on Ruby 4.0.1
  • The test suite must pass
  • The runtime must load correctly (require “gd/gd”)
  • Any workaround must be documented, not hidden

The first signal: archhdrdir is missing

The initial failure was not in C code, but during compilation.

Running:

ruby -rrbconfig -e 'p RbConfig::CONFIG.values_at("rubyhdrdir", "archhdrdir")'

on Ruby 4.0.1 inside Docker returned:

["/usr/local/include/ruby-4.0.0", nil]

For native extensions, this is a red flag.

archhdrdir is where architecture-specific headers (such as ruby/config.h) live. If it is nil, mkmf may recurse, fail to link, or pick up headers from another Ruby installation.

However, the headers themselves were present on disk:

/usr/local/include/ruby-4.0.0/x86_64-linux/ruby/config.h

So this was not a missing dependency — it was a packaging issue.


What not to do

Before explaining the fix, it’s important to be explicit about what was intentionally avoided:

  • ❌ Installing ruby-dev (breaks Ruby images)
  • ❌ Patching extconf.rb
  • ❌ Vendoring Ruby headers
  • ❌ Rescuing LoadError at runtime
  • ❌ Claiming “Ruby 4 support” without proof

All of those create long-term maintenance debt.


The correct workaround (Docker only)

In Ruby 4.0.x Docker images, the architecture headers exist but are not registered in RbConfig.

Article content

The correct and minimal workaround is to export the header path explicitly at build time:

export RUBYARCHHDRDIR=/usr/local/include/ruby-4.0.0/x86_64-linux

This tells mkmf where the headers already are. Nothing is copied, patched, or overridden.

Once this variable is set:

  • extconf.rb runs normally
  • the extension compiles
  • linking succeeds
  • runtime loading works as expected

This workaround is only needed during compilation, and only affects Docker-based Ruby 4.0 builds.


A second gotcha: Bundler + path gems + native extensions

One more subtle issue appeared in CI.

When Bundler uses a gem from source via path: “.”, native extensions are not reliably built automatically. As a result, gd/gd.so may not exist even though bundle install succeeds.

Locally, this was masked because the extension had already been built.

The fix was to make the build step explicit in CI, matching the canonical native extension flow:

cd ext/gd
ruby extconf.rb
make
cd ../..

This is not Ruby-4-specific; it applies to Ruby 3 as well. The important part is that CI must compile the extension before running specs when testing from source.


What was validated

After fixing the environment and build flow, the following were verified on Ruby 4.0.1:

  • Native extension compiles successfully
  • require “gd/gd” loads correctly
  • Test suite passes
  • No API or behavior changes were required

No changes were made to the public API, and no Ruby-version-specific code paths were introduced.


The release

These changes resulted in ruby-libgd 0.2.4, a patch release whose purpose is explicit compatibility validation.

From the CHANGELOG:

Verified compatibility with Ruby 4.0.1

This keeps the contract with users clear and honest.


Closing thoughts

Native extensions tend to surface packaging and tooling issues earlier than pure-Ruby gems. Ruby 4.0 is no exception.

The important takeaway is not the workaround itself, but the process:

  • validate in a clean environment
  • understand why something fails
  • fix it at the correct layer
  • document the result

With that, Ruby 4.0.1 support in ruby-libgd is real, tested, and reproducible.

Article content

Leave a comment