Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 117 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,147 @@
## Old Rubies, meet new macOS and Ubuntu
## Old Rubies, meet new macOS, Ubuntu, and Arch

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 and Ubuntu. Not for production use. macOS (and Homebrew) or Ubuntu are required.
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](https://github.com/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.

### Prerequisites

**macOS:** Xcode command line tools and [Homebrew](https://brew.sh).

**Ubuntu:**

```bash
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-dev
```

**Arch / Omarchy:**

```bash
sudo pacman -S --needed autoconf base-devel bison git gmp libffi libyaml openssl patch readline zlib
```

### mise

```bash
git clone https://github.com/basecamp/ruby-dev

RUBY_BUILD_DEFINITIONS="~/path/to/ruby-dev" mise use ruby@1.8.7-p374
export RUBY_BUILD_DEFINITIONS="$PWD/ruby-dev"
mise install ruby@1.8.7
```

`RUBY_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-warning-that-applies-to-all-of-the-above); 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:

```bash
export RUBY_BUILD_DEFINITIONS="/absolute/path/to/ruby-dev"
mise use ruby@1.8.7-p374
```

### rbenv

Same idea — point `RUBY_BUILD_DEFINITIONS` at the checkout:

```bash
git clone https://github.com/basecamp/ruby-dev

RUBY_BUILD_DEFINITIONS="./ruby-dev" rbenv install 1.8.7-p374
RUBY_BUILD_DEFINITIONS="$PWD/ruby-dev" rbenv install 1.8.7-p374
```

or
Or skip the environment variable and hand ruby-build the definition file directly:

```bash
git clone https://github.com/basecamp/ruby-dev
cd ruby-dev
rbenv install ./1.8.7-p374
```

or
Or grab a single definition without cloning:

```bash
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.)

### ruby-build on its own

You don't need a version manager. ruby-build installs a Ruby into any prefix you name:

```bash
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 --version
```

`RUBY_BUILD_DEFINITIONS` works here too, and is the better choice when something else is
invoking ruby-build on your behalf:

```bash
RUBY_BUILD_DEFINITIONS="$PWD" ruby-build 1.8.7-p374 ~/.rubies/1.8.7-p374
```

This 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.

### A warning that applies to all of the above

Whichever tool you use, the definitions are only found if ruby-build can actually see them:

- **`RUBY_BUILD_DEFINITIONS` must 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.

### Testing

`test/build` builds definitions in throwaway Docker containers, so a clean-machine build
is checked without touching your own toolchain.

```bash
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 # everything
```

Builds 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):

```bash
JOBS=4 MAKE_JOBS=8 test/build all
```
73 changes: 62 additions & 11 deletions test/build
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ cd "$(dirname "$0")/.."
PLATFORMS="ubuntu-noble arch"
VERSIONS=$(ls -1 [0-9]* 2>/dev/null | sort -rV)

# Builds run concurrently. JOBS caps how many containers are in flight; MAKE_JOBS caps
# make parallelism inside each one. The product is what actually hits the CPU, so the
# defaults aim for a mild oversubscribe rather than JOBS x nproc meltdown.
CORES=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
JOBS=${JOBS:-$(( CORES / 4 ))}
(( JOBS < 1 )) && JOBS=1
(( JOBS > 12 )) && JOBS=12
MAKE_JOBS=${MAKE_JOBS:-4}

# Use multi-platform builder if available (faster cross-arch builds)
BUILDER=""
if docker buildx ls 2>/dev/null | grep -A2 "^multiarch" | grep -q "running"; then
Expand All @@ -24,6 +33,13 @@ Examples:
test/build arch all # Test all versions on Arch
test/build all 2.7.8 # Test Ruby 2.7.8 on all platforms
test/build all # Test everything

Environment:
JOBS=$JOBS Containers built concurrently
MAKE_JOBS=$MAKE_JOBS make -j inside each container

Results stream in as builds finish, so they arrive out of order. A sorted
summary is printed at the end.
EOF
exit 1
}
Expand Down Expand Up @@ -90,8 +106,6 @@ test_ruby() {

[[ -n "$target_platform" ]] && platform_flag="--platform $target_platform"

printf " %-14s %-14s " "$platform" "$version"

# Use version-specific verify script if it exists, otherwise use defaults
local verify_script
if [[ -f "test/verify/$version" ]]; then
Expand All @@ -108,15 +122,23 @@ test_ruby() {
$verify_script
"

if output=$(docker run --rm $platform_flag "$image" bash -c "$build_script" 2>&1); then
local started=$SECONDS output elapsed
if output=$(docker run --rm $platform_flag -e MAKE_OPTS="-j${MAKE_JOBS}" \
"$image" bash -c "$build_script" 2>&1); then
elapsed=$(( SECONDS - started ))
ruby_version=$(echo "$output" | grep -o 'ruby [0-9].*\]' | tail -1)
echo "✓ $ruby_version"
return 0
# One printf so concurrent jobs can't interleave mid-line.
printf ' %-14s %-14s ✓ %-62s %4ds\n' "$platform" "$version" "$ruby_version" "$elapsed"
echo "pass" > "$RESULTS/$platform.$version.status"
else
echo "✗ FAILED"
echo "$output" | tail -20
return 1
elapsed=$(( SECONDS - started ))
printf ' %-14s %-14s ✗ %-62s %4ds\n' "$platform" "$version" "FAILED" "$elapsed"
echo "fail" > "$RESULTS/$platform.$version.status"
# Keep the log for the end-of-run report rather than interleaving it with
# other jobs' output as it happens.
echo "$output" | tail -30 > "$RESULTS/$platform.$version.log"
fi
return 0
}

[[ $# -lt 1 ]] && usage
Expand All @@ -142,15 +164,44 @@ for p in $platforms; do
build_image "$p"
done

# Run tests
# Run tests, up to $JOBS at a time
RESULTS=$(mktemp -d)
trap 'rm -rf "$RESULTS"' EXIT

total=0
for p in $platforms; do for v in $versions; do total=$(( total + 1 )); done; done

echo ""
echo "Running $total build(s), $JOBS at a time (make -j$MAKE_JOBS each)..."
echo ""

for p in $platforms; do
for v in $versions; do
# Throttle: wait for a slot before launching the next build. `wait -n` would be
# tidier but needs Bash 4.3, and macOS still ships 3.2 as /bin/bash — there it
# fails instantly and spins the loop on a core. Poll instead; builds run for
# minutes, so a second of latency costs nothing.
while (( $(jobs -rp | wc -l) >= JOBS )); do
sleep 1
done
test_ruby "$p" "$v" &
done
done
wait

# Summarize in stable order, since results streamed in as they finished.
failed=0
for p in $platforms; do
for v in $versions; do
test_ruby "$p" "$v" || ((failed++)) || true
if [[ $(cat "$RESULTS/$p.$v.status" 2>/dev/null) != "pass" ]]; then
failed=$(( failed + 1 ))
echo ""
echo "=== $p $v failed ==="
cat "$RESULTS/$p.$v.log" 2>/dev/null || echo "(no output captured)"
fi
done
done

echo ""
[[ $failed -eq 0 ]] && echo "All tests passed." || echo "$failed test(s) failed."
[[ $failed -eq 0 ]] && echo "All $total tests passed." || echo "$failed of $total test(s) failed."
exit $failed
Loading