CI完了を待ってPRをマージするbotを作るためのライブラリを公開しました

CIを待って自動でPRをマージするbotを作ると便利だった - soutaroブログで、紹介した、CIが完了したときに自動でPRをマージするbotですが、皆さんが簡単に実装できるようnpm packageにしました。

github.com

動くと良いですね。(npm packageはまだ試してない。)

CIを待って自動でPRをマージするbotを作ると便利だった

GitHubを使っている場合「Pull Requestが作られたら自動でCIを実行し、グリーンになることを確認してからマージする」というのは割と広く採用されている開発のフローだと思います。PRごとにCIすることを確実にすることで、うっかりビルドを壊すようなPRをマージしてしまうことを防ぎ、いつの間にかmasterが壊れることを防ぎます。*1「CIが通っていないPRをマージしない」というのは、GitHubでそういう制約を設けることもできますし、そこまでやらずにチーム内の約束として共有しても良いでしょう。(ユビレジでは後者を採用しています。)

問題は、CIは一瞬では終わらないことです。ユビレジの場合、RailsのWeb appのCIには15分ほどかかりますし、iPad appのCIは30分かかります。CIのためのリソースにも限りがあるので、状況によっては既に15個CIのジョブが溜まっていて、完了までに1時間かかるなんてこともあります。もちろんこれは仕方がないことで、丁寧にコードレビューしてからPRをマージするような場合には問題にはなりません。どうせレビューするのに時間がかかるので、CI待ちの時間は相対的に小さなものとなります。しかし、ちょっとしたtypoの修正とか、コードレビューでokが出てからrebaseしたような場合だと、結構辛い。typoの修正は30秒くらいでレビューが終わったりしてすぐにマージしたくなるものですし、rebaseしただけのようなものも何十分もCIを待つのは苦痛です。CIを待たずにマージすることもできるようにはなっていますが、これはCIが動かないとか本当に急いでデプロイしたい場合の最後の手段なので、CIを待つのがダルい程度の場合にやっちゃうのは避けたい(避けるように訓練しています)。CIが完了するまでの間に他の作業を始めてしまったりすると、今度はマージするのを忘れたりするわけです。これはこれで辛くて、Webサイトのtypoの修正であれば早くデプロイした方が良いですし、下手をすると別のPRがマージされて再度rebaseしなくてはいけなくなったりして、とても辛い。

問題は、CIが完了すればマージして良いPRがあったら、CIが完了するまでそのことを覚えておいて、CI後に確実にマージしなくてはいけないことです。どう考えても、これは人類がやるべき仕事ではなくて、コンピュータにやらせるべき仕事です。つまり「CIが終わったらPRを自動でマージしてくれるbot」が必要です。必要だったので作りました。何ヶ月か動かしていますが、良い感じです。

f:id:soutaro:20160308113820p:plain

  • CI後に自動でPRをマージするには、ShipItというLabelを付けます
    • やっぱりこれはまずいな、と思ったら、Labelを取れば良い
    • 不安な場合は、コメントを付ければSlackに通知が行くので、どうしても止めたい人がそれを見たら反応します
  • GitHubのHookでHubotにCIの完了が通知されたら、PRを探して、ShipItラベルが付いていたらマージします

これが便利に使えるための暗黙の背景としては、

  • 開発チームは誰でもPRをマージできる(基本的には、PRを作った人が覚悟を決めてマージボタンを押す)
  • マージされるとWeb appはProduction環境に自動でデプロイされる
  • うっかり間違ったバージョンをデプロイしてしまっても、Herokuの機能で簡単にロールバックできる

などがあると思います。*2

とても便利なので、皆さんも簡単に実装できるようnpm packageにしました。

github.com

*1:とは言っても、複数のPRが同時にopenされる状況では、masterが壊れることを確実に防げるわけではありませんが。

*2:「開発チームは誰でもPRをマージできる」というのは、実は過去に問題になったこともあるのですが、それでも他の色々な条件を考えてポリシーとして受け入れています。

Swiftでas!と書く場合のガイドライン

