Trying to run an old, unsupported Ruby? You're in the right place.
This is a collection of ruby-build definitions for local dev on macOS, Ubuntu, and Arch (including Omarchy). Not for production use.
Available definitions:
| Version | OpenSSL | Bundler |
|---|---|---|
| 1.8.7-p374 | 1.0.2u | 1.17.3 |
| 1.9.3-p551 | 1.0.2u | 1.17.3 |
| 2.3.3 | 1.0.2u | 1.17.3 |
| 2.3.8 | 1.0.2u | 1.17.3 |
| 2.5.9 | 1.1.1w | 1.17.3 and 2.3.27 |
| 2.7.8 | 1.1.1w | 1.17.3 and 2.4.22 |
Modern OpenSSL dropped the APIs these Rubies need, so each definition brings its own. On macOS the OpenSSL 1.0 builds come from basecamp/homebrew-dev; everywhere else OpenSSL is compiled from source into the Ruby's own prefix, so nothing lands system-wide and nothing is shared between versions.
Every definition builds with -O3 -fno-strict-overflow. Both halves matter:
-O3 — leaving RUBY_CFLAGS empty is not "use configure's default". ruby-build exports
it as CFLAGS, which supersedes configure's own optflags in the compile line, so an empty
value builds at -O0. That's 2–3.5× slower. Confusingly, RbConfig::CONFIG["optflags"]
still reports -O3 in that case; time a build rather than believing it.
-fno-strict-overflow — these sources predate the compilers building them, and their
fixnum overflow checks assume signed overflow wraps. That's undefined behaviour, and GCC
exploits it from -O2 up. Build 1.8.7 at -O3 without this flag and it compiles cleanly,
runs, loads every stdlib — and evaluates 2**64 to 0, typed Fixnum. Silent wrong
arithmetic. test/build asserts against exactly this, so the trap can't come back.
-march=native is deliberately not used. It measured ~12% slower than plain -O3 on
Zen 4, worst on numeric loops (−16% integer, −21% float), and microbenchmarks are the
friendliest case it gets. It would also make binaries non-portable between machines for no
gain.
1.9.3 caps _FORTIFY_SOURCE at 2 under GCC. Ubuntu's GCC raises it to 3 automatically
whenever optimization is on, and level 3's object-size inference trips on 1.9.3 — the build
aborts with *** buffer overflow detected ***. This isn't a hardening regression: fortify
does nothing without optimization, so while these were building at -O0 there was none at
all. Only 1.9.3 needs the cap.
1.9.3 through 2.7.8 carry backports of two regexp compiler fixes, applied to the release
tarball before configure. The patches live in patches/<major.minor>/, and each
definition's apply_patches step applies its series in name order. Both fixes are
reachable from any pattern a user can get into Regexp.new:
- ruby/ruby#16801 (CVE-2026-110264): more than
32,767 range repeats or null checks in one pattern wrap a 16-bit ID negative. For range
repeats the first match then writes out of bounds:
Regexp.new("(?:foobar){0,100}" * 100000) =~ "x"crashes the process. Patched, both are aRegexpError. - ruby/ruby#19020: nested bounded repeats
overflow the size arithmetic, so
Regexp.new("(?:.{90000,}){90000}")never returns, and doesn't answer SIGTERM either. Patched, it compiles at once.
The other two fixes in the same family, ruby/ruby#13332 and
Bug #22384, fix a double free that later Rubies
introduced, so there's nothing to backport. 1.8.7 predates Onigmo: its own engine caps
quantifiers at 32,767 and a compiled pattern at 64 KB, and rejects all of these with a
RegexpError.
test/build asserts each fix against the installed Ruby, so a patch that stops applying
fails the build rather than shipping an unpatched Ruby quietly.
macOS: Xcode command line tools and Homebrew.
Ubuntu:
sudo apt-get install -y autoconf bison build-essential curl git libdb-dev libffi-dev \
libgdbm-dev libgmp-dev libncurses5-dev libreadline-dev libssl-dev libyaml-dev \
patch uuid-dev zlib1g-devArch / Omarchy:
sudo pacman -S --needed autoconf base-devel bison git gmp libffi libyaml openssl patch readline zlibgit clone https://github.com/basecamp/ruby-dev
export RUBY_BUILD_DEFINITIONS="$PWD/ruby-dev"
mise install ruby@1.8.7RUBY_BUILD_DEFINITIONS must be an absolute path and must be set for every command that
builds a Ruby — mise shells out to ruby-build, which reads it from the environment. See
the warning below; a bad path fails silently.
Fuzzy versions resolve to the definitions here, so ruby@1.8.7 gets you 1.8.7-p374 and
ruby@2.3 gets you 2.3.8.
To pin a project to one:
export RUBY_BUILD_DEFINITIONS="/absolute/path/to/ruby-dev"
mise use ruby@1.8.7-p374Same idea — point RUBY_BUILD_DEFINITIONS at the checkout:
git clone https://github.com/basecamp/ruby-dev
RUBY_BUILD_DEFINITIONS="$PWD/ruby-dev" rbenv install 1.8.7-p374Or skip the environment variable and hand ruby-build the definition file directly:
git clone https://github.com/basecamp/ruby-dev
cd ruby-dev
rbenv install ./1.8.7-p374Or grab a single definition without cloning:
curl -O https://raw.githubusercontent.com/basecamp/ruby-dev/main/1.8.7-p374
rbenv install ./1.8.7-p374That works for 1.8.7 only. 1.9.3 through 2.7.8 apply patches from patches/ beside the
definition (see Regexp compiler fixes), so they need the checkout;
on their own they stop with an error rather than build without them.
(Note the leading ./ - this is a path to a specific build definition for ruby-build, not a version specific for it to look for among its built-in definitions.)
You don't need a version manager. ruby-build installs a Ruby into any prefix you name:
git clone https://github.com/basecamp/ruby-dev
cd ruby-dev
ruby-build ./1.8.7-p374 ~/.rubies/1.8.7-p374
~/.rubies/1.8.7-p374/bin/ruby --versionRUBY_BUILD_DEFINITIONS works here too, and is the better choice when something else is
invoking ruby-build on your behalf:
RUBY_BUILD_DEFINITIONS="$PWD" ruby-build 1.8.7-p374 ~/.rubies/1.8.7-p374This is also the form to use with chruby or any other manager that just wants a directory
of Rubies — build into its search path (~/.rubies for chruby) and it'll pick them up.
Whichever tool you use, the definitions are only found if ruby-build can actually see them:
RUBY_BUILD_DEFINITIONSmust be an absolute path. Build tools run from temporary working directories, so a relative path usually resolves to nowhere.- A wrong path fails silently. ruby-build appends its own bundled definition directory to the search list and takes the first match, so a typo'd or unexpanded path doesn't error — it quietly builds the upstream definition instead, which is exactly the one that fails on a modern compiler. Confusing compiler errors are the usual symptom.
"~/path/to/ruby-dev"does not expand. The tilde is literal inside double quotes. Use"$HOME/path/to/ruby-dev"or leave it unquoted.
If a build fails in a way that looks nothing like the notes in this repo, check that the definition was actually picked up before debugging the compiler error.
There's no cloud CI here. Run bin/ci before merging, and it signs off the commit for you
on success:
bin/ci # lint + the full build matrix, then gh signoff
bin/ci --lint # lint only, no Docker — seconds, good for a quick check
bin/ci arch # lint + Arch only
bin/ci arch 2.7.8 # lint + a single buildOnly a full run signs off. Anything narrower reports its results and explicitly declines to sign, because a green tick that covered one platform is worse than no tick.
The lint pass is cheap and catches the two mistakes that otherwise surface ten minutes into
a Docker build: a syntax error in a definition (ruby-build sources these, so a stray quote
is a build failure), and an install_package URL with no #sha256 (ruby-build silently
skips verification when the checksum is missing).
Sign-off needs the extension:
gh extension install basecamp/gh-signofftest/build builds definitions in throwaway Docker containers, so a clean-machine build
is checked without touching your own toolchain. bin/ci runs it for you; use it directly
when you want a specific slice.
test/build arch 1.8.7-p374 # one version on one platform
test/build ubuntu-noble all # every version on one platform
test/build all # everythingBuilds run concurrently, so results stream in out of order and a sorted summary with any
failure logs is printed at the end. Tune the load with JOBS (containers at a time,
defaults to cores/4) and MAKE_JOBS (make -j inside each, defaults to 4):
JOBS=4 MAKE_JOBS=8 test/build all