Менеджер пакетов Gem: практики

В статье "Gem Packaging: Best Practices" автор рассматривает принципы работы менеджера пакетов gem, его ошибки и способы их избежания. Автор: Josh Peek

Понимание Пути загрузки Ruby

Когда вы вызываете такие функции, как load или require, Ruby просматривает файлы в своем пути загрузки (load path). Это позволяет вам указать относительные пути загрузки вместо полных.

В начальном состоянии путь загрузки содержит пути к стандартной библиотеке Ruby. Есть три альтернативных имени, которые указывают на глобальный путь загрузки Ruby: $:, $-I, $LOAD_PATH. Вы можете добавить к этому списку, в конец или в начало, ваши собственные библиотеки. Путь загрузки может быть изменен из командной строки при помощи флажка-I.

Вот начальная конфигурация пути загрузки на моем Макинтоше.

  >> $LOAD_PATH
  => ["/Users/josh/.rip/active/lib",
      "/Library/Ruby/Site/1.8",
      "/Library/Ruby/Site/1.8/powerpc-darwin9.0",
      "/Library/Ruby/Site/1.8/universal-darwin9.0",
      "/Library/Ruby/Site",
      "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8",
      "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/powerpc-darwin9.0",
      "/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/universal-darwin9.0",
      "."]

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

Уважайте глобальные пути загрузки

При упаковке нового rubygem, чтобы поделиться с миром, вам следует быть осторожнее с файлами, которые вы размещаете непосредственно в каталоге lib/. Rubygems (и почти все другие менеджеры пакетов ruby) добавляют каталог lib/ вашего gem к пути загрузки. Это означает, что любой файл, на верхнем уровне каталога LIB / будет легко доступен любому пользователю, использующему gem.

