FM2+KillerのLinuxセットアップ: 既存のManjaro Linux (Arch Linux系)環境をクローンする

あらまし

元々、メインのLinux環境はFM2+Killerだったが、Z400に移行したことで位置づけが変わった。

メインの作業環境はZ400上にあり、既に構築済みだ。
FM2+Killerは冗長環境として、同様にLinuxを構築し、いつでも使えるようにしておくのが望ましい。

可能であれば、ハードウェアも全く同じものを用意すれば、簡単にクローンできる。
だが、Z400とFM2+Killerでは色々と違いがある。特に大きいのはグラフィックスカードの種類と、ディスプレイの数の違いだ。

さらに、FM2+Killerは

  • 障害時にメインと同様に使うことができるアカウント
  • サテライト的に使う一時作業用アカウント
  • 彼女がうちにいる時に作業に使うアカウント

の3つをセットアップする

インストール

私が使っているのはManjaro Linuxである。当然ながら同じManjaro Linuxを導入する。

既にManjaro Linux 15.09がリリースされているが、15.09はUEFIにインストールすることができないバグがある。特に

0.8.13 XFceをインストールし、アップグレードする。
違いはあとから埋める。

通常どおり、yaourt -Syuuでアップグレードした上で、新しいカーネル(4.1)を導入する。4.2でないのは、AMDユーザーに勧めない、ということなので。

インストーラで作るユーザーはメイン環境と同じユーザーにすること。
でないと、UID/GIDの違いでディスクを差し替えただけでは動かなくなる。

パッケージを揃える

メイン環境と同じパッケージが入っていればもし作業環境を作るにしても、少ない手間で可能だ。

Arch Linuxにパッケージを揃える機能はなさそうだったので、スクリプトを書いた。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-


PLIST = File.exist?(".previous_autoyaourt_target_list") ? File.open(".previous_autoyaourt_target_list", "r").each.map {|i| i.strip[/^\S+/] } : nil 
TLIST = ARGF.each.map {|i| i.strip[/^\S+/] }
EXCLUDE = [ /nvidia/i, /^linux/, /^libva/, /catalyst/, /maxthon/, /^opencl/, /^ocl-/, /^libcl/ ]

File.open(".previous_autoyaourt_target_list", "w") {|i| i.puts(TLIST) }


loop do
  clist = IO.popen("pacman -Q", "r") { |io| io.each.map {|i| i.strip[/^\S+/] } }
  to_install = ( TLIST - clist ).delete_if {|i| EXCLUDE.any? {|r| r === i } }
  
  if to_install.empty?
    break
  end
  
  system("yaourt", "-S", *to_install[0, 15]) or abort "Yaourt failed!!!"
end

if PLIST && ! ( dlist = PLIST - TLIST ).empty?
  
  puts "*******************************************"
  puts "CLEAN UP PHASE"
  puts "*******************************************"
  
  
  
  system("yaourt", "-R", *dlist)
end

Manjaro Linuxは標準でyaourtは入れているが、Rubyが入っていない。
Rubyをインストールし、元となる環境で

pacman -Q > target-paclist

のようにした上でこれを持ってくる必要がある。そして

ruby yaourtsyncer.rb target-paclist

のようにするわけだ。

--noconfirmオプションはつけていない。問題が発生することがあり、またひとつずつ確認したほうが安全だからだ。

別にパッケージデータそのものを持ってきて(/var/cache/pacman/pkg/以下にある)インストールする方法もあるのだが、今回は1台クローンするだけの話だし、健康にビルドしていく。

事前にsudoのタイムアウトを外しておいたほうが良いが、visudoはあるのにviがない。先にviをインストールしておく。gvimをインストールしてリンクしておいても構わない。

もし手間を短縮するなら--noconfirmをつけてもいいのだが、いずれにせよ手をかけねばならない状態になったり、失敗した時にいちいち外したりしないといけなかったりする。

これでうまくいかないのが、旧リポジトリからいれているパッケージ、失われたパッケージ、壊れてしまったパッケージだ。

旧リポジトリから入れているのがxcursor-lcd-*、なくなってしまったのがjoyutils、などなど。
これらはmakepkgでビルドしたものに関してはパッケージを持っているだろうし、pacmanやyaourtで入れたのなら/var/cache/pacman/pkg以下にある。これを導入する。

グラフィックスドライバに関連するものについては個別の環境によるため、除外している。
カーネルは元々yaourtでいれるようにはなっていないので、これも除外。

ちなみに、fcitx-mozc-utやJava関連、inkscape-gtk3-bzrはビルドが非常に長いので流用したほうが良い。

ユーザー

残り2つのユーザーを追加し、設定する。

これらユーザーはこのコンピュータのローカルなものなので、好きなように設定して構わない。

サテライト環境ではsshfsを用いてマウントすることで、UID/GIDの差を吸収できる。

冗長環境

当然ながらユーザーの設定も、元のPCに合わせたものにしたい。

私の環境ではホームディレクトリの下のディレクトリにbtrfsサブボリュームがマウントされており、また別のディレクトリがEncFSのマウントポイントにもなっている。

単純に持ってきてしまうと、btrfsの膨大なデータをコピーしてしまうし(5TB近い)、EncFSのデータを復号化したまま持ってきてしまう。

