tagline: From openSUSE
Build Service Tutorial · Tips & Tricks · Cross Distribution Howto · Packaging checks
Desktop menu categories · RPM Macros · Scriptlets · Init scripts · How to write good changes
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
zypper ar http://download.opensuse.org/repositories/devel:/languages:/ruby/openSUSE_12.2/devel:languages:ruby.repo zypper ar http://download.opensuse.org/repositories/devel:/languages:/ruby:/extensions/openSUSE_12.2/devel:languages:ruby:extensions.repo zypper ref zypper in rubygem-gem2rpm
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 spec file:
gem fetch foo gem2rpm foo-1.0.0.gem > rubygem-foo.spec
Edit the specfile as needed:
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
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.
Note that if your project builds for a distribution which has rpm < 4.9.0, such as openSUSE 1.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.
The thread starting with this post has a lot of details on the background to the current policy.