Rubyアドベントカレンダー、7日目の記事です。前日はjerrywdleeさんでした。
今年の半分ぐらいの時間をかけて、「Webで使える mrubyシステムプログラミング入門」という本をリリースしました。
多くの知人に手に取っていただけているようで何よりですが、今回は、この本の執筆を支える周辺技術の話について、Rubyアドベントカレンダーの場をお借りして公開しようと思います。
Re:VIEW (Re:VIEW Starter) によるPDF生成の自動化
今回の本は、Re:VIEW というフォーマットを用いて執筆しています。
ただし、カスタマイズ性などの観点で、Re:VIEWの一種のディストリビューションである「Re:VIEW Starter」を用いて仮PDFを作成し、定期確認することにしました。
上記の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" # Original from https://hub.docker.com/r/kauplan/review2.5/dockerfile ENV REVIEW_VERSION 2.5.0 ENV REVIEW_PEG_VERSION 0.2.2 # ENV NODEJS_VERSION 14 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/* # noto mapfile: https://github.com/kauplan/docker-review/tree/kauplan/review2.5/noto 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に渡しています。参考:
タグを打って定期リリースするメリットは、レビュワーの方に依頼してから大きく享受できたように思います。レビュワーの方々には、 このタグに紐づくリリースの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 さんです!