
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.

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.

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/gdruby extconf.rbmakecd ../..
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.
