gemのリポジトリにGemfile.lock をコミットするか、gitignoreするか

TL;DR

2017-07-20 から、bundle gem my-awesome-gem の生成物で、Gemfile.lockをgit ignoreしなくなった。
Stop gitignoring Gemfile.lock in default template #5822

なんでgit ignoreしていたのか、なぜするべきなのか、どうしてしなくなったのか、あたりを整理する。
なおわたしはignoreしているほうがいいとおもってる。

きっかけ

わたしがこう反応したら、

そりゃgemの開発者は無いと困るけどさ 利用者のが圧倒的に多いじゃん
理解できねー

こういう反応を複数もらったので。

あれ、利用者がgemfile.lockあってなんか困ります?

直接それが原因では困らないが、実質困るケースが増えると思う。

環境

$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]

$ bundle --version
Bundler version 1.16.2

$ bundle gem test-pack
(snip)

$ cat .gitignore
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

定説的なやつ

たとえばrailsアプリなど、アプリケーションはGemfile.lockをコミットすべき。
gemはGemfile.lockをコミットしないべき。

どっかできいたことあるはず。んで、よく引き合いに出されるのがClarifying the Roles of the .gemspec and Gemfile

日本語訳はgemspecとGemfileの役割をはっきりさせておく
許可取ってるか取ってないかわからない(許可取ってたらごめんね)翻訳ですが、これを勝手に許可無く掘り起こしてる人がいる。

今読み返すと、それコミットしてもいいでしょ、って言えるぐらいだなー。なので掘り下げる。

gem installやbundle installでrubygems.org 及びそのクローンの場合

metadataをみてるはず(要出典)。

metadataってなんだよ

$ bundle exec rake build

test-pack-0.1.0.gemができる
中身は
checksums.yaml.gz
data.tar.gz
metadata.gz

metadataの中身はこう。

--- !ruby/object:Gem::Specification
name: test-pack
version: !ruby/object:Gem::Version
  version: 0.1.0
platform: ruby
authors:
- Sanemat
autorequire:
bindir: exe
cert_chain: []
date: 2018-07-22 00:00:00.000000000 Z
dependencies:
- !ruby/object:Gem::Dependency
  name: bundler
  requirement: !ruby/object:Gem::Requirement
    requirements:
    - - ">="
      - !ruby/object:Gem::Version
        version: '0'
  type: :development
  prerelease: false
  version_requirements: !ruby/object:Gem::Requirement
    requirements:
    - - ">="
      - !ruby/object:Gem::Version
        version: '0'
- !ruby/object:Gem::Dependency
  name: rake
  requirement: !ruby/object:Gem::Requirement
    requirements:
    - - ">="
      - !ruby/object:Gem::Version
        version: '0'
  type: :development
  prerelease: false
  version_requirements: !ruby/object:Gem::Requirement
    requirements:
    - - ">="
      - !ruby/object:Gem::Version
        version: '0'
description: fix me
email:
- o.gata.ken@gmail.com
executables: []
extensions: []
extra_rdoc_files: []
files:
- ".gitignore"
- CODE_OF_CONDUCT.md
- Gemfile
- Gemfile.lock
- LICENSE.txt
- README.md
- Rakefile
- bin/console
- bin/setup
- lib/test/pack.rb
- lib/test/pack/version.rb
- test-pack.gemspec
homepage: http://example.com
licenses:
- MIT
metadata:
  allowed_push_host: 'TODO: Set to ''http://mygemserver.com'''
post_install_message:
rdoc_options: []
require_paths:
- lib
required_ruby_version: !ruby/object:Gem::Requirement
  requirements:
  - - ">="
    - !ruby/object:Gem::Version
      version: '0'
required_rubygems_version: !ruby/object:Gem::Requirement
  requirements:
  - - ">="
    - !ruby/object:Gem::Version
      version: '0'
requirements: []
rubyforge_project:
rubygems_version: 2.7.6
signing_key:
specification_version: 4
summary: fix me
test_files: []

rubyとかgit ls-filesとかもう残ってない。
そして、bundle exec rake buildの場合、my-awsome-gem.gemspecの中身だけ反映されている。
Gemfileにだけ書いたものはmetadataに入ってない。

与太話: Gemfile 消したら bundle exec rake build 動かなかった :thinking:

Gemfile, Gemfile.lock

これがbundlerの独自ファイルなのは、忘れてるけど言われりゃそうだろうなと思う。

my-awsome-gem.gemspec

これはbundlerの独自ファイルだっけ? 実態として作らなきゃいけないかは定かではない。
たとえばいまはseattlerbぐらいでしか使われていないhoeの場合、gemspecファイルがリポジトリにないが、動的に作っているのか、何だったか忘れた(要出典)
使用例: https://github.com/seattlerb/ruby_parser

