Рекомендации по упаковке Ruby

JRuby Gems: Хотя в Fedora есть полностью работающий JRuby, интегрированный с системным RubyGems, мы решили не включать сюда рекомендации по упаковке, специфичные для JRuby, так как они требуют дополнительной работы. Они появятся здесь, как только мы посчитаем, что всё охвачено должным образом. Вы можете связаться с нами в списке рассылки Ruby-SIG в случае любых вопросов о подготовленных рекомендациях по упаковке JRuby.

Существуют три основные категории пакетов ruby: RubyGems, не-gem пакеты Ruby и приложения, написанные на Ruby. Эти рекомендации содержат разделы, общие для всех них, а также разделы, применимые к каждой из них по отдельности. Обязательно прочитайте все рекомендации, относящиеся к типу пакета ruby, который вы собираете.

Совместимость с Ruby

Каждый пакет Ruby ОБЯЗАН указывать, что он зависит от интерпретатора Ruby (это не применяется к RubyGems). Используйте виртуальное требование ruby(release), чтобы этого добиться:

Requires: ruby(release)

Если пакет требует Ruby определённой версии(й), сделайте требование версионированным, например так:

Requires: ruby(release) >= 1.9.1
Альтернативные интерпретаторы: Альтернативные интерпретаторы Ruby (в настоящее время JRuby) также Provide: ruby(release). Это означает, что чистые пакеты RubyGems (они являются общими для интерпретаторов) НЕ ДОЛЖНЫ иметь Requires: ruby или Requires: jruby, чтобы их зависимости удовлетворялись любым из этих интерпретаторов.
Излишне конкретное версионирование ruby(release): Обратите внимание, что если требование к версии ruby(release) слишком конкретное, это может привести к привлечению неожиданного интерпретатора. Например, ruby(release) = 1.8 потребует пакет JRuby, поскольку это единственный пакет, который его предоставляет.

Совместимость с разными интерпретаторами

Большинство чистых пакетов Ruby будут работать на всех интерпретаторах Ruby. Однако есть случаи, когда пакеты используют специфичные для интерпретатора функции (например, fork()) и не будут работать на других интерпретаторах (JRuby). В этом случае пакет ДОЛЖЕН требовать этот интерпретатор. Например, пакет, использующий fork, ДОЛЖЕН явно указывать Requires: ruby. В случае такого пакета упаковщик ДОЛЖЕН завести ошибку с просьбой к upstream обеспечить поддержку другого интерпретатора(ов). Это ДОЛЖНО быть задокументировано в spec-файле.

Строки shebang

В Fedora /usr/bin/ruby реализован через Rubypick. Rubypick — это инструмент, похожий на RVM или rbenv. Он позволяет выбирать интерпретатор для выполнения сценария Ruby. Rubypick направляет всё, выполняемое через /usr/bin/ruby, в /usr/bin/ruby-mri или /usr/bin/jruby. По умолчанию он запускает MRI (Matz’s Ruby Implementation), но пользователь может явно указать интерпретатор, используя _mri_ или _jruby_ в качестве первого параметра. Например:

ruby _jruby_ jruby_script.rb
gem _mri_ install foo
rails _jruby_ s

Использование переменной окружения RUBYPICK может дать те же результаты. Переменную окружения можно использовать, чтобы установить один интерпретатор в качестве глобального по умолчанию:

export RUBYPICK=_jruby_
ruby jruby_script.rb  # Will use jruby
gem install foo        # Will also use jruby

