Question
Fix OpenSSL Header Errors When Installing Ruby Gems on macOS
Question
After installing macOS El Capitan, installing EventMachine version 1.0.7 fails because the native extension compiler cannot find openssl/ssl.h. OpenSSL is installed through Homebrew, and the header exists at the path shown below. Why does --with-ssl-dir appear not to work, and how can the gem be built with the correct OpenSSL path?
$ ls /usr/local/Cellar/openssl/1.0.2a-1/include/openssl/ssl.h
/usr/local/Cellar/openssl/1.0.2a-1/include/openssl/ssl.h
$ gem install eventmachine -v '1.0.7' -- --with-ssl-dir=/usr/local/Cellar/openssl/1.0.2a-1/include
The build ultimately fails with:
fatal error: 'openssl/ssl.h' file not found
#include <openssl/ssl.h>
Short Answer
You will learn how Ruby installs gems with native extensions, how C/C++ header lookup works, and why OpenSSL build options usually require an installation prefix rather than the include directory itself. You will also learn how to diagnose failed gem builds using compiler flags and Ruby's build log.
Concept
Some Ruby gems include native extensions: C or C++ code that must be compiled during installation. EventMachine is one example. Its source contains this include statement:
#include <openssl/ssl.h>
For that statement to work, the compiler must receive an include search path whose directory contains the openssl folder. Given this file:
/usr/local/Cellar/openssl/1.0.2a-1/include/openssl/ssl.h
the needed include directory is:
/usr/local/Cellar/openssl/1.0.2a-1/include
However, Ruby's extension configuration commonly treats --with-ssl-dir as an OpenSSL prefix. A prefix is the directory that contains both include and lib:
/usr/local/Cellar/openssl/1.0.2a-1/
├── include/
│ └── openssl/ssl.h
└── lib/
└── libssl...
When given a prefix, the build system can derive both paths:
- headers:
PREFIX/include - libraries:
PREFIX/lib
If you pass PREFIX/include to --with-ssl-dir, a build script may incorrectly look in PREFIX/include/include, which does not exist. This is why the option can look as though it was ignored.
This distinction matters beyond OpenSSL: native gems often need headers for compilation and libraries for linking. Both are usually installed under one dependency prefix.
Mental Model
Think of an OpenSSL installation as a building:
- The prefix is the building's address.
includeis the room containing instruction manuals (header files).libis the room containing reusable machine parts (libraries).
--with-ssl-dir asks for the building's address so the build script can find both rooms. Giving it the include room instead of the building address can cause it to search for a room named include inside the include room.
The compiler sees:
#include <openssl/ssl.h>
and combines each include path with openssl/ssl.h. If it has -I/path/to/include, it correctly checks:
/path/to/include/openssl/ssl.h
Syntax and Examples
For a Homebrew-installed dependency, first ask Homebrew for its prefix instead of hard-coding a versioned Cellar directory:
brew --prefix openssl
On modern Homebrew installations, the formula may be versioned:
brew --prefix openssl@1.1
Pass the returned prefix to the gem's extension configuration:
gem install eventmachine -v 1.0.7 -- --with-ssl-dir="$(brew --prefix openssl)"
For the path in the question, the equivalent command is:
gem install eventmachine -v 1.0.7 -- --with-ssl-dir=/usr/local/Cellar/openssl/1.0.2a-1
Notice that this path does not end in /include.
If an extension does not support a --with-*-dir option reliably, explicitly provide compiler and linker flags:
gem install eventmachine -v 1.0.7 -- \
--with-cppflags="-I$(brew --prefix openssl)/include" \
--with-ldflags="-L$(brew --prefix openssl)/lib"
-I.../includetells the compiler where to find headers.-L.../libtells the linker where to find libraries.
The exact OpenSSL formula and compatibility requirements depend on the gem, Ruby version, and macOS version. Very old gems may require an older dependency version or a gem upgrade.
Step by Step Execution
Consider this installation layout:
/opt/local/openssl/
├── include/openssl/ssl.h
└── lib/libssl.dylib
And this build command:
gem install example-gem -- --with-ssl-dir=/opt/local/openssl
A typical extension configuration script performs these steps:
-
RubyGems downloads and unpacks the gem source.
-
It runs the gem's
extconf.rbconfiguration script. -
The script interprets
/opt/local/opensslas the OpenSSL prefix. -
It adds an include flag similar to:
-I/opt/local/openssl/include -
It adds a library search flag similar to:
-L/opt/local/openssl/lib -
The C++ compiler processes:
#include <openssl/ssl.h> -
It combines the include flag with the header name and finds:
/opt/local/openssl/include/openssl/ssl.h -
The linker later locates the OpenSSL library under
/opt/local/openssl/lib.
By contrast, if you pass /opt/local/openssl/include as --with-ssl-dir, the configuration script may create this incorrect path:
-I/opt/local/openssl/include/include
The compiler then cannot find openssl/ssl.h.
Real World Use Cases
Native dependency paths occur whenever a package compiles against software installed outside the operating system's default locations.
- Ruby gems:
eventmachine, database drivers, image-processing gems, and cryptography extensions may compile C code. - OpenSSL integration: applications that make TLS connections need OpenSSL headers at compile time and OpenSSL libraries at link time.
- Database clients: a gem that uses PostgreSQL may need the PostgreSQL prefix so it can find
libpqheaders and libraries. - Python or Node.js extensions: packages with C extensions use the same
-Iand-Lconcepts. - CI pipelines: build machines often install dependencies in custom locations, so build scripts receive paths through environment variables or configuration flags.
The general debugging question is: "Does the tool want the dependency prefix, a header directory, a library directory, or all three?"
Real Codebase Usage
In real projects, developers avoid relying on accidental system paths.
Use a stable package-manager prefix
Instead of committing a versioned Cellar path, query the package manager:
OPENSSL_PREFIX="$(brew --prefix openssl)"
gem install eventmachine -- --with-ssl-dir="$OPENSSL_PREFIX"
A versioned Cellar directory changes whenever Homebrew upgrades the formula. A package-manager prefix is more stable.
Use explicit flags when configuration is incomplete
Some extension scripts do not derive paths correctly. In that case, specify the two build stages directly:
bundle config set --local build.eventmachine \
"--with-cppflags=-I$(brew --prefix openssl)/include --with-ldflags=-L$(brew --prefix openssl)/lib"
Then bundle install can reuse the configuration for the project.
Read the build log
When a native gem fails, RubyGems reports the path to mkmf.log. That log contains compiler checks and often reveals the actual -I, -L, and -l options used. Check it before trying random flags.
Prefer maintained dependency versions
A project may be pinned to an old gem, Ruby release, or OpenSSL API. Upgrade the gem when possible, because new macOS and OpenSSL releases can remove interfaces expected by older native extensions.
Common Mistakes
Passing the include directory to a --with-*-dir option
This is the issue shown in the question:
# Often wrong: ssl-dir usually expects the prefix
gem install eventmachine -- --with-ssl-dir=/path/to/openssl/include
Use the parent directory that contains both include and lib:
# Usually correct
gem install eventmachine -- --with-ssl-dir=/path/to/openssl
Using a header path as an include path
-I must point to a directory, not directly to ssl.h:
# Wrong
gem install example-gem -- --with-cppflags=-I/path/include/openssl/ssl.h
# Correct
gem install example-gem -- --with-cppflags=-I/path/include
Forgetting the linker path
Finding a header only solves compilation. A later link step can fail if the library directory is missing:
ld: library not found for -lssl
Provide the library path too:
--with-ldflags="-L/path/to/openssl/lib"
Assuming an existing file means the compiler can see it
This command only proves the file exists:
ls /path/to/include/openssl/ssl.h
It does not prove the compiler was invoked with -I/path/to/include.
Hard-coding a versioned Homebrew Cellar path
/usr/local/Cellar/openssl/1.0.2a-1
That path can disappear after an upgrade. Prefer brew --prefix when it is available.
Mixing incompatible versions
An old native gem may not compile against a newer OpenSSL release, even when all paths are correct. A missing-header error is a path problem; API-related compiler errors after the header is found can be a compatibility problem.
Comparisons
| Input or flag | What it should point to | Example |
|---|---|---|
--with-ssl-dir=... | Dependency prefix containing include and lib | /opt/openssl |
--with-cppflags=-I... | Header search directory | /opt/openssl/include |
--with-ldflags=-L... | Library search directory | /opt/openssl/lib |
#include <openssl/ssl.h> | Header name relative to an include directory | openssl/ssl.h |
| Build phase | Needs | Typical failure |
|---|---|---|
| Compilation | Header files and -I paths | fatal error: 'openssl/ssl.h' file not found |
| Linking | Library files and -L / -l options | library not found for -lssl or undefined symbols |
--with-ssl-dir is convenient because it may configure both phases. Explicit --with-cppflags and --with-ldflags offer more control when convenience options do not work.
Cheat Sheet
-
A native Ruby gem runs
extconf.rb, then compiles C/C++ code. -
#include <openssl/ssl.h>needs an include path ending in/include. -
If the file is
PREFIX/include/openssl/ssl.h, then:- OpenSSL prefix:
PREFIX - compiler include directory:
PREFIX/include - linker library directory:
PREFIX/lib
- OpenSSL prefix:
-
Usually use:
gem install GEM_NAME -- --with-ssl-dir="$(brew --prefix openssl)" -
For explicit flags:
gem install GEM_NAME -- \ --with-cppflags="-I$(brew --prefix openssl)/include" \ --with-ldflags="-L$(brew --prefix openssl)/lib" -
--separates RubyGems options from options passed to the native extension'sextconf.rb. -
mkmf.logis the key log file for diagnosing Ruby native extension build checks. -
Header missing: inspect
-Ipaths. -
Library missing: inspect
-Lpaths and library names. -
Prefer a package-manager prefix over a hard-coded versioned installation directory.
FAQ
Why does openssl/ssl.h exist but the compiler says it is missing?
The compiler searches only its configured include directories. The file can exist on disk while the required -I.../include flag is absent or incorrect.
Should --with-ssl-dir end with /include?
Usually no. Extension scripts commonly interpret --with-ssl-dir as an OpenSSL prefix and add /include and /lib themselves. Check the gem's build documentation or use explicit flags if needed.
What does the -- do in gem install?
It separates options for the gem command from arguments passed to the gem's native extension configuration script, usually extconf.rb.
What is the difference between -I and -L?
-I adds a header search directory for compilation. -L adds a library search directory for linking.
How can I find the correct Homebrew OpenSSL path?
Use Homebrew's prefix command for the installed formula, for example:
brew --prefix openssl
If your system uses a versioned formula, use that formula name instead.
Why can a build still fail after the header is found?
The next failure may involve linking or source compatibility. For example, an old gem may call an OpenSSL API that a newer OpenSSL version no longer provides.
Where can I see the exact compiler command used by a Ruby gem?
Inspect the mkmf.log path printed by the failed installation. It records configuration checks and compiler output.
Mini Project
Description
Create a small shell helper that installs a Ruby gem with a Homebrew-provided OpenSSL dependency. The helper demonstrates the difference between an OpenSSL prefix, its header directory, and its library directory.
Goal
Build a reusable command that discovers the OpenSSL prefix and passes correct native-extension flags to RubyGems.
Requirements
Use brew --prefix to discover the OpenSSL installation path.
Store the result in a clearly named shell variable.
Install a gem while passing OpenSSL header and library search paths.
Print the resolved prefix before installation.
Use shell quoting so paths are handled safely.
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.