gem パッケージを作って公開する ~ 更新 公開停止まで ~

僕らは、誰かの作ってくれた gem の恩恵を日頃から受け続けています。
gem を作れるようになって誰かに貢献したい。

参考

実行環境

Docker で環境を用意しています。

  • amazonlinux:latest
    • Ruby 3.0.0(rbenv で導入)

とりあえず インストールできるようになるまで

bundler をインストールして、以下コマンドと選択でひな形を作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ gem install bundler

$ bundle gem test_gem

Enter a test framework. rspec/minitest/test-unit/(none): # 今回はそのままEnter
Enter a CI service. github/travis/gitlab/circle/(none): # 今回はそのままEnter

Do you want to license your code permissively under the MIT license?
# ~~省略~~
You can read more about the MIT license at https://choosealicense.com/licenses/mit. y/(n): y

Do you want to include a code of conduct in gems you generate?
# ~~省略~~
For suggestions about how to enforce codes of conduct, see https://bit.ly/coc-enforcement. y/(n): y

Do you want to include a changelog?
# ~~省略~~
see https://keepachangelog.com y/(n): y


Do you want to add rubocop as a dependency for gems you generate?
# ~~省略~~
For more information, see the RuboCop docs (https://docs.rubocop.org/en/stable/) and the Ruby Style Guides (https://github.com/rubocop-hq/ruby-style-guide). y/(n): y

gem のひな形は以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ tree test_gem/
test_gem/
|-- CHANGELOG.md
|-- CODE_OF_CONDUCT.md
|-- Gemfile
|-- LICENSE.txt
|-- README.md
|-- Rakefile
|-- bin
| |-- console
| `-- setup
|-- lib
| |-- test_gem
| | `-- version.rb
| `-- test_gem.rb
`-- test_gem.gemspec

編集前の test_gem.rb は、以下のようになっています。

test_gem/lib/test_gem.rb[修正前]
1
2
3
4
5
6
7
8
# frozen_string_literal: true

require_relative "test_gem/version"

module TestGem
class Error < StandardError; end
# Your code goes here...
end

編集して以下のように直します。

test_gem/lib/test_gem.rb[修正後]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# frozen_string_literal: true

require_relative "test_gem/version"

module TestGem
class Error < StandardError; end
# Your code goes here...


class TestGemClass
attr_reader :name

def initialize(name)
@name = name
end

def call
puts "Hey! #{@name}"
end
end
end

また、test_gem.gemspec の変更が必須です。

test_gem/test_gem.gemspec[修正前]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# frozen_string_literal: true

require_relative "lib/test_gem/version"

Gem::Specification.new do |spec|
spec.name = "test_gem"
spec.version = TestGem::VERSION
spec.authors = ["TODO: Write your name"]
spec.email = ["TODO: Write your email address"]

spec.summary = "TODO: Write a short summary, because RubyGems requires one."
spec.description = "TODO: Write a longer description or delete this line."
spec.homepage = "TODO: Put your gem's website or public repo URL here."
spec.license = "MIT"
spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")

spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"

spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."

# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path(__dir__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

# Uncomment to register a new dependency of your gem
# spec.add_dependency "example-gem", "~> 1.0"

# For more information and examples about making a new gem, checkout our
# guide at: https://bundler.io/guides/creating_gem.html
end

TODO などの記載が残っていると、ビルド時にエラーとなるので、一旦適当に書き換えます。
(本来は、内容を検討して記述すべきものです!)

test_gem/test_gem.gemspec[修正後]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# frozen_string_literal: true

require_relative "lib/test_gem/version"

Gem::Specification.new do |spec|
spec.name = "test_gem"
spec.version = TestGem::VERSION
spec.authors = ["Write your name"]
spec.email = ["Write your email address"]

spec.summary = "Write a short summary, because RubyGems requires one."
spec.description = "Write a longer description or delete this line."
spec.homepage = "https://ccbaxy.xyz"
spec.license = "MIT"
spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")

spec.metadata["allowed_push_host"] = "Set to 'http://mygemserver.com'"

spec.metadata["homepage_uri"] = "https://ccbaxy.xyz"
spec.metadata["source_code_uri"] = "https://ccbaxy.xyz"
spec.metadata["changelog_uri"] = "https://ccbaxy.xyz"

# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path(__dir__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

# Uncomment to register a new dependency of your gem
# spec.add_dependency "example-gem", "~> 1.0"

# For more information and examples about making a new gem, checkout our
# guide at: https://bundler.io/guides/creating_gem.html
end

ビルドしてインストールします。

1
2
3
4
5
6
7
8
9
10
cd test_gem
rake build
test_gem 0.1.0 built to pkg/test_gem-0.1.0.gem.

cd ..
gem install test_gem/pkg/test_gem-0.1.0.gem

# もしくは、Gemfileに
# gem "test_gem",:path => 'test_gem'
# を書いて bundle install

irb を起動して、test_gem を呼び出してみます。

1
2
3
4
5
6
7
irb(main):001:0> require 'test_gem'
=> true
irb(main):002:0> t =TestGem::TestGemClass.new('hoge')
=> #<TestGem::TestGemClass:0x0000000002938c38 @name="hoge">
irb(main):003:0> t.call
Hey! hoge
=> nil

Gem に定義したメソッドを呼び出すことができました。

名前空間がちょっと邪魔だなぁ

gem のひな形は、module になっています。
普段目にする gem の多くは、直接クラスとして公開されているものが多いと感じます。
(自分の観測範囲では。)

直接クラスとして公開される gem test_gem_2 を作ってみます。

test_gem_2/lib/test_gem_2.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# frozen_string_literal: true

require_relative "test_gem_2/version"

class TestGem2
attr_reader :name

def initialize(name)
@name = name
end

def call
puts "Hey! #{@name}"
end
end

併せて、version.rbmodule の記述を class に修正が必要です。

test_gem_2/lib/test_gem_2/version.rb
1
2
3
4
5
6
# frozen_string_literal: true

# module を class に書き換える
class TestGem2
VERSION = "0.1.0"
end

ビルドしてインストールします。

1
2
3
4
5
6
7
8
9
$ cd test_gem_2/
$ rake build
$ cd ..

# Gemfileに
# gem "test_gem_2",:path => 'test_gem_2'
# を記述

$ bundle install

irb を起動して、test_gem_2 を呼び出してみます。

1
2
3
4
5
6
7
irb(main):001:0> require 'test_gem_2'
=> true
irb(main):002:0> t =TestGem2.new('hoge')
=> #<TestGem2:0x000000000222fc70 @name="hoge">
irb(main):003:0> t.call
Hey! hoge
=> nil

直接クラスを呼び出せる gem を作れました。

gem を Rubygems で公開する

アカウント登録

Rubygems - 新規登録 でアカウント作成します。

トークン作成

Rubygems - Edit settings で、API キーを取得します。

権限は、Push rubygemYank rubygem だけにしておきます。

credentials の取得

トークンの記載されたファイルを作成します。
生で書いても良さそうなものですが、RubyGems - Guide - MAKE YOUR OWN GEMに倣って取得してみます。

1
2
3
4
5
$ mkdir .gem
$ curl -u [ユーザー名] https://rubygems.org/api/v1/api_key.yaml > .gem/credentials;
Enter host password for user '[ユーザー名]': # Rubygemsのログインパスワードを入力します。

chmod 0600 .gem/credentials

credentials の中身は以下のようになっています。書いているのは発行したものと同じ API トークンです。

.gem/credentials
1
2
3
---
:rubygems_api_key: rubygems_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
:status: :ok

gemspec の記載修正

test_gem/test_gem_2.gemspec を消しておきます。
Rubygems 以外へプッシュするための設定だそうです。以下の記述を削除します。

test_gem_2/test_gem_2.gemspec
1
spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"

Rubygems にプッシュ

Rubygems に push したいだけですが、git での管理と、github への push が必要でした。
一旦 github での管理登録をします。

1
2
3
4
5
6
7
# .gitignore に
# .gem/
# を書き足し、トークンをgit管理外に外して
# 一旦コミット・(任意のリポジトリに)プッシュします。
git add -A
git commit -m "first commit"
git push

それでは、Rubygems にプッシュします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ rake release
Username for 'https://github.com': [githubのユーザー名]
Password for 'https:// [githubのユーザー名]@github.com': [githubのユーザーのパスワード]

test_gem_2 0.1.0 built to pkg/test_gem_2-0.1.0.gem.
Tag v0.1.0 has already been created.
Enter your RubyGems.org credentials.
Don't have an account yet? Create one at https://rubygems.org/sign_up
Email: [rubygemsに登録しているアカウントのメールアドレス]
Password: [rubygemsに登録しているアカウントのパスワード]

Signed in with API key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Pushing gem to https://rubygems.org...
Successfully registered gem: test_gem_2 (0.1.0)
Pushed test_gem_2 0.1.0 to rubygems.org

Rubygems にプッシュできたようです。確認してみます。

Rubygems のプロフィールページを確認すると登録されているのがわかります。

Rubygems から取得して gem を使ってみる

一旦ローカルでインストールしたtest_gem_2をアンインストールします。

1
2
3
4
5
$ gem list test_gem_2

*** LOCAL GEMS ***

gem uninstall test_gem_2

Gemfile の記載を修正して取得先をローカルからデフォルトにします。

Gemfile[修正]
1
2
#gem "test_gem_2",:path => 'test_gem_2'
gem "test_gem_2"

改めてインストールします。

1
2
3
4
5
6
$ bundle install
$ gem list test_gem_2

*** LOCAL GEMS ***

test_gem_2 (0.1.0)

インストール済み gem に公開した test_gem_2 が表示されました。
irb を起動して確認します。

1
2
3
4
5
6
7
irb(main):001:0> require 'test_gem_2'
=> true
irb(main):002:0> t =TestGem2.new('hoge')
=> #<TestGem2:0x0000000001221e28 @name="hoge">
irb(main):003:0> t.call
Hey! hoge
=> nil

自分で作成した gem を Rubygems にプッシュし取得、使用できました。

Rubygems にプッシュした gem の更新

内容を更新した gem を更新するには、test_gem_2/lib/test_gem_2/version.rb を更新の必要がありました。

test_gem_2/lib/test_gem_2/version.rb
1
2
3
4
5
6
# frozen_string_literal: true

class TestGem2
# バージョンを更新
VERSION = "0.1.1"
end

リリースのコマンドは更新でも同じ。

1
2
3
4
5
6
7
$ bundle info test_gem_2
* test_gem_2 (0.1.1)
Summary: Write a short summary, because RubyGems requires one.
Homepage: https://github.com/Octo8080X/test_gem_2
Path: /usr/src/app/test_gem_2

$ rake release

旧バージョンを削除

Gemfile に gem "test_gem_2", "0.1.0"のように指定すると、古いバージョンを取得できます。
古いバージョンは取得させたくない!ということもあるでしょう。
旧バージョンを配信されないようにしてみます。

1
2
3
4
5
6
7
$ gem yank test_gem_2 -v 0.1.0
Yanking gem from https://rubygems.org...
The existing key doesn't have access of yank_rubygem on RubyGems.org. Please sign in to update access.
Email: [Rubygemsのアカウントのメールアドレス]
Password: [Rubygemsのアカウントのパスワード]
Added yank_rubygem scope to the existing API key
Successfully deleted gem: test_gem_2 (0.1.0)

バージョン履歴には、更新後の 0.1.1 だけが表示されています。

全バージョンを表示すると、0.1.0 は yanked と書かれています。

この状態で、再度インストールすると次のようにエラーになります。

1
2
3
4
5
6
7
8
$ bundle install
Don't run Bundler as root. Bundler can ask for sudo if it is needed, and installing your bundle as root will break this
application for all non-root users on this machine.
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Your bundle is locked to test_gem_2 (0.1.0) from rubygems repository https://rubygems.org/ or installed locally, but
that version can no longer be found in that source. That means the author of test_gem_2 (0.1.0) has removed it. You'll
need to update your bundle to a version other than test_gem_2 (0.1.0) that hasn't been removed in order to install.

以下のように書き換えてバージョン指定を外します。

Gemfile
1
gem "test_gem_2"

改めてインストールすると、公開された最新版、0.1.1 を取得できます。

1
2
3
4
5
6
$ bundle install
$ gem list test_gem_2

*** LOCAL GEMS ***

test_gem_2 (0.1.1)

今回は、gem を自作し、Rubygems で公開・更新・旧バージョンの公開停止をしてみました。
github へのプッシュが必須であったり手間は比較的多いですが、公開自体はかなり簡単でした。
有益な gem を作って公開してみたいですね。

ではでは。