as!って言うのは、要するにダウンキャストできない場合にプログラムを終了させるものである。

if let x = expr as? SomeClass {
  f(x)
} else {
  fatalError()
}

と書くのであれば、

f(expr as! SomeClass)

と、同じことなので、as!で書いた方が良い。その方がタイプ数が減るし、コードの見た目も簡潔になって理解しやすくなる。長いコードはそれだけで苦痛だし、なにか間違ったプログラミングをしていることを示すシグナルにもなる。ダウンキャスト失敗の場合に終了するためだけにifを書くことは、全体的なコードの乱雑さを増してしまい、本当の問題の見落としに繋がる。

ただし、as?とかas!とかダウンキャストをしてる時点で、そのプログラムには潜在的な問題があると考えることもできて、本当にダウンキャストが必要なのか3回くらい考えた方が良い。

as!とObjective Cのキャストの違い

  • Objective Cのキャストは常に成功する
  • as!は失敗してエラーになることがある

この違いをまず頭に入れよう。次のObjective Cプログラムはエラーにならずに実行できる。

NSArray *x = (NSArray *)@"Hello World";

キャストは、コンパイルエラーにならないし実行時エラーにもならない。NSArrayにあってNSStringにないメソッドを呼び出すと実行時エラーになるが、キャストそのものは失敗しない。この性質は、3年に1回くらいは便利なこともあるが、大体は不便である。不正にキャストしたxが、ずっと先の全然ちがうところで実行時エラーを起こしたりするので、間違いを探すのに苦労することになる。

一方、次のSwiftプログラムはコンパイルはできるが実行するとエラーになる。

let x = "Hello World" as! Array<String>

Swiftは、不正なキャストに失敗してエラーを発生させる能力を獲得したのだ*1。これは、Objective Cに比べるとかなり改善されていて、不正なキャストからずっと先の全然違うところで実行時エラーになってデバッグに苦労することがなくなる。

この点で、Objective CのキャストとSwiftのキャストはかなり性質が違うものである。isKindOfClass:でテストせずにキャストするObjective Cのコードはかなり書かないほうが良いが、as!するSwiftのコードはかなりマシであると言える。

さらに、キャストに失敗したときに何が起きるかは決まっている。あなたのプログラムが終了するだけである。iOSに影響が及ぶことはないし(あったとしたらそれはOSの問題だ)、iPhoneが壊れることもないし、鼻から悪魔が出てくることもない。(ただし、アプリケーションやユーザーの性質によっては、それでも強制終了しない方が良い場合はあるとは思う。)

as! した方が良い場合

ダウンキャストに失敗した場合にそこから回復する手段がないとき、つまり直ちにプログラム終了するしかない場合はas!してしまう方が良い。「キャストに失敗したらプログラム終了」というのがas!の意味なので、わざわざ冗長にifを書く必要はどこにもない。

as?した方が良い場合

ダウンキャストに失敗しても処理が続けられる場合は、as!してはいけない。例えば、View Controllerに書かれているような、ユーザーとのインタラクションを担うようなコードの場合は、ダウンキャストの失敗というなにかプログラム実行の大前提が崩れるような事態にも、適切なエラー回復の手段を提供できる可能性がある。

どのくらい頑張ってエラー回復するかというのは、アプリケーションやユーザーの性質に関する問題なのでなんとも言えない。

コードレビューをしていてas!を見つけたら

本当にプログラムを終了する以外に回復の方法がないのか、議論して良いし、議論するべきである。適切な回復の方法があるのであれば、そちらに直す。また、as!as?もしないことについても検討する。標準ライブラリとかObjective Cとの相互運用を除けば、「enumにしてswitchする」「protocolを追加して分岐しなくて良いようにする」などの方法がある。

Lintとどうつきあうか

個人的には、as!に警告するルールはオフにするのがお勧めだけど(僕はLint大嫌いなので、多分これは少し極端な見解だろうとは思う)。まー現実的には、堂々と

// swiftlint:disable:next force_cast

と書くのが良いだろう。これを見たレビュアーは強制終了するほかにエラー回復の方法がないのかあなたに説明を求めるだろうし、あなたは説明できなくてはいけない。

