LinuxカーネルのビルドをNEW GAME! Opsで実現するぞい！

[Linux Advent Calendar 2016](http://qiita.com/advent-calendar/2016/linux)、21日目の記事です。

以前、NEW GAME! Ops実現に向けて[Slack Botサンプルを作る記事](http://qiita.com/furandon_pig/items/8ae4f9a185edd34af712)を書いたことがあったのですが、最近またSlack Bot熱が上昇してきました。
というのも、ここ最近、某社のAdvent Calendarをひたすら書くという日々を過ごしていて、その記事ネタの一つとしてSlack Botでビルドを実行するサンプルスクリプトを書いてみたところ、思っていたよりも便利で「これが...Botの能力(ちから)...！」と感銘を受けたワケです。

そこで今日のLinux Advent Calendarでは、[NEW GAME!](http://newgame-anime.com/)をインスパイアしつつ、Slack BotからLinuxカーネルをビルドする例を紹介しようと思います。

## LinuxカーネルのビルドをSlack Botから実行する

### まずは実行例

まずは実行例を示しつつ、動作イメージを説明できればと思います。

適当なSlackチャンネルで「リリースされているLinuxカーネル」と入力すると、[The Linux Kernel Archives](https://www.kernel.org/)に掲載されているカーネルバージョン一覧を表示します。

![img701.png](https://qiita-image-store.s3.amazonaws.com/0/61137/3a28d45a-3cc0-0001-c841-d91995c88042.png)

「Linuxカーネル<バージョン番号>のビルド」と入力すると、おもむろにビルドが開始されます。

![img703.png](https://qiita-image-store.s3.amazonaws.com/0/61137/a2d9ee0c-7fe7-a72f-571c-8daf4b87c210.png)

まだサンプルなので、機能は上記の2つだけです。以下に実際のコードを示しながら動作の詳細を紹介します。

### Botの動作とカーネルビルド出力との連携

Linuxカーネルビルドのコマンド実行をBot(というかRubyスクリプト)から実行するのは、`IO.popen`を使えば良いとはいえ、ちょっと面倒そうです。だいたい日が変わるギリギリでAdvent Calendarを投稿するような筆者にはシェルスクリプトで実装するような手早い方法でないとダメそうです...。

というわけで、Linuxカーネルのビルド部分は以下のようなシェルスクリプトで実行してみます。単にビルドの手順をベタ書きしているだけですが、`echo 'MSG: ...'`という部分は、Ruby+Slack Bot側で"MSG:"を含む行はSlackへの応答として返す文字列になります。

```bash
#!/bin/sh

# DRY_RUN=echo とかでdry-runできるぞい。
DRY_RUN=

if [ $# -eq 0 ]; then
	echo 'MSG: エラーだぞい。'
	exit 1
fi
url=$1

if [ ! -f `basename ${url}` ]; then
  echo "MSG: 以下のファイルをダウンロードするぞい。<BR>\`\`\`<BR>${url}<BR>\`\`\`"
  ${DRY_RUN} curl -O ${url}
fi

if [ ! -d `basename ${url} .tar.xz` ]; then
  echo "MSG: ファイルを展開するぞい。"
  ${DRY_RUN} tar Jxf `basename ${url}`
fi

${DRY_RUN} cd `basename ${url} .tar.xz`
echo "MSG: \`make allnoconfig\` を実行するぞい。"
${DRY_RUN} make allnoconfig

echo "MSG: \`make\` を実行するぞい。"
${DRY_RUN} make
```

Slack Bot側の実装(Ruby)は以下のようになります。前述のシェルスクリプトを`IO.popen`で実行し、1行ずつ取得した標準出力の内容を必要に応じてSlackに投稿するという処理になっています。

```rb
#!/usr/bin/env ruby
# coding: utf-8

require 'net/http'
require 'uri'
require 'erb'
include ERB::Util
require 'json'

require 'nokogiri'
require 'open-uri'

require 'slack'

def post(msg, channel)
    param = { 
      token:    ENV['SLACK_TOKEN'],
      channel:  channel,
      text:     msg,
      username: '涼風青葉＠イーグルジャンプ',
      icon_url: 'http://newgame-anime.com/assets/special/twticon/ng_icon_1.jpg'
    }   
    Slack.chat_postMessage(param)
end

Slack.configure {|config|
    config.token = ENV['SLACK_TOKEN']
}
client = Slack.realtime

start_time = Time.now.to_i

is_working = false

client.on :message do |data|
  post_time = data["ts"].sub(/\..*$/, "").to_i
  next if post_time < start_time

  msg = data["text"]

  releases = {}
  buf = File.open("_kernel.org.txt", "r") {|f| f.read }
  dom = Nokogiri::XML(buf)

  dom.xpath("//table[@id='releases']//tr").each do |rel|
    list = rel.xpath("td")
    obj = {}
    obj["label"]        = list[0].text
    obj["version"]      = list[1].text
    obj["release_date"] = list[2].text
    obj["url"]          = list[3].xpath("a/@href")
    releases[list[1].text] = obj
  end

  if msg =~ /^リリースされているLinuxカーネル/

    msg = "現在リリースされているLinuxカーネルの一覧を表示するぞい。\n"
    msg = msg << "\`\`\`\n"
    formatted_str = ''

    releases.each_pair do |k, rel|
      fmt_label        = sprintf("%12s", releases[k]["label"])
      fmt_version      = sprintf("%16s", releases[k]["version"])
      fmt_release_date = sprintf("%10s", releases[k]["release_date"])
      fmt_url          = sprintf("%s",   releases[k]["url"].text.gsub(/^.*linux-/, 'linux-'))

      formatted_str = formatted_str << <<-EOS
      #{fmt_label}  #{fmt_version}  #{fmt_release_date}  #{fmt_url}
      EOS
    end
    msg = msg << formatted_str
    msg = msg << "\`\`\`"
    post(msg, data["channel"])
  end

  if msg =~ /^Linuxカーネル(.*)のビルド/
    target_version = $1

    target_release = nil
    releases.each_pair do |k, rel|
      if releases[k]["version"] == target_version
        target_release = releases[k]
        break
      end
    end

    if target_release == nil
      post('そんなバージョンは見つからないぞい。', data["channel"])
      next
    end

    if is_working == true
      post('いまビルド中ぞい。', data["channel"])
      next
    end

    post("Linuxカーネル `#{target_version}` をビルドするぞい！", data["channel"])
    is_working = true

    cmd = "./build_kernel.sh #{target_release['url']}"
    puts cmd  # debug

    begin
      Thread.new do
        start = Time.now
        IO.popen(cmd, "r+") do |proc|
          last_msg_type = ''

          while (line = proc.gets)
            msg = line.chomp
            puts msg  # debug

            post($1.gsub(/<BR>/, "\n"), data["channel"]) if msg =~ /^MSG:(.*)$/

            if msg =~ /^ /
              msg_type = msg.split(" ")[0]  
              if last_msg_type != msg_type
                post("`#{msg}` ぞい！", data["channel"])
                last_msg_type = msg_type
              end
            end
          end
        end
        finish = Time.now

        msg = "ビルド時間は `#{(finish - start).round(2).to_s} 秒` ぞい！"
        post(msg, data["channel"])

        is_working = false
      end
    rescue Exception => e
      puts e.message
      puts e.backtrace.inspect
    end
  end
end

puts "ready."
client.start
```

### シェルスクリプトの出力をSlackに投稿する

例えば、Linuxカーネルのビルドを走らせると、シェルスクリプトからは以下のような出力がなされます。

```
$ bundle exec ruby KernelBuildBot.rb
ready.
./build_kernel.sh https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.9.tar.xz
MSG: 以下のファイルをダウンロードするぞい。<BR>```<BR>https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.9.tar.xz<BR>```
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 88.8M  100 88.8M    0     0  2905k      0  0:00:31  0:00:31 --:--:-- 3084k
MSG: ファイルを展開するぞい。
```

"MSG: ファイルを展開するぞい"の部分がSlackに投稿されています。

![img703.png](https://qiita-image-store.s3.amazonaws.com/0/61137/493ea6bb-cc14-0130-feb7-8567003c4e91.png)

それと、長いビルドが終わるまでSlackに投稿がないと、ちゃんと処理が走っているのか少し不安になります。そこで例えば以下のような出力における、空白から始まる行についてもSlackに投稿してみます。ごく短時間に大量の投稿はマズイ気がするので、第一カラムの文字(`HOSTCC`とか)が前回と同じ場合は出力をスキップする方法でSlackへの投稿スピードを抑えるようにしてみます。

```
MSG: `make allnoconfig` を実行するぞい。
  HOSTCC  scripts/basic/fixdep
  HOSTCC  scripts/kconfig/conf.o
  SHIPPED scripts/kconfig/zconf.tab.c
  SHIPPED scripts/kconfig/zconf.lex.c
  SHIPPED scripts/kconfig/zconf.hash.c
  HOSTCC  scripts/kconfig/zconf.tab.o
```

それでも以下のような感じで、もりもりとSlackに投稿されます。

![img704.png](https://qiita-image-store.s3.amazonaws.com/0/61137/40e17b52-2e73-d3fa-4677-9513823ee851.png)

「ぞい！」の多さにそこはかとなく狂気を感じます...。

![img705.png](https://qiita-image-store.s3.amazonaws.com/0/61137/c7e81f41-4cb0-eae3-3ed1-01fdb1579445.png)

それと、ビルド処理は再入できないようにしてあります。
以下のようにビルド中に再度ビルドしようとするとエラーになります。

![img700.png](https://qiita-image-store.s3.amazonaws.com/0/61137/89dbacea-ab47-0843-83d0-47c7f9564088.png)

無事にビルドが完了すると、ビルド時間が表示されます。
意外とビルド時間の測定が行えるのは便利かもしれないと思っています。

![img706.png](https://qiita-image-store.s3.amazonaws.com/0/61137/c86d691d-c177-0981-b97e-c00903150576.png)

## まとめ

[NEW GAME!](http://newgame-anime.com/)Ops実現に向けて、LinuxカーネルのビルドをSlack Botから行うサンプルを作成してみました。ビルドしたカーネルをAmazon S3にアップロードしたり、VMで起動してみたりすると実用的になりそうです。