Criando sua própria RubyGem


Leia em 3 minutos

Todo mundo já deve ter usado pelo menos uma vez na vida as famosas gems. Elas ajudam muito na hora distribuir e usar bibliotecas Ruby, sem dúvida nenhuma. Mas o que nem todo mundo sabe é que criar uma gem é muito mais fácil do que parece. Neste artigo você verá como criar e distribuir sua própria gem.

Entendo a gem

Uma gem nada mais é que um arquivo .tar.gz.

$ tar -ztvf bundler-0.9.26.gem
-rw-r--r--  0 wheel  wheel   47579 Dec 31  1969 data.tar.gz
-rw-r--r--  0 wheel  wheel     993 Dec 31  1969 metadata.gz

Neste arquivo, você encontra o código Ruby propriamente dito (data.tar.gz) e os metadados (metadata.gz), que é a gem specification (ou apenas gemspec) em formato YAML. Esta gemspec informa, dentre outras coisas, qual é o nome da gem, a versão e dependências do pacote, além dos arquivos que compoem a biblioteca.

Uma gem é composta por uma estrutura mais ou menos definida de arquivos e diretórios, como você pode ver abaixo.

Estrutura de arquivos e diretórios de uma gem

O diretório lib é de longe o mais importante. É nele que todo código Ruby fica armazenado. E é ele que é adicionado ao GEM_PATH, utilizado pelo RubyGems (gerenciador de pacotes) para saber quais bibliotecas estão disponíveis para uso.

O RubyGems substitui o método Kernel#require com sua própria implementação, que faz uma busca pela gem que você precisa utilizando os diretórios adicionados ao GEM_PATH. Então, sempre que você utilizar o método require "some_library", o RubyGems irá procurar por um arquivo lib/some_library.rb, independente de qual gem tenha este arquivo.

Criando a biblioteca HelloWorld

Vamos criar nossa biblioteca HelloWorld, que será empacotada posteriormente como uma gem. O primeiro passo é gerar nossa estrutura de diretórios.

$ mkdir -p hello_world/{lib/hello_world,test}

Crie um arquivo de testes em tests/hello_world_test.rb. Nosso módulo terá um único método chamado say.

require "test/unit"

class HelloWorldTest < Test::Unit::TestCase
  def test_say_hello_to_the_world
    assert_equal "Hello World!", HelloWorld.say
  end
end
 

Você pode executar este teste com o comando ruby test/hello_world_test.rb. Ele irá falhar, como era de se esperar.

$ ruby test/hello_world_test.rb
Loaded suite test/hello_world_test
Started
E
Finished in 0.000388 seconds.

  1) Error:
test_say_hello_to_the_world(HelloWorldTest):
NameError: uninitialized constant HelloWorldTest::HelloWorld
    test/hello_world_test.rb:5:in `test_say_hello_to_the_world'

1 tests, 0 assertions, 0 failures, 1 errors
 

Agora, crie o arquivo lib/hello_world.rb. Nossa implementação é bastante simples.

module HelloWorld
  def self.say
    "Hello World!"
  end
end

Execute o comando ruby test/hello_world_test.rb mais uma vez. Como era de se esperar, o teste irá… falhar novamente. Isso acontece porque não estamos carregando o módulo HelloWorld. Embora você possa resolver isso de várias maneiras diferentes, vamos fazer do modo mais organizado: iremos criar uma rake task para executar nossos testes, adicionando o diretório lib ao $LOAD_PATH.

Crie um arquivo Rakefile com o seguinte código:

require "rake/testtask"

Rake::TestTask.new do |t|
  t.libs << "lib"
  t.test_files = Dir["test/**/*_test.rb"]
end

Altere o arquivo test/hello_world_test.rb, adicionando a linha que irá carregar o arquivo hello_world.rb.

require "test/unit"
require "hello_world"  # Added

class HelloWorldTest < Test::Unit::TestCase
  def test_say_hello_to_the_world
    assert_equal "Hello World!", HelloWorld.say
  end
end

Rode o teste mais uma vez, só que agora usando o comando rake test.

$ rake test
(in /Users/fnando/Sites/github/hello_world)
Loaded suite /Users/fnando/.gem/spaces/active/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
.
Finished in 0.00031 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

Ótimo! Nossos testes estão passando e como terminamos toda nossa implementação, já podemos criar e distribuir nossa gem.

Gerando nosso pacote

Para gerar um arquivo .gem, precisamos escrever nossa gemspec. Crie um arquivo hello_world.gemspec e adicione o código abaixo.

Gem::Specification.new do |s|
  s.name        = "hello_world"
  s.version     = "0.1.0"
  s.description = "A simple gem that says hello to the world!"
  s.summary     = "Say hello!"
  s.author      = "Nando Vieira"
  s.files       = Dir["{lib/**/*.rb,README.rdoc,test/**/*.rb,Rakefile,*.gemspec}"]
end

As opções da classe Gem::Specification são auto-descritivas; para saber mais sobre estas e outras opções, consulte a documentação.

Agora vem a parte mais fácil. Execute o comando gem build hello_world.gemspec e um arquivo hello_world-0.1.0.gem será gerado.

Você pode instalar este arquivo localmente utilizando o comando gem install hello_world-0.1.0.gem --local. Para listar o que foi instalado, execute gem contents hello_world.

$ gem contents hello_world
/Users/fnando/.gem/spaces/active/gems/hello_world-1.0/lib/hello_world/version.rb
/Users/fnando/.gem/spaces/active/gems/hello_world-1.0/lib/hello_world.rb
/Users/fnando/.gem/spaces/active/gems/hello_world-1.0/README.rdoc
/Users/fnando/.gem/spaces/active/gems/hello_world-1.0/test/hello_world_test.rb
/Users/fnando/.gem/spaces/active/gems/hello_world-1.0/Rakefile
/Users/fnando/.gem/spaces/active/gems/hello_world-1.0/hello_world.gemspec

E, claro, nossa gem também será listada com o comando gem list.

$ gem list hello_world -d

*** LOCAL GEMS ***

hello_world (1.0)
    Author: Nando Vieira
    Installed at: /Users/fnando/.gem/spaces/active

    Say hello!

Distribuindo nossa gem

Para distribuir sua gem, é preciso criar uma conta no site RubyGems.org.

Depois, basta executar gem push hello_world-0.1.0.gem; informe seu e-mail e senha cadastrados no RubyGems.org quando solicitado. Se tudo der certo, você verá uma mensagem como esta:

$ gem push hello_world-0.1.0.gem
Pushing gem to RubyGems.org...
Successfully registered gem: hello_world (0.1.0)

Você pode acessar sua gem em http://rubygems.org/gems/hello_world.

DICA: Antes de escrever sua gem, verifique se o nome está disponível. Um simples gem list nome_da_gem -rd já te dará a resposta. A gem hello_world já existia, por isso foi publicada como http://rubygems.org/gems/fnando-hello_world.

Versionamento dos pacotes

É sua responsabilidade definir como será o versionamento de sua gem. Eu utilizo o formato MAJOR.MINOR.PATCH, como em 1.2.3.

No arquivo lib/hello_world/version.rb você pode ter algo como:

module HelloWorld
  module Version
    MAJOR = 0
    MINOR = 1
    PATCH = 0
    STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
  end
end

E para finalizar…

Gems como Jeweler (que sempre uso) também podem ajudar na hora de criar, distribuir e manter suas bibliotecas. Mas antes de utilizar uma dessas bibliotecas, entender o processo de criação de gems é fundamental. Afinal, é fazendo que se aprende!