Ruby Packaging mini-HOWTO: различия между версиями

Материал из ALT Linux Wiki
(Import from freesource.info)
 
Нет описания правки
 
(не показано 38 промежуточных версий 7 участников)
Строка 1: Строка 1:
[[Category:Policy]]
{{Устарело}}
{{MovedFromFreesourceInfo|AltLinux/Policy/Ruby}}
{{Stub}}


== Ruby Packaging Policy ==
Основные правила сборки приложений и модулей ruby изложены в [[Ruby Policy|Ruby Packaging Policy]]. Цель этого документа — объяснить на простых примерах, как следует поступать в различных ситуациях при упаковке ruby-приложений, а также показать, как можно собирать простые модули.


{| border="1"
<tt>rubygems</tt> не рассматриваются в данном HOWTO, так как их дополнительная функциональность (например, установка нескольких версий gem’а), необходимая в некоторых случаях (shared hosting, web-приложения), конфликтует с наличием дистрибутивного пакетного менеджера. Другие дистрибутивы занимают аналогичную позицию в отношении к rubygems: [http://pkg-ruby-extras.alioth.debian.org/rubygems.html позиция Debian], [http://pkg-ruby-extras.alioth.debian.org/upstream-devs.html их рекомендации апстриму].
|-
|
Статус
|
Действующая политика с 16 апреля 2008
|-
|
Автор
|
[http://freesource.info/wiki//SirRaorn Alexey I. Froloff]
|-
|
Обязательно в
|
4.1 и выше
|-
|
Метабаг
|
пока не создан
|}


Для ситуаций, в которых дополнительная функциональность <tt>rubygems</tt> необходима, сам <tt>rubygems</tt> упакован и работает.


Правила упаковки модулей и программ на языке Ruby.
== Модули, которые нельзя собирать никогда-никогда ==


__TOC__
Тут перечислены модули, которых всячески стоит избегать.  Это гадкие, негодные модули, не надо их использовать.


* [http://rubyforge.org/projects/rubyinline RubyInline] - модуль, позволяющий делать "вставки" на языке C.  На самом деле в процессе работы включаемый C-код "заворачивается" в native extension, компилится и скомпилированный модуль загружается в программу.


=== Пакет ruby и его обновление ===
== Общие принципы сборки модулей ==


Основная часть интерпретатора ruby находится в пакете libruby.  Это библиотека libruby.so.X.Y и каталоги, входящие в <tt>$LOAD_PATH</tt>.
При сборке пакета в дистрибутив используется «нативная» сборка — с помещением файлов в специальные каталоги, которые находятся в <tt>$LOAD_PATH</tt>.


В системе и в публичном репозитарии может находиться только она версия пакета libruby.
Сборка включает в себя:


При сборке новой версии ruby из-за изменения <tt>$LOAD_PATH</tt> необходимо пересобрать все пакеты, имеющие зависимости (прямые или косвенные) на пакет libruby.
* компиляцию бинарных модулей (если они есть);
* выполнение тестов (если они есть и их выполнение возможно в hasher);
* установку файлов в соответствующие каталоги;
* генерацию документации в формате <tt>ri</tt> (class reference);


=== Внешний вид пакетов ===
Модули устанавливаются в так называемый vendor dir. В ALT Linux это <tt>/usr/share/ruby/vendor_ruby/RUBY.VERSION</tt> и <tt>/usr/lib/ruby/RUBY.VERSION/ARCHITECTURE</tt>. Поскольку по умолчанию установка модулей идёт в site dir, при сборке пакета надо использовать модуль <tt>vendor_specific</tt>, вызывая интерпретатор ruby как <tt>ruby -rvendor_specific</tt>. Для этого есть макрос <tt>%ruby</tt>.


Пакеты должны иметь префикс "ruby-".  Если оригинальное название не включает этот префикс, он добавляется к имени пакета, например rcairo -> ruby-rcairo.  Исключение составляют пакеты, оригинальное название которых содержит постфикс "-ruby", например "sqlite3-ruby".
== Внутри тарбола ==


Префикс "ruby-module-" является излишним и допустим только для модулей, собирающихся из пакета ruby.
Внутри тарбола с модулем (или программой) могут находиться следующие файлы и каталоги:


Пакеты должны иметь группу Development/Ruby.
* '''bin/''' — скрипты, будут установлены в <tt>%_bindir</tt>;
* '''ext/''' — компилируемые модули (при использовании <tt>setup.rb</tt>), будут установлены в <tt>%ruby_sitearchdir</tt>;
* '''lib/''' — pure-ruby модули, будут установлены в <tt>%ruby_sitelibdir</tt>;
* '''data/''' — произвольные данные(при использовании <tt>setup.rb</tt>), будут установлены в <tt>%_datadir/ИМЯМОДУЛЯ</tt>;
* '''test/''' — unit-тесты;


Вышесказанное относится к pure-module пакетам, а не end-user программам, написанным на языке ruby (например alexandria).  В этом случае имя и группа пакета должны соответствовать действительности.
Также как правило присутствует один или несколько «сценариев сборки», о них расскажу далее.


Пакет должен иметь архитектуру noarch если не содержит архитектурно-зависимых компонентов, упаковка ruby не накладывает дополнительных ограничений.
== Собираем модуль ==


=== Расположение файлов ===
Существует несколько, различного уровня «стандартности», способов сборки модулей ruby.


Все модули, которые могут использоваться в "обычной программе на языке ruby", должны помещать свои файлы в так называемые "vendor dirs", описываемые макросами <tt>%ruby_sitelibdir</tt> и <tt>%ruby_sitearchdir</tt>.  Каталоги <tt>%ruby_libdir</tt> и <tt>%ruby_archdir</tt> используются исключительно для модулей, собирающихся из пакета ruby.
Забегая вперёд, хочу сказать, что самым простым и удобным способом будет использование <tt>setup.rb</tt>, скопированного из пакета <tt>ruby-tool-setup</tt> (конечно, кроме случая когда в корне проекта находится файл <tt>extconf.rb</tt>).


=== Макросы RPM ===
=== <tt>extconf.rb</tt> AKA MkMf ===


Макросы находятся в пакете rpm-build-ruby. Рекомендуется добавить в spec строчку:
Аналог configure, использует модуль <tt>mkmf</tt>, входящий в стандартную поставку ruby. Внутри скрипта проверяется наличие необходимых заголовочных файлов и библиотек, на выходе генерится Makefile, который обрабатывается стандартным make. Используется только в тех проектах, где есть бинарные модули. Если «рядом» с <tt>extconf.rb</tt> находится файл <tt>depend</tt>, его содержимое добавляется к <tt>Makefile</tt>. Исходные тексты и заголовочные файлы бинарного модуля как правило тоже лежат «рядом» с <tt>extconf.rb</tt>.


<pre>BuildRequires(pre): rpm-build-ruby</pre>
Типичные секции <tt>%build</tt> и <tt>%install</tt> выглядят следующим образом:


{| border="1"
<pre>
|-
%build
|
%ruby_configure <опции extconf.rb>
'''Макрос'''
%make_build
|
 
'''описание'''
%install
|-
%make_install DESTDIR=%buildroot install
|colspan="2"
</pre>
''каталоги''
 
|-
=== <tt>setup.rb</tt> имени Minero Aoki ===
|
 
%ruby_sitearchdir
Скрипт сборки и установки общего назначения. Как правило используется для сборки и установки pure-ruby модулей. Имеет некоторое количество стандартных опций, может собирать бинарные модули, находящиеся в каталоге '''ext/''' (как правило там присутствует <tt>extconf.rb</tt>, см. выше).
|
 
%_libdir/ruby/vendor_ruby/$VERSION/$ARCH/
Типичные секции <tt>%build</tt> и <tt>%install</tt> выглядят следующим образом:
|-
 
|
<pre>
%ruby_sitelibdir
%build
|
%ruby_config <опции setup.rb>
%_datadir/ruby/vendor_ruby/$VERSION/
|-
|
%ruby_archdir
|
%_libdir/ruby/$VERSION/$ARCH/
|-
|
%ruby_libdir
|
%_datadir/ruby/$VERSION/
|-
|
%ruby_siteincludedir
|
/usr/include/ruby/$VERSION/ (изменится в ruby >= 1.9)
|-
|
%ruby_includedir
|
/usr/include/ruby/$VERSION/
|-
|
%ruby_ri_sitedir
|
%_datadir/ri/$VERSION/site/
|-
|colspan="2"
''опции поиска зависимостей''
|-
|
%set_ruby_req_method
|
strict, normal или relaxed
|-
|
%add_ruby_lib_path
|
добавляет путь для поиска Provides
|-
|
%add_ruby_weakprov_path
|
добавляет путь для поиска "статически слинкованных" модулей (пример использования в пакете ruby-actionpack)
|-
|colspan="2"
''сборка и установка''
|-
|
%ruby_vendor
|
ruby -rvendor-specific
|-
|
%rdoc
|
вызов rdoc (для секции %indtall) с опциями --ri-site --all
|-
|colspan="2"
''setup.rb имени Minero Aoki''
|-
|
%_ruby_setup_rb
|
путь к setup.rb (по умолчанию setup.rb)
|-
|
%ruby_setup_rb
|
%ruby_vendor %_ruby_setup_rb
|-
|
%ruby_config
|
%ruby_setup_rb config
|-
|
%ruby_build
%ruby_build
|
 
%ruby_setup_rb setup
%install
|-
|
%ruby_install
%ruby_install
|
</pre>
%ruby_setup_rb install --prefix=%buildroot
 
|-
=== <tt>install.rb</tt> ===
|colspan="2"
 
''mkmf и extconf.rb''
Иногда это самописный скрипт, иногда встречается одна из первых версий <tt>setup.rb</tt>. Как правило используется только для установки pure-ruby модулей. Стандартных макросов для поддержки <tt>install.rb</tt> нет.
|-
 
|
=== <tt>Rakefile</tt> и остальные случаи ===
%ruby_configure
 
|
Сценарий для <tt>rake</tt>. Обычно может иметь task’и <tt>build</tt> (если есть бинарные модули) и <tt>test</tt>, но последнее время не имеет task’а <tt>install</tt>. Зато использует <tt>rubygems</tt>.
%ruby_vendor extconf.rb --ruby=/usr/bin/ruby
 
|-
Для вызова <tt>rake</tt> и <tt>rake install</tt> есть два стандартных макроса <tt>%rake</tt> и <tt>%rake_install</tt> соответственно.
|colspan="2"
 
''rake и Rakefile''
Если task <tt>install</tt> не определён или вообще отсутствует установочный скрипт (в случае pure-ruby модуля), можно использовать <tt>setup.rb</tt> из пакета <tt>ruby-tool-setup</tt> примерно следующим образом:
|-
 
|
<pre>
%rake
%prep
|
%setup
%ruby_vendor %_bindir/rake
%patch -p1
|-
%update_setup_rb
|
</pre>
%rake_install
 
|
Далее используются макросы <tt>%ruby_config</tt>, <tt>%ruby_build</tt> и <tt>%ruby_install</tt>.
DESTDIR=%buildroot %rake install
 
|}
Очень часто в Rakefile включена поддержка создания .gem. Поскольку rubygems использовать при сборке пакетов нельзя, следует воспользоваться [http://pkg-ruby-extras.alioth.debian.org/upstream-devs.html рецептом из Debian].
 
== Запускаем тесты ==
 
С осени 2009 года наш rpm поддерживает секцию %check, все тесты имеет смысл вынести в нее:
 
<pre>
%check
%ruby_test_unit -Ilib:test test
</pre>
 
По какой-то причине у меня ([[Участник:ТимурБатыршин]]) такая конструкция не заработала, и я воспользовался другой:
<pre>
%check
for test in $(find test/ -name '*_test.rb'); do
        ruby -Ilib $test || exit 1
done
</pre>
 
== l10n при помощи <tt>ruby-gettext</tt> ==
 
Существует специальный модуль <tt>ruby-gettext</tt> который обеспечивает локализацию сообщений, совместимую с <tt>gettext</tt>. Если локализация используется, в каталоге с исходниками есть файлы вида <tt>po/ЯЗЫК/ИМЯ-ПАКЕТА.po</tt>. При сборке файлы <tt>*.po</tt> надо скомпилить в <tt>ЯЗЫК.mo</tt>, это делается следующим хуком для <tt>setup.rb</tt>:
 
<source lang="ruby">
=begin
  post-setup.rb
 
  Copyright (C) 2001-2006 Masao Mutoh
  This program is licenced under the same licence as Ruby.
=end
 
require 'gettext/utils'
 
begin
  GetText.create_mofiles
rescue
  puts "GetText.create_mofiles failed."
end
</source>
 
Этот код надо поместить в файл <tt>post-setup.rb</tt> и положить в корень проекта, рядом с <tt>setup.rb</tt>. Скомпилированные файлы локализации будут помещены в <tt>data/locale/</tt>, после чего <tt>setup.rb</tt> установит их в /usr/share/locale/. Для упаковки этих фалов в пакет можно пользоваться макросом <tt>%find_lang</tt>.
 
== Пакуем документацию ==
 
Документация в формате RI генерируется при помощи утилиты <tt>rdoc</tt>, находящейся в пакете <tt>ruby-tool-rdoc</tt>. Для этого существует стандартный макрос <tt>%rdoc</tt> предназначенный для использования в секции <tt>%install</tt> (обычно одной из последних строк). В качестве аргументов этого макроса перечисляются файлы и каталоги с исходниками и при необходимости другие опции утилиты <tt>rdoc</tt>.
 
Для pure-ruby модулей как правило используется конструкция:
 
<pre>
%rdoc lib/
</pre>
 
Если присутствуют бинарные модули:
 
<pre>
%rdoc *.c lib/
</pre>
 
Документацию желательно паковать в подпакет <tt>%name-doc</tt>. При этом паковать следует только документацию для основных классов модуля, описание расширений сторонних классов паковать не нужно.
 
== Складываем файлы в пакеты ==
 
Pure-ruby модули помещаются в <tt>%ruby_sitelibdir</tt>, бинарные модули в <tt>%ruby_sitearchdir</tt>, документация в формате RI в <tt>%ruby_ri_sitedir</tt> (при этом файл <tt>%ruby_ri_sitedir/created.rid</tt> упаковывать не нужно).
 
== Добро пожаловать в реальный мир ==
 
В теории всё выглядит красиво, однако на практике среднего размера модуль представляет собой «нечто», что может работать в любой помойке. Однако мы делаем не помойку, поэтому местечковые хаки нам не нужны.
 
=== Не загрязняем <tt>$LOAD_PATH</tt> (<tt>$:</tt>) ===
 
Очень часто в коде можно увидеть конструкции вида:
 
<source lang="ruby">
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
</source>
 
Эта конструкция добавляет в <tt>$LOAD_PATH</tt> некоторый путь. Сделано это для того, чтобы модуль (или исполняемый скрипт) можно было использовать из любого места. Поскольку в нашем случае все файлы пакуются в стандартные места, подобные конструкции не нужны. В большинстве случаев такие конструкции можно безболезненно удалить.
 
=== Используем существующие модули и размаскируем зависимости ===
 
Поскольку наша помойка не является «любой», её ТТХ нам известны. Например, известно что это Linux, есть Iconv и так далее. Поэтому специфичный код, предназначенный для работы на других платформах в наших пакетах не нужен (а иногда бывает и вреден, поскольку даже «мёртвый» код может порождать зависимости, которые в некоторых случаях превращаются в unmet’ы).
 
Также мы можем превратить опциональную зависимость в явную. Пример:
 
<source lang="ruby">
begin
  require 'iconv'
rescue LoadError
  module Iconv
    # Далее следует некоторый код, который в результате предоставляет API
    # похожий на API модуля iconv, возможно урезанный функционально.
  end
end
</source>
 
В данном случае '''всю''' эту сложную конструкцию можно заменить на одну строку <tt>require 'iconv'</tt>. Как бонус мы получаем зависимость на <tt>ruby(iconv)</tt> и полную функциональность данного модуля.
 
Реальные примеры можно посмотреть в пакете [http://git.altlinux.org/people/raorn/packages/?p=ruby-gettext.git ruby-gettext].
 
=== Отрываем rubygems ===
 
В реальной жизни можно встретить множество вариантов использования rubygems. Вот самые распространённые:
 
Пример 1:
 
<source lang="ruby">
require 'rubygems'
require 'some/module'
require 'some/other/module'
</source>
 
Пример 2:
 
<source lang="ruby">
begin
  require 'rubygems'
  require 'some/module'
  require 'some/other/module'
rescue LoadError
  require 'some/module'
  require 'some/other/module'
end
</source>
 
Пример 3:
 
<source lang="ruby">
begin
  require 'some/module'
  require 'some/other/module'
rescue LoadError
  require 'rubygems'
  require 'some/module'
  require 'some/other/module'
end
</source>
 
Все эти три примера можно привести к одному виду:
 
<source lang="ruby">
require 'some/module'
require 'some/other/module'
</source>
 
Этим мы убираем зависимость на rubygems и размаскируем скрытые зависимости. Таким образом наш модуль автоматически получает зависимсти на <tt>ruby(some/module)</tt> и <tt>ruby(some/other/module)</tt>.
 
=== Файлы специального вида (темплейты и плагины) ===
 
Очень часто модуль (или приложение) носит с собой какие-то данные, которые обычно находятся в каталоге с gem’ом. Выглядит это приблизительно вот так:
 
<source lang="ruby">
  def mock_framework_path(framework_name)
    File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "plugins", "mock_frameworks", framework_name))
  end
</source>
 
В таком случае этот каталог (в нашем примере «<tt>plugins</tt>») помещается в <tt>/usr/share/ИМЯМОДУЛЯ</tt> а код заменяется на такой:
 
<source lang="ruby">
  def mock_framework_path(framework_name)
    File.expand_path(File.join("/usr/share/ИМЯМОДУЛЯ/plugins/mock_frameworks", framework_name))
  end
</source>
 
=== Потрошим новый пакет ===
 
При сборке нового пакета следует пробежать глазами его код на тему вышеописанных конструкций. Начать можно с <tt>grep -r</tt> по каталогам <tt>lib/</tt> и <tt>bin/</tt> по паттернам '<tt>__FILE__</tt>', '<tt>ubygems</tt>', '<tt>LoadError</tt>', '<tt>\$:</tt>', '<tt>\$LOAD_PATH</tt>'.
 
== Некоторые хитрости упаковки плагинов ==
 
Как правило любой «плагин» можно разделить на три или менее частей — «модули», «данные» и «скрипт инициализации». Обычно при загрузке «плагина» его «скрипт инициализации» загружает «модули» и в процессе работы приложения код «модулей» использует «данные». Из чего состоит «плагин», как он загружается и как работает зависит от приложения. Существует несколько довольно известных приложений, под которые написано довольно много плагинов. Некоторые из них возможно упаковать в rpm пакеты.
 
=== Ruby On Rails ===
 
Ruby on Rails считает «плагином» всё, где в корне проекта есть файл <tt>init.rb</tt>. В свою очередь, «проектом» может считаться любой каталог в <tt>vendor/plugins/</tt> и все gem’ы. В нашей сборке RoR к дефолтному списку путей поиска плагинов (состоящему только из <tt>vendor/plugins/</tt>) добавлен <tt>%_datadir/rails/plugins/</tt>.
 
# Пакет должен называться <tt>rails-plugin-ИМЯПЛАГИНА</tt>.
# Пакет должен иметь зависимость на <tt>ruby-railties >= 2.1.0-alt2</tt>.
# Сборка и установка «модулей» производится при помощи <tt>setup.rb</tt> как описано выше.
# Файл <tt>init.rb</tt> и всё что может понадобиться плагину (<tt>about.yml</tt>, <tt>generators</tt>, <tt>tasks</tt> и т. п.) нужно скопировать в <tt>%buildroot%_datadir/rails/plugins/ИМЯПЛАГИНА/</tt>.
 
Однако, бывают случаи, когда собранный таким образом плагин не работает с криками про Uninitialized constant. Скорее всего в таком плагине используется механизм автозагрузки модулей из ActiveSupport. Он не ищет модули в стандартном $LOAD_PATH, поэтому устанавливать «модули» приходится в базовый каталог «плагина», например используя опцию <tt>--rbdir=%_datadir/rails/plugins/ИМЯПЛАГИНА/lib</tt> для <tt>%ruby_config</tt>.
 
=== GemPlugin ===
 
Эта система плагинов появилась из проекта [http://mongrel.rubyforge.org/ mongrel], но в настоящее используется и в других проектах. Я не буду описывать принцип работы этой системы, расскажу только что нужно для того, чтобы «упакеченый» плагин можно было использовать.
 
# Пакет должен иметь зависимость на каталог <tt>%_datadir/gem_plugin</tt>.
# Файл <tt>lib/ИМЯПЛАГИНА/init.rb</tt> перед установкой должен быть перемещён в <tt>init.rb</tt> (в корень проекта).
# Сборка и установка «модулей» производится при помощи <tt>setup.rb</tt> как описано выше.
# Каталог <tt>resources</tt> нужно скопировать в <tt>%buildroot%_datadir/gem_plugin/ИМЯПЛАГИНА/</tt>.
# Файл <tt>init.rb</tt> (который перед установкой вытащили из <tt>lib/</tt>) тоже нужно скопировать в <tt>%buildroot%_datadir/gem_plugin/ИМЯПЛАГИНА/</tt>.
# Записать в файл <tt>%buildroot%_datadir/gem_plugin/ИМЯПЛАГИНА/dependencies.yml</tt> список зависимостей «плагина».
 
Файл dependencies.yml нужен для самого gem_plugin, чтобы решить какие именно плагины загружать для приложения, которое это попросило. Его можно создать посмотрев в GEM Specification, которая находится в файле <tt>Rakefile</tt> или <tt>ИМЯПЛАГИНА.gemspec</tt>:


<source lang="ruby">
Gem::Specification.new do |s|
  # ...
  s.add_dependency(%q<gem_plugin>, [">= 0.2.3"])
  s.add_dependency(%q<daemons>, [">= 1.0.3"])
  s.add_dependency(%q<fastthread>, [">= 1.0.1"])
  # ...
end
</source>


=== Зависимости ===
Нас интересуют только строки с <tt>add_dependency</tt> и только имена gem’ов. В нашем примере файл <tt>dependencies.yml</tt> должен выглядеть следующим образом:


В пакете rpm-build-ruby (начиная с версии 1:0.0.1-alt1 или в branch/4.1) реализован автоматический поиск зависимостей.  Таким образом все зависимости, поставленные вручную, являются не только бесполезными, но и вредными (за исключением особых случаев).
<source lang="text">
---
- gem_plugin
- daemons
- fastthread
</source>


Зависимости имеют вид <tt>ruby(ИМЯ/МОДУЛЯ)</tt> и не привязаны к конкретной версии ruby.  Для модулей, расположенных внутри каталогов, входящих в <tt>$LOAD_PATH</tt> дополнительно появляется зависимость на конкретный каталог.  Стандартные каталоги принадлежат пакету libruby.  Это косвенная привязка к версии ruby.  Такие пакеты необходимо пересобрать при изменении версии ruby.
== Генератор пакетов ==


Зависимости, найденные внутри блоков begin/rescue и внутри методов не экспортируются. Также не экспортируются зависимости, которые невозможно разрешить в имя модуля, например '<tt>require "rake/tasks/#{task}.raketask"</tt>'.
Я заметил, что при упаковке нового пакета мне приходится производить много одинаковых действий и заниматься копипастеньем. Поэтому я нарисовал [http://git.altlinux.org/people/raorn/public/?p=ruby-spec-templates.git примерный генератор] git-репозитория для нового ruby-пакета.


В случае "маскирования" зависимостей в коде (например при помощи блока begin/rescue) мантейнер пакета должен самостоятельно принять решение, убрать "маскировку" патчем, добавить в спек <tt>Requires: ruby(ИМЯ/МОДУЛЯ)</tt> или оставить всё как есть в зависимости от величины ущерба для функциональности пакета.
<pre>
.../mkrubypkgrepo [--arch] [--doc] [--rails] path/to/name-version.tar.gz
</pre>


Автоматический поиск зависимостей не отслеживает изменение <tt>$LOAD_PATH</tt>, в этих случаях надо вручную добавить необходимые каталоги к путям поиска при помощи макроса <tt>%add_ruby_lib_path</tt>.
* '''--arch''': генерить спек для архитектурно-зависимого пакета
* '''--doc''': паковать RI документацию в %name-doc
* '''--rails''': генерить спек для RoR плагина


=== Rubygems ===
Скрипт инициализирует в текущем каталоге git репозиторий, импортирует распакованный тарболл в ветку upstream и генерирует начальный спек. После генерации как обычно обработать напильником.


Зависимость на rubygems является недистрибутивной, поскольку скрывает реальные зависимости.  Зависимости пакетов должны разрешаться пакетами.
<!-- vim: set ft=mediawiki spell spelllang=ru,en wrap lbr: -->


Есть мнение, неоднократно доказанное экспериментально, что любой пакет можно отучить требовать rubygems без ущерба для его функциональности.
[[Категория:Packaging]]
[[Категория:Ruby]]
{{Category navigation|title=HOWTO|category=HOWTO|sortkey={{SUBPAGENAME}}}}

Текущая версия от 13:44, 21 декабря 2023

48px-Mail-mark-junk red.svg.png
Эта статья конкретно протухла.
Статья уже не подходит под современные реалии, под современные версии Альта или сломана совсем. Возможно, её уже не доработать и не поместить в архив.


Stub.png
Данная страница находится в разработке.
Эта страница ещё не закончена. Информация, представленная здесь, может оказаться неполной или неверной.


Основные правила сборки приложений и модулей ruby изложены в Ruby Packaging Policy. Цель этого документа — объяснить на простых примерах, как следует поступать в различных ситуациях при упаковке ruby-приложений, а также показать, как можно собирать простые модули.

rubygems не рассматриваются в данном HOWTO, так как их дополнительная функциональность (например, установка нескольких версий gem’а), необходимая в некоторых случаях (shared hosting, web-приложения), конфликтует с наличием дистрибутивного пакетного менеджера. Другие дистрибутивы занимают аналогичную позицию в отношении к rubygems: позиция Debian, их рекомендации апстриму.

Для ситуаций, в которых дополнительная функциональность rubygems необходима, сам rubygems упакован и работает.

Модули, которые нельзя собирать никогда-никогда

Тут перечислены модули, которых всячески стоит избегать. Это гадкие, негодные модули, не надо их использовать.

  • RubyInline - модуль, позволяющий делать "вставки" на языке C. На самом деле в процессе работы включаемый C-код "заворачивается" в native extension, компилится и скомпилированный модуль загружается в программу.

Общие принципы сборки модулей

При сборке пакета в дистрибутив используется «нативная» сборка — с помещением файлов в специальные каталоги, которые находятся в $LOAD_PATH.

Сборка включает в себя:

  • компиляцию бинарных модулей (если они есть);
  • выполнение тестов (если они есть и их выполнение возможно в hasher);
  • установку файлов в соответствующие каталоги;
  • генерацию документации в формате ri (class reference);

Модули устанавливаются в так называемый vendor dir. В ALT Linux это /usr/share/ruby/vendor_ruby/RUBY.VERSION и /usr/lib/ruby/RUBY.VERSION/ARCHITECTURE. Поскольку по умолчанию установка модулей идёт в site dir, при сборке пакета надо использовать модуль vendor_specific, вызывая интерпретатор ruby как ruby -rvendor_specific. Для этого есть макрос %ruby.

Внутри тарбола

Внутри тарбола с модулем (или программой) могут находиться следующие файлы и каталоги:

  • bin/ — скрипты, будут установлены в %_bindir;
  • ext/ — компилируемые модули (при использовании setup.rb), будут установлены в %ruby_sitearchdir;
  • lib/ — pure-ruby модули, будут установлены в %ruby_sitelibdir;
  • data/ — произвольные данные(при использовании setup.rb), будут установлены в %_datadir/ИМЯМОДУЛЯ;
  • test/ — unit-тесты;

Также как правило присутствует один или несколько «сценариев сборки», о них расскажу далее.

Собираем модуль

Существует несколько, различного уровня «стандартности», способов сборки модулей ruby.

Забегая вперёд, хочу сказать, что самым простым и удобным способом будет использование setup.rb, скопированного из пакета ruby-tool-setup (конечно, кроме случая когда в корне проекта находится файл extconf.rb).

extconf.rb AKA MkMf

Аналог configure, использует модуль mkmf, входящий в стандартную поставку ruby. Внутри скрипта проверяется наличие необходимых заголовочных файлов и библиотек, на выходе генерится Makefile, который обрабатывается стандартным make. Используется только в тех проектах, где есть бинарные модули. Если «рядом» с extconf.rb находится файл depend, его содержимое добавляется к Makefile. Исходные тексты и заголовочные файлы бинарного модуля как правило тоже лежат «рядом» с extconf.rb.

Типичные секции %build и %install выглядят следующим образом:

%build
%ruby_configure <опции extconf.rb>
%make_build

%install
%make_install DESTDIR=%buildroot install

setup.rb имени Minero Aoki

Скрипт сборки и установки общего назначения. Как правило используется для сборки и установки pure-ruby модулей. Имеет некоторое количество стандартных опций, может собирать бинарные модули, находящиеся в каталоге ext/ (как правило там присутствует extconf.rb, см. выше).

Типичные секции %build и %install выглядят следующим образом:

%build
%ruby_config <опции setup.rb>
%ruby_build

%install
%ruby_install

install.rb

Иногда это самописный скрипт, иногда встречается одна из первых версий setup.rb. Как правило используется только для установки pure-ruby модулей. Стандартных макросов для поддержки install.rb нет.

Rakefile и остальные случаи

Сценарий для rake. Обычно может иметь task’и build (если есть бинарные модули) и test, но последнее время не имеет task’а install. Зато использует rubygems.

Для вызова rake и rake install есть два стандартных макроса %rake и %rake_install соответственно.

Если task install не определён или вообще отсутствует установочный скрипт (в случае pure-ruby модуля), можно использовать setup.rb из пакета ruby-tool-setup примерно следующим образом:

%prep
%setup
%patch -p1
%update_setup_rb

Далее используются макросы %ruby_config, %ruby_build и %ruby_install.

Очень часто в Rakefile включена поддержка создания .gem. Поскольку rubygems использовать при сборке пакетов нельзя, следует воспользоваться рецептом из Debian.

Запускаем тесты

С осени 2009 года наш rpm поддерживает секцию %check, все тесты имеет смысл вынести в нее:

%check
%ruby_test_unit -Ilib:test test

По какой-то причине у меня (Участник:ТимурБатыршин) такая конструкция не заработала, и я воспользовался другой:

%check
for test in $(find test/ -name '*_test.rb'); do
        ruby -Ilib $test || exit 1
done

l10n при помощи ruby-gettext

Существует специальный модуль ruby-gettext который обеспечивает локализацию сообщений, совместимую с gettext. Если локализация используется, в каталоге с исходниками есть файлы вида po/ЯЗЫК/ИМЯ-ПАКЕТА.po. При сборке файлы *.po надо скомпилить в ЯЗЫК.mo, это делается следующим хуком для setup.rb:

=begin
  post-setup.rb

  Copyright (C) 2001-2006 Masao Mutoh
  This program is licenced under the same licence as Ruby. 
=end

require 'gettext/utils'

begin
  GetText.create_mofiles
rescue
  puts "GetText.create_mofiles failed."
end

Этот код надо поместить в файл post-setup.rb и положить в корень проекта, рядом с setup.rb. Скомпилированные файлы локализации будут помещены в data/locale/, после чего setup.rb установит их в /usr/share/locale/. Для упаковки этих фалов в пакет можно пользоваться макросом %find_lang.

Пакуем документацию

Документация в формате RI генерируется при помощи утилиты rdoc, находящейся в пакете ruby-tool-rdoc. Для этого существует стандартный макрос %rdoc предназначенный для использования в секции %install (обычно одной из последних строк). В качестве аргументов этого макроса перечисляются файлы и каталоги с исходниками и при необходимости другие опции утилиты rdoc.

Для pure-ruby модулей как правило используется конструкция:

%rdoc lib/

Если присутствуют бинарные модули:

%rdoc *.c lib/

Документацию желательно паковать в подпакет %name-doc. При этом паковать следует только документацию для основных классов модуля, описание расширений сторонних классов паковать не нужно.

Складываем файлы в пакеты

Pure-ruby модули помещаются в %ruby_sitelibdir, бинарные модули в %ruby_sitearchdir, документация в формате RI в %ruby_ri_sitedir (при этом файл %ruby_ri_sitedir/created.rid упаковывать не нужно).

Добро пожаловать в реальный мир

В теории всё выглядит красиво, однако на практике среднего размера модуль представляет собой «нечто», что может работать в любой помойке. Однако мы делаем не помойку, поэтому местечковые хаки нам не нужны.

Не загрязняем $LOAD_PATH ($:)

Очень часто в коде можно увидеть конструкции вида:

$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')

Эта конструкция добавляет в $LOAD_PATH некоторый путь. Сделано это для того, чтобы модуль (или исполняемый скрипт) можно было использовать из любого места. Поскольку в нашем случае все файлы пакуются в стандартные места, подобные конструкции не нужны. В большинстве случаев такие конструкции можно безболезненно удалить.

Используем существующие модули и размаскируем зависимости

Поскольку наша помойка не является «любой», её ТТХ нам известны. Например, известно что это Linux, есть Iconv и так далее. Поэтому специфичный код, предназначенный для работы на других платформах в наших пакетах не нужен (а иногда бывает и вреден, поскольку даже «мёртвый» код может порождать зависимости, которые в некоторых случаях превращаются в unmet’ы).

Также мы можем превратить опциональную зависимость в явную. Пример:

begin
  require 'iconv'
rescue LoadError
  module Iconv
    # Далее следует некоторый код, который в результате предоставляет API
    # похожий на API модуля iconv, возможно урезанный функционально.
  end
end

В данном случае всю эту сложную конструкцию можно заменить на одну строку require 'iconv'. Как бонус мы получаем зависимость на ruby(iconv) и полную функциональность данного модуля.

Реальные примеры можно посмотреть в пакете ruby-gettext.

Отрываем rubygems

В реальной жизни можно встретить множество вариантов использования rubygems. Вот самые распространённые:

Пример 1:

require 'rubygems'
require 'some/module'
require 'some/other/module'

Пример 2:

begin
  require 'rubygems'
  require 'some/module'
  require 'some/other/module'
rescue LoadError
  require 'some/module'
  require 'some/other/module'
end

Пример 3:

begin
  require 'some/module'
  require 'some/other/module'
rescue LoadError
  require 'rubygems'
  require 'some/module'
  require 'some/other/module'
end

Все эти три примера можно привести к одному виду:

require 'some/module'
require 'some/other/module'

Этим мы убираем зависимость на rubygems и размаскируем скрытые зависимости. Таким образом наш модуль автоматически получает зависимсти на ruby(some/module) и ruby(some/other/module).

Файлы специального вида (темплейты и плагины)

Очень часто модуль (или приложение) носит с собой какие-то данные, которые обычно находятся в каталоге с gem’ом. Выглядит это приблизительно вот так:

  def mock_framework_path(framework_name)
    File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "plugins", "mock_frameworks", framework_name))
  end

В таком случае этот каталог (в нашем примере «plugins») помещается в /usr/share/ИМЯМОДУЛЯ а код заменяется на такой:

  def mock_framework_path(framework_name)
    File.expand_path(File.join("/usr/share/ИМЯМОДУЛЯ/plugins/mock_frameworks", framework_name))
  end

Потрошим новый пакет

При сборке нового пакета следует пробежать глазами его код на тему вышеописанных конструкций. Начать можно с grep -r по каталогам lib/ и bin/ по паттернам '__FILE__', 'ubygems', 'LoadError', '\$:', '\$LOAD_PATH'.

Некоторые хитрости упаковки плагинов

Как правило любой «плагин» можно разделить на три или менее частей — «модули», «данные» и «скрипт инициализации». Обычно при загрузке «плагина» его «скрипт инициализации» загружает «модули» и в процессе работы приложения код «модулей» использует «данные». Из чего состоит «плагин», как он загружается и как работает зависит от приложения. Существует несколько довольно известных приложений, под которые написано довольно много плагинов. Некоторые из них возможно упаковать в rpm пакеты.

Ruby On Rails

Ruby on Rails считает «плагином» всё, где в корне проекта есть файл init.rb. В свою очередь, «проектом» может считаться любой каталог в vendor/plugins/ и все gem’ы. В нашей сборке RoR к дефолтному списку путей поиска плагинов (состоящему только из vendor/plugins/) добавлен %_datadir/rails/plugins/.

  1. Пакет должен называться rails-plugin-ИМЯПЛАГИНА.
  2. Пакет должен иметь зависимость на ruby-railties >= 2.1.0-alt2.
  3. Сборка и установка «модулей» производится при помощи setup.rb как описано выше.
  4. Файл init.rb и всё что может понадобиться плагину (about.yml, generators, tasks и т. п.) нужно скопировать в %buildroot%_datadir/rails/plugins/ИМЯПЛАГИНА/.

Однако, бывают случаи, когда собранный таким образом плагин не работает с криками про Uninitialized constant. Скорее всего в таком плагине используется механизм автозагрузки модулей из ActiveSupport. Он не ищет модули в стандартном $LOAD_PATH, поэтому устанавливать «модули» приходится в базовый каталог «плагина», например используя опцию --rbdir=%_datadir/rails/plugins/ИМЯПЛАГИНА/lib для %ruby_config.

GemPlugin

Эта система плагинов появилась из проекта mongrel, но в настоящее используется и в других проектах. Я не буду описывать принцип работы этой системы, расскажу только что нужно для того, чтобы «упакеченый» плагин можно было использовать.

  1. Пакет должен иметь зависимость на каталог %_datadir/gem_plugin.
  2. Файл lib/ИМЯПЛАГИНА/init.rb перед установкой должен быть перемещён в init.rb (в корень проекта).
  3. Сборка и установка «модулей» производится при помощи setup.rb как описано выше.
  4. Каталог resources нужно скопировать в %buildroot%_datadir/gem_plugin/ИМЯПЛАГИНА/.
  5. Файл init.rb (который перед установкой вытащили из lib/) тоже нужно скопировать в %buildroot%_datadir/gem_plugin/ИМЯПЛАГИНА/.
  6. Записать в файл %buildroot%_datadir/gem_plugin/ИМЯПЛАГИНА/dependencies.yml список зависимостей «плагина».

Файл dependencies.yml нужен для самого gem_plugin, чтобы решить какие именно плагины загружать для приложения, которое это попросило. Его можно создать посмотрев в GEM Specification, которая находится в файле Rakefile или ИМЯПЛАГИНА.gemspec:

Gem::Specification.new do |s|
  # ...
  s.add_dependency(%q<gem_plugin>, [">= 0.2.3"])
  s.add_dependency(%q<daemons>, [">= 1.0.3"])
  s.add_dependency(%q<fastthread>, [">= 1.0.1"])
  # ...
end

Нас интересуют только строки с add_dependency и только имена gem’ов. В нашем примере файл dependencies.yml должен выглядеть следующим образом:

---
- gem_plugin
- daemons
- fastthread

Генератор пакетов

Я заметил, что при упаковке нового пакета мне приходится производить много одинаковых действий и заниматься копипастеньем. Поэтому я нарисовал примерный генератор git-репозитория для нового ruby-пакета.

.../mkrubypkgrepo [--arch] [--doc] [--rails] path/to/name-version.tar.gz
  • --arch: генерить спек для архитектурно-зависимого пакета
  • --doc: паковать RI документацию в %name-doc
  • --rails: генерить спек для RoR плагина

Скрипт инициализирует в текущем каталоге git репозиторий, импортирует распакованный тарболл в ветку upstream и генерирует начальный спек. После генерации как обычно обработать напильником.