Исполняемые файлы Ruby, которые, как известно, работают только на одной реализации Ruby, ДОЛЖНЫ использовать эту конкретную реализацию в своём shebang (#!/usr/bin/ruby-mri или #!/usr/bin/jruby), чтобы гарантировать, что они выполняются с использованием этой реализации. Весь остальной код ДОЛЖЕН использовать #!/usr/bin/ruby.

Рекомендации по именованию

  • Пакеты, содержащие Ruby Gems, ДОЛЖНЫ называться rubygem-%{gem_name}.

  • Имя пакета расширения/библиотеки ruby ДОЛЖНО начинаться с интерпретатора, для которого он собран (ruby, jruby и т. д.) и затем имени UPSTREAM. Например: ruby-UPSTREAM. Если имя upstream UPSTREAM содержит ruby, это СЛЕДУЕТ опустить из имени. Например, драйвер базы данных SQLite для ruby называется sqlite3-ruby. Соответствующий пакет Fedora СЛЕДУЕТ называть ruby-sqlite3, а не ruby-sqlite3-ruby.

  • Пакеты приложений, которые в основном предоставляют пользовательские инструменты, написанные на Ruby, ДОЛЖНЫ вместо этого следовать общим рекомендациям по именованию.

Макросы

Не-gem пакеты ruby и пакеты ruby gem устанавливаются в определённые стандартные места. Пакеты ruby-devel и rubygems-devel содержат макросы, полезные для соответствующих типов пакетов. Альтернативные интерпретаторы ruby будут иметь эквивалентные места (будут добавлены в эту таблицу позже).

Макрос Развёрнутый путь Использование

Из ruby-devel; предназначено для не-gem пакетов.

%{ruby_vendorarchdir}

/usr/lib{64}/ruby/vendor_ruby

Место для специфичных для архитектуры (например, *.so) файлов.

%{ruby_vendorlibdir}

/usr/share/ruby/vendor_ruby

Место для независимых от архитектуры (например, *.rb) файлов.

%{ruby_sitearchdir}

/usr/local/lib{64}/ruby/site_ruby

Место для локальных специфичных для архитектуры (например, *.so) файлов.

%{ruby_sitelibdir}

/usr/local/share/ruby/site_ruby

Место для локальных независимых от архитектуры (например, *.rb) файлов.

Из rubygems-devel; предназначено для gem-пакетов.

%{gem_dir}

/usr/share/gems

Верхний каталог структуры Gem.

%{gem_instdir}

%{gem_dir}/gems/%{gem_name}-%{version}

Каталог с фактическим содержимым Gem.

%{gem_libdir}

%{gem_instdir}/lib

Папка lib Gem.

%{gem_cache}

%{gem_dir}/cache/%{gem_name}-%{version}.gem

Кэшированный Gem.

%{gem_spec}

%{gem_dir}/specifications/%{gem_name}-%{version}.gemspec

Файл спецификации Gem.

%{gem_docdir}

%{gem_dir}/doc/%{gem_name}-%{version}

Документация rdoc Gem.

%{gem_extdir_mri}

%{_libdir}/gems/ruby/%{gem_name}-%{version}

Каталог для расширений MRI Ruby Gem.

Независимость от интерпретатора и макросы каталогов

Возможно, вы заметили, что в приведённой выше таблице есть разные каталоги для не-gem библиотек на разных интерпретаторах ruby, но только один набор каталогов для библиотек rubygem. Это потому, что код, написанный для одного интерпретатора ruby, часто будет работать на всех интерпретаторах ruby, которые поставляет Fedora (ruby, jruby и т. д.). Однако некоторый код использует методы, недоступные на всех интерпретаторах (см. [Different Interpreters Compatibility]). Rubygems имеют возможность поставлять разные версии кода в одном gem, чтобы gem мог работать на всех версиях интерпретатора, поэтому нам нужен только один общий каталог для rubygems, который могут использовать все интерпретаторы.

Стандартные каталоги ruby %{vendorlib} не имеют этой возможности. По этой причине не-gem библиотеки должны размещаться в каталогах для каждого интерпретатора и ДОЛЖНЫ иметь отдельный подпакет (или пакет в зависимости от upstream) для каждого поддерживаемого интерпретатора.

Библиотеки

Эти рекомендации применимы только к пакетам Ruby, основная цель которых — предоставление библиотеки Ruby; пакеты, которые в основном предоставляют пользовательские инструменты, написанные на Ruby, ДОЛЖНЫ вместо этого следовать рекомендациям по приложениям Ruby.

RubyGems

RubyGems — это собственный формат упаковки Ruby. Gems содержат много тех же метаданных, которые нужны RPM, что делает возможным довольно плавное взаимодействие между RPM и Gems. Эта рекомендация гарантирует, что Gems упакованы как RPM таким образом, что такие RPM хорошо сочетаются с остальной частью дистрибутива, и позволяет конечному пользователю удовлетворить зависимости Gem, установив соответствующий Gem, упакованный как RPM.

И RPM, и Gems используют похожую терминологию; есть spec-файлы, имена пакетов, зависимости и т. д. для обоих. Чтобы свести путаницу к минимуму, термины, относящиеся к концепциям Gem, будут явно обозначаться с префиксом «Gem», например, «Gem specification» (.gemspec). Неквалифицированный термин «пакет» в дальнейшем всегда означает RPM.

  • Spec-файлы ДОЛЖНЫ содержать определение %{gem_name}, которое является именем из спецификации Gem.

  • Source пакета СЛЕДУЕТ указывать как полный URL к выпущенному архиву Gem; в противном случае spec-файл ДОЛЖЕН содержать комментарий, объясняющий, почему был выбран другой Source.

  • Версия пакета ДОЛЖНА быть версией Gem.

  • Пакет ДОЛЖЕН иметь BuildRequires: rubygems-devel, чтобы подтянуть необходимые для сборки макросы.

  • НЕ СЛЕДУЕТ указывать Requires или Provides для rubygem, поскольку они генерируются автоматически.

  • НЕ СЛЕДУЕТ указывать Requires: ruby(release), если только вы не хотите явно указать совместимость с версией Ruby. Автоматически генерируемой зависимости от RubyGems (Requires: ruby(rubygems)) достаточно.

Фильтрация Requires и Provides

Runtime-зависимости requires и provides автоматически генерируются генератором зависимостей RPM. Однако иногда он может добавить дополнительные зависимости, противоречащие реальности. Чтобы исправить это, генератор зависимостей нужно переопределить, чтобы дополнительные зависимости можно было отфильтровать. Подробности см. в AutoProvidesAndRequiresFiltering.

Сборка gems

Поскольку gems — это не просто формат архива, а инкапсуляция как архива, так и информации, используемой для сборки библиотеки Ruby, сборка RPM из gem выглядит немного иначе, чем для других RPM.

Пример spec-файла для сборки gems выглядел бы так:

%prep
%autosetup -p1 -n  %{gem_name}-%{version}

# Modify the gemspec if necessary

%build
# Create the gem as gem install only works on a gem file
gem build ../%{gem_name}-%{version}.gemspec

# %%gem_install compiles any C extensions and installs the gem into ./%%gem_dir
# by default, so that we can move it into the buildroot in %%install
%gem_install

%install
mkdir -p %{buildroot}%{gem_dir}
cp -a ./%{gem_dir}/* %{buildroot}%{gem_dir}/

# If there were programs installed:
mkdir -p %{buildroot}%{_bindir}
cp -a ./%{_bindir}/* %{buildroot}%{_bindir}

# If there are C extensions, copy them to the extdir.
mkdir -p %{buildroot}%{gem_extdir_mri}
cp -a .%{gem_extdir_mri}/{gem.build_complete,*.so} %{buildroot}%{gem_extdir_mri}/
%prep

RPM (начиная с 4.14) может напрямую распаковывать архивы gem, поэтому мы можем вызвать gem unpack, чтобы извлечь исходники из gem. Затем мы вызываем %setup -n %{gem_name}-%{version}, чтобы сообщить rpm, в какой каталог распаковался gem. На том же уровне каталога также автоматически создаётся файл %{gem_name}-%{version}.gemspec. Этот файл .gemspec будет использоваться для повторной сборки gem позже. Если нам нужно изменить .gemspec (например, если версия зависимостей неверна для Fedora или .gemspec использует старые, больше не поддерживаемые поля), мы сделаем это здесь. Патчи к самому коду также можно делать здесь.

%build

Затем мы собираем gem. Поскольку %gem_install работает только с архивами gem, мы затем воссоздаём gem с помощью gem build. Созданный файл gem затем используется %gem_install для сборки и установки кода во временный каталог, который по умолчанию ./%{gem_dir}. Мы делаем это, потому что команда %gem_install одновременно собирает и устанавливает код за один шаг, поэтому нам нужен временный каталог для размещения собранных исходников перед их установкой в разделе %install.

Макрос %gem_install принимает два дополнительных параметра:

-n <gem_file>

Позволяет переопределить gem, используемый для установки. Это может быть полезно для преобразования устаревших spec-файлов, так что вы можете указать %{SOURCE0} в качестве gem для установки.

-d <install_dir>

Может переопределить назначение установки gem. Однако мы не рекомендуем использовать этот параметр.

Макрос %gem_install НЕ ДОЛЖЕН использоваться для установки в %{buildroot}
%install

Здесь мы фактически устанавливаем в %{buildroot}. Мы создаём нужные нам каталоги, а затем копируем то, что было установлено во временные каталоги, в иерархию %{buildroot}. Наконец, если этот ruby gem создаёт разделяемые объекты, они перемещаются в специфичный для архитектуры путь %{gem_extdir_mri}.

Патчинг требуемых версий gem

Одна из распространённых потребностей в патчинге — изменить чрезмерно строгие требования к версиям в upstream .gemspec. Это может быть связано с тем, что upstream .gemspec упоминает только версии, которые они явно тестировали, но мы знаем, что другая версия тоже будет работать, или потому что мы знаем, что поставляемые нами пакеты применили исправления для проблемного поведения без повышения номера версии (например, бэкпортированные исправления). Чтобы скорректировать такие зависимости, вы можете использовать макросы %gemspec_add_dep и %gemspec_remove_dep.

Например, если вы хотите использовать любую версию Aruba вместо чрезмерно конкретной версии, запрошенной upstream, вы можете использовать в разделе %prep следующие две строки:

%gemspec_remove_dep -g aruba "~> 0.14.2"
%gemspec_add_dep -g aruba
Используйте макросы только поверх сгенерированного .gemspec: Макросы %gemspec_add_dep и %gemspec_remove_dep надёжно работают только с .gemspec, сгенерированными с помощью команды ruby spec. Пожалуйста, не используйте макросы на upstream .gemspec-файлах.
Обязательно проверяйте: Не просто меняйте версии без проверки, что новая версия работает. Бывают случаи, когда upstream чрезмерно строг, но бывают и случаи, когда требование к версии было указано из-за конкретной обнаруженной ошибки или изменения API в минорном выпуске.

Упаковка для использования как Gem, так и не-Gem

Упаковка для использования без Gem больше не нужна: Изначально модули rubygem не помещались в путь библиотек ruby, поэтому мы упаковывали rubygems для использования как с gems, так и без них. Это позволяло работать коду, использующему require('ARubyModulePackagedAsAGem');. Текущий модуль rubygem добавляет все gems в путь библиотек ruby при его require. Следовательно, упаковщики НЕ ДОЛЖНЫ создавать не-Gem подпакеты rubygems для новых пакетов. Поскольку большинство пакетов Ruby в Fedora теперь упакованы как установленные gems, вам может потребоваться пропатчить код, чтобы использовать require('rubygem') как можно раньше в программе, чтобы гарантировать правильное обнаружение этих ruby-компонентов. Пакеты для ruby gems, которые в настоящее время создают не-gem подпакет, СЛЕДУЕТ адаптировать, чтобы прекратить поставку не-gem подпакета (с надлежащими Obsoletes и Provides в основном пакете rubygem).

Не-Gem пакеты

Не-Gem пакеты Ruby ДОЛЖНЫ требовать пакет ruby-devel во время сборки с помощью BuildRequires: ruby-devel и МОГУТ указывать минимальную версию ruby, необходимую им для сборки.

Архитектура сборки и размещение файлов

Следующее влияет только на файлы, которые пакет устанавливает в %{ruby_vendorarchdir} и %{ruby_vendorlibdir} (фактические файлы библиотеки Ruby). Все остальные файлы в пакете Ruby ДОЛЖНЫ соответствовать общим соглашениям упаковки Fedora.

Site против Vendor: Ранее использовались %{ruby_sitelibdir} и %{ruby_sitearchdir}. Однако, поскольку они предназначены только для локальных установок, пожалуйста, вместо этого используйте %{ruby_vendorlibdir} и %{ruby_vendorarchdir}.

Чистые пакеты Ruby

Чистые пакеты Ruby ДОЛЖНЫ собираться как noarch-пакеты.

Файлы библиотеки Ruby в чистом пакете Ruby ДОЛЖНЫ размещаться в %{ruby_vendorlibdir} (или его соответствующем подкаталоге). Spec-файл ДОЛЖЕН использовать этот макрос.

Пакеты Ruby с бинарным содержимым/разделяемыми библиотеками

Для пакетов с бинарным содержимым, например, драйверов баз данных или любых других привязок Ruby к библиотекам C, пакет ДОЛЖЕН быть специфичным для архитектуры.

Бинарные файлы в пакете Ruby с бинарным содержимым ДОЛЖНЫ размещаться в %{ruby_vendorarchdir} (или его соответствующем подкаталоге). Файлы Ruby в таком пакете СЛЕДУЕТ размещать в %{ruby_vendorlibdir}. Spec-файл ДОЛЖЕН использовать эти макросы.

Для пакетов, создающих разделяемые библиотеки C с помощью extconf.rb

export CONFIGURE_ARGS="--with-cflags='%{optflags}'"

СЛЕДУЕТ использовать для правильной передачи CFLAGS в Makefile. Также, чтобы разместить файлы в правильных папках во время сборки, передайте --vendor в extconf.rb следующим образом:

extconf.rb --vendor

Приложения

Приложения — это

  • программы, предоставляющие пользовательские инструменты, или

  • веб-приложения, обычно создаваемые с использованием Rails, Sinatra или подобных фреймворков.

RPM-пакеты ДОЛЖНЫ подчиняться правилам FHS. Их СЛЕДУЕТ устанавливать в %{_datadir}. Следующий макрос может вам помочь:

%global app_root %{_datadir}/%{name}

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

Вот сокращённый пример:

%global app_root %{_datadir}/%{name}

Summary: Deltacloud REST API
Name: deltacloud-core
Version: 0.3.0
Release: 3%{?dist}
Group: Development/Languages
License: Apache-2.0 AND MIT
URL: https://incubator.apache.org/deltacloud
Source:  https://rubygems.org/gems/%{name}-%{version}.gem
Requires: rubygem-haml
#...
Requires(post):   chkconfig
#...
BuildRequires: rubygem-haml
#...
BuildArch: noarch

%description
The Deltacloud API is built as a service-based REST API.
You do not directly link a Deltacloud library into your program to use it.
Instead, a client speaks the Deltacloud API over HTTP to a server
which implements the REST interface.

%package doc
Summary: Documentation for %{name}
Group: Documentation
Requires:%{name} = %{version}-%{release}

%description doc
Documentation for %{name}

%prep
%setup -q -n %{name}-%{version}

%build

%install
mkdir -p %{buildroot}%{app_root}
mkdir -p %{buildroot}%{_initddir}
mkdir -p %{buildroot}%{_bindir}
cp -r * %{buildroot}%{app_root}
mv %{buildroot}%{app_root}/support/fedora/%{name} %{buildroot}%{_initddir}
find %{buildroot}%{app_root}/lib -type f | xargs chmod -x
chmod 0755 %{buildroot}%{_initddir}/%{name}
chmod 0755 %{buildroot}%{app_root}/bin/deltacloudd
rm -rf %{buildroot}%{app_root}/support
rdoc --op %{buildroot}%{_defaultdocdir}/%{name}

%post
# This adds the proper /etc/rc*.d links for the script
/sbin/chkconfig --add %{name}

%files
%{_initddir}/%{name}
%{_bindir}/deltacloudd
%dir %{app_root}/
%{app_root}/bin
#...

%files doc
%{_defaultdocdir}/%{name}
%{app_root}/tests
%{app_root}/%{name}.gemspec
%{app_root}/Rakefile

%changelog
#...

Обратите внимание, что хотя источником является RubyGem, мы должны устанавливать файлы вручную в %{_datadir}/%{name}, %{_bindir} и т. д., чтобы следовать FHS и общим рекомендациям по упаковке. Если требуются дополнительные специфичные для Fedora файлы (файлы systemd .service, конфигурации), их СЛЕДУЕТ

  • добавлять через другие теги %SOURCE

Source1: deltacloudd-fedora
  • размещать в соответствующих местах на этапе %install

install -m 0755 %{SOURCE1} %{buildroot}%{_bindir}/deltacloudd

Запуск наборов тестов

Если для пакета доступен набор тестов (даже отдельно, например, не включённый в gem, но доступный в upstream-репозитории), его СЛЕДУЕТ запускать в %check. Набор тестов — единственный автоматизированный инструмент, который может обеспечить базовую функциональность пакета. Его запуск особенно полезен, когда требуются массовые пересборки. Вы МОЖЕТЕ пропустить выполнение набора тестов, если не все зависимости сборки удовлетворены, но это ДОЛЖНО быть задокументировано в spec-файле. Отсутствующие зависимости сборки для включения набора тестов СЛЕДУЕТ как можно скорее упаковать для Fedora и снова включить набор тестов.

Тесты НЕ СЛЕДУЕТ запускать с помощью Rake, так как Rake почти всегда привлекает некоторые ненужные зависимости, такие как hoe или gemcutter. По аналогичным причинам СЛЕДУЕТ избегать зависимости от Bundler. Также СЛЕДУЕТ избегать фреймворков покрытия кода, таких как SimpleCov и Coveralls.

Тестирование с разными реализациями Ruby

Чтобы запустить тесты с другой реализацией Ruby, такой как JRuby, добавьте BuildRequires: jruby. Затем используйте переключение интерпретатора Rubypick:

ruby _jruby_ -Ilib -e 'Dir.glob "./test/test_*.rb", &method(:require)'

Если ваш пакет запускает модульные тесты для ruby-mri и предполагается, что он будет работать под альтернативными интерпретаторами, то он должен запускать модульные тесты и под всеми альтернативными интерпретаторами. Это единственный доступный нам метод проверки совместимости кода под каждым интерпретатором. Применяются те же правила: вы можете это опустить, если нужные вам библиотеки недоступны для конкретного альтернативного интерпретатора, но у вас ДОЛЖЕН быть комментарий с объяснением.

Использование фреймворков тестирования

Сообщество Ruby поддерживает множество фреймворков тестирования. Следующие разделы демонстрируют, как запускать наборы тестов с использованием наиболее распространённых из них.

MiniTest / Test::UNIT

MiniTest, как и Test::UNIT, поставляется вместе с Ruby. Чтобы их использовать, вам нужно использовать BuildRequires: rubygem-minitest или BuildRequires: rubygem-testunit соответственно. Чтобы выполнить набор тестов, вы можете использовать что-то вроде:

%check
ruby -Ilib -e 'Dir.glob "./test/**/*_test.rb", &method(:require)'

Возможно, вам потребуется изменить -Ilib на -Ilib:test или использовать немного другой шаблон сопоставления для test_*.rb и т. д. Ожидается, что упаковщики будут использовать правильный шаблон для каждого gem.

RSpec

Чтобы запустить тесты с помощью RSpec >= 3, вы добавляете BuildRequires: rubygem-rspec и используете что-то вроде:

%check
rspec -Ilib spec

Наборы тестов, не включённые в пакет

Иногда вам приходится получать тесты отдельно от пакета gem upstream. Например, предположим, вы упаковываете rubygem-delorean версии 1.2.0, который размещён на Github. Тесты не включены в сам Gem, поэтому вам нужно получить их и соответствующим образом скорректировать spec-файл:

# git clone https://github.com/bebanjo/delorean.git && cd delorean
# git checkout v1.2.0
# tar -czf rubygem-delorean-1.2.0-specs.tgz spec/
Source1: %{name}-%{version}-specs.tgz

# ...
%prep
%setup -q -n %{gem_name}-%{version} -b 1

# ...

%check
pushd ./%{gem_instdir}
# Link the test suite into the right place in source tree.
ln -s %{_builddir}/spec .

# Run tests
rspec spec
popd

# ...
  • Обязательно включайте версию тестов в имя источника, чтобы при обновлении до новой версии rpmbuild завершился ошибкой, поскольку он не найдёт правильный %{SOURCE1} (и это напомнит вам также обновить тесты).

  • Добавьте команды, которые вы использовали для получения тестов, в spec-файл в виде комментариев. Это значительно облегчит вам задачу в следующий раз, когда вам понадобится их получить.

  • Запустите тесты как обычно.