Пример плохого gem:

  `-- lib
      |-- foo
      |   `-- cgi.rb
      |-- foo.rb
      |-- erb.rb
      `-- set.rb

Вызов любых файлов из вашего пакета, по вашему усмотрению, может показаться безопасным, из-за использования пространств имен (namespace). Но если каталог lib / добавлен в начало переменной $LOAD_PATH то это нарушает работу библиотек erb и set. Функция require 'erb' будет использовать не файл erb из библиотеки Ruby, а файл erb из пакета (gem).

Безопасным (и верным) способом было бы переместить ваши файлы в другой каталог. Это просто осуществить, создайте папку в каталоге lib с тем же именем, что и ваш gem. Затем разместите все файлы в каталог lib/foo/вместо lib /.

Это своего рода "серая зона".. Нет строгого правила, что вы должны поместить все ваши файлы в папку с именем вашего пакета. Хорошо иметь несколько файлов в корневом каталоге Lib до тех пор, пока они запрашиваются отдельно. Переместите в другое место файлы, которые вы не хотите, чтобы запрашивались напрямую.

Запрос других файлов относительно друг друга

  require File.join(File.dirname(__FILE__), "foo", "bar")
  # or
  require File.expand_path(File.join(File.dirname(__FILE__), "foo", "bar"))

Если вы используете File.dirname(__FILE__) с помощью функции require, то вы допускаете ошибку.

Исправить это просто, укажите относительные имена файлов.

  require "foo/bar" 

Интересно, что 3 предыдущих примера использующие require, отличаются друг от друга коренным образом. Ruby может отследить лишь то, какие файлы он запрашивал с помощью полного пути, указанного вами. Первый относится, к вашему текущему каталогу (”./lib/foo/bar”), второй - путь поиска системных библиотек (”/usr/local/lib/ruby/gems/foo/lib/foo/bar”), и третий пример является относительным к пути загрузки (”foo/bar”).Функция require воспринимает каждый файл как отдельный, поэтому это может закончиться тем, что один и тот же файл будет загружаться несколько раз.

Зависимость от файлов вне пути загрузки

Это более важный пример, чем предыдущие.

  module Rack
    module Test
      VERSION = ::File.read(::File.join(::File.dirname(__FILE__), "..", "..", "VERSION")).strip
      # ...
    end
  end

Ваши gem-папки могут быть отделены и реорганизованы при установке. Если кто-нибудь захочет "продать" вашу библиотеку, им лишь надо скопировать содержимое каталога lib/. Информация вне lib / не является важной для запуска кода. Никогда не создавайте папки lib или test на одном уровне. Минимальный инсталлятор, такой как rip, установит только содержимое папок bin и lib. Любой файл, необходимый для вашего пакета, должен находиться в каталоге lib и должен содержать корректное пространство имен в папке, чтобы избежать конфликтов. Если Вы попытаетесь установить эту версию rack-test с помощью rip, то require 'rack/test' завершится неудачей, потому что каталог ../VERSION не существует.

Библиотеки не должны модернизировать переменную $LOAD_PATH

IПакеты не должны модернизировать путь загрузки, это задача менеджеров пакетов. Когда rubygems активизируют gem, то он добавляет каталог lib вашего пакета к переменной $LOAD_PATH , которая может обычно обрабатываться через любую другую библиотеку или приложение. Безопасно считать, что вы можете относительно require любой файл из вашей папки lib.

  unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__)))
    $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)))
  end

Такой код можно удалять безопасно.

СОВЕТ: Конфигурируйте путь загрузки во время тестирования

Если вы попытаетесь создать свою библиотеку, то все проблемы, связанные с путем загрузки добавят вам головной боли. Это тот случай, когда могут пригодиться rake-тесты.

Rake автоматически добавит каталог lib в ваш путь во время тестирования. Так что вам не нужно использовать File.join (File.dirname (ФАЙЛ), ”..”, “lib”, “foo”) в ваших тестах. Вы можете добавить тестовый каталог к вашему пути загрузки с помощью “test_helper”, в который необходимо добавить следующий код.

  Rake::TestTask.new do |t|
    t.libs << 'test'
  end

К сожалению, Rspec даже не добавляет каталог lib к вашему пути. Вы можете исправить это таким образом:

  Spec::Rake::SpecTask.new do |t|
    t.libs << 'lib'
  end

Если вы хотите запустить изолированный тест, вы можете добавить каталог lib к переменной $LOAD_PATH с помощью флагов командной строки. (С этого начались долгие дебаты)

  ruby -Ilib test/test_foo.rb

Используйте константу VERSION

Если вы создаете Awesome gem, то необходимо предоставить переменную Awesome:VERSION. При использовании Rubygems, возможен запрос того, какую версию gem вы используете, а при использовании альтернативного  менеджера пакетов, единственный способ узнать, какая версия загружена, только через интроспекцию.

Старайтесь не зависеть от rubygems

Если я пользуюсь вашей библиотекой, развертываю ваше приложение, или запускаю ваши тесты, у меня может не возникнуть желание использовать rubygems. Когда вы используете “require ‘rubygems’” в своем коде, вы лишаете меня такой возможности. – Ryan Tomayko

Можно безопасно удалить require "rubygems"  из вашей библиотеки, так как загружающий его код вероятно уже это сделал. Людям становится труднее (но не невозможно) использовать альтернативные способы создания пути загрузки, потому что нет никакого способа "отменить требование" rubygems. Существует много других возможностей управлять пакетами, как например rip, bundler, или задание переменной $LOAD_PATH вручную.

Избегайте указания зависимостей rubygem в каталоге lib/. Это означает удаление всех команд вида gem "foo". Если вам необходимо определить свою зависимость от gem в вашем gemspec, это создаст жесткую зависимость от Rubygems. Пакет Rubygems перехватывает метод require и автоматически разрешает такие зависимости во время выполнения. Вынос указания зависимости из библиотеки более гибок, т.к. другие менеджеры пакетов, могут управлять зависимостями во время инсталляции.

В время удаления gem, не обрамляйте код проверки загрузки с помощью rescue Gem::LoadError или rescue Gem::Exception. Если вам необходимо корректно обработать эти ошибки, имейте в виду, что Gem::LoadError наследуется от LoadError, поэтому это можно заменить на rescue LoadError.

  # Bad
  begin
    gem "rack" 
    require "rack" 
  rescue Gem::LoadError
    puts "Could not load 'rack'" 
    exit
  end

  # Good
  begin
    require "rack" 
  rescue LoadError
    puts "Could not load 'rack'" 
    exit
  end

Зачем мне это нужно?

Я не знал ни об одной из этих проблем, пока Ryan не написал свою статью почему запрашиваемый rubygems некорректен в начале 2009 года. И я почувствовал, что многие другие Ruby-программисты просто пропускают эти проблемы, так как rubygems был нашим единственным выходом. Основы управления библиотекой Ruby построены на системе пути загрузки и для каждого автора ruby gem важно понять, как это работает.

Мы над этим работаем

Исправить каждую библиотеку ruby, это легче сказать чем сделать (существует  больше 12 000 библиотек ,  размещенных на RubyForge ). Rails в настоящее время нарушает несколько правил. Но мы упорно трудимся, чтобы это исправить. Мы также должны исправить все gems, от которых мы зависим. В идеале, мы хотели бы, чтобы Rails 3 загружался без rubygems и можно было бы использовать разные менеджеры пакетов.

Оригинал статьи на weblog.rubyonrails.org

Перевод КОМТЕТ komtet.ru

Вам также может помочь