*1:ちなみに、不正なキャストに失敗する能力は、JavaC++などのCやObjective Cよりも新しい言語は、だいたい1990年代に獲得しているものである。2016年にもなって失敗する能力が欠けているObjective Cが異端であるとも言える。

それでenumとclassのどっちにすれば良いの?

この記事には

enumが便利なのは

  • ある状態では有効だけど、別の状態では有効ではない値を定義するとき

です。

と書きましたが、「それはサブクラスっていうやつではないか」という声があります(どこに?)。その通りで、enumとクラスはよく似ている側面があって、enumでできることはクラスでも(再コンパイルの量とか、網羅性の検査とか、isKindOf:とか、キャストとかの問題を見なかったことにすれば)できるという話でした。

ここで疑問になるのは、

  • なぜSwiftにはenumとclassという似たような機能が二つ用意されているのか(TMTOWTDI?)
  • 何を考えてenumとclassを使い分ければ良いのか

でしょう。

enumとclassの違い

Swift的には「classは状態を持てるがenumは持てない」という違いがあります。これはSwiftプログラミング言語の設計として導入した恣意的な制限で*1、もう少し抽象的にはenum (ADT) とclassには次のような違いがあります。

  • enumは個々のcaseを追加するのは大変だけど、値の操作を追加するのは楽
  • classは個々のサブクラスを追加するのは楽だけど、操作を追加するのは面倒

enumにcaseを追加すると、全体的にあなたのコードは全体的にコンパイルし直しで、多分switchがエラーになりまくります。黙って実行時エラーにされるよりはマシですが、一つ一つ確認して、丁寧に手作業で修正してまわらないといけません。めんどくさい。一方で、クラスにサブクラスを追加した場合はなにが起きるかというと、全体のコンパイルは必要ないし、既にスーパークラスを使っている部分もソースコードを修正する必要がありません。楽。

逆に、enumに操作を追加するのは簡単で、switchを書けばそれで終わりです。プログラミングしてて、「あ、ここはそれぞれのcaseで別々の操作が必要だな!」とか多分考えもせずに、無意識にswitchを書くことになるでしょう。classに操作を追加するのは、スーパークラスメソッドを追加して、サブクラスで全部実装してまわると言うことです。まずメソッド名を考えないといけませんし、中身が一行だけだったりするとそもそもfuncとか書くのも面倒ですし、サブクラスを全部探すのも面倒ですし、ファイルを開いてまわる時点で面倒です。ああなんて面倒。

継承とオブジェクトの仕組みはある種のプログラミングを楽にしましたが、一方でad-hocに操作を追加できないことが問題になることもあります。isKindOf:やキャストのような仕組みでこの問題は回避できますが、一方で安全性が失われます。実行時の型検査によって危険性は押さえられていますが、もっと型システムを活用したい場面はたくさんあります。このようなad-hocに操作を追加したいようなデータ型には、enumのようなアブローチが向いています。

さて、enumとclassのどちらを使えば良いかはかなり明らかになりました。

  • caseは増えなさそうだけど、操作が増えそうな場合はenumを使う
  • 操作は現時点でだいたいわかっているけど、caseが増えそうな場合はclassを使う

簡単ですね。

ちなみに個々の型についてenumとclassのどちらを選ぶかですが、僕の場合は、ADTの方が先に頭にあるのでまずはenumで書けないか考えることになります。それで

  1. 状態を変更する方が楽に実装できそうな予感がある
  2. 継承して実装を使い回したい
  3. 関連する操作が(直感的に)すでにわかっている

など、enumで書くのは微妙な感じがする場合には、classにします。

Expression Problem

enumとclassの違いは、関数型プログラミング言語とオブジェクト指向プログラミング言語の違いとして考えることができます。この問題はExpression Problemという名前で昔々に議論されていたりします。

WikipediaからリンクされているWadlerのメール(論文*2)には、次のような文章があります。

