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がよりリリースされやすくなって利用者が困るの説明でした。

広告

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まだ全然思うように書けないけど。

1個のライブラリにまとめたくなる呪縛

rubyのgemは1個のgemがやろうとすることが多すぎる。npm moduleは1メソッドが1packageぐらいの粒度。だから自分はnpmが好き。

って普段から言ってるのに、似たようなことやるからこのgemにメソッド生えてたほうがいいかな、とメソッド増やそうとしてしまった。気づいたから良かった。packageわけよう。rubyだから、nodejsだから、というわけでもないと思うんだが何なんだこの呪縛は。

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

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

APIはこんな感じです。

NaturalNumberString.positive_integer_string?(value) -> boolean
NaturalNumberString.zero_or_positive_integer_string?(value) -> boolean

詳細: yard docs

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

if ENV['FOO']
  # do something
end

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

if ENV['FOO'] && !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') #=> true
NaturalNumberString.positive_integer_string?('100000000000') #=> true

NaturalNumberString.positive_integer_string?(nil) #=> false
NaturalNumberString.positive_integer_string?('') #=> false
NaturalNumberString.positive_integer_string?(1) #=> false
NaturalNumberString.positive_integer_string?('1.1') #=> false
NaturalNumberString.positive_integer_string?('-1') #=> false
NaturalNumberString.positive_integer_string?('0') #=> false

NaturalNumberString.zero_or_positive_integer_string?('0') #=> 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=>"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."}

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から使う | 實松アウトプット

bundle updateしたあとBUNDLED WITHを元に戻すrestore_bundled_with書いた

Bundler v1.10.0からbundle updateしたversionがGemfile.lockのBUNDLED WITH sectionに記録されるようになった。

Ruby – BUNDLED WITH で Gemfile.lock が更新されてしまう件 – Qiita

なのだけど、それはそれでめんどうなので、bundle updateしたあとに使う、BUNDLED WITHだけを元に戻すrestore_bundled_withを書いた。

packsaddle/ruby-restore_bundled_with

使い方はこちらに書いた。
restore_bundled_withを使ってBundlerのBUNDLED WITHをうまく取り扱う – Saddler – checkstyle to anywhere

tl;drに書いたとおり、やることのスジはあまり良くない。

  • Bundler v1.10.0からBUNDLED WITHのsectionが導入された。
  • restore_bundled_withを使うことで、Bundler開発チームの精神を逸脱して(!)、異なるversionのBundlerとBUNDLED WITHをうまく取り扱う。

BundlerのversionはGemfileの管轄外だとは思うんだよなあ。そしてruby directiveにも同じことを思ってる。