Créer son propre Gem et le publier

Publié le 26 avril 2012 par Nicolas Cavigneaux | back

Cet article est publié sous licence CC BY-NC-SA

La meilleure façon de rendre son code ré-utilisable et de le partager est de publier un Gem qui pourra ainsi être chargé dans les projets via un Gemfile.

Bundler propose de créer pour nous la structure par défaut à utiliser pour pouvoir publier notre code sous forme de Gem.

Nous allons voir comment créer un Gem simple pour pouvoir ré-utiliser notre code à travers d’autres projets.

Création du squelette

La première chose à faire est de créer la structure de votre Gem. Nous pourrions créer tout le nécessaire à la main mais bundler peut s’occuper de ça pour nous.

Nous ne parlerons pas de gems tierces comme Hoe ou Echoe qui permettent de gérer la création de gems car nous considérons que bundler est amplement suffisant dans la majorité de cas. De plus ces gems ont été écrits avant que bundler (et son système de création de gems) n’existe. L’intérêt en est aujourd’hui limité.

Nous allons créer une lib très simple qui ajoutera une méthode à la classe String. Cette méthode devra retourner un hashage SHA1 de la chaîne. L’intérêt de cette article n’étant pas le code en lui même, nous nous cantonnerons à cet exemple simple.

Vous pouvez accéder à l’intégralité du code de notre exemple depuis le dépôt Github dédié.

  bundle gem string_to_sha1

    create  string_to_sha1/Gemfile
    create  string_to_sha1/Rakefile
    create  string_to_sha1/.gitignore
    create  string_to_sha1/string_to_sha1.gemspec
    create  string_to_sha1/lib/string_to_sha1.rb
    create  string_to_sha1/lib/string_to_sha1/version.rb
  Initializating git repo in /Users/cavigneaux/Synbioz/string_to_sha1

La commande crée pour nous un répertoire portant le nom du Gem et y inclut plusieurs fichiers. Un dépôt Git est également initialisé automatiquement.

Le fichier string_to_sha1.gemspec est celui qui permet de générer le gem. Voyons ce qu’il contient par défaut:

# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "string_to_sha1/version"

Gem::Specification.new do |s|
  s.name        = "string_to_sha1"
  s.version     = StringToSha1::VERSION
  s.authors     = ["Nicolas Cavigneaux"]
  s.email       = ["nico@bounga.org"]
  s.homepage    = ""
  s.summary     = %q{TODO: Write a gem summary}
  s.description = %q{TODO: Write a gem description}

  s.rubyforge_project = "string_to_sha1"

  s.files         = `git ls-files`.split("\n")
  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  s.require_paths = ["lib"]

  # specify any dependencies here; for example:
  # s.add_development_dependency "rspec"
  # s.add_runtime_dependency "rest-client"
end

On voit donc assez clairement ce qu’on doit modifier dans ce fichier puisque les chaînes en question sont marquées d’un “TODO”.

Pour résumer, cette spécification de Gem contient donc :

  • le nom sous lequel le gem sera connu
  • sa version
  • le ou les auteurs sous forme d’un tableau
  • le ou les emails des auteurs / contacts
  • la page de présentation du projet (page dédiée, github, rdoc, …)
  • un résumé à modifier pour décrire succintement le gem
  • un descriptif plus précis
  • le projet RubyForge associé (facultatif)

En plus de cela, d’autres attributs sont définis automatiquement. Ils sont déduits de la liste de fichiers gérés par Git. On a donc une génération automatique de la liste des fichiers à inclure dans le gem, de ceux qui servent aux tests ainsi que des éventuels executables.

Cette technique est très pratique puisque si vous utilisez Git pour versionner votre code, la liste de fichier dans le Gemfile sera toujours à jour sans que vous n’ayez à vous en soucier. Il reste évidemment possible de définir cette liste de fichier (tableau) à la main ou encore d’adapter les commandes pour utiliser votre SCM préféré.

Voici un exemple d’utilisation avec Mercurial :

s.files         = `hg manifest`.split("\n")
s.test_files    = `hg manifest`.split("\n").select { |f| f =~ /^(test|spec|features)/ }
s.executables   = `hg manifest`.split("\n").select { |f| f =~ /^bin/ }.map{ |f| File.basename(f) }

Le code

Voyons maintenant les fichiers qui contiendront notre code :

lib/string_to_sha1/version.rb :

module StringToSha1
  VERSION = "0.0.1"
end

qui permet de définir la version de votre Gem avant publication. Il faudra donc penser à incrémenter la version avant chaque publication.

Pour mémoire, les numéros de version sont souvent composés de la façon suivante :

X.Y.Z où :

  • X est le numéro de version majeur
  • Y est le numéro de version mineur
  • Z est le numéro de patch

On va donc incrémenter X à chaque ajout de fonctionnalité majeure, ou de modifications rendant la nouvelle version incompatible avec l’ancienne. On incrémentera Y pour chaque ajout de fonctionnalité mineure et Z pour chaque release de patch (correction de bug) qui sera faite.

lib/string_to_sha1.rb :

require "string_to_sha1/version"

module StringToSha1
  # Your code goes here...
end

C’est ce fichier qui sera chargé automatiquement lorsque votre gem sera chargé dans un projet. On peut mettre notre code directement dans ce fichier ou découper notre code en unités logiques qu’on placera dans des fichiers dédiés dans lib/.

Cette seconde méthode est certainement la plus propre si votre projet contient plus que quelques lignes de code. Dans notre exemple, le code étant très simple, nous nous cantonnerons à ce fichier. Voici donc les modifications à apporter pour pouvoir convertir facilement une chaîne en hashage SHA1 :

require "string_to_sha1/version"
require "digest/sha1"

class String
  def to_sha1
    Digest::SHA1.hexdigest(self)
  end
end

On charge la librairie sha1 et on ré-ouvre la classe String (Monkey Patching) pour y ajouter notre méthode.

Voilà, nous avons notre code, il ne reste plus qu’à publier le gem pour pouvoir l’utiliser dans d’autres projets.

Environnement de développement

Avant de passer à la publication, voyons deux fichiers dont nous n’avons pas parlé jusque là, ce sont le Gemfile et le Rakefile :

Gemfile

source "http://rubygems.org"

# Specify your gem's dependencies in string_to_sha1.gemspec
gemspec

Une seule instruction est présente dans ce Gemfile, gemspec. Ce fichier n’est en fait qu’un “proxy” qui va aller chercher ses dépendances directement dans string_to_sha1.gemspec. Une bonne pratique est de tester son code pour s’assurer qu’il fonctionne comme prévu, nous pourrions donc vouloir charger des outils de test :

string_to_sha1.gemspec :

# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "string_to_sha1/version"

Gem::Specification.new do |s|
  s.name        = "string_to_sha1"
  s.version     = StringToSha1::VERSION
  s.authors     = ["Nicolas Cavigneaux"]
  s.email       = ["nico@bounga.org"]
  s.homepage    = "http://github.com/synbioz/string_to_sha1"
  s.summary     = %q{Add SHA1 hashing from string}
  s.description = %q{This gem add a facility method to easily convert existing string to SHA1 hash.}

  s.rubyforge_project = "string_to_sha1"

  s.files         = `git ls-files`.split("\n")
  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  s.require_paths = ["lib"]

  s.add_development_dependency "minitest"
  s.add_development_dependency "guard-minitest"
end

On peut maintenant utiliser bundler pour installer nos dépendances de développement :

$ bundle

Fetching source index for http://rubygems.org/
Installing ffi (1.0.11) with native extensions
Installing thor (0.14.6)
Installing guard (1.0.1)
Installing guard-minitest (0.5.0)
Installing minitest (2.12.1)
Using string_to_sha1 (1.0.0) from source at /Users/cavigneaux/Synbioz/string_to_sha1
Using bundler (1.0.21)

Il est donc très simple pour n’importe qui de reprendre le code et d’installer les dépendances de développement sur sa machine pour contribuer.

Si vous pensez publier publiquement votre Gem, il est de bon ton d’ajouter un fichier README qui explicitera votre projet, ses fonctionnalités, les moyens d’y contribuer, les commandes pour préparer l’environnement de développement, …

Rakefile :

require "bundler/gem_tasks"

Que nous apporte bundle ?

$ rake -T

rake build    # Build string_to_sha1-0.0.1.gem into the pkg directory
rake install  # Build and install string_to_sha1-0.0.1.gem into system gems
rake release  # Create tag v0.0.1 and build and push string_to_sha1-0.0.1.gem to Rubygems

Nous avons donc une tâche pour construire le gem, une pour l’installer sur le système à des fins de test et une autre pour releaser notre Gem sur Rubygems.org et tagguer notre code par la même occasion. Des outils simples mais utiles en phase de développement.

Publication

Notre première version est prête, nous allons donc modifier le Gemspec avant de lancer la construction du gem.

string_to_sha1.gemspec :

# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "string_to_sha1/version"

Gem::Specification.new do |s|
  s.name        = "string_to_sha1"
  s.version     = StringToSha1::VERSION
  s.authors     = ["Nicolas Cavigneaux"]
  s.email       = ["nico@bounga.org"]
  s.homepage    = "http://github.com/synbioz/string_to_sha1"
  s.summary     = %q{Add SHA1 hashing from string}
  s.description = %q{This gem add a facility method to easily convert existing string to SHA1 hash.}

  s.files         = `git ls-files`.split("\n")
  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  s.require_paths = ["lib"]

  s.add_development_dependency "minitest"
  s.add_development_dependency "guard-minitest"
end

lib/string_to_sha1/version.rb :

module StringToSha1
  VERSION = "1.0.0"
end

Nous sommes maintenant prêts pour la génération :

$ gem build string_to_sha1.gemspec

  Successfully built RubyGem
  Name: string_to_sha1
  Version: 1.0.0
  File: string_to_sha1-1.0.0.gem

Et voilà un gem tout frais, prêt à l’utilisation, vous pouvez donc par exemple l’installer avec rake install puis le tester en console :

>> require "rubygems"
=> true
>> require "string_to_sha1"
=> true
>> s = "Bundler is a great tool!"
=> "Bundler is a great tool!"
>> s.to_sha1
=> "82c66f602918c5050041d8687e3413b3ae9f5e73"

Il semble donc que notre gem fonctionne ! Nous pouvons passer à l’étape suivante, le partager.

Github

C’est souvent une bonne idée de publier son code open-source sur Github pour gagner en visibilité mais aussi parce que d’autres outils intéressants sont couplés à Github.

Il faudra donc commencer par créer un dépôt pour accueillir votre code. Une fois le dépôt préparé, il faut définir cette adresse comme origine dans la config git de notre projet et y pousser notre code :

$ git remote add origin git@github.com/synbioz/string_to_sha1
$ git add .
$ git commit -m "First release"
$ git push origin master

Vous remplacerez bien évidemment l’url par celle de votre dépôt. Votre code est maintenant visible sur github, peut être cloné ou encore forké.

Autre petit plus, il est désormais possible pour quiconque d’utiliser votre gem dans son Gemfile en utilisant directement le code hébergé par Github :

gem "string_to_sha1", git: "git://github.com/synbioz/string_to_sha1.git"
gem "string_to_sha1", git: "git://github.com/synbioz/string_to_sha1.git", branch: "develop"
gem "string_to_sha1", git: "git://github.com/synbioz/string_to_sha1.git", tag: "v1.0.0"

Cette possibilité est très intéressante car non seulement vous pouvez avoir accès à tout instant sous forme de gem à la dernière version du code mais vous pouvez aussi préciser une branche, un tag ou une version à utiliser !

Rubygems

L’étape suivante consiste à publier son gem sur Rubygems.org, le dépôt officiel de gems.

Nous allons donc utiliser la tâche rake dédiée pour publier notre gem :

$ rake release

string_to_sha1 1.0.0 built to pkg/string_to_sha1-1.0.0.gem
Tagged v1.0.0
Pushed git commits and tags
Pushed string_to_sha1 1.0.0 to rubygems.org

En une seule commande, notre gem a été généré et publier sur Rubygems. Notre code a également été taggé en v1.0.0 et pushé sur notre dépôt github.

Parfait, exactement ce que nous voulions faire.

Notez que si c’est la première fois que vous publiez sur Rubygems, la commande vous demandera d’entrer vos identifiants.

Sur github, un tag est apparût et notre gem figure maintenant sur Rubygems

Il est désormais possible pour quiconque d’ajouter votre gem à son projet via le Gemfile de manière classique :

gem "string_to_sha1"

Ce sera donc la dernière version disponible sur Rubygems qui sera chargée.

Vous connaissez maintenant tout (ou presque) sur les rouages de la publication de gems, voyons tout de même quelques derniers points trop souvent délaissés.

Documentation du code

N’oubliez pas de commenter votre code pour faciliter sa lecture, sa compréhension et donc les contributions. Vous pouvez documenter votre code via RDoc ou son extension YARD qui permet d’aller plus loin.

Votre code documenté et publié sur Github, vous n’aurez plus à vous soucier de rien, vous aurez accès à la documentation de votre gem auto-générée et disponible sur rubydoc.info.

Si cette dernière n’apparaît pas, vous pouvez (en haut à droite de la page d’accueil) demander une génération de la documentation à partir de votre dépôt git.

Notez que le README est la première page affichée, la barre de gauche vous permettant de naviguer à travers fichiers, classes et méthodes.

Communiquer et buzzer

Maintenant que votre gem a été publié dans les règles de l’art, il est temps de faire connaître votre travail au reste de la communauté.

Deux sites sont absolument incontournables pour vous faire connaître, Ruby Toolbox qui permet de rechercher un gem par catégorie, comparer un gem avec ses alternatives, etc. Il y a ensuite Ruby Flow qui est un site d’agrégation des news relatives à Ruby, un vrai tremplin pour votre gem s’il est différenciant !

En ce qui concerne la communauté francophone, il y a un équivalent Ruby Live

Si vous souhaitez en savoir plus sur la création de gems, l’API de RubyGems.org et plus encore, je vous conseille ces guides.

J’espère donc que cet article de découverte vous encouragera a publier votre travail et à entretenir la force de la communauté Ruby, son ouverture et son sens du partage.

L’équipe Synbioz.

Libres d’être ensemble.