In a functional language, the rows are fixed (cases in a datatype declaration) but it is easy to add new columns (functions). In an object-oriented language, the columns are fixed (methods in a class declaration) but it is easy to add new rows (subclasses).

http://homepages.inf.ed.ac.uk/wadler/papers/expression/expression.txt

ここで行 (row) と列 (column) は、それぞれデータの種類と操作を指しています。和訳。

関数型ブログラミング言語では、行は固定されている(datatype宣言のcase)が列を足すのは簡単(関数)。 オブジェクト指向プログラミング言語では、列は固定されているが(クラス宣言に含まれるメソッド)行を足すのは簡単(サブクラス)。

さて、「それでは両方とも足せるようにするのはどうしたら良いでしょうか?」というのが、Expression Problemです。回答はいくつかあって、

  • OCamlではPolymorphic Variantという「すごいenum」を用意して解決した
  • Scalaもなんかうまいこと解決した(上のWadlerの論文は「俺の考えたGJならExpression Problemが良い感じに解ける」というものなので、きっと関連があるんだろうと思っていますが、Scalaはよく知らないのでわかりません)
  • Haskellもなんかできるらしい(よく知りません)

とかです。

ちなみに「既存のクラス階層にメソッドを追加しようとするとちょう大変」という側面を見れば、SwiftのExtensionやObjective-CのCategory、C#のExtension Methodsといった機能によって、ある程度は解決される問題でもあります*3GoFで言えばVisitorパターンがこの問題に取り組むものですね(キャストが必要なので型安全ではない)。

*1:というと、少しニュアンスが違うかもしれなくて、enumがimmutableなのは常識的な設計なんだけど、でも別にmutableであっていけないわけでもない。

*2:URLにpaperって入っている。

*3:でも再帰的なものを考えるとあまり上手くいかない。

SwiftのenumがObjective Cで欲しいときはどうするか

Swiftではenumという種類のデータ型を定義できます。Cのenumと違って、引数を持てるようになっています。これが便利なのは

  • ある状態では有効だけど、別の状態では有効ではない値を定義するとき

です(すごい雑な説明なので、他の使い方も調べておきましょう)。

Swiftenumとは

例えばあなたが最強のレジアプリを開発しているとしましょう。販売する商品の価格には次の二通りがあります。

  • 単価と税率が決まっているもの(単価¥100、税8%)
  • 全体の金額からの割合であるようなもの(10%割引)

enumを使わないで普通のクラスにしようとすると、こんな感じになるはずです。

class MenuItem {
  let isPercentage: Bool
  let unitPrice: NSDecimalNumber!
  let vatPercentage: NSDecimalNumber!
  let percentage: Int!
}

isPercentageが偽のときには、unitPricevatPercentageが設定されて、percentagenilになります。isPercentageが真のときはその逆。optionalがたくさんあって嫌な感じがしますし、いつか普通に間違えそうです。うっかりisPercentageな商品のunitPriceにアクセスしてエラーになるかもしれませんし、それを見た新人がisPercentageなのにunitPriceに0を設定するみたいな間違いを犯すかもしれません。少しでも状況を改善するためにはvalidationのコードを書くこともできますが、退屈なのでできるだけやりたくないですね。

こういうことを防ぐにはenumを使います。

enum MenuItemPrice {
  case Unit(price: NSDecimalNumber, vatPercentage: NSDecimalNumber)
  case Percentage(percentage: Int)
}

class MenuItem {
  let price: MenuItemPrice
}

optionalがなくなってやばそうな感じはなくなり、割引の項目の価格にはpriceがないので間違えてアクセスしようとするとコンパイラが教えてくれます。安心。priceの中身にアクセスしようとすると、必ずswitchが必要になって少し面倒ですが、諦めましょう。

switch price {
  case Unit(let p):
    ...
  case Percentage(let p):
    ...
}

ちなみにswitchを使うと、caseが網羅されているかもコンパイラがチェックしてくれるので、後で価格の種類が増えたときにも安心です。

Objective Cでどうするか

