Rubyアドベントカレンダー、7日目の記事です。前日はjerrywdleeさんでした。
qiita.com
今年の半分ぐらいの時間をかけて、「Webで使える mrubyシステムプログラミング入門」という本をリリースしました。
www.c-r.com
多くの知人に手に取っていただけているようで何よりですが、今回は、この本の執筆を支える周辺技術の話について、Rubyアドベントカレンダーの場をお借りして公開しようと思います。
今回の本は、Re:VIEW というフォーマットを用いて執筆しています。
reviewml.org
ただし、カスタマイズ性などの観点で、Re:VIEWの一種のディストリビューションである「Re:VIEW Starter」を用いて仮PDFを作成し、定期確認することにしました。
kauplan.org
上記のRe:VIEW Starterのプロジェクトテンプレート機能(Webアプリ)のおかげで、以下のコマンド一つでPDFが作成できるようになりました。
$ ./bin/rake pdf
ただし、Re:VIEW Starterはその設計思想からRe:VIEW 2.5系を利用しているので、そのバージョンを指定したGemfileを用意します。
また、PDF生成に必要なTeXパッケージが入ったイメージを作成し、その中でビルドする必要があります。以下はそのDockerfileです。
FROM ruby:2.7.1-slim-buster
LABEL maintainer="udzura@udzura.jp"
ENV REVIEW_VERSION 2.5.0
ENV REVIEW_PEG_VERSION 0.2.2
ENV LANG en_US.UTF-8
RUN gem install bundler -v=2.0.2 --no-document
RUN apt-get update && \
apt-get install -y --no-install-recommends \
locales git-core curl ca-certificates && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen
RUN locale-gen en_US.UTF-8 && update-locale en_US.UTF-8
RUN apt-get update && \
apt-get install -y --no-install-recommends \
texlive-lang-japanese texlive-fonts-recommended texlive-latex-extra lmodern fonts-lmodern tex-gyre fonts-texgyre texlive-pictures \
ghostscript gsfonts zip ruby-zip ruby-nokogiri mecab ruby-mecab mecab-ipadic-utf8 poppler-data cm-super \
graphviz gnuplot python-blockdiag python-aafigure \
texlive-plain-generic texlive-fonts-extra \
fonts-noto-cjk fonts-noto-cjk-extra && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
COPY noto/ /usr/share/texlive/texmf-dist/fonts/map/dvipdfmx/ptex-fontmaps/noto/
RUN texhash && kanji-config-updmap-sys noto
PDFを定期作成する技自体は @sugamasao(id:seiunsky) さんがパーフェクトシリーズで実施していた記憶がありますが、CIを行うメリットとして:
- なるべく現実的な書籍に近い形で原稿の確認ができる。
- 原稿のフォーマットが壊れていないことを定期確認できる。
などが挙げられます。
Re:VIEW/Re:VIEW Starterの詳細なセットアップ方法は公式のドキュメントなどに譲りますが、上記のDockerfileの記述も参考にできるかもしれません。
PDF の Continuous Delivery
また、原稿については定期的にタグを打ち、リリースを作成してPDFを添付するようにしました。PDFまではできているのでGitHub Actionの組み合わせで難なく実現できます。
その際に利用したGitHub Actionのワークフローを公開します。イメージは先述のDockerfileから作成しています。
name: Upload Release Asset
on:
push:
tags:
- '*'
jobs:
release:
name: Upload Release Asset
runs-on: ubuntu-latest
container: udzura/review-pdf-ja:review2.5-ruby2.7
steps:
- name: Checkout code
uses: actions/checkout@v1
- name: Build project
id: create_build
run: |
bundle install --jobs 4 --retry 3
./bin/rake pdf
echo "::set-output name=current_tag::$(echo ${{ github.ref }} | awk -F'/' '{print $3}')"
zip --junk-paths mruby-handson-book.$(echo ${{ github.ref }} | awk -F'/' '{print $3}').zip mruby-handson-book.pdf
- name: Create Release
id: create_release
uses: actions/create-release@v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Release Asset
id: upload_release_asset
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./mruby-handson-book.${{ steps.create_build.outputs.current_tag }}.zip
asset_name: mruby-handson-book.${{ steps.create_build.outputs.current_tag }}.zip
asset_content_type: application/zip
少し困っていた点は ${{ github.ref }}
がそのままタグ名になるのはどうも actions/create-release
の中だけのようで、今回は別途 outputs の仕組みを用いてタグ名部分のみを抜き出し後続のstepに渡しています。参考:
swfz.hatenablog.com
タグを打って定期リリースするメリットは、レビュワーの方に依頼してから大きく享受できたように思います。レビュワーの方々には、 このタグに紐づくリリースのPDFで レビューしていただくよう指示することができるので、修正の反映やそれらの前後関係などを共有しやすくなったように思います。
もちろん、テキストではなくPDFで読むことで間違いに気づきやすいというメリットについては、レビュワーにとっても大きなものになっていたと思います。
その他のTips
pdfinfo
コマンドの結果や、 wc -m
の結果などを定期表示するのは、分量の見積もりやモチベーションの助けになるので良いかと思います。章ごとのマルチバイト文字数を把握できたのは、章によって極端に文字数が多い・少ないという現象を早期に検知することにつながったので良かったです(結局ある程度偏ってしまいましたが)。
最終的な手元の原稿の文字数を公開しておきます。4 〜 7、 9 章は意図して分量をほぼ同じようにしたつもりです。8章はこの本のクライマックスで(9章は裏面)、長くなるのは仕方ないので、諦めました。
$ rake count
find contents -depth 1 -name '*.re' | sort | xargs wc -m
837 contents/00-mruby-handson-book.re
17106 contents/01-what-is-syspro.re
16604 contents/02-hello-mruby.re
28998 contents/03-creating-mgem.re
39834 contents/04-accessing-procfs.re
41093 contents/05-writing-c-in-mruby.re
43192 contents/06-implementation-of-file-stat.re
40665 contents/07-introduce-mod-mruby.re
68884 contents/08-write-mruby-module.re
44780 contents/09-secure-coding.re
5195 contents/10-afterwords.re
78864 contents/a1-mruby-debugging-tools.re
3402 contents/a2-mruby-3.re
429454 total
ちなみに、Re:VIEWコメントの一覧表示(TODOの把握のため)や、リスト内の1行が長すぎる箇所の検知などもRake taskを作って実施できるようにしていました。めちゃくちゃ雑なコードですが... 今回あまりにもRubyのコードがない記事なので、タスクの一部を公開してみます。
これが、リスト内のプログラムコードなどが長過ぎる箇所を検知、表示するRakeタスクです。状態付きスキャナみたいなのを自力で書いてて涙ぐましい。
LINE_MAX_SIZE = 90
desc "長いリスト内のコードを表示"
task :longline do
chap = ENV['CHAPTER']
res = Dir.glob("contents/*.re").sort
if chap
res.select! {|re| re.include?(chap) }
end
keys = []
warned_files = {}
warned = {}
list_in = nil
res.each do |re|
lines = File.read(re).lines.map(&:chomp)
lines.each_with_index do |l, i|
if list_in
if l =~ %r<^//\}>
list_in = nil
end
if l.size > LINE_MAX_SIZE
unless keys.include?(list_in)
keys << list_in
warned_files[list_in] = re
warned[list_in] ||= []
end
warned[list_in] << [re, i+1, l]
end
else
if l =~ %r<^//(list|cmd)(?:\[([-_\w]+)\])?>
if $2
list_in = $2
else
nr = re.scan(/\d\d/).flatten.first
list_in = "__cmd__#{nr}"
end
end
end
end
end
keys.each do |name|
fname = warned_files[name]
puts "#{fname}@\e[31m#{name}\e[0m:"
warned[name].each do |(re, i, l)|
puts "%-41s %s" % [re.sub("contents/", "") + (":%04d:"%i), l[0, LINE_MAX_SIZE] + "..."]
end
puts
end
puts "-" * 32
puts "#{keys.size} lists"
end
最後に
Rubyを用いてmrubyの本を書くための周辺Tipsを公開しました。
原稿自体もそうですが、必要な確認や周辺作業は 自動化 しないと厳しかったというのが実感です。みなさんも、いろいろな雑事をRubyで自動化しよう!(なんか綺麗にまとまったぞ?)
明日のご担当は @mtsmfm さんです!