Ruby and Nokogiri Gem Compatibility

We encounter gem installation issues gazillion times. Currently there is no tool that can tell us whether a given version of Ruby is compatible with a given version of gem or not. We have to look at the Rubygems home page for the required Ruby version for a given gem. It is not reliable because it is not tested. Let's manually test the compatibility of a given ruby gem with a specific version of Ruby. Docker is a great tool for this experiment. Create a Dockerfile.

FROM alpine:3.9
RUN apk add --no-cache ruby ruby-bundler

Build the image:

docker build --tag alpine-ruby .

Check the Ruby installation:

$ docker run --rm alpine-ruby ruby -e 'puts "Hello World"'
Hello World

The default installation is Ruby 2.5.3:

docker run --rm alpine-ruby ruby -e 'puts RUBY_VERSION'
2.5.3

You can also run this command:

docker container run alpine-ruby ruby -v

Let's play in the bash terminal.

docker container run alpine-ruby /bin/bash

This gives error:

docker: Error response from daemon: OCI runtime create failed: container_linux.go:344: starting container process caused "exec: \"/bin/bash\": stat /bin/bash: no such file or directory": unknown.
ERRO[0000] error waiting for container: context canceled 

The reason: Alpine does not include bash. Modify the Dockerfile:

FROM alpine:3.9
RUN apk add --no-cache bash ruby ruby-bundler

We can make it easier by changing the specific version to latest. This way we don't have to spend time on looking up the latest alpine image in the documentation.

FROM alpine:latest
RUN apk add --no-cache bash ruby ruby-bundler

This will install the bash. But we need a way to get inside a running container and install software. So let's specify the CMD instruction in the Dockerfile:

FROM alpine:latest
RUN apk add --no-cache bash ruby ruby-bundler
CMD bash

Re-build the image.

docker image build -t alpine-ruby .

We can now go the terminal inside the container:

docker container run -it alpine-ruby 

Let's install nokogiri gem:

$ gem install nokogiri

The error message:

Fetching: mini_portile2-2.4.0.gem (100%)
Successfully installed mini_portile2-2.4.0
Fetching: nokogiri-1.10.1.gem (100%)
Building native extensions. This could take a while...
ERROR:  Error installing nokogiri:
    ERROR: Failed to build gem native extension.

    current directory: /usr/lib/ruby/gems/2.5.0/gems/nokogiri-1.10.1/ext/nokogiri
/usr/bin/ruby -r ./siteconf20190220-12-13zajlq.rb extconf.rb
mkmf.rb can't find header files for ruby at /usr/lib/ruby/include/ruby.h

extconf failed, exit code 1

Gem files will remain installed in /usr/lib/ruby/gems/2.5.0/gems/nokogiri-1.10.1 for inspection.
Results logged to /usr/lib/ruby/gems/2.5.0/extensions/x86_64-linux/2.5.0/nokogiri-1.10.1/gem_make.out

Let's check the log file from the above output:

cat /usr/lib/ruby/gems/2.5.0/extensions/x86_64-linux/2.5.0/nokogiri-1.10.1/gem_make.out
current directory: /usr/lib/ruby/gems/2.5.0/gems/nokogiri-1.10.1/ext/nokogiri
/usr/bin/ruby -r ./siteconf20190220-12-13zajlq.rb extconf.rb
mkmf.rb can't find header files for ruby at /usr/lib/ruby/include/ruby.h

extconf failed, exit code 1
bash-4.4# gcc
bash: gcc: command not found

Install gcc.

FROM alpine:latest
RUN apk add --no-cache bash gcc ruby ruby-bundler
CMD bash

Rebuild the image and run the container.

gcc -v
gcc version 8.2.0 (Alpine 8.2.0) 

We need ruby-dev to get past another error. Add it to Dockerfile and rebuild the image. The error is now:

bash-4.4# gem install nokogiri
Fetching: mini_portile2-2.4.0.gem (100%)
Successfully installed mini_portile2-2.4.0
Fetching: nokogiri-1.10.1.gem (100%)
Building native extensions. This could take a while...
ERROR:  Error installing nokogiri:
    ERROR: Failed to build gem native extension.

    current directory: /usr/lib/ruby/gems/2.5.0/gems/nokogiri-1.10.1/ext/nokogiri