残った問題は一つだけで、2010年から開発されているあなたの最強のレジアプリはObjective Cで書かれているので、こういう良い感じのenumSwiftで定義してもそれをアプリケーションから使うことは出来ないということです。関連するコードだけでもSwiftで書き直せれば良いのですが、いろいろな事情でそうもいかないことは多いでしょう。

仕方がないのでこうします。

@interface MenuItemPrice : NSObject
@end

@interface UnitPrice : MenuItemPrice

@property (nonatomic) NSDecimalNumber *price;
@property (nonatomic) NSDecimalNumber *vatPercentage;

@end

@interface PercentagePrice : MenuItemPrice

@property (nonatomic) NSInteger percentage;

@end

@interface MenuItem

@property (nonatomic) MenuItemPrice *price;

@end

これで「単価が決まっている価格なのにうっかりpercentageにアクセスしてしまう」ことは防げるようになりました。

こうやって定義した値を使う方法はいくつかあります。

  1. MenuItemPriceメソッドを追加してサブクラスでオーバーライドする
  2. isKindOfClassして場合分けする

必要なメソッドがわかっている場合は、1.の方法が良いでしょう。例えば「価格を文字列の表現に変換する」みたいな操作は、それでいいと思います。私の場合は、さっさと諦めて、isKindOfClassで場合分けすることが多いですね。

if ([menuItem.price isKindOfClass:[UnitPrice class]]) {
  UnitPrice *price = (UnitPrice *)menuItem.price;
  ...
}

isKindOfClassのような操作はオブジェクト指向プログラミングでは避けるべきとされていますが、ここでは別にOOしたいわけではないので良いことにします。キャストが少し面倒なので、ヘルパーを足すことがあります。

@interface MenuItemPrice

- (void)asUnitPrice:(void (^)(UnitPrice *))block;
- (void)asPercentagedPrice:(void (^)(PercentagedPrice *))block;
- (void)switchWithUnitPriceCase:(void (^)(UnitPrice *))unitCase percentagedPriceCase:(void (^)(PercentagedPrice *))percentagedCase;

@end

もしも価格の種類が増えた場合にコンパイルエラーになるようにするため、switchWithUnitPriceCase:percentagedPriceCase:も入れてみました。

まとめ

Swiftenumって要するに代数的データ型 (Algebraic data type / ADT) なので、Objective Cに欲しい場合は、どうしてもOOPLでADTしたい場合のパターンで逃げることができます。

MacでATOKを使っている場合にバックスラッシュを入力する

MacのEl Capitanでは仮名漢字変換が変わっていて、スペースを叩かなくても自動で変換してくれるようになっている。これはこれでなかなか便利なのだけど(今、何ヶ月かぶりにATOKに切り替えてみたら変換を忘れまくるので驚いた)、一つ不満があって、漢字にしすぎること。とは言っても、イマドキの仮名漢字変換はだいたい僕の好みに比べると漢字にしすぎるんだけど、自動でやってくれるというのが問題が大きかった。なにかを入力していて、後で「ときに」とか「ことが」とかを足そうとしたときなんかに酷くて、「時に」とか「事が」とかに自動で変換してしまうので、わざわざ平仮名に変換し直す手間が発生する。これがいやなのでとりあえずはATOKに戻すことにしたのだった。(ちなみに、この挙動は「自動変換を始めるのはある程度の長さが入力されてから」みたいな制約を入れると簡単に改善できる気がするけど、考えてないとは思えないので、今の挙動がトータルで最適と判断されているんだろう。)

さて、ATOKほインストールしてまずやったことは、バックスラッシュが入力されるようにすることである。Macではキーボード左上の「¥」と印字されているキーを押すと、¥が入力されるのだが、XcodeとかSublime Textとかでプログラミングしているとこの挙動は少し都合が悪い(ちなみにRubyMineとかIDEAとかTerminalだとデフォルトがバックスラッシュになる)。Optionと同時に押すことでバックスラッシュが入力できるが、プログラミングしているときに困るという話なので(頻度の問題)、いちいち同時押しとかしたくない。Mac標準の仮名漢字変換では、英字モードの時にはバックスラッシュが入力できるオプションが用意されているのだが、ATOKには見当たらないのだった。

