Building old Rubies on Ubuntu 22.04

7 minute read

Recently I built a new computer, and with that comes a fresh installation of Ubuntu LTS, which is currently at version 22.04.

I now need to replicate my development environment. For Ruby, that means installing chruby for automatically switching Ruby versions between projects, and ruby-install for installing different Ruby versions. These are the tools that I’ve been using for several years as my default for those tasks.

Looking at my old machine, I had quite a few old versions of Ruby installed. I will need to install at least some of these versions on my new machine to maintain existing Ruby projects I’m working on:

$ chruby
   ruby-2.1.1
   ruby-2.1.5
   ruby-2.2.3
   ruby-2.2.4
   ruby-2.3.0
   ruby-2.3.1
   ruby-2.3.3
   ruby-2.4.0
   ruby-2.4.1
   ruby-2.4.2
   ruby-2.5.0
   ruby-2.5.3
   ruby-2.5.5
   ruby-2.5.7
   ruby-2.6.2
   ruby-2.7.2
   ruby-2.7.3
   ruby-2.7.6
   ruby-3.0.1

The first version I’m going to try installing is the oldest Ruby 2.x I had been using:

$ ruby-install ruby-2.7.6

This results in the following error:

compiling ripper.c
ripper.c: In function ‘ripper_yyparse’:
ripper.c:6309:12: error: ‘YYEMPTY’ undeclared (first use in this function)
 6309 |   yychar = YYEMPTY; /* Cause a token to be read.  */
      |            ^~~~~~~