これを避けるため、マスター環境のサブボリュームマウントポイントを同様に作り、sshfsでマウントし、

$ rm -rf (^(.mountpoint))(#qD)

して

$ rsync -avH -x --exclude=/.cache -e ssh "$HOSTNAME":./ ~/

これはなるべく、コンソール上で作業したほうがよい。rootでは、FUSEを使うため支障がある。

~/.cacheは特に同期する必要がなく、同期するととても時間がかかるので省略。--delete-afterなどはとても危険。

私の場合、~/.sshがシンボリックリンクなので失敗する。
この状態で削除とリンクを同時に行えばよい。リンク自体は別にsshfsを解除していても-fオプションで可能。

なお、これで気がついたのだが、こうしてまっさらにしてコピーしても、Cinnamonの壁紙とテーマはなぜか反映されず、ローカルの設定が保たれる。ローカルで設定した後に吹き飛ばしてもだ。

なお、このテストの家庭で全データを吹き飛ばしかけた。
幸い気づくのが早く、重要なデータの損失はなかった。

rsyncのファイルシステムをこえない-xオプションは非常に便利。

なお、autostart関連は除外したかったのだが、うまくいかなかった。
これは--excludeの書き方の問題。

Markdownで書かれたスライドをHTMLとポータブルPDFと印刷用PDFにする

Aki SI&EのPR用に書いたものだが、Markdownで書き、Web(HTML)、配布用PDF、印刷用PDFの3種類を生成する考えでいた。

手でやろうかと思っていたのだが、再生成の回数も多そうだったので、自動化できるようにした。

Markdownで記述したドキュメントと、オリジナルサイズの画像ディレクトリ、mogrify -geometry x300でリサイズした画像ディレクトリが用意されている。
Web用のものについてはHTMLで出力し、リサイズされた画像のWebサーバー上のコピーを参照しなければならない。

PDFについては、MarkdownをLaTeXに変換してという方法もあるにはあったが、画像を含めた細かなデザインを施したかったため、HTMLをベースにPDFに変換することにした。

MarkdownコンバータはRubyのKramdownを使用。改ページはないため、wkhtmltopdfを用いてPDFに変換することとした。
参照しているディレクトリの違いで2回ループを回す。

それぞれHTMLは別に出力し、テンポラリファイルとして出力したHTMLを元にPDFのテンポラリファイルを生成、unitepdfを使ってこれを結合する。

専用のコードなので多くをハードコーディングしているが、これで正常に機能する。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-

require 'kramdown'
require 'erb'

def treat_md(file, findex)
  $article_body = doc = Kramdown::Document.new(File.read(file)).to_html
  
  $article_title = doc[/<h1[^>]*>(.*?)<\/h1>/, 1]
  
  foot = Array.new
  if findex > 0
    foot << '<a href="' + ( SOURCEFILES[findex - 1].sub(/\.md$/, '')  + ".html" ) + '">前へ</a>'
  end
  
  foot << '<a href="/si/" target="_top">Aki SI&Eトップページへ</a>'
  
  if findex < ( SOURCEFILES.length - 1 )
    foot << '<a href="' + ( SOURCEFILES[findex + 1].sub(/\.md$/, '')  + ".html" ) + '">次へ</a>'
  end
  
  $article_footer = foot.join
end


TEMPLATE = <<'END'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" http://www.w3.org/TR/xhtml1/dtd/xhtml1-transitional.dtd">
<html>
  <head>
    <title><%= $article_title %></title>
    <meta http-equiv="content-language" content="ja" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <style>
body {
% case $mode
% when :Web
  font-size: 130%;
  color: #333;
  font-weight: bold;
  margin: 0px;
  padding: 0px;
% else
  font-size: 18pt;
  color: #000;
  font-weight: bold;
  margin: 2cm auto;
  padding: auto;
% end
  background-color: #fff;
  font-family: "Migu 1C"
}   

h3 {
  font-size: 135%;
}

#Container {
  margin: auto auto 100px;
  max-width: 950px;
  text-align: center;
}

img {
% case $mode
% when :Web
  height: 300px;
% else
  height: 18em;
  max-width: 100%;
% end
}

ul {
  text-align: left;
  margin: auto 1.8em;
}


#ArticleFooter {
% if $mode != :Web
  display:none;
% end
  position: fixed;
  bottom: 0px;
  margin: 0px;
  width: 100%;
  max-height: 100px;
  background-color: #339;
  color: #fff;
  text-align: center;
}

#ArticleFooter a {
  padding: 0 0.8em;
  text-decoration: none;
  color: #fff
}