適当に検索したところ、Key4Remapを使えみたいな話しか出てこないので諦めて、英字入力にはATOKを使わないことにした。と同時に、せっかくなのでこの方法を書いておくことにした。(Key4Remapを使っている人はそれでいいと思うけど、僕はわざわざインストールしたくないので別の方法を採った。)

こうする。

f:id:soutaro:20160218080328p:plain

システム環境設定でキーボードのページに進み、ATOKから「半角英字」のチェックを外す。

f:id:soutaro:20160218080454p:plain

僕は「ひらがな」以外を全部オフにしたけど、そこは好みで。これで、

  • 平仮名は「ATOK
  • 英字は「日本語」

という設定になり、「日本語」の設定で¥の入力を設定しておけばバックスラッシュが入力できるようになった。

問題としては

  • 変換中に「英数」を押すと変換中のものが確定されてしまう

という点があり、入力のスタイルによっては気に入らないかもしれない。その場合はKey4Remapなどでなんとかするのが良いだろう。

bundle execを殺す

「バグの希釈」の項目を見て思い出した。

2015年に書いたコードの中で、一番意味不明で気が利いてたなーと自分では思っているやつは「lib/bundler/setup.rbに空のファイルを置く」というものだったりする。会社で少し説明したんだけど、あまりうけなかったのでここに書いておこう。

ユビレジで販売している製品の中にユビレジエクステンションというのがあって、簡単に言うとRaspberry Piに電源とWi-FiアダプタとUSBハブを付けたもので、HTTPでJSONをPOSTするとレシートが印刷できる機械になっている。ふつーにDebianが動いていて、Rubyで書いたHTTPサーバとかLED点滅のためのdaemonとかレシート印刷のためのdaemonとかが動いている。問題は、特定の処理でHTTPサーバとかを再起動するときに、なんか上手く行かない子がいることが、出荷後に判明したことだった。調べてみると、再起動が上手くいかないというのは、タイムアウトしていることがわかった。

なんで上手くいく子と上手くいかない子がいるのかは良くわかっていない。SDカードを読み書きしながら動作しているので、なんかの拍子にSDカードがなんかダメになるんだと思う。SDカードをddでコピーして試すと平気で動いたりする。(これは本題ではない。)

さて、再起動と一言で言ってもなにに時間がかかっているのだろうか。主にprintfデバッグなどを繰り返したところ、Rubyプログラムの先頭にたどり着くまでに時間がかかっていることがわかった。どうしようもないじゃんそれ……とかいいつついろいろ試行錯誤していると、

$ bundle exec ruby -e "puts :hello"

と、

$ ruby -e "puts :hello"

で実行時間が大きく違うことがわかる。後者の方が速い。まあ当たり前感がある。Bundlerを起動してからrubyを起動するのと、rubyだけ起動するのでは、後者の方が速いのは道理だ。そして、出荷済のバージョンではうっかりbundle execしているのだった。

また、普通にGemfileとかGemfile.lockとかがある場所でbunde exec複数同時に実行するとめちゃくちゃ遅くなることもわかった。きっと、複数rubyが一斉にbundler経由でgemをロードすると、CPUを使いすぎるとかディスクアクセスが多すぎるとかで、なかなか終わらないんだろう。ちなみにbundle execしないで3つのrubyを実行すると、あんまり遅くない。(「めちゃくちゃ」というのは「3つ同時に起動すると10倍時間がかかるようになる」くらいの感じ。「あんまり」というのは、3〜4倍くらいの感じ。)

$ bundle exec ruby -e "puts :hello" & bundle exec ruby -e "puts :hello" & bundle exec ruby -e "puts :hello"

これで、「普通に起動したときには上手く起動するのに、(特定の場面で)サービスを再起動したときには上手くいかない」理由もわかった。普通に起動するときには、LED点滅daemonだけは先に起動してから(起動中にLEDを点滅させたいので)レシート印刷daemonとHTTPサーバを起動するが、問題となっている再起動のときには3つを一斉に起動している。2つ同時起動の場合はなんとかタイムアウトせずにすむが、3つだとダメなことがある、そんなラインにタイムアウトを設定してしまったのだろう。