つまり

metadataに反映されるかされないかだけで、どうでもいい、とも言える

bundle installでgit repositoryをsourceにする場合

リポジトリのGemfileを見に行くんじゃない? (要出典)
このときlockみてるのかなあ 見てないんじゃないの(要出典)

gem install, bundle installのしくみ

このmetadataをかき集めて、それを満たす最大のバージョンをインストールする。(要出典)

前半ここまで。

Gemfile.lock

アプリケーションを作っている時に、Gemfile.lockなかったら、バージョン固定できなくてつらい。
これは説明いいよね。

gemとGemfile.lock

ここは思想の違い。
lockがあれば、CIで固定のバージョンで通ったことを確認できる。
この組み合わせだと動くよ、ってテストの範囲では言うことができる。

ただ、それってなんの意味があるの、利用者の手元でgem install, bundle installするときに、
なんの関係もないじゃん、って思ってしまう。利用者ではmetadata見るだけなので。

lockがない場合、たとえば新しいcontributorがバグフィックスしたり、新しいフィーチャー持ってくるときに、pull requestを送ったら依存でぶっ壊れてテストが真っ赤というのがよくある。

Over time, however, it became clear that this practice forces the pain of broken dependencies onto new contributors, while leaving existing contributors potentially unaware of the problem.
https://github.com/bundler/bundler/issues/5879#issue-244213907

大体の場合、new contributorsは、自分のfixやfeature入れる前に、もうぶっ壊れてることを報告して、何なら自分で直したりして、test greenにしてから、自分の変更を入れる必要がある。
new contributorsはテンション下がるよね。
まあそれはそうなんだけど、それはもうぶっ壊れてるんだよね。

つまりGemfile.lockをコミットすることで、最新バージョンで動かす責任をnew contributorsからほかのcomittersに移したと言える。
新しい人が開発しやすくなってよかったね。gemとしてリリースするものが動くかどうかはcommiterの責任ですよ。
めでたしめでたし。

なわけないじゃん。。利用者がinstallしようとするときの組み合わせではない古いlockの組み合わせで確認オッケーリリースって事故しか見えない。

そんな事故起こりうるの?って疑問には、だってそれがpainだからゆうてるやん、ってことで。

じゃあどうやって保証するのというと、保証なんてできないので、lockをignoreしておいて、定期的にciを動かしてfailしたら直し続けるしかない。
lockありのテストとlock消してbundle/gem install してテスト、の両方やるでもいいよ。

同じ話の言い換えで、ぶっ壊れてることに気づきづらくなるというのもある。Gemfile.lockを消したpull requestを定期的に送るのかな。

利用者が困る

そりゃgemの開発者は無いと困るけどさ 利用者のが圧倒的に多いじゃん
理解できねー

ぶっ壊れgemがよりリリースされやすくなって利用者が困るの説明でした。

float と big decimalをなんとなくベンチした

rubyで
3.14 * 10
=> 31.400000000000002
ってなるよなーって思ってbig decimalベンチしてみた。

手元のthinkpad x1 carbon 2016 にubuntu 16.04

$ ruby -v
ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-linux]

$ cat float-vs-big-decimal.rb
require 'bigdecimal'
require 'benchmark'

n = 1000000
Benchmark.bm do |r|
  r.report "float" do
    n.times do
      3.14 * 10
    end
  end
  r.report "big decimal" do
    n.times do
      BigDecimal("3.14") * BigDecimal("10")
    end
  end
end
(rc->0)
$ ruby float-vs-big-decimal.rb 
       user     system      total        real
float  0.060000   0.000000   0.060000 (  0.052816)
big decimal  0.960000   0.000000   0.960000 (  0.967326)

生成コストでフェアじゃない?気もするけど 最後人間に読めるようにto_f するとか細かいこと気にするなってことで。16倍、思ったほどではなかった もうbigdecimal常用でいいのかな。もちろん用途によるんだけど。
キャッシュするとかベンチになってないとかあるかも。あんまりわかってないから。

$ ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]
$ ruby float-vs-big-decimal.rb                                                  
       user     system      total        real
float  0.060000   0.000000   0.060000 (  0.055150)
big decimal  1.020000   0.000000   1.020000 (  1.020329)

手元のrubyははじめ謎のバージョンで止まってたままだった。けど2.4にあげたら20倍が16倍になってなるほどはやくなってるんだ。

