openSUSE:Packaging Ruby

Jump to: navigation, search


Packaging Ruby itself

Ruby packaging up to and including 13.1

Ruby was packaged as 'multi-version' up to and including openSUSE 13.1

  • ruby - A wrapper package, setting up symlinks to the actual binaries
  • ruby20, ruby19, ... - The actual binaries, installing version-specific versions like /usr/bin/ruby20, /usr/bin/ruby19
  • ruby-common - A set of RPM macros required to package Ruby GEMs

Ruby packaging scheme after 13.1

The following packaging scheme applies to openSUSE after 13.1

  • ruby - This provides binaries (ruby, irb, rake, gem, ...) and a minimal set of documentation (changelog, readme, news, ...)

As a user, installing ruby is sufficient to get started. All other required packages are installed via dependencies.

The former ruby-common package, containing RPM macros for packaging gems, is merged with ruby.
Packaging (non-binary) gems now no longer requires ruby-devel.

  • ruby-stdlib- This provides the /usr/lib64/ruby/2.1.x/ directory tree.

Having this seperately allows for smaller maintainance updates in case a fix of the standard library is needed.

  • ruby-doc - This provides the full Ruby documentation including samples, reducing the size of the old Ruby package by almost 6MB.
  • ruby-devel, ruby-devel-extra, ruby-doc-ri - These contain all files required to build software using the Ruby API. ruby-devel-extra contains include files of the Ruby-internal API used by some gems.

This scheme has been discussed on the opensuse-ruby mailing list

Why was the packaging scheme changed after openSUSE 13.1 ?

  • To revert the ruby, rubyXY, and ruby-common split

Initially done to allow multiple Ruby versions in parallel.
This ability wasn't used much, rubygem packages didn't follow, and developers use rvm or rbenv to achieve the same effect.
rom a buildservice perspective, this split cause more headaches than it provided value.

  • Ruby is part of inst-sys (for YaST)

As you know, size matters. Looking at the ruby20 package, it has an install size of 18MB.
However, du -sh /usr/share/doc/packages/ruby20 reports 5.9 MB just for documentation.

  • Better maintenance support

A split between binaries, shared libraries, and Ruby stdlib is desirable.
Having shared libraries in separate packages also complies with the openSUSE shared library packaging policy

Updating from previous openSUSE releases

Installing the new ruby package will remove your former multiversion Ruby packages.

I want multiple Ruby versions in parallel !

Use rvm or rbenv if you need multiple Ruby versions to work with.

For application deployment requiring different Ruby versions, Linux containers and Docker provide a much better solution.


How to package Ruby gems into rpms

Ruby library modules are distributed around the Ruby community mostly in the form of gems, Ruby's own package format. In some cases it makes sense to repackage gems as rpms for openSUSE via the Open Build Service (see openSUSE:Ruby Gem Strategies for many details on the pros and cons of doing this). In those cases, you can follow the below process for packaging the gems, which should be pretty easy.

A recipe for packaging gems

For Tumbleweed:

zypper in rubygem\(gem2rpm\)

For other versions, if the dependency from shown above cannot be resolved, add the devel:languages:ruby and devel:languages:ruby:extensions repositories.

Add a new package in your home project (or preferably in a subproject like home:$USER:ruby):

osc mkpac rubygem-foo
cd rubygem-foo

If you have been using rvm, make sure it doesn't get in the way of the below steps, by ensuring you are using the system-wide Ruby installation:

rvm use system

Download the respective gem file and generate the RPM <code.spec file:

gem2rpm -o rubygem-foo.spec --fetch foo

Edit the .spec file as needed:

vi rubygem-foo.spec

In particular, ensure that the License: field is correct. gem2rpm deliberately omits BuildRequires dependencies on other gems (see below).

