How to build up-to-date VIPS packages for Debian

Some context

The excellent image_processing Ruby gem recently hit 1.0 and added support for a libvips based backend, in addition to the original ImageMagick one. I read a lot of good things about libvips and its architecture had always seemed much better and modern than ImageMagick, so I decided to spend some time optimizing the image generation code on Talegraph and experiment with the two backends and various settings. I settled on libvips as it generated smaller files and was much faster than shelling out to ImageMagick’s convert utility (see the official benchmark to get an idea of how fast and efficient libvips is).

I work on MacOS and installing libvips was easy using brew: brew install vips installs the latest libvips release (8.6.3 when I am writing this). Unfortunately, when pushing the changes to my staging environment (based on Debian Linux), things became a bit more complex.

First, ruby-vips (used behind the hood by image_processing) required libvips 8.2 or above. This forces you to use the most recent Debian version, Stretch, as it provides a 8.4 libvips package, where the previous version (Jessie) only provides libvips 7.40. But libvips is quickly improving and image_processing requires some functions only available in libvips 8.6. I did not want to switch to a new distribution with more up-to-date packages, so I decided to build my own libvips.

Building custom libvips packages

My goal here is to build proper .deb packages that I can easily install on any system. I do not want to locally build the library on every system, as it would install a lot of additional packages and takes much more time than just installing the package. This is especially important on CI where every run starts from a base image.

As Debian already provides a libvips package (but for an older version), it is easy to start from it. The following steps are run on a base Debian Stretch VM, but it should also work on Jessie.

First we need to install the build-essential package to have the needed build tools (compiler, headers, …), as well as the devscripts packages containing a few useful commands to work with Debian packages:

$ apt install build-essential devscripts

Now, we need to download the latest libvips source code from the Github releases page:

$ curl -LO
$ tar xvf vips-8.6.3.tar.gz
$ cd vips-8.6.3

Then we download the build directory from the Debian Package page:

curl -LO
tar xvf vips_8.4.5-1.debian.tar.xz

We now have a debian directory which contains all the files needed to build the package. But those are for libvips 8.4.5 and we need to make a few changes before building it.

Let’s update the Debian changelog with our new version. For this we can use the dch CLI, available in the devscripts package. Let’s add a changelog entry to debian/changelog, using our name and email:

$ NAME="Renaud Chaput" dch -v 8.6.3-1 "Update to version 8.6.3"

Now, before building the package we need to install all the libvips dependencies. The easiest way is to run mk-build-deps (included in the devscripts package we installed earlier). This will create an empty vips-build_deps package that does not install any files but depends on the needed packages. Thus installing this package will pull all dependencies in one go!

$ mk-build-deps
dpkg-deb: building package 'vips-build-deps' in '../vips-build-deps_8.6.3-1_all.deb'.

The package has been created.
Attention, the package has been created in the current directory,
not in ".." as indicated by the message above!
$ sudo apt install ./vips-build-deps_8.6.3-1_all.deb

Whew, that was a lot of deps :) Now that they are installed, we can (finally!) proceed to build the package:

$ debuild -i -us -uc -b
dh_install -ppython-vipscc
dh_install: Cannot find (any matches for) "debian/tmp/usr/lib/python*.*" (tried in "." and "debian/tmp")
dh_install: python-vipscc missing files: debian/tmp/usr/lib/python*.*
dh_install: missing files, aborting
/usr/share/cdbs/1/rules/ recipe for target 'binary-install/python-vipscc' failed
make: *** [binary-install/python-vipscc] Error 255
dpkg-buildpackage: error: fakeroot debian/rules binary gave error exit status 2
debuild: fatal error at line 1116:
dpkg-buildpackage -rfakeroot -us -uc -i -b failed

Unfortunately, the build should not succeed and you should have got an error. This is because we are trying to package the VIPS python bindings, which have been deprecated in libvips 8.6 and are no longer built by default, so the required files can not be found and dpkg can’t package them. As you should no longer use these binding (and move to pyvips), let’s disable the python-vipscc package:

Open debian/control in your favorite text editor and comment out (by adding #) the block beginning with Package: python-vipscc. This will prevent this package to be built.

Finally launch debuild again, and this time it should work:

$ debuild -i -us -uc -b
Now running lintian...
Finished running lintian.

And we are done! The generated packages are in the parent directory:

$ ls ../*.deb

We can now install the package locally and test that everything works:

$ sudo apt install ../libvips42_8.6.3-1_amd64.deb ../libvips-dev_8.6.3-1_amd64.deb
$ sudo apt install ruby ruby-dev
$ sudo gem install ruby-vips
$ irb(main):001:0> require "vips"
=> true
irb(main):002:0> l = 10, 10
=> #<Image 10x10 uchar, 1 bands, b-w>

Everything is working 🎉 If it does not on your system, you might get an error like this one:

LoadError: Could not open library 'glib-2.0': /usr/lib/x86_64-linux-gnu/glib-2.0: cannot read file data: Is a directory.
Could not open library '': cannot open shared object file: No such file or directory
from /var/lib/gems/2.3.0/gems/ffi-1.9.23/lib/ffi/library.rb:147:in `block in ffi_lib'
from /var/lib/gems/2.3.0/gems/ffi-1.9.23/lib/ffi/library.rb💯in `map'

This is because the ffi gem is loading shared libraries naively, using their name (here it tries to load a library). This works on most systems as there are symlinks from those simple names to the real .so, but Debian packages do not include those links. You need the -dev packages to get them. In our case, we solve this with by installing our libvips-dev package as well (and it will pull libglib2.0-dev). Once those packages are installed, we can check that the symlinks exists, and can require "vips" without error.

$ ls -l /usr/lib/x86_64-linux-gnu/
lrwxrwxrwx 1 root root 38 Mar 19  2017 /usr/lib/x86_64-linux-gnu/ -> /lib/x86_64-linux-gnu/

Distribution and installation

Now that the packages are built, just copy the files to the target system and install them. If you already have a custom package repository, you can use it. I did not want to setup one, so I did it manually.

I copied all the generated .deb to a Google Cloud Storage bucket to make them available from anywhere. You can obviously choose another way of distributing the files, it was the easiest one for me.

Installing libvips on CI

On CI, I am using the following script to download, check and install the packages before installing the gems:

mkdir /tmp/install_vips
cd /tmp/install_vips

curl -O
curl -O

echo "13de4ec10cc5474bba219d1f68522074c69966d2d809a54e90a4ad5d4d28b081  libvips42_8.6.3-1_amd64.deb
7495c3ab4b5a26a7ef4ec046970871e1855e9e533fe4b5cdc457b469b00738f1  libvips-dev_8.6.3-1_amd64.deb" | shasum -c -a 256

if [ $? != 0 ]; then echo "Bad checksum" && exit 2; fi

sudo apt install ./libvips-dev_8.6.3-1_amd64.deb ./libvips42_8.6.3-1_amd64.deb

It takes less than 30 seconds on CircleCI, much better than building from source at each run!

Installing libvips on production servers

My servers are managed using Chef. Unfortunately the apt_package resource does not accept local packages (feature request here), so I am using this recipe instead:

# attributes/vips.rb
default["vips"].tap do |vips|
  vips["version"] = "8.6.3-1"
  vips["repo_url"] = ""
  vips["checksums"]["libvips42"] = "13de4ec10cc5474bba219d1f68522074c69966d2d809a54e90a4ad5d4d28b081"
  vips["checksums"]["libvips-dev"] = "7495c3ab4b5a26a7ef4ec046970871e1855e9e533fe4b5cdc457b469b00738f1"

# recipes/vips.rb
vips_attr = node["vips"]

%w[libvips42 libvips-dev].each do |package|
  deb_name = "#{package}_#{vips_attr["version"]}_amd64.deb"
  remote_file "/var/chef/cache/#{deb_name}" do
    source "#{vips_attr["repo_url"]}/#{node["lsb"]["codename"]}/#{deb_name}"
    backup false
    mode "644"
    checksum vips_attr["checksums"][package]

  # Can not use dpkg_package as we need to install deps
  bash "install #{package}" do
    code "apt-get install -q -y /var/chef/cache/#{deb_name}"
    not_if "dpkg-query -W #{package}"