The message in your terminal is a summary, not the error. *** extconf.rb failed *** means the gem’s configure script gave up; why it gave up is in mkmf.log, and RubyGems prints the exact path to it. Open that file and read it from the bottom:
To see why this extension failed to compile, please check the mkmf.log which can be found here:
/tmp/hw_gemlab/gems/extensions/universal-darwin-25/2.6.0/hw_extfail-0.0.1/mkmf.log
Everything below was executed on macOS 26.6 (arm64) with the system Ruby 2.6.10p210 and RubyGems 3.0.3.1, into a scratch GEM_HOME. Output is unedited. Linux package names are cited from distribution conventions and were not run on Linux.
Table of Contents
What mkmf.log actually contains
Take the simplest possible case: an extension that needs a header the machine does not have. The terminal gives you this much —
ERROR: Error installing ./hw_extfail-0.0.1.gem:
ERROR: Failed to build gem native extension.
...
checking for hw_totally_missing.h... no
missing hw_totally_missing.h -- install the -dev package
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.
— and mkmf.log gives you the compiler invocation, the throwaway source file it fed in, and the compiler’s own words:
have_header: checking for hw_totally_missing.h... -------------------- no
"xcrun clang -I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/... -c conftest.c"
conftest.c:3:10: fatal error: 'hw_totally_missing.h' file not found
3 | #include <hw_totally_missing.h>
| ^~~~~~~~~~~~~~~~~~~~~~
1 error generated.
checked program was:
/* begin */
1: #include "ruby.h"
2:
3: #include <hw_totally_missing.h>
/* end */
mkmf works by compiling tiny probe programs and recording whether each one built. conftest.c is that probe. The include paths on the compiler line are the most useful thing in the file — if the header you expect is installed somewhere not listed there, you have found your problem, and the fix is a --with-…-dir flag rather than another apt install. The missing-header example above can be reproduced on your own Ruby: ruby-gem-extconf-rb-failed.sh builds and installs a gem whose extconf.rb needs hw_totally_missing.h, into a separate GEM_HOME, and checks the failing exit status, the mkmf.log path RubyGems prints, the compiler’s “file not found” line and the missing-compiler error.
Most of the errors in mkmf.log are supposed to be there
This is the trap that makes the log look unreadable. A real gem probes for dozens of optional features, and every unavailable one leaves a compiler error behind. Installing mysql2 here produced a log containing five error: lines — and none of them was the cause:
checking for rb_absint_size()... yes
checking for rb_absint_singlebit_p()... yes
checking for rb_gc_mark_movable()... no
checking for rb_wait_for_single_fd()... yes
checking for rb_enc_interned_str() in ruby.h... no
Each no is a deliberate question that got a deliberate answer, and each one wrote error: use of undeclared identifier … into the log. Grepping mkmf.log for “error” is therefore a good way to chase five red herrings. Read the end of the file, and read the last lines of the terminal output, which is where the fatal one lands.
A real failure, and the flag that fixed it
In this case the fatal error never reached a compiler at all. mysql2‘s own extconf.rb aborted first:
$ gem install mysql2
Building native extensions. This could take a while...
ERROR: Error installing mysql2:
ERROR: Failed to build gem native extension.
...
-----
Cannot find library dir(s) /AppleInternal/Library/BuildRoots/.../MacOSX26.6.Internal.sdk/usr/local/libressl-by-deployment-target/14.0/lib
-----
*** extconf.rb failed ***
That path is the tell. Apple’s system Ruby was built against an internal SDK that does not exist on any user’s machine, so the TLS library directory it advertises is a dead reference — the compiler/SDK mismatch case, in its most literal form. The gem’s own extconf.rb checks the directory exists and aborts. Pointing it at a real OpenSSL fixed it outright:
$ gem install mysql2 -- --with-openssl-dir="$(brew --prefix openssl@3)"
Building native extensions with: '--with-openssl-dir=/opt/homebrew/opt/openssl@3'
This could take a while...
Successfully installed mysql2-0.5.7
1 gem installed
Everything after -- is passed straight to extconf.rb. That is the mechanism behind every --with-mysql-config=, --with-pg-config= and --with-opt-dir= recipe you will find; the log’s “Provided configuration options” block lists the ones the gem accepts.
The four causes worth checking, in order
- Missing development headers. The library being installed is not enough — you need its headers:
libpq-dev/postgresql-develforpg,libmysqlclient-dev/mysql-develformysql2,libxml2-devfornokogiri. Symptom:fatal error: 'something.h' file not foundinmkmf.log. (Package names cited, not run.) - Missing Ruby headers. Distribution Rubies split them out —
ruby-devon Debian/Ubuntu,ruby-develon RHEL-family. Symptom: the probe fails onruby.hitself, so every check inmkmf.logfails. - No working compiler. mkmf could not build even an empty program, so nothing about the gem is at fault. Pointing
mkmfat a compiler that does not exist reproduces it exactly:checking for stdio.h... mkmf.rb:467:in `try_do': The compiler failed to generate an executable file. (RuntimeError) You have to install development tools first.That second line is the whole diagnosis. On macOS:
xcode-select --install, then checkxcode-select -pnames a directory that exists. On Linux:build-essentialorDevelopment Tools. - SDK, library or Ruby-version mismatch — the
mysql2case above. Symptom: the header or library is installed, but the path in the log is one you have never seen. Fix it with--with-…-dirrather than by reinstalling things.
One thing that does not help: gem install --verbose. It prints more of RubyGems’ own activity, not more of the compiler’s; mkmf.log is where the compiler output goes either way. Two things worth knowing: a failed gem install exits 1, which is what your CI should be checking, and the unpacked gem is left on disk (“Gem files will remain installed in … for inspection”), so you can cd into it and run ruby extconf.rb by hand with different flags until it works.
Finally, the version underneath all this: the Ruby used here is 2.6.10, and ruby-lang.org lists the 2.6 branch as status: eol with an EOL date of 2022-04-12. If you are hitting extension failures on a system Ruby that old, install a current one with rbenv, rvm or mise before spending an afternoon on SDK paths — the gem you are fighting may simply have dropped support for it. (And if gem -v prints nothing at all rather than a version, you are one level further out: RubyGems ships inside Ruby itself, so a missing gem command means a missing Ruby, not a missing RubyGems package.) For a worked example of reading the last line rather than the wall above it, three different failures all get called “the sqlite3 mkmf error” and each one has a different fix.