Also, Requires and Provides fields are automatically generated for gem dependencies via the ruby-common package (although you'll need to build directly against devel:languages:ruby rather than against openSUSE:12.2 so that the build uses version 1.0 from devel:languages:ruby, rather than version 1.9.3 from the main 12.2 repository), so you should only include manual Requires / Provides for non-gem packages. See below for how this works.

Add a changelog entry, build the package locally, test, then commit your package:

osc vc
....
osc build --local
osc vc
....
osc ci

If you feel your package is ready for wider adoption, file a submitrequest against devel:languages:ruby:extensions build service repository:

osc sr devel:languages:ruby:extensions

Updating gem packages to a new gem version

If you want to update a gem package to a newer version of the gem but other packages still depend on the old version, then you should ensure that there are two separate packages, with the older one containing a suffix in the package name. For example, when updating gem foo from 0.6.3 to 0.7.1, if another gem depends on foo ~> 0.6 then there should be a rubygem-foo-0_6 package containing 0.6.3 alongside rubygem-foo which contains the latest version (0.7.1). This suffixing scheme is known to be sub-optimal, but there appears to be some inertia or resistance to addressing it at the time of writing.

If you want to update a gem package which uses the suffix scheme to a newer version of the gem, and you are sure that no other packages still depend on the old version, then please take the opportunity to drop the suffix. However, for historical reasons there are some packages such as rails for which the suffix should never be dropped.

(FIXME: this section needs more info)

gem2rpm takes the license from the output file when there is no license in the gemspec, so updating an existing .spec file can be done via

gem2rpm *.gem -o *.spec

and then following the same steps as above.

Omission of BuildRequires

gem2rpm deliberately omits BuildRequires dependencies on other gems to reduce build latency in the Build Service - see this post for more details. To prevent gem install failing in the %install phase due to missing gem install-time gem dependencies, the template from which gem2rpm generates .spec files invokes the %gem_install macro with the -f flag.

Compensating for lack of BuildRequires

The disadvantage of the above optimization is no edges of the rpm install-time dependency tree are checked at rpm build-time, so you would only discover broken dependencies when you actually installed the gem rpms. (The rpm is installed immediately after being built, but with --force --nodeps so dependencies are ignored.)

For example, if A and B are two gems within the project where A depends on B, OBS could build A successfully without knowing whether B installs successfully. B might have unresolvable automatically generated run-time dependencies (i.e. Requires) on other gems or Ruby itself, or unresolvable manually specified run-time dependencies, or it might simply conflict with files from other packages (gems or otherwise).

As a workaround, you can implement a full check of the rpm install-time dependency tree of all gems in your OBS project which happens at build-time, by creating a small package that itself BuildRequires all gems in the repository. This has the added advantage of checking for conflicts between gems on independent branches of the dependency tree.

There is an example of such a package in devel:languages:ruby:extensions, called all-good. That means for a rails update, the scheduler will only need one round of updates: update all gems and then let the scheduler calculate if all-good can be expanded or if something misses dependencies. (ruby-common has a generate_buildrequires.sh script which will automatically update the BuildRequires for you.)

How gem dependencies are automatically handled

The ruby-common package from devel:languages:ruby includes various rpm macros and scripts which combine to automatically calculate and embed into the gem package Requires: headers which reflect the dependencies from this gem onto other gems, and also Provides: headers which support any required dependencies of other gems on this gem.

The %install section of the gem's .spec file should contain a line

%gem_install -f

This triggers a macro defined in /etc/rpm/macros.suse-ruby which invokes /usr/lib/rpm/gem_install.sh, which will install the gem into rpmbuild's install root (a.k.a. buildroot), and also install a foo.gemspec file (which is generated from the YAML-serialized metadata.gz embedded in the gem file) under a specifications/ subdirectory in the same hierarchy.

After the %install phase of the rpmbuild process, RPM's automatic dependency generator mechanism kicks in. At this point, the %__rubygems_path macro defined in /usr/lib/rpm/fileattrs/rubygems.attr, which is a regular expression matching the full path to the foo.gemspec file generated above, causes rpm to run the %__rubygems_requires and %__rubygems_provides macros defined in the same file. These run /usr/lib/rpm/rubygemsdeps.rb which extracts the dependencies from the foo.gemspec file and converts them into Requires: and Provides: headers.

Be aware that there are currently border cases for which this system does not work: if the .gemspec file has version ranges e.g.

   s.add_dependency "net-ssh", ">= 2.6.6", "< 2.8.0"

then the system cannot guarantee this constraint since zypper could falsely satisfy it by having rubygem-net-ssh-2_1-2.1.4-2.7.x86_64 and rubygem-net-ssh-2.8.0-26.1.x86_64 co-installed. Therefore it will probably be safer to patch it so that the constraint is tightened up to use the ~> operator, e.g.

   s.add_dependency "net-ssh", "~> 2.6.6"

Future versions of rpm may support version ranges, at which time it would be possible to close this loophole.

Note that if your project builds for a distribution which has rpm < 4.9.0, such as openSUSE 11.4 and SLE11, then it will not have the fileattrs pluggable dependency generator available. Therefore the automatic generation can only be triggered by using a patched version of rpm. This has been done for openSUSE 11.4 and SLE11 in the devel:languages:ruby:backports project.

Format of automatically generated Requires: headers

Here is an example for version 0.3.2 of the oa-openid gem.

The automatically generated Requires: headers are:

   ruby(abi) = 1.9.1
   rubygem(1.9.1:oa-core) = 0.3.2
   rubygem(1.9.1:rack-openid:1.3) >= 1.3.1
   rubygem(1.9.1:ruby-openid-apps-discovery:1.2) >= 1.2.0

This is equivalent to the following Ruby dependencies:

   oa-core = 0.3.2
   rack-openid ~> 1.3.1
   ruby-openid-apps-discovery ~> 1.2.0

And in fact that is exactly what is listed at http://rubygems.org/gems/oa-openid/versions/0.3.2 so we can see it works nicely. There is a subtlety around the way the pessimistic ~> version constraint operator is implemented. If the gem's rack-openid dependency was instead

   rack-openid => 1.3.1

then the corresponding Requires: header would have been:

   rubygem(1.9.1:rack-openid) >= 1.3.1

i.e. without the :1.3 in the symbol.

Format of automatically generated Provides: headers

The automatically generated Provides: headers provide symbols in the same format as the Requires:

   rubygem(1.9.1:oa-openid) = 0.3.2
   rubygem(1.9.1:oa-openid:0) = 0.3.2
   rubygem(1.9.1:oa-openid:0.3) = 0.3.2
   rubygem(oa-openid) = 0.3.2

(Of course this is necessary, otherwise the whole system would break down.)

But they also cover the old version-suffixed naming scheme for backwards compatibility:

   rubygem-oa-openid-0 = 0.3.2
   rubygem-oa-openid-0_3 = 0.3.2

It is hoped we can drop these suffixed symbols in the future.

Common problems building rubygems

My rubygem-* package fails to build

If your rubygem-* package does not build on Factory, regenerate the .spec file with

 gem2rpm <foo>.gem > rubygem-<foo>.spec

Typical gem packaging problems

The gem contains binary parts, fails to install because it does not honor the new pathes

Use the new %gem_* macros as outlined above

The gem contains binary parts, fails to build because ruby.h is missing

Add ruby-devel to the BuildRequires(Formerly, all rubygems BuildRequired ruby-devel via ruby-common. However, the majority of rubygems don't have binaries and build/package fine with just ruby installed.)

History

  • New Ruby packaging for openSUSE > 13.1
  • The thread starting with this post has a lot of details on the background to the current policy.