#ArticleFooter a:hover {
  text-decoration: underline;
}
    </style>
  </head>
  <body>
    <div id="Container">
    <%= $article_body.gsub(
      %r{src="img/},
      %(src="#{$img_type}_img/)
    ) 
    %>
  </div>
    <div id="ArticleFooter">
      <%= $article_footer %>
    </div>
  </body>
</html>
END

$article_body = nil
$article_footer = nil
$article_title = nil

SOURCEFILES = Dir.entries(".").select {|i| i =~ /\.md$/ }.sort

# HTML
$img_type = "/img/si/intro_slideshow/resized"
$mode = :Web

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  File.open("out/#{x.sub(".md", ".html")}", "w") do |f|
    f.puts(ERB.new(TEMPLATE, nil, "%<").result)
  end
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

# Web PDF
$img_type = "resized"
$mode = :PDF

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  fn = "out/_tmp_#{x.sub(".md", ".html")}"
    File.open(fn, "w") do |f|
      f.puts(ERB.new(TEMPLATE, nil, "%<").result)
    end
  system('wkhtmltopdf "%s" "%s"' % [ fn, fn.gsub(".html", ".pdf") ])
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

system 'pdfunite out/_tmp_*.pdf out/portable.pdf'
system 'rm out/_tmp_*'

# Printable
$img_type = "original"
$mode = :PDF

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  fn = "out/_tmp_#{x.sub(".md", ".html")}"
    File.open(fn, "w") do |f|
      f.puts(ERB.new(TEMPLATE, nil, "%<").result)
    end
  system('wkhtmltopdf "%s" "%s"' % [ fn, fn.gsub(".html", ".pdf") ])
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

system 'pdfunite out/_tmp_*.pdf out/printable.pdf'
system 'rm out/_tmp_*'

完成したものについては

Google DOCs Viewerはスライドショー向きではないので、ウェブ版を使うか、PDFをダウンロードするのがお勧め。

PureBuilder2(3) Markdown for Blog

今やブログは主に Markdownで書かれている。

当初、blogtrというPuredoc用のスクリプトを書いたが、ブログ程度であれば圧倒的にMarkdownで事足りることが多いからだ。
実のところ、ACCSコンテンツなどでもMarkdownで十分なケースは多く、そのためにPureBuilder2はMarkdownをサポートする。

Markdownを変換し、ヘッダーを操作するblogmdはPureBuilder1.5で既に実装されたが、これはPandocを使ったスクリプトである。
PureBuilder2は全面的にRubyでいく予定であるため、MarkdownトランスレータにはKramdownを使用している。

これに合わせてKramdownを採用することにした。
機能的にはほとんど変わらないが、唯一の違いとして、手前に

* TOC
{:toc}

を入れるようにした。
これにより、KramdownはTOCを自動生成する。Pandocにはなかった便利な機能で、ちゃんとオフセットしたTOCを生成してくれる。

ちなみに、PureBuilderのMarkdown用ライブラリがKramdownをモンキーパッチングで拡張し、自動的にPureDocオブジェクトのメタ情報としてヘッダーを埋め込むので、XHTPureDocを使ってTOCを作ることもできなくはない。

まだpush予定はないため、コードを掲載する。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-

require "purebuilder/purebuilder"
require "kramdown"
require "optparse"

opt = {}
op = OptionParser.new

op.on("-m", "--marshal") {|v| opt[:marshal] = true }
op.on("-M", "--without-meta") {|v| opt[:marshal] = false }
op.parse!(ARGV)


# String for Kramdown's TOC
TOC_PREFIX = "* TOC\n{:toc}\n\n"

# Get article file
sourcefile = ARGV.length == 1 ?  ARGV[0] : nil
sourcestr = ARGF.read

pbp = PureBuilder::Parser.new(sourcestr, sourcefile)
pbp.proc_header

html = Kramdown::Document.new(TOC_PREFIX + sourcestr.gsub(//m, "") ).to_html

if opt[:marshal]
  Marshal.dump({body: html, head: DOC.meta}, STDOUT)
else
  puts html
end

これだけ見るとシンプルだが、PureBuilderとの関係が深く、結構中まで突っ込んでみることになってしまった。
PureBuilderの設計があまりよくないのかもしれないとも思ったが、そもそもPureBuilderのMarkdownサポートは「MarkdownをPureDocに見せかける」ものなので、PureBuilderとPureDocの密結合はやむをえまい。

これで初めて動かすこととなったPureBuilderだが、これによってPureBuilder、さらにPureBuilder登場によって修正されたPureDocのバグが発見され、デバッグにかなりの手間を費やした。

基本的にはシンプルだが、オプションへの対応を加えたため、いくらか複雑化した。
オプションは、将来的にパイプしてAPI経由でのブログアップロードに対応するため、メタを含めたMarshalで渡すためのものだ。

Tweets保存のためのMikutterプラグイン

とりあえずコード

# -*- coding: utf-8 -*-
require 'json'

Plugin.create(:save_timeline) do
  
  logdir = "#{ENV['HOME']}/.mikutter/plugin/save_timeline/log"

  on_update do | service, messages |
    File.open("#{logdir}/#{BOOT_TIME.strftime("%y%m%d%H%M%S")}.#{service.user || "default"}", "a") do |f| 
      messages.each do |msg|
        f.puts JSON.dump msg.instance_variable_get(:@value) rescue puts $!
      end
    end

end

#  on_direct_messages do |service, dms|
#  end

end

プラグインの書き方を調べながら書いた。 色々インスペクションしたので、そこに時間がかかった。内容的には難しくない。

messagesArrayなのだけれど、その各要素はmsg.inspectするとHashに見えるが、実際はMessageクラスのオブジェクトだった。 Messageクラスはその内容をそのまま出力するメソッドがないようなので、msg.instance_variable_get(:@value)の形でデータを取得している。 もちろん、@valueに値が格納されていることを確認するまでが一番手間だった(全体で言えば、Messageクラスであることになかなか気づかなかった部分に時間がかかった)。

JSONライブラリは出力に際して1行にまとめてくれるため、単純に行出力していけば、行単位でパースして処理できるログファイルができあがる。

このあと、flockに対応させた。

ダイレクトメッセージも対応したかったが、on_direct_messagesの取り扱いがよくわからなかったので、そのままにした。

追記

GitHubにてコードは公開中。

Mikutterのプラグインページにも掲載させていただいた。

PureBuilder2 (2)

Kramdown拡張でPDocオブジェクト化

PureBuilder2はもともと思っていたよりもかなり大規模なものになっているが、MarkdownオブジェクトをPureDocと同様に扱えるようにする、というのが今回のテーマ。

例えばテンプレートで

DOC.body

のように書かれている場合がある。この場合は当然、HTMLへ変換したのであればHTML文字列が得られなくてはいけない。 また、

DOC.meta["title"]

のようにもアクセスできる。 それだけなら単にアクセッサを拡張してやればいい話なのだが、PureDocオブジェクトはTOCのためのループ機能が組み込まれている。 これにより章立てをループさせることができ、簡単に任意の形式でTOCを組める。 これはどうしてもパース時に情報を取らなくてはいけない。

もし、HTMLに出力するものである、というのであれば、単純に結果のHTMLをパースして取得する方法もある。 だが、KramdownライブラリはLaTeXとPDFをサポートする。PureDocもゆくゆくはLaTeX形式での出力をサポートする予定である。

であれば、やはりKramdownでのMarkdownパース時にTOCを作りたい。

基本的な方針としては、実際にPureDocオブジェクトを使用する。 これはパーサ/コンバータを含まないベースクラスで、本来は直接このクラスのインスタンスを生成することは想定していなかった。 だが、外側から使用するメソッドは一通り持っており、インターフェイスは揃っている。

DOC.bodyで返すべき@bodyDOC.body=を用いて入れ、DOC.metaに関してはPureDocクラスが持っている機能によってドキュメントから取り込むといったことが可能。 そのため、DOCPureDocインスタンスであり、Kramdownの結果はDOC.body=によって入れるだけだ。

だが、DOC.stock_ehaderを用いてヘッダを入力し、TOCを生成できるようにしなければいけない。 そこで、Kramdownに手を入れる必要があった。

ソースコードを追っていったが、結局Kramdown::Parser::Kramdown#new_block_elをオーバーライドするのが良いと分かった。 ヘッダを取得するパートはあるが、new_block_elメソッドはメソッド自体が短く、あくまでパース時に各エレメント対して呼ばれるものだ。何のために呼ばれているかを判定する必要もなく、引数を丸々渡すだけで良いため、overrideしやすかった。

require 'kramdown'

# Override Kramdown
class Kramdown::Parser::Kramdown

  alias _new_block_el_orig new_block_el
  

def new_block_el(*arg)

   	if arg[-1].kind_of?(Hash)
    
      case arg[0]
      
      # Is Header?
      when :header
        p arg[-1][:level]
        p arg[-1][:raw_text]
      end
      
    end
    
    _new_block_el_orig(*arg)

end end

p Kramdown::Document.new(ARGF.read).to_html

というテストコードを書き、実際に動作することを確認、when :header部分を

::DOC.stock_header(arg[-1][:level], arg[-1][:raw_text])

と書き換えた。

KramdownはPure Rubyで書かれているため、扱いやすいし、ソースコードを書くのも楽だ。 だが、できればサブクラス化するなど、もう少しスマートな方法でできればよかったな、と思う。クラスが細かく分割されて連携しているため、置き換えるのはかなり難しいと判断した。

Kramdownは非常に良いライブラリなのは間違いない。

forkの代わりに

RubyのKernel.forkをはじめとするfork機能(例えば、IO.popen-を渡すことを含む)はWindowsでは動作しない。 Perlerだった私としてはこれはかなり不満な点だ。Perlはコミュニティの努力により、forkがWindows上で動作する。これは、Windows版Perlではforkをエミュレートするためだ。

今回は、設定やドキュメントオブジェクトなどをセットアップした状態で、forkによって環境を独立させたいと考えていた。 これはグローバルなオブジェクトに変更を加えるためであり、また出力先の制御をSTDOUT.reopenによって行うことができるかということについて考えていたためだ。

RubyのforkとWindowsについて検索すると、「forkは邪悪だ、threadを使え」という内容があふれる。 だが、今回は並列化のために使いたいわけではないため、Threadは用を成さない。

また、大量のドキュメントを変換する際のオーバーヘッド低減という目的もある。

Unicorn(Webアプリケーションサーバー)がこのforkによるCOWを活用した設計となっている。Unicornはどうしているのかと調べてみたら、UnicornもMongrelもWindowsでは動作しないらしい。

というわけで、forkの利用は諦めて、グローバルな名前に対する変更をいなす方向とした。

グローバルな名前のオブジェクトが変更されるのは、ほとんど

DOC.is {
...
}

という書式で記述するためだ。 これはPureDocドキュメントを分かりやすく記述するためであり、実際にテンプレートもDOCオブジェクトを利用したデザインとなっている。 つまり、DOCはthe PureDoc objectであることを期待している。

この設計を維持するため、Delegateライブラリを使用することとした。 実体はDOCではなく、DOCはただのDelegatorというデザインだ。これはDOCに実体はなく、ただの代名詞となるわけだ。

DOC = SimpleDelegator.new(nil)

とすることにより、まずDOCという名前を用意しておく。 実際に新しいドキュメントを生成する場合は、

::DOC.__setobj__ @@config[:puredoc_class].new

のようにする。 これにより、DOCが意味するドキュメントを入れ替えることができ、DOCを変更しても、変更されるのはDOCではなく、移譲されているドキュメントであり、DOCをまた新しいドキュメントにすることもできる。

PureBuilder2

PureBuilder2とは

PureBuilder2は現行のPureBuilderを置き換える新しいサイトビルドツールだ。 コマンド一発でウェブサイトの更新が可能になる。「動的生成の事前作業化」が可能となる。

PureBuilderからの主な変更点は次の通り

  • Rubyでの実装 (Windowsで動作可能に)
  • MarkdownとeRuby対応

変更点は少ないようにみえるが、PrueBuilderとは互換性はないし、コードも新規に起こす。

Markdownへの対応

Markdownへの対応はKramdownライブラリを使用した。 非常に使いやすく、問題はないように見えた。 何を何に出力するかは、指定クラスの入れ替えによるポリモーフィズムによる。

このラッパークラスはごく簡単だと思ったのだが、そうはいかなかった。

現状、PureDocはライブラリであり、ドキュメントがRubyコードである。 これを出力するためのユーティリティはZshスクリプトになっている。

PureBuilderはその多くをPureDocに依存している。 PureBuilderが直接に依存していなくても、テンプレート側はPureDocオブジェクトを触れることになっているし、現状テンプレートを呼ぶところまでがPureBuilderの仕事なので、当然テンプレートではドキュメントを出力するために、PureDocオブジェクトを必要とする。

だが、当然ながらKramdownオブジェクトはPureDocオブジェクトと互換性がない。 機能を維持するためには、単にKramdownを呼び出すラッパーではなく、Kramdownを内部で使うPureDoc互換クラスが必要となる。

予定とは比べ物にならないほど大変な作業だ。

PureDocのインターフェイス

加えて、今のところPureDoc Translatorが保持している機能については、PureBuilderから使用することができなくなる。 旧来のPureBuilderは、コマンドとしてPureDoc Translatorを呼んでいたため問題がなかったが、ライブラリとして使うとTranslatorは使えない。

PureDocにその機能があるにはかかわらずPureDocに組み込まれていないのは、PureDocの仕様によるものだ。

PureDocの

##-----
...
##-----

という形式でYAMLをヘッダーとして埋め込めるという仕様は、PureDocにはないが、便宜上の拡張としてTraslatorにあり、PureBuilderはそれを前提として使用する。

これをPureDocに組み込むのであれば、PureDocクラスにその機能をいれてしまえば良い。 要はこの仕様をPureDocに取り込むか、PureBuilderに取り込むかの話なのだが、おそらくはPureDocに取り込むのが妥当なところ。

一方同じようにこのヘッダを取り扱いながら、ヘッダにLast UpdateとSinceを書き込む機能があったりするが、これはあきらかにPureDocではなくPureBuilderに実装されるべき機能だ。

一応、いまのところ次の方針を考えている。

  • meta取り込みはPureDoc#read_meta(text)で行う。このヘッダはコメントになっているので、テキストを与える必要がある。これはPureDocライブラリが勝手に実行することはない
  • PureBuilderはpuredoc.metaによりsincelast-updateを確認し、ない場合は追記する

PureBuilder2のおおまかなモデル

purebuilder本体はRubyライブラリとなり、基本的には各ディレクトリの.rebuild_rulesまたは.up*ファイルが処理手順となる。

これらをまとめて呼ぶためのスクリプトが、purebuilder.rebuildallになる。

対象ファイルに対して

PureBuilder.build(file, outputdir, extname)

とすることでビルドできる形だ。

インタープリタ起動役は

rebuildallでrebuildスクリプトのインタープリタは拡張子によって判断するが、拡張子がない場合はperlを使う。

これは、perlはshebang行を解釈するためだ。この機能はsh/bash/zsh/rubyにないことを確認している。 Perlは「Shebangを解釈できないダメなシェルに代わって」起動するそうだが、どうやらLinuxが解釈するだけで、シェルに解釈を期待するのは厳しそうだ。

MailDeliver2 : 完全にプログラマブルなフィルタ機能を持つMDA

MailDeliver2がついに完成した。 おおよそできてはいたのだが、テストできておらず、テストしたらまだまだ足りない機能があることに気づき、 また不完全な部分が多かったため、2日かけてデバッグ、テスト、修正を繰り返した。

概要

保存・配送

Mail Deliverは完全にプログラマブルなフィルタを持つMDAと、それを補助するユーティリティ群である。 フィルタ、ソーター、通知の基本機能を備える。

基本的にはLinuxあるいはUNIX向けで、MH形式のメールフォルダに対して保存することになっている。

localdelivがMDAだ。その役割としては、フィルタを呼び出し、フィルタに従って処理することにある。 フィルタはメールの保存の機能を兼ねる。もしフィルタがメールを保存も破棄もしなかった場合は、localdeliv自身がデフォルトのメールフォルダへと保存する。

デフォルトのメールフォルダもフィルタ同様の方法で決定することができる。 MailDeliver2はRubyで書かれており、保存先メールフォルダの決定はRubyのprocオブジェクトによって行われる。このprocオブジェクトにはメールのヘッダの情報に加え、それを取り扱いやすくした情報を加えたメールオブジェクトが渡される。 そのため、静的な文字列ではなく、より動的に保存先を決定することができる。 ほとんどの場合、これで事足りるだろう。

例えば、設定サンプルでは次のように書かれている。

DefaultRule: ->(mail) { "inbox/address/#{mail.in || "Default"}/#{mail.domain || mail.address }/#{mail.address}" },

これはつまり、

inbox/address/<ユーザーのアカウント名 | Default>/<相手のドメイン名>/<相手のメールアドレス>

というフォルダに保存される、ということを意味している。 多数のメールアドレスを使用して使い分けている人は、そのメールアドレスによって相手の意味は既に分かれているだろう。まず、自分のどのアカウントに送られたものかをフォルダで分けてしまうことで相手を分類でき、この3段階の分け方があれば「だいたい分かる」。

ちなみに、副次的な効果として、詐欺にひっかかりにくくなる。受け取りアドレスとドメインを必然的に意識するからだ。 メールの「名前」をAmazonにしてお知らせを装った詐欺メールを大量に受け取っているが、ドメインがAmazonでないためすぐ分かる。 銀行関連もすぐわかるだろう。なぜならば、名前はメールを開くまでわからないが、アドレスは到着した時点から既に意識しているからだ。

フィルタは完全にプログラマブルであり、Rubyで自由に書くことができる。 もちろん、保存と破棄はよくある処理なのでそれを助ける機能がある。

さらに、保存してそのメールの処理を完了するかどうかは自由に選べる。 例えば複数のフォルダに保存するとか、特別に直ちに通知するとか、あるいは保存はするけれど通知の対象からは外す、ということも可能だ。 destroymail()は通知のためのファイルを削除するため、savemail()のあとdestroymail()すると通知はされない。

フィルタは条件自体がプログラマブルなので、例えば差出人によってのみ判定できる、というようなことはない。条件に単にtrueと書けば、常に適用されるフィルタが書ける。 現在デフォルトではclamAVしかサポートしないが、もっと強力な何らかのフィルタによって判定することもできる。その場合は、その判定処理を条件式に書けば良い。条件式に渡されるメールオブジェクトから完全なメール本文を得ることも可能で、IO.popen$?を用いて外部プログラムを条件として使用できる。

そのためにルール記述がやや難しいことは否定しないが、最低限であればマニュアル通りに記述することで、常識的に判断できる人ならば動かすことはできるだろう。

通知

通知機能はプラグイン方式を取っている。

そのため、して欲しい通知方式を好きに組み合わせることができる。 現在はnotify-sendを用いたGUI表示機能と、play(SOX)を用いた音声通知機能を備える。

ディスプレイ表示機能はプログラマブルではない。選択式だ。 「アドレス」または「Fromの値」のどちらかで、各差出人あたりの通数か、 または総数を通知する。

音声通知機能はプログラマブルだ。 音声ファイルは静的に指定しなければならないが、適否に関してはProcオブジェクトなので、その気になれば判定とは関係のないプログラムを書くこともできる。

その重要性は、アドレスのマッチングにしても

  • 完全一致
  • case insensitiveな一致
  • Glob
  • 正規表現

とそれらの否定から選べることにある。また、論理和、論理積も使用可能だ。 一致だけでは大量のルールを書かなくてはいけない場合に、同じファイルを再生させる場合も楽になる。 設定ファイルの中でインスタンス変数を定義しておくことで、例えば複数のアドレスをマッチさせる、というようなことも可能だ。 マスター設定ファイル内でアドレスごとのグループを設定しておくこともできる。

何がしたかったかというと、「ケータイメールのような使い心地」だ。 ケータイならば、着信音で相手が分かる。通知で、メールを開かなくても誰から来たかも分かる。 「着メロ」機能があるMUAは極めて少ない。着メロ機能と強力な振り分け機能を兼ね備えるものはない。 そもそも、振り分けのルール記述が極めて面倒だ。

そのような、大量のメールを受け取る人が、効率的にルールを書くことができ、メールを確認したり処理するための効率を大幅に向上させる、画面にかじりついていなくても、その時がきたことをアクティブに教えてくれる、という使い心地を、MUAではなく、MDAで実現した形となる。

1からの変更

まず、完全にRubyになった。 フルプログラマブルにするために、今まで開発効率からZshを使用していた部分に関しても、全てRubyとした。外部プログラムでなく、ライブラリの呼び出しとすることで、連続したマッチングも高速化でき、また渡せる情報も大幅に増えた。

一部手段としてUNIX系OS固有のものを使用しているが、恐らくWindowsに対して移植可能なものになったというのもメリットだろうか。 問題はKernel.forkを使用していることと、playコマンドやnotify-sendを使っていること、Sound Notifyはログを/dev/nullに書いていることだろう。

当然、このこととセットになって、よりプログラマブルになった。 そもそもの出発点が、フィルタが保存する内容には関与できない(保存するフォルダを出力するだけだったため、必ず保存されるし、保存内容を加工することもできない)という点が要求を満たさないケースがあったことについてだ。

これに対応したため、メールの破棄や、保存内容の加工も可能になった。 メールオブジェクトがメール全文を持っており、それがそのまま書き込む内容でもある。 メールを破棄した場合は、通知にも残らない。

構造がすっきりして、手を加えることもしやすくなった。 これまで通知系はプログラム自体を変更して、手元で専用のものにしていたが、汎用性があるものとなり、 単にプラグインフォルダに入れているものが適用されることとなった。

このあたりは、自分用だったものが、使ってもらうことを考えた変更が加えられたといっていい。

このほか、開発効率を優先して非常に複雑な構成(トリッキー)だったプログラムが、しっかりと設計されたものに変更されたため、挙動を把握しやすく、プログラマブルな部分がちゃんと活かせるようになった。 従来はフィルタが動くはずのものを書いても動かないことが多く、デバッグも難しかった。 今回はデバッグしやすいようにログもわかりやすいしてある。 これは、プログラマブルなためにユーザー定義部分でプログラムのエラーがでる場合が多いからだ。

プログラム

見どころは多いが、いくつか紹介。

プラグイン

単純にロードしているが、

class StandardNotify
  PLUGINS = []

と名前に約束を作り、プラグインは自身のオブジェクトをStnadardNotify::PLUGINSにpushする。 プラグインはfireメソッドをインターフェイスとして義務付けられている。

メールの準備

ヘッダとボディは次のようにして取得。

head, body = NKF.nkf( "-w -Lu -m", mailstr ).split(/\r?\n\r?\n/, 2)

ヘッダは次のコード

    head.each_line do | l |
      if l =~ /^\s*$/
        break
      elsif l =~ /^\s+/
        headerlines.last.concat(l.lstrip)
      else
        headerlines.push(l)
      end

    end
    
    headerlines.each do |i|
      if i =~ /\A([-_A-Za-z0-9]+)\s*:/ # match header format?
        mailobj[$1.upcase] = $'.strip
      else
        next
      end
    end

caseやspaceなどを守らない変なメールに対応するための措置を取っている。

メールアドレスの抜き出し

恐らく最もテクニカルだ。

  def extract_addr(f)
    if f =~ /(?:[^"<]*(?>"[^"]*"))*<([^>]+)>/ # Do From term have NAME<addr> format?
      address = $1.delete("\" \t/")
    else
      address = f.delete("\" \t/")
    end
    
    address
  end

単に仕様だけでなく、実際に使われている形式に則っている。 アドレスをクォートしているものに対しては対応しない。

メール方向の判定

    if @maildeliv_conf[:MyAddress].any? {|k, v| in_k =k; File.fnmatch(v, from) }
      mailobj.direction = :send
      mailobj.address = to
      mailobj.in = in_k
      
    elsif @maildeliv_conf[:MyAddress].any? {|k, v| in_k =k; File.fnmatch("*" + v + "*", mailobj["TO"]) }
      mailobj.direction = :recieve
      mailobj.address = from
      mailobj.in = in_k
      
    else
      
      mailobj.direction = :unknown
      mailobj.address = from
      mailobj.in = nil
    end

fromtoもアドレスを抽出したものだ。 差出人アドレスがマイアカウントとして定義されたものと一致するか?をチェックしている。 ちなみに、Toは単一とは限らないので、Fromを先に判定するのが確実で好ましい。

Linux Tips

YouTubeのプレイリストからタイトルを抽出する

結局使わなかったのだが、ワンライナーで書いた。 比較的素直なHTMLなので解析は簡単。行指向ではないので、PerlでなくRubyにした。

$ curl 'https://www.youtube.com/playlist?list=<playlistid>' | ruby -e "s = STDIN.read" -e 's.scan(/<a class="[^"]*pl-video-title-link[^"]*"[^>]*>(.*?)</m) {puts $1.strip }' | grep -v 動画は削除されました

ffmpegでh.264/aacな360pのmp4を

元動画は1080pのmovまたはmp4。 オーディオはいじらず、元々aac(ac3)。

$ ffmpeg -i <infile> -vcodec libx264 -s 640x360 -crf 34 -strict -2 <outfile>.mp4

ちなみに480p(16:9)は720×280。 -crfの値は18-28が推奨されている(小さいほど高ビットレート)が、今回はモバイル向けなので34を指定。

なお、6の増減でビットレートはおよそ1:2の変動となる。

ffmpegでCowon M2向けの動画を作る

COWON M2はXVidとmp3のAVI動画で、解像度は320×240またはWMVをサポートするとある。

WMVだと結構サイズが大きいので、AVIで作る。 ソースは前回と同じくh.264*ac3のMOVまたはh.264*m4aのmp4。

$ ffmpeg -i <infile> -vcodec libxvid -acodec libmp3lame -b:v 372k -b:a 128k -s 320x240 <outfile>.avi

随分としょぼい解像度の上にアスペクト比も壊れる(プレイヤー側で調整することは可能)が、案外見られる。 ただし、360pでも細部は潰れてしまっているのでよく分からない部分は出てしまう。

XineのUIの文字化けを直す

fontにHerveticaを要求しているので、フォントエイリアスを設定すれば良い。

ネストされた構造のためのPureDocのTOC展開

ネストTOC機能

文書からTOCを作る上で、やはり構造的にネストしたいことはあると思う。 最もポピュラーなのは、ulをネストさせることだろう。

だが、難しいのは、例えば最初にh4が来て、次にh2が来て、などということがありうるのだ。そして、h3は存在しないかもしれない。

間の全てのレベルが存在することにするのか。順に礼儀正しく登場すると仮定していいのか。

結局だが、汎用性のある仕様として次のようにした。

  • 最低レベルはオフセットかまたは実際に使われた最も大きいヘッダーに基づく(数え方としてはmin)
  • レベルの変遷に応じて変遷分proc4openproc4closeを呼ぶ。例えば->(l, ol) { "<ul>" }のように書く。
  • 当該レベルまではopen/closeした後はproc4eachを呼ぶ。

    def nest_expand(proc4open, proc4close, proc4each, offset=nil) result = [] mi = self.min { |i| i.level } or return nil mi = mi.level

     if ! offset.respond_to?(:to_int) || offset > ( mi - 1 )
             offset = mi - 1
     end
    
     cur = offset
    
     self.each do |i|
         if i.level > cur
             (i.level - cur ).times {|n| result << proc4open.call( ( i.level - (i.level - cur - 1 - n) ), ( i.level - offset - (i.level - cur - 1 - n) )) }
         elsif i.level < cur
             result << (cur - i.level).times {|n| proc4close.call( (cur - n), ( cur - n - offset ) ) }
         end
    
         result << proc4each.call(i.level, (i.level - offset), i.title)
    
         cur = i.level
     end
    
     result.join

    end

eRubyでは内部のメソッドがputsすればいいような言い方をされることが多いが、それは先に出力されてしまっていたので、置換できるようにするために一旦配列に格納した。

テンプレート側の記述量が多く、また直感的でないというデメリットはあるが、なんとかうまく処理できた。

instance_evalと定数

しかし、むしろ苦戦したのは、ProfileでTOCを含めることだった。

Profileは基本的にそれ自体がPureDocを拡張したRubyコードである。

文章としてヘッダーを含めているわけでもないので、TOCを作るためのとっかかりがないのだ。

そこで結局は

  • テンプレート側でテーブル手前にリンクを貼る
  • profileであとから各カテゴリをヘッダとして登録する

という方法を取ったのだが、意外な理由でうまくいなかった。 というのは、

instance_evalで評価した場合、そのコンテキストが認識する定数にアクセスできない」

のだ。PureDocはソースをObject#instance_evalを使って解析するため、この問題にひっかかっってヘッダーの登録ができなかった。

そこで、PureDocに登録用のメソッドを追加することとなった。

簡単に書いているが、profileは整頓されていない部分が多く、結構大変だった。

Rubyのサブクラス内のスーパークラスのネストされたクラス

意味が分からないと思うが、ちょっとした疑問だ。

class A
  class B
  end
end

Class AA < A
  B
end

このコードではAAの中のBA::Bになる。 つまり、AAのコンテキストの中でBは使用可能だ。

class A
  class B
    def hi
      puts "Hi"
    end
  end
end

class AA < A
  class B
    def hihi
      puts "HiHi"
    end
  end
  
  def initialize
    @b = B.new
  end
  
  def b
    @b.hi
    @b.hihi
  end
end

aa = AA.new
aa.b

このコードでは

class AA
  class B
  end
end

AA::Bが作られ、@b#hiがないためエラーとなる。

class A
  class B
    def hi
      puts "Hi"
    end
  end
end

class AA < A
  class B < B
    def hihi
      puts "HiHi"
    end
  end
  
  def initialize
    @b = B.new
  end
  
  def b
    @b.hi
    @b.hihi
  end
end

aa = AA.new
aa.b

この場合は、

class B < B

によって、A::BをスーパークラスとするサブクラスAA::Bが作られる。

この定義によってBという名前がオーバーライドされることとなる。

class A
  class B
    def hi
      puts "Hi"
    end
  end
end

class AA < A
  B = B
  class B
    def hihi
      puts "HiHi"
    end
  end
  
  def initialize
    @b = B.new
  end
  
  def b
    @b.hi
    @b.hihi
  end
end

aa = AA.new
aa.b

これが本来意図するところだ。 オープンクラスを用いてスーパークラス内で定義されたクラスを拡張したいのだろう。

そこで

B = B

によって

AA::B = A::B

とした上でクラスを開けば良いのだ。

だが、今回の場合はPureDocのために実験した。PureDocではサブクラス内での#instance_evalによって評価された時に呼ばれるメソッドが名前でこのクラスのインスタンスを生成するため、あくまでもサブクラス(AA相当)の中に閉じ込められたクラス(B)でしかない。

ということは、そのクラスは

AA::B = A::B

ではなく

AA::B < A::B

であることが本来望ましいのではないだろうか。