ripper.c:6309:12: note: each undeclared identifier is reported only once for each function it appears in
ripper.c:6441:22: error: ‘YYerror’ undeclared (first use in this function); did you mean ‘yyerror’?
 6441 |     {
      |                      ^      
      |                      yyerror
ripper.c:6447:16: error: ‘YYUNDEF’ undeclared (first use in this function); did you mean ‘T_UNDEF’?
 6447 |     {
      |                ^      
      |                T_UNDEF
make[2]: *** [Makefile:289: ripper.o] Error 1
make[2]: Leaving directory '/home/abe/src/ruby-2.7.6/ext/ripper'
make[1]: *** [exts.mk:265: ext/ripper/all] Error 2
make[1]: Leaving directory '/home/abe/src/ruby-2.7.6'
make: *** [uncommon.mk:295: build-ext] Error 2
!!! Compiling ruby 2.7.6 failed!

After some digging, it turns out this is due to having a version of GNU Bison that’s too new for our poor old Ruby. I have Bison version 3 while my Ruby build expects version 2:

$ bison -V
bison (GNU Bison) 3.8.2
Written by Robert Corbett and Richard Stallman.

Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

I can work around this by downloading and building an old version of Bison. I’m going to install it to /tmp because I only need it temporarily for building these versions of Ruby:

$ cd ~/Downloads
$ wget http://ftp.gnu.org/gnu/bison/bison-2.7.1.tar.gz
$ tar -xvf bison-2.7.1.tar.gz
$ cd bison-2.7.1
$ ./configure --prefix=/tmp
$ make

Oops - now I’ve hit another error:

fseterr.c:77:3: error: #error "Please port gnulib fseterr.c to your platform! Look at the definitions of ferror and clearerr on your system, then report this to bug-gnulib."
   77 |  #error "Please port gnulib fseterr.c to your platform! Look at the definitions of ferror and clearerr on your system, then report this to bug-gnulib."
      |   ^~~~~
make[3]: *** [Makefile:1915: fseterr.o] Error 1
make[3]: Leaving directory '/home/abe/Downloads/bison-2.7.1/lib'
make[2]: *** [Makefile:1672: all] Error 2
make[2]: Leaving directory '/home/abe/Downloads/bison-2.7.1/lib'
make[1]: *** [Makefile:1558: all-recursive] Error 1
make[1]: Leaving directory '/home/abe/Downloads/bison-2.7.1'
make: *** [Makefile:1488: all] Error 2

Apparently the error has to do with a glibc issue, which can be fixed with the following patch:

$ wget https://raw.githubusercontent.com/rdslw/openwrt/e5d47f32131849a69a9267de51a30d6be1f0d0ac/tools/bison/patches/110-glibc-change-work-around.patch
$ patch -p1 < 110-glibc-change-work-around.patch

Now I should be able to finish building bison successfully:

$ make
$ make install

And now I can use my new (well, old) version of bison like so:

$ PATH=/tmp/bin:$PATH bison -V
bison (GNU Bison) 2.7.12-4996
Written by Robert Corbett and Richard Stallman.

Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Now to retry my Ruby build with this version of bison:

$ PATH=/tmp/bin:$PATH ruby-install ruby 2.7.6

Eventually, this outputs another error:

Traceback (most recent call last):
	11: from ./tool/rbinstall.rb:947:in `<main>'
	10: from ./tool/rbinstall.rb:947:in `each'
	 9: from ./tool/rbinstall.rb:950:in `block in <main>'
	 8: from ./tool/rbinstall.rb:799:in `block in <main>'
	 7: from ./tool/rbinstall.rb:835:in `install_default_gem'
	 6: from ./tool/rbinstall.rb:835:in `each'
	 5: from ./tool/rbinstall.rb:845:in `block in install_default_gem'
	 4: from ./tool/rbinstall.rb:279:in `open_for_install'
	 3: from ./tool/rbinstall.rb:846:in `block (2 levels) in install_default_gem'
	 2: from /home/abe/src/ruby-2.7.6/lib/rubygems/specification.rb:2430:in `to_ruby'
	 1: from /home/abe/src/ruby-2.7.6/lib/rubygems/core_ext/kernel_require.rb:83:in `require'
/home/abe/src/ruby-2.7.6/lib/rubygems/core_ext/kernel_require.rb:83:in `require': cannot load such file -- openssl (LoadError)
make: *** [uncommon.mk:373: do-install-all] Error 1
!!! Installation of ruby 2.7.6 failed!

Looks like there’s an issue with OpenSSL. One can read the full compile error in ~/src/ruby-2.7.6/ext/openssl/mkmf.log, but the gist is that I’m using OpenSSL 3 while my old Ruby expects an older version:

$ openssl version
OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)

I can work around this error by compiling an old version of OpenSSL:

$ sudo apt install build-essential checkinstall zlib1g-dev
$ cd ~/Downloads
$ wget https://www.openssl.org/source/openssl-1.1.1q.tar.gz
$ tar xf openssl-1.1.1q.tar.gz
$ cd ~/Downloads/openssl-1.1.1q
$ ./config --prefix=/opt/openssl-1.1.1q --openssldir=/opt/openssl-1.1.1q shared zlib
$ make
$ make test
$ sudo make install

Finally, I can tell ruby-install to use the old version of OpenSSL like so, and the build finally succeeds:

$ PATH=/tmp/bin:$PATH ruby-install ruby 2.7.6 -- --with-openssl-dir=/opt/openssl-1.1.1q
...
>>> Successfully installed ruby 2.7.6 into /home/abe/.rubies/ruby-2.7.6

Re-evaluating my tools

So that was a headache. When it comes to my Ruby tools, what I want out of my “Ruby installer” tool is one that simply handles installing whatever Ruby version I ask it to with as little handholding from me as possible.

In the past, ruby-install had functioned seamlessly in the way that I wanted, but now with the passage of time I’ve discovered that ruby-install is really only meant for installing the most current versions of Ruby, and in fact says so right on the tin under the “Anti-Features” section of its README:

Does not support installing unsupported/unmaintained versions of Ruby.

I respect that the project sets these boundaries, and it’s purely my own ignorance that I wasn’t aware of that. But as I expect to always have some Ruby projects on older Ruby versions, it means that I need a different tool that better suits my needs.

asdf and ruby-build

One tool that came up during the debugging of my build issues is ruby-build. It’s a tool similar to ruby-install, except it doesn’t blush at patching over build issues like I had encountered.

If I delete the Ruby 2.7.6 I installed with ruby-install, and instead try installing it with ruby-build, it “just works:”

$ rm -rf ~/.rubies/ruby-2.7.6
$ ruby-build 2.7.6 ~/.rubies/ruby-2.7.6

To follow progress, use 'tail -f /tmp/ruby-build.20230113133057.3060907.log' or pass --verbose
Downloading openssl-1.1.1s.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/c5ac01e760ee6ff0dab61d6b2bbd30146724d063eb322180c6f18a6f74e4b6aa
Installing openssl-1.1.1s...
Installed openssl-1.1.1s to /home/abe/.rubies/ruby-2.7.6

Downloading ruby-2.7.6.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.6.tar.bz2
Installing ruby-2.7.6...

WARNING: ruby-2.7.6 is nearing its end of life.
It only receives critical security updates, no bug fixes.

Installed ruby-2.7.6 to /home/abe/.rubies/ruby-2.7.6

Explictly installing it to ~/.rubies means I can continue using chruby for Ruby version switching between projects.

But while I’m re-evaluating tools, why not reconsider chruby?

One competing tool I’ve become aware of is asdf, which is a language-agnostic version switcher. On my old development machine, I had chruby for Ruby switching, n for Node switching, and pyenv for Python switching (Python in particular is a mess with this stuff in my opinion). All of these tools can be replaced by asdf, using its plugin system.

asdf’s Ruby plugin also supports building and installing Ruby versions with asdf install ruby 2.7.6, which defers to ruby-build under the hood. As expected from the ease of ruby-build, this also “just works.”

So, I’m currently giving asdf a trial and it’s going fine so far. I did have to enable a flag for it to recognize .ruby-version files.

Nix

I also dipped my toe into Nix by reading through the intro documentation and doing some simple examples, but I’m not ready to start porting my projects over to it just yet.

Nix is appealing to me because:

  • it solves exactly the problem of system dependencies that created my Ruby build hell, and
  • it can also formalize other runtime project dependencies (e.g. ImageMagick), and
  • it can be used to ship the project as a build artifact to PaaS like Fly.io so that I don’t have to write Dockerfiles

However, it seems that Nix will require a moderate time investment to learn properly before I can make full use of it, which at this moment I don’t have.

I’m also tentatively a little unsure if it’s the best tool to handle formalizing every project dependency - say, like, Postgres versions.

On my development machine, I have several Postgres clusters running various versions including 9.6, 10, 12, and 13:

$ pg_lsclusters
Ver Cluster Port Status Owner    Data directory               Log file
9.6 main    5432 online postgres /var/lib/postgresql/9.6/main /var/log/postgresql/postgresql-9.6-main.log
10  main    5433 online postgres /var/lib/postgresql/10/main  /var/log/postgresql/postgresql-10-main.log
12  main    5435 online postgres /var/lib/postgresql/12/main  /var/log/postgresql/postgresql-12-main.log
13  stuff   5436 online postgres /stuff/data                  /var/log/postgresql/postgresql-13-stuff.log
13  main    5434 online postgres /var/lib/postgresql/13/main  /var/log/postgresql/postgresql-13-main.log

Each of those clusters has many databases running on it. My naive understanding of Nix is that if I Nix-ified every one of those projects, they’d each get their own entirely different instance of Postgres running - so I would have potentially several dozen Postgres’s running. That seems kinda wasteful to me.

Then again, I suppose I could just not Nix-ify the Postgres dependency. Or perhaps just put the postgres-client version dependency in the Nix config.

And I’m not quite sure how it will work with different Rails environments (development, test, production) of the same project - each of which share most dependencies, but have different databases and sometimes different runtime dependencies, and in the case of production a special build pipeline.

I definitely plan to invest more time into Nix to ascertain this stuff because it has a lot of promise!

Updated: