必要最小のgemの作り方とインストール

gemは、Rubyのライブラリ管理のコマンド。Rubyのライブラリを検索・インストール・アップデート・削除など、苦労最小で操作する仕組みを提供してくれる。今までgemを使って、多くの素晴らしいライブラリをインストールしてきた。しかし、自分が作ったものをgemでインストール可能な形式で公開したことはなかった...。
できることならgemでインストールできるようにしてみたい。では、自作のRubyコードは、どうすればgemでインストールできるようになるのか?果たして、簡単にできることなのか?調べてみた。

作業環境

  • Rubyバージョン
$ ruby --version
ruby 2.0.0p481 (2014-05-08 revision 45883) [universal.x86_64-darwin13]
  • Gemバージョン
    • 最新のGemにアップデートしておいた。
$ sudo gem update --system
Password:
Updating rubygems-update
Fetching: rubygems-update-2.4.2.gem (100%)
Successfully installed rubygems-update-2.4.2
Parsing documentation for rubygems-update-2.4.2
Installing ri documentation for rubygems-update-2.4.2
Installing darkfish documentation for rubygems-update-2.4.2
Installing RubyGems 2.4.2
RubyGems 2.4.2 installed
Parsing documentation for rubygems-2.4.2
Installing ri documentation for rubygems-2.4.2
...中略...

$ gem --version
2.4.2

方針

  • 現在Webを検索すると、bundlerを使った方法が多く紹介されていた。
  • その手順を雛形として覚えれば用は足りるのだけど、もっと基本的な所からちゃんと知っておきたい。
  • Ruby1.9以降、gemは標準ライブラリとして添付されるようになった。
  • よって、Rubyのリファレンスマニュアルにその使い方も書かれている。
  • それを読みながら、必要最小の作業を探ってみる。

基本的な流れ

  1. 公開したいrubyコードを書いたファイルを用意する。
  2. libフォルダを作って、クラスやモジュールを定義したファイルをlibフォルダに移動する。
  3. binフォルダを作って、コマンドとして実行したいファイルをbinフォルダに移動する。
  4. .gemspecファイルを用意する。
  5. .gemspecファイルを指定してgem buildする。
  6. .gemファイルが完成する。
  7. .gemファイルと同じ階層でgem installする。

思いっきり要約すると、.gemspecファイルを追加して、gem build、gem installすればいいのだ。

手順1 公開したいrubyコードを書いたファイルを用意
  • ~/Desktop/jcal/に以下のファイルを用意した。
jcal.rb           # 日本のカレンダーを出力するコマンド(実行権限のあるファイル)
jpdate.rb         # Dateを継承したクラス(日本の年号と祝日を出力する機能が追加されている)
jpdate/era.rb     # 日本の年号を返すモジュール
jpdate/holiday.rb # 日本の祝日を返すクラス
手順2 libフォルダを作って、クラスやモジュールを定義したファイルをlibフォルダに移動
手順3 binフォルダを作って、コマンドとして実行したいファイルをbinフォルダに移動
  • lib・binフォルダを作って、それぞれのファイルを移動した。
  • binフォルダに入れるjcal.rbは、拡張子.rbを削除しておいた。
    • コマンド名に拡張子が混ざると入力が面倒なので。
    • binに入れたファイル名がそのままコマンド名となる。
  • lib・binというフォルダ名はデフォルト設定であり、次項のgemspecファイルで変更することもできるが、特に理由がない限りデフォルトのまま使うのが無難。
bin/jcal              # 日本のカレンダーを出力するコマンド(実行権限のあるファイル)
lib/jpdate.rb         # Dateを継承したクラス(日本の年号と祝日を出力する機能が追加されている)
lib/jpdate/era.rb     # 日本の年号を返すモジュール
lib/jpdate/holiday.rb # 日本の祝日を返すクラス
手順4 .gemspecファイルを用意
  • jpdate.gemspecファイルを追加した。
    • 「gemの名前.gemspec」という命名規則になっているようだ。
  • 世界中の開発者が参照することを考えれば、英語で書くべき。
    • ...なのだが、日本の祝日を日本語で出力する仕様なので、日本語使いまくり。
Gem::Specification.new do |s|
  s.name              = 'jpdate' # このgemの名前
  s.version           = '0.0.1'  # このgemのバージョン
  s.summary           = '日本の祝日・年号を返すJPDateクラス' # 一言で説明
  s.description       = '明治6年1月1日 太陽暦以降の日本の祝日・年号をz返すJPDateクラス(Dateを継承)。jcalコマンドでカレンダー出力も可能。' # 詳細な説明
  s.files             = ['bin/jcal', 'lib/jpdate.rb', 'lib/jpdate/era.rb', 'lib/jpdate/holiday.rb'] # このgemにまとめるべきファイル
  s.executables       = ['jcal'] # 実行ファイル名
  s.authors           = ['zariganitosh'] # 作者の名前
  s.email             = 'XXXX@example.com' # 作者のメールアドレス
  s.homepage          = 'https://github.com/zarigani/jcal' # 関連するWebサイトのURL
end
  • 上記.gemspecファイルは、ライブラリ&コマンドをインストールする場合の必要細小な設定になると思う。
    • ライブラリだけの場合(コマンドなし)は、binフォルダ不要、s.executablesの設定不要、となる。
    • コマンドだけの場合(ライブラリなし)は、libフォルダ不要、となる。
bin/jcal              # 日本のカレンダーを出力するコマンド(実行権限のあるファイル)
lib/jpdate.rb         # Dateを継承したクラス(日本の年号と祝日を出力する機能が追加されている)
lib/jpdate/era.rb     # 日本の年号を返すモジュール
lib/jpdate/holiday.rb # 日本の祝日を返すクラス
jpdate.gemspec
手順5 .gemspecファイルを指定してgem build
  • 上記jpdate.gemspecを指定して、gem buildしてみる。
$ cd ~/Desktop/jcal
$ gem build jpdate.gemspec
  Successfully built RubyGem
  Name: jpdate
  Version: 0.0.1
  File: jpdate-0.0.1.gem
手順6 .gemファイルが完成
  • jpdate-0.0.1.gemが作成された!
    • 「gemの名前-gemのバージョン.gem」という命名規則になっているようだ。
bin/jcal              # 日本のカレンダーを出力するコマンド(実行権限のあるファイル)
lib/jpdate.rb         # Dateを継承したクラス(日本の年号と祝日を出力する機能が追加されている)
lib/jpdate/era.rb     # 日本の年号を返すモジュール
lib/jpdate/holiday.rb # 日本の祝日を返すクラス
jpdate.gemspec
jpdate-0.0.1.gem

手順7 .gemと同じ階層でgem install
  • jpdate-0.0.1.gemと同じ階層でgem installすることで、gem管理のライブラリ&コマンドとしてjpdateがインストールされた!
$ cd ~/Desktop/jcal
$ sudo gem install jpdate
Password:
Successfully installed jpdate-0.0.1
Parsing documentation for jpdate-0.0.1
Installing ri documentation for jpdate-0.0.1
1 gem installed
  • あるいは、特定のgemファイルを指定してgem installすることもできる。
$ sudo gem install ~/Desktop/jpdate-0.0.1


必要最小の手順としては、ここまで。

      • 手順8として、gemを公開する手順もメモしておこうと思ったが、
      • gemを作る目的 == 必ずしもgemを公開すること ではないので、
      • gemを公開する手順は、次回以降に書き直してみる。

どこにインストールされたのか?

  • インストールは成功したのだけど、その実体はどこに存在しているのだろう?
$ gem which jpdate
/Library/Ruby/Gems/2.0.0/gems/jpdate-0.0.1/lib/jpdate.rb
  • /Library/Ruby/Gems/2.0.0/gems/jpdate-0.0.1/以下に、.gemspecで指定したファイルが配置されていた。
    • bin/jcal
    • lib/jpdate.rb
    • lib/jpdate/era.rb
    • lib/jpdate/holiday.rb
  • gem env gemhomeで出力されるパスが、デフォルトのインストール場所になるようだ。
  • 自分のgem環境では、/Library/Ruby/Gems/2.0.0 となっていた。
$ gem env gemhome
/Library/Ruby/Gems/2.0.0
  • gem env gempathで出力されるパスが、gemが管理するライブラリの場所。(:区切り)
    • /Users/zari/.gem/ruby/2.0.0
    • /Library/Ruby/Gems/2.0.0
    • /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/gems/2.0.0
$ gem env gempath
/Users/zari/.gem/ruby/2.0.0:/Library/Ruby/Gems/2.0.0:/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/gems/2.0.0
  • gem listなどした時に、上記パスからファイルを見つけようとしてくれる。
  • すべての設定はgem envで確認できる。
$ gem env
RubyGems Environment:
  - RUBYGEMS VERSION: 2.4.2
  - RUBY VERSION: 2.0.0 (2014-05-08 patchlevel 481) [universal.x86_64-darwin13]
  - INSTALLATION DIRECTORY: /Library/Ruby/Gems/2.0.0
  - RUBY EXECUTABLE: /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby
  - EXECUTABLE DIRECTORY: /usr/bin
  - SPEC CACHE DIRECTORY: /Users/zari/.gem/specs
  - SYSTEM CONFIGURATION DIRECTORY: /Library/Ruby/Site
  - RUBYGEMS PLATFORMS:
    - ruby
    - universal-darwin-13
  - GEM PATHS:
     - /Library/Ruby/Gems/2.0.0
     - /Users/zari/.gem/ruby/2.0.0
     - /System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/gems/2.0.0
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :backtrace => false
     - :bulk_threshold => 1000
  - REMOTE SOURCES:
     - https://rubygems.org/
  - SHELL PATH:
     - /usr/bin
     - /bin
     - /usr/sbin
     - /sbin
     - /usr/local/bin
     - /opt/X11/bin
     - /usr/local/git/bin
     - /Applications/Xcode.app/Contents/Developer/usr/bin
     - /usr/local/Cellar/ruby/1.9.3-p194/bin

コマンドの中身

  • gem envでEXECUTABLE DIRECTORY: /usr/bin となっているので、コマンドは/usr/binにインストールされるようだ。
  • 確認してみると、/usr/bin/jcal がちゃんとある。
$ ls /usr/bin/jcal
/usr/bin/jcal*
  • /Library/Ruby/Gems/2.0.0/gems/jpdate-0.0.1/bin/jcalも存在するのだけど、コマンドラインから実行されるのは/usr/bin/jcalのはず。(コマンドサーチパスなので)
  • では、/Library/Ruby/Gems/2.0.0/gems/jpdate-0.0.1/bin/jcalと/usr/bin/jcalは同じものなのだろうか?
  • /usr/bin/jcalを確認してみると、以下のようなrubyコードとなっていた。
#!/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby -W0
#
# This file was generated by RubyGems.
#
# The application 'jpdate' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0"

if ARGV.first
  str = ARGV.first
  str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
  if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
    version = $1
    ARGV.shift
  end
end

gem 'jpdate', version
load Gem.bin_path('jpdate', 'jcal', version)
  • なるほど。/usr/bin/jcalは、/Library/Ruby/Gems/2.0.0/gems/jpdate-X.X.X/bin/jcalをバージョン指定して実行するラッパーになっているようだ。
    • ifブロック内はアンダースコアで囲まれたバージョン表記かどうかを判定している。
      • $1には、正規表現/\A_(.*)_\z/の1番目の括弧()内でマッチした文字が代入されている。
      • つまり、アンダースコアが取り除かれたバージョン表記になっているはず。"_0.0.1_" => "0.0.1"
    • gem 'jpdate', versionは、$LOAD_PATHにjpdate-versionのパスを追加する。
      • requireした時、指定したversionのjpdateが利用されることになるのだ。
    • Gem.bin_path('jpdate', 'jcal', version)は、指定したversionのjpdateのjcalコマンドへのフルパスを返す。
      • つまり、"/Library/Ruby/Gems/2.0.0/gems/jpdate-X.X.X/bin/jcal"
      • そして、jcalコマンドへのフルパスをloadする = jcalコマンドを実行する、ことになるのだ。
  • よって、jcalコマンドの第一引数に_0.0.1_のようなバージョン表記が指定されている場合...
    • 指定されたバージョンのjpdateライブラリを使って、
    • 指定されたバージョンのjcalコマンドを呼び出す。
    • もちろん、バージョン表記に続けてjcalコマンド本来のオプションや引数も指定できる。
$ jcal _0.0.1_ -m 12

そんな、仕組みなのだ。

ドキュメントもインストールされる

  • ところで、gemがインストールするのはライブラリやコマンドだけでなく、同時にドキュメント(クラスやメソッドなどの解説)もインストールしている。
    • ライブラリやコマンドは、gem env gemhomeが出力するパスのgems配下にインストールされる。
    • 一方、ドキュメントは、gem env gemhomeが出力するパスのdoc配下にインストールされる。
$ gem env gemhome
/Library/Ruby/Gems/2.0.0
  • よって、jpdateバージョン0.0.1のドキュメントは、/Library/Ruby/Gems/2.0.0/doc/jpdate-0.0.1にインストールされるのだ。
  • ソースコードやコメントからドキュメントを生成するriやrdocの機能によって、gem install時にドキュメントは自動生成されている。

ドキュメントを見る方法

  • 例えば、JPDateクラスについて知りたい時は...
シェルのコマンドラインから読む(riを読む)
  • riコマンドの引数にクラス名を指定する。
$ ri JPDate


  • ドキュメントを閲覧中は、lessコマンドの操作に準じている。(自分の環境では)
    • q=終了
    • b・space=前ページ・次ページ
    • /キーワード=末尾方向へキーワードを検索する
    • ?キーワード=先頭方向へキーワードを検索する
irb環境から読む(riを読む)
irb(main):001:0> help

Enter the method name you want to look up.
You can use tab to autocomplete.
Enter a blank line to exit.

    
  • ri同様、ドキュメントを閲覧中は、lessコマンドの操作に準じている。
  • helpモードは何も入力せずにreturnで終了する。
Webブラウザで読む(rdocを読む)
  • 以下のコマンドを実行すると、gemサーバーが起動して、Webブラウザがドキュメントのトップページを開いてくれる。
$ gem server -l
  • トップページからリンクを辿って、JPDateクラスのページを開いてみた。
ワンライナー(rdocを読む)
エイリアス登録して、gemserverコマンドで素早く開く
alias gemserver='gem server -l >&/dev/null &'
  • gemserverは、バックグラウンドジョブとして起動する。
  • gemserverを終了する時は...
    • jobsコマンドでjob番号を確認して、
    • killコマンドに%ジョブ番号を指定する。
$ gemserver
[1] 42634

$ jobs
[1]+  Running                 gem server -l >&/dev/null &

$ kill %1
[1]+  Exit 1                  gem server -l >&/dev/null
rdocを作り直して、素早く開く
  • 自分でrdocを$TMPDIRに作り直してみた。
$ gem which jpdate|xargs dirname|xargs rdoc -o ${TMPDIR}doc;open ${TMPDIR}doc/index.html
Parsing sources...
100% [ 4/ 4]  /Library/Ruby/Gems/2.0.0/gems/jpdate-0.0.1/lib/jpdate/version.rb    

Generating Darkfish format into /private/var/folders/vn/kljwl7mj5007q233pcrkrgsc0000gn/T/doc...

Files:       4

Classes:     2 (0 undocumented)
Modules:     1 (0 undocumented)
Constants:   5 (2 undocumented)
Attributes:  0 (0 undocumented)
Methods:     7 (3 undocumented)

Total:      15 (5 undocumented)
 66.67% documented

Elapsed: 0.2s
  • メソッドをクリックすると、ソースコードが表示されるところが好き。
gemspecの詳細な設定項目
  • 設定可能なメソッドを出力してみた。
irb(main):001:0> Gem::Specification.instance_methods.select {|i| i =~ /\w+=$/}.sort.each {|i| p i};nil
:activated=
:author=
:authors=
:autorequire=
:base_dir=
:bindir=
:cert_chain=
:date=
:default_executable=
:description=
:email=
:executable=
:executables=
:extension_dir=
:extensions=
:extra_rdoc_files=
:files=
:full_gem_path=
:has_rdoc=
:homepage=
:ignored=
:installed_by_version=
:license=
:licenses=
:loaded_from=
:metadata=
:name=
:original_platform=
:platform=
:post_install_message=
:rdoc_options=
:require_path=
:require_paths=
:required_ruby_version=
:required_rubygems_version=
:requirements=
:rubyforge_project=
:rubygems_version=
:signing_key=
:specification_version=
:summary=
:test_file=
:test_files=
:version=
  • デフォルト値を出力してみた。
irb(main):001:0> Gem::Specification.new do |s|
irb(main):002:1*   Gem::Specification.instance_methods.grep(/\w+=$/).sort.each do |i|
irb(main):003:2*     default = s.send(i[0..-2]) rescue nil
irb(main):004:2>     next if [nil, [], {}].include?(default)
irb(main):005:2>     print i, " "
irb(main):006:2>     p default
irb(main):007:2>   end
irb(main):008:1> end

activated= false
base_dir= "/Library/Ruby/Gems/2.0.0"
bindir= "bin"
date= 2014-10-16 00:00:00 UTC
extension_dir= "/Library/Ruby/Gems/2.0.0/extensions/universal-darwin-13/2.0.0/-"
full_gem_path= "/Library/Ruby/Gems/2.0.0/gems/-"
has_rdoc= true
installed_by_version= #
original_platform= "ruby"
platform= "ruby"
require_path= "lib"
require_paths= ["lib"]
required_ruby_version= #=", #]]>
required_rubygems_version= #=", #]]>
rubygems_version= "2.4.2"
specification_version= 4