/usr/bin/ruby -r ./siteconf20190220-6-1309qm6.rb extconf.rb
checking if the C compiler accepts ... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
    --with-opt-dir
    --without-opt-dir
    --with-opt-include
    --without-opt-include=${opt-dir}/include
    --with-opt-lib
    --without-opt-lib=${opt-dir}/lib
    --with-make-prog
    --without-make-prog
    --srcdir=.
    --curdir
    --ruby=/usr/bin/$(RUBY_BASE_NAME)
    --help
    --clean
/usr/lib/ruby/2.5.0/mkmf.rb:456:in `try_do': The compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.
    from /usr/lib/ruby/2.5.0/mkmf.rb:574:in `block in try_compile'
    from /usr/lib/ruby/2.5.0/mkmf.rb:521:in `with_werror'
    from /usr/lib/ruby/2.5.0/mkmf.rb:574:in `try_compile'
    from extconf.rb:138:in `nokogiri_try_compile'
    from extconf.rb:162:in `block in add_cflags'
    from /usr/lib/ruby/2.5.0/mkmf.rb:632:in `with_cflags'
    from extconf.rb:161:in `add_cflags'
    from extconf.rb:416:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /usr/lib/ruby/gems/2.5.0/extensions/x86_64-linux/2.5.0/nokogiri-1.10.1/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /usr/lib/ruby/gems/2.5.0/gems/nokogiri-1.10.1 for inspection.
Results logged to /usr/lib/ruby/gems/2.5.0/extensions/x86_64-linux/2.5.0/nokogiri-1.10.1/gem_make.out

Let's check the mkmf.log:

cat /usr/lib/ruby/gems/2.5.0/extensions/x86_64-linux/2.5.0/nokogiri-1.10.1/mkmf.log
"gcc -o conftest -I/usr/include/ruby-2.5.0/x86_64-linux-musl -I/usr/include/ruby-2.5.0/ruby/backward -I/usr/include/ruby-2.5.0 -I. -Os -fomit-frame-pointer -fno-omit-frame-pointer -fno-strict-aliasing   -Os -fomit-frame-pointer -fno-omit-frame-pointer -fno-strict-aliasing -fPIC  conftest.c  -L. -L/usr/lib -L. -Wl,--as-needed -fstack-protector -rdynamic -Wl,-export-dynamic     -lruby  -lpthread -lgmp -ldl -lcrypt -lm   -lc "
In file included from /usr/include/ruby-2.5.0/ruby/ruby.h:29,
                 from /usr/include/ruby-2.5.0/ruby.h:33,
                 from conftest.c:1:
/usr/include/ruby-2.5.0/ruby/defines.h:112:10: fatal error: stdio.h: No such file or directory
 #include <stdio.h>
          ^~~~~~~~~
compilation terminated.
checked program was:
/* begin */
1: #include "ruby.h"
2: 
3: int main(int argc, char **argv)
4: {
5:   return 0;
6: }
/* end */

The problem is in some header file:

/usr/include/ruby-2.5.0/ruby/defines.h:112:10: fatal error: stdio.h: No such file or directory

Resolution:

Add libc-dev package to Dockerfile.

apk add libc-dev

libc-dev is a meta package to pull in correct libc package. Now zlib is missing; necessary for building libxml2. We need zlib-dev package. Add it to Dockerfile.

FROM alpine:3.9
RUN apk add --no-cache bash gcc libc-dev ruby-dev zlib-dev ruby ruby-bundler
CMD bash

Rebuild the image, we now get:

