Question

Fix OpenSSL Header Errors When Installing Ruby Gems on macOS

rubymacosopensslhomebreweventmachine

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.
  • include is the room containing instruction manuals (header files).
  • lib is 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
Take Quiz

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.../include tells the compiler where to find headers.
  • -L.../lib tells 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:

  1. RubyGems downloads and unpacks the gem source.

  2. It runs the gem's extconf.rb configuration script.

  3. The script interprets /opt/local/openssl as the OpenSSL prefix.

  4. It adds an include flag similar to:

    -I/opt/local/openssl/include
    
  5. It adds a library search flag similar to:

    -L/opt/local/openssl/lib
    
  6. The C++ compiler processes:

    #include <openssl/ssl.h>
    
  7. It combines the include flag with the header name and finds:

    /opt/local/openssl/include/openssl/ssl.h
    
  8. 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 libpq headers and libraries.
  • Python or Node.js extensions: packages with C extensions use the same -I and -L concepts.
  • 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?"

Take Quiz

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 flagWhat it should point toExample
--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 directoryopenssl/ssl.h
Build phaseNeedsTypical failure
CompilationHeader files and -I pathsfatal error: 'openssl/ssl.h' file not found
LinkingLibrary files and -L / -l optionslibrary 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.

Take Quiz

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
  • 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's extconf.rb.

  • mkmf.log is the key log file for diagnosing Ruby native extension build checks.

  • Header missing: inspect -I paths.

  • Library missing: inspect -L paths 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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more