あとBenchmark.bm の後ろの数字繰り返し回数かと思って Benchmark.bm 1000000 do |r| ってやってすごい文字数空白出てきた。はてなマーク出しながら10回ぐらいやっておかしくねって気づいた。ラベルの幅て。

Update dependencies

json gemが1.xだとruby2.4で動かなくて、2.xだとruby1.9で動かない。
みたいなので結構おっくうになってたのだけど、ちょっとずつバージョンを上げ始めた。

http://rubies.travis-ci.org/ みながらここにあげる

- 2.0.0
- 2.1.10
- 2.2.6
- 2.3.3
- 2.4.0

今上げてるのが、nodejsにかぶれたあとのライブラリ群なので、まだそんなに苦労はしていない。まだ。

いまのところで引っかかるのは、
– rubocopがどこかのバージョンから1.9サポートをdropしている
– public_suffixがどこかのバージョンから1.9サポートをdropしている

このへん。
rubocopはgemspecではなくGemfileで入れてることが多いので、gem 'rubocop' if RUBY_VERSION >= 2.0.0みたいにした。
octokit -> sawyer -> addressable -> public_suffix
を使っているライブラリはもうどうしようもないので、1.9サポートをdropした。

1.9なんてもういいだろって思うけど、travis-ciって便利サイトがデフォルトruby1.9で、他の言語で使って欲しいツール・ライブラリでは1.9もサポートしたいのよね。出来る限り。
で、出来ない限りになってしまったので、1.9サポートdropはしかたない。
あとはmac os xっていう便利プラットフォームもsystem rubyが2.0とかいう化石使ってるので、2.0サポートも出来る限りしたい。
こういう小さいツール・ライブラリを作るのにrubyを使うのは向かない、ってことになってしまうのがなんともはや。
golangでやろう、って方向へいきますね。golangまだ全然思うように書けないけど。

Rubyのハテナってbool返すんじゃないの?

rubyのNumeric#nonzero?ってself | nilを返すんだ。ハテナってbool返すんじゃないの?
って聞かれた。
自分もこの前までそう思ってたんだけど、たしかそうじゃない便利ケースがAPIデザインケーススタディに出てきた、
ってうろ覚え知識を披露した。なにの時便利だったかは覚えてなかったので、ふわふわ。
なので調べた。

instance method Numeric#nonzero? (Ruby 2.3.0) http://docs.ruby-lang.org/ja/2.3.0/method/Numeric/i/nonzero=3f.html

Rubyではnilとfalseが偽、それ以外の値は全て真とみなされるので、このInteger#nonzero?の振る舞いは、0に対して偽、
それ以外に対して真を返すものと言えます。つまり名前に反する動作というわけではありません。
(snip)
意外な動作は学習コストを上げるので、理由がなければ避けるべき
(snip)
Enumerable#sort メソッドに与えるブロックの中の記述を簡単にするため。
APIデザインケーススタディ 田中哲

enum.sort {|a, b| c = a.x <=> b.x; c != 0 ? c : a.y <=> b.y }
が
enum.sort {|a, b| (a.x <=> b.x).nonzero? || a.y <=> b.y }

と記述できる。

具体的には自分でハテナつきメソッドを書くとき、気を使うときは、
わざわざtrue/false を返すように、最後絶対bool返すぞ!って foo ? true : falseって書くことはある。
どこかで何かを読んで、しかも最近、必ずしもそうしなくて良くて、
truthy/falsy 返すだけで名前に反するわけではない、と何かで読んだ。
そのなにかがAPIデザインケーススタディだった。

別のものを返すことで使いやすいケースだったりそういう使い方しかないケースでは、bool以外が返ることがある。
だったはずだから、ドキュメントにあるはず。

documentみていく。
ObjectやEnumerableにはbool返すのしか生えてなかった。

Numeric#nonzero? -> self | nil
Encoding.compatible?(obj1, obj2) -> Encoding | nil
Kernel.#autoload?(const_name) -> String | nil

だんだん見ていくとだいたいboolなんだけど、いくつかboolじゃないものもあった。
上記は全部ではなく、ぱっと見て出てきたもの。

ほぼだいたいboolだがそうでもないのもいるから、truthy/falsyで判別すればびっくりしなそう。

APIデザインケーススタディ読んだ

ようやく積んでた本を読んだ。読んだというか、途中まで読んでてきつくなったのでパラパラ眺めてどうにか一周終わらせた、ってかんじ。話の内容は多分面白くて書いてある中身もきっと面白いんだけど、対象に今の自分があまり興味がなかったから仕方ない。

APIデザインケーススタディ ――Rubyの実例から学ぶ。問題に即したデザインと普遍の考え方 | Gihyo Digital Publishing … 技術評論社の電子書籍