/usr/lib/ruby/gems/2.5.0/gems/mini_portile2-2.4.0/lib/mini_portile2/mini_portile.rb:380:in `spawn': No such file or directory - make (Errno::ENOENT)

Add build-base to Dockerfile and retry.

gem install nokogiri
Fetching: mini_portile2-2.4.0.gem (100%)
Successfully installed mini_portile2-2.4.0
Fetching: nokogiri-1.10.1.gem (100%)
Building native extensions. This could take a while...
Successfully installed nokogiri-1.10.1
ERROR:  While executing gem ... (Gem::DocumentError)
    RDoc is not installed: cannot load such file -- rdoc/rdoc
bash-4.4# gem list nokogiri

*** LOCAL GEMS ***

nokogiri (1.10.1)

We successfully installed nokogiri gem on Ruby 2.5.3.

bash-4.4# ruby -v
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux-musl]
bash-4.4# gem list nokogiri

*** LOCAL GEMS ***

nokogiri (1.10.1)

Installation successful. Let's do a quick test, stealing sample code from nokogiri home page:

#! /usr/bin/env ruby

require 'nokogiri'
require 'open-uri'

# Fetch and parse HTML document
doc = Nokogiri::HTML(open('https://nokogiri.org/tutorials/installing_nokogiri.html'))

puts "### Search for nodes by css"
doc.css('nav ul.menu li a', 'article h2').each do |link|
  puts link.content
end

Output:

### Search for nodes by css
Install with included libraries (RECOMMENDED)¶
Install with system libraries¶
Install with custom / non-standard libraries¶

Let's break things now by installing the oldest nokogiri gem.

gem install nokogiri -v 1.0.0
Fetching: rake-12.3.2.gem (100%)
Successfully installed rake-12.3.2
Fetching: hoe-3.17.1.gem (100%)
Successfully installed hoe-3.17.1
Fetching: nokogiri-1.0.0.gem (100%)
Building native extensions. This could take a while...
ERROR:  Error installing nokogiri:
    ERROR: Failed to build gem native extension.

    current directory: /usr/lib/ruby/gems/2.5.0/gems/nokogiri-1.0.0
/usr/bin/ruby -rrubygems /usr/lib/ruby/gems/2.5.0/gems/rake-12.3.2/exe/rake RUBYARCHDIR=/usr/lib/ruby/gems/2.5.0/extensions/x86_64-linux/2.5.0/nokogiri-1.0.0 RUBYLIBDIR=/usr/lib/ruby/gems/2.5.0/extensions/x86_64-linux/2.5.0/nokogiri-1.0.0
rake aborted!
NameError: uninitialized constant Config

(See full trace by running task with --trace)

rake failed, exit code 1

Gem files will remain installed in /usr/lib/ruby/gems/2.5.0/gems/nokogiri-1.0.0 for inspection.
Results logged to /usr/lib/ruby/gems/2.5.0/extensions/x86_64-linux/2.5.0/nokogiri-1.0.0/gem_make.out
bash-4.4# ruby -v
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux-musl]

The nokogiri gem installation fails for the versions: 1.0.0, 1.2.0, 1.3.0, 1.4.0, 1.5.0, 1.6.0.

Nogokiri 1.7.0 works and the required ruby version is >= 2.1.0 as per the rubygems home page. We can write a bash shell script to automate the installation of the gem.

#!/bin/bash

echo 'installing nokogiri gem'
gem install nokogiri --no-ri --no-rdoc

success=$?
if [ $success -eq 0 ]; then
  echo "Installation successful"
else
  echo 'Failed to install nokogiri'
fi

exit $success

Wouldn't it be nice if we had an API that would tell us if a given version of Ruby and Nokogiri gems are compatible or not? That's what DeveloperTask is all about. Next steps: Create a script that will take a configurable Ruby version and Nokogiri gem versions to figure out if they are compatible or not. Are there any tool that is available to map an Ubuntu package to an Alpine package? Please let me know.

References

Alpine Ruby Dockerfile
Build Minimal Docker Container for Ruby apps
Install Ruby on Rails


Related Articles


Ace the Technical Interview

  • Easily find the gaps in your knowledge
  • Get customized lessons based on where you are
  • Take consistent action everyday
  • Builtin accountability to keep you on track
  • You will solve bigger problems over time
  • Get the job of your dreams

Take the 30 Day Coding Skills Challenge

Gain confidence to attend the interview

No spam ever. Unsubscribe anytime.