どうやって直したら良いだろうか。いくつか方法がある。

  • サービス起動時のbundle execをやめる
  • サービスを3つ同時に再起動しないようにする
  • タイムアウトを長くする

どれでも良い。簡単に直せる。

……まだ出荷してなかったらね。そして既に出荷は開始されているのだった。

実は、こういうこともあろうかと、最近のバージョンにはiPadアプリ経由で更新ができる仕組みを仕込んである。あるんだけど、上に挙げたやつは、どれもソフトウェアアップデートでは更新ができないファイルに書かれたプログラムなのだった。うーむ。

どうなっているのか、もう少しちゃんと説明しよう。

  • アプリケーションの本体は1個のディレクトリにすべて格納されている。binとかlibとかvendorとかがあって、/var/ubiregi/1.3とかに保存されて、/var/ubiregi/currentみたいなリンクを張る。
  • /etc/init.dに置かれた起動スクリプトbundle exec ruby -I /var/ubiregi/current/lib /var/ubiregi/current/bin/a.rbなどとしてサービスを起動する
  • ソフトウェアアップデートは/usr/local/ubiregi以下に展開すると良い感じになるようなディレクトリをtgzにしたものの形で処理される
  • ソフトウェアアップデートはファイルの展開しかしないので、/etc/init.d以下のファイルを変更することはできない
  • ソフトウェアアップデートはソフトウェアアップデートdaemonによって処理され、このソフトウェアアップデートdaemonをソフトウェアアップデートすることはできない

そういうわけで「bundle execで実行されたアプリケーションが、rubyが実行されるまでの間に、bundle execをなかったことにする」そんな方法がないか考えることになった。(回答は、LOAD_PATHが通っている場所にbundler/setup.rbを置くことである。最初に書いた通り。)

bundle execがやっていることを見ると、次の二つである。

  1. bundle execで実行する実行ファイルをGemfile.lockを見ながら探す
  2. RUBYOPT環境変数-rbundler/setupを設定する

今回は1.は関係ない。RUBYOPTってなんだったっけ。

Rubyインタプリタにデフォルトで渡すオプションを指定します。

環境変数 (Ruby 2.1.0)

なるほど。そして-rrequireだ。つまり、bundle execすると

require 'bundler/setup'

と書かなくて良くなるわけだ。ってことはlib/bundler/setup.rbに空のファイルを置いておけば、Bundlerの処理をスキップできるのではないだろうか。問題は、このRUBYOPTコマンドライン引数の-Iのどっちが先に処理されるかである。(サービス起動のコマンドをもう一度。)

$ bundle exec ruby -I /var/ubiregi/current/lib /var/ubiregi/current/bin/a.rb

-Iの処理がRUBYOPTよりも後なら、-rbundler/setupは標準ライブラリを見に行ってしまうので、本物のbundler/setup.rbをロードしてしまう。-Iの処理が先なら、/var/ubiregi/current/lib/bundler/setup.rbをロードするので、アプリケーションで乗っ取ることができる。rubyのマニュアルにはなんとも書いてないので、実際に実行して試してみるしかない。

-Iが先だった。

というわけで、無事Bundlerを無効にする方法が見つかって、ソフトウェアアップデートによってこの問題は解決されたのだった。bundler/setupがなくなるので、Bundlerの手を借りずにgemをロードしないといけないけど、それはStandaloneモードで解決できる。

--standalone[=<list>]

bundle-install(1) - Install the dependencies specified in your Gemfile

(ちなみにStandaloneモードだと起動がかなり速いので、Gemfile.lockの処理がなんか変なことやってて遅い気がする。わからないけど。)

そういうわけで、空のファイルを追加する謎のコミットによって問題が解決されたのだった。(うっかり不必要にbundle execしてたのが本当の問題なんだけど。)あと、あんまりやらないとは思うけど、うっかりbundler/setup.rbみたいなファイルを作るとbundle execが動かなくなるかもしれないので、注意しましょう。