第1章─I/O, 第2章─ソケット, 第3章─プロセス, 第4章─時刻, 第5章─数,文字列 とあって、要は自分が一番好きなプレゼンの使いやすいライブラリ API デザイン. 日本Rubyカンファレンス2006 コレと近い内容なんだけど、I/O, ソケット、プロセス、時刻まであんまり興味がないので、その、あれ。

URI.encode_www_form出てきてやっと興味あるとこキタ! と思ったらそれが最後でしょんぼり。

なので、あとは思い出したことを並べる。

使いやすいライブラリAPIデザインから、自分は強烈な印象を受けていて、過去の自分の名作 https://github.com/sanemat/slip/blob/b49f8a609741c29054cacf0fe84a2a6d377bba53/slip.rb#L14 に色濃く出ている。

脱線すると、httpライブラリの好みの変遷としては、何使ってたか覚えていない -> 感銘を受けて open-uriに -> httparty で楽して楽しく -> REST厨なのでrest-client -> faraday でインターフェースを統一するんだ -> net-http やっぱり標準ライブラリだね、あとeasy, simpleだとsimpleよりのがいい イマココ

コレも好き。matz を説得する方法. RubyKaigi2008

さがしてまわっちゃったけどここにあった。Publications of Tanaka Akira

正の整数の文字列か判定するNaturalNumberString gem書いた

正の整数の文字列か判定するNaturalNumberString gem書いた。

APIはこんな感じです。

NaturalNumberString.positive_integer_string?(value) -&gt; boolean
NaturalNumberString.zero_or_positive_integer_string?(value) -&gt; boolean

詳細: yard docs

環境変数を取得してあれこれするライブラリを書いている。

if ENV['FOO']
  # do something
end

これでいいかと思ったら、あるCIサービスでは、falsyなときにENV['FOO']がないのだが、別のCIサービスでは、falsyなときはENV['FOO'] == ''なことがある。

if ENV['FOO'] &amp;&amp; !ENV['FOO'].empty?
  # do something
end

これでいいかと思ったら、

TRAVIS_PULL_REQUEST the pull request number if the current job is a pull request, or “false” if it’s not a pull request.
http://docs.travis-ci.com/user/environment-variables/#Convenience-Variables

文字列’false’… いや文字列しか使えないから仕方ないのは知ってるけど。なのでカッとなって書いた。

NaturalNumberString.positive_integer_string?('1') #=&gt; true
NaturalNumberString.positive_integer_string?('100000000000') #=&gt; true

NaturalNumberString.positive_integer_string?(nil) #=&gt; false
NaturalNumberString.positive_integer_string?('') #=&gt; false
NaturalNumberString.positive_integer_string?(1) #=&gt; false
NaturalNumberString.positive_integer_string?('1.1') #=&gt; false
NaturalNumberString.positive_integer_string?('-1') #=&gt; false
NaturalNumberString.positive_integer_string?('0') #=&gt; false

NaturalNumberString.zero_or_positive_integer_string?('0') #=&gt; true

文字列’0’の扱いだけ変えるmethodもある。

なおnodejsでも似たような発想のことやってるの前に作った。sanemat/node-boolify-string

*.gemspec ファイルをいい感じにparseするgem書いた

うっとなったので *.gemspec ファイルをいい感じにRuby Hash objectにするgem parse_gemspec と、
*.gemspec ファイルをいい感じにJSON にする CLIツールのgem parse_gemspec-cli かいた。

それぞれこう使う。

Ruby

require 'parse_gemspec'
require 'pp'

pp ParseGemspec::Specification.load('parse_gemspec.gemspec').to_hash_object
{:name=&gt;"parse_gemspec",
 :version=&gt;"0.5.0",
 :authors=&gt;["sanemat"],
 :description=&gt;"Parse *.gemspec file. Convert to Ruby Hash object.",
 :email=&gt;["o.gata.ken@gmail.com"],
 :homepage=&gt;"https://github.com/packsaddle/ruby-parse_gemspec",
 :licenses=&gt;["MIT"],
 :metadata=&gt;{},
 :summary=&gt;"Parse *.gemspec file. Convert to Ruby Hash object."}

CLI

$ parse-gemspec-cli parse_gemspec.gemspec | jq .
{
  "name": "parse_gemspec",
  "version": "0.5.0",
  "authors": [
    "sanemat"
  ],
  "description": "Parse *.gemspec file. Convert to Ruby Hash object.",
  "email": [
    "o.gata.ken@gmail.com"
  ],
  "homepage": "https://github.com/packsaddle/ruby-parse_gemspec",
  "licenses": [
    "MIT"
  ],
  "metadata": {},
  "summary": "Parse *.gemspec file. Convert to Ruby Hash object."
}

