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.
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-p374(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