filesとdependenciesが未対応。

filesは最近のだとgit ls-files使ってる場合が多いので、どうしようかな。いい方法が思いつかない。
specification: files, many gems use git ls-files -z

dependenciesはPlain Old Ruby Objectでどう表現しようかな、json表現どうしようかな、というところで止めてる。
specification: dependencies’ PORO format and json format

具体的な利用方法はこんな感じ。
conventional-changelog(npm)をRuby pruductから使う | 實松アウトプット

conventional-changelog(npm)をRuby pruductから使う

conventional-changelogにactiveなメンテナーが増えて、v0.1.0に上がって以降(2015-09-25 時点でv0.4.3)だいぶ使いやすくなっている。
Ruby productから使う時の使い方をまとめた。

下記エントリーの続き。

bin/changelogを作って使う、と以前書いていたけど、設定ファイルだけ書いて、あとはコマンドラインのconventional-changelogから使えるようになった。実際に使っている例 parse_gemspec-cli

具体的にはこんなpackage.json書いて、npm run changelogする。

{
  "devDependencies": {
    "conventional-changelog": "0.4.3",
    "urijs": "^1.16.1"
  },
  "scripts": {
    "changelog": "conventional-changelog -i changelog.md --overwrite --preset angular --context .conventional-changelog.context.js"
  }
}

contextの指定で、無指定だとpackage.json読んでバージョンやリポジトリのurl取得するところを、Rubyプロダクト用に書き換えてやれば良い。
設定すべき値はconventional-changelog-writer のオプション。これを.conventional-changelog.context.jsにかく。ファイル名はどうでもいい。

'use strict';
var execSync = require('child_process').execSync;
var URI = require('urijs');

var gemspec = JSON.parse(execSync('bundle exec parse-gemspec-cli parse_gemspec-cli.gemspec'));
var homepageUrl = gemspec.homepage;
var url = new URI(homepageUrl);
var host = url.protocol() + '://' + url.authority();
var owner = url.pathname().split('/')[1];
var repository = url.pathname().split('/')[2];

module.exports = {
  version: gemspec.version,
  host: host,
  owner: owner,
  repository: repository
};

gemspec を読んで json を吐き出す parse_gemspec-cli gemを作った。パッケージはparse_gemspec-cli、コマンドは parse-gemspec-cli。今のところ name, version, homepage しか取り出せないけど、changelogの用途にはこれで良い。

これでgemを書くときにもchangelog環境が快適になった。

$ conventional-changelog --help
Options
(snip)
    -c, --context             A filepath of a javascript that is used to define template variables
    --git-raw-commits-opts    A filepath of a javascript that is used to define git-raw-commits options
    --parser-opts             A filepath of a javascript that is used to define conventional-commits-parser options
    --writer-opts             A filepath of a javascript that is used to define conventional-changelog-writer options

conventional-changelogのコマンドが、細かくmoduleに分割したあとにも、使うmoduleの設定書いたうえでCLIツールからそのまま使えるの、よく出来ている。

changelogとrubygem

changelog, nodeのmoduleを書くときはajoslin/conventional-changelogを使う。ただ、version bumpを誰がするのか、開発中は v1.2.3.beta とbetaとか付けるか、pushするのは誰か、releaseするのは誰か、がそんなに自明でないので、ややこしい。

Perlの場合

perlの人に言うと、「それMinillaで」と言われる。チュートリアル眺める限り、良さそうに見える、があんまり深入りしない。

Minilla::Tutorial – Minilla チュートリアルドキュメント – perldoc.jp

Rubyの場合

releaseはbundler付属の bundle exec rake release使うのが標準(だと思う)。

pull_request-create 0.1.0 built to pkg/pull_request-create-0.1.0.gem.
Tagged v0.1.0.
Pushed git commits and tags.
Pushed pull_request-create 0.1.0 to rubygems.org.

packageをbuildして、git tagして、commitとtagを(githubに)pushして、packageをrubygems.orgにpushする。

なので、changelogをどうにかして作る、手動でversionをbumpして、commit, rake releaseとなる。

いくつか見たchange logツールが、既に存在するtagに対して(あるいはgithubにpush済みのtagに対して)、処理するので、しっくりこない。

Git tagとGitHub ReleasesとCHANGELOG.mdの自動化について | Web Scratch
https://github.com/skywinder/Github-Changelog-Generator/wiki/Alternatives
試してみる。

Edited: 2015-03-21 16:44
conventional-changelogがrubyから使いづらいのでどうにかしたい | 實松アウトプット
続き書いた