そろそろ、以前から気になっていた Sphinx を触ってみようかと。スタンドアロン版ソフトウェアをこよなく愛す私は、当然のようにスタンドアロン版を選択。(以降、スタンドアロン版インストーラー SphinxInstaller-1.2.20131210-py2.7-win32.zip を使用)
しかし使ってみて、コレジャナイ・・・orz。うん、私が悪かった。「スタンドアロン」ではなくて、「ポータブル」と言うべきだった。上記はスタンドアロンではあるが、ポータブルではない。具体的には、インストールしたディレクトリを別の場所に移動すると動かなくなる。これでは zip で固めてメンバーへ配布、という訳にはいかない。
調べると、bin/sphinx-*-script.py 中に絶対パスが記述されているのが原因のようだ。特に shebang 行がまずい。shebang は bin/sphinx-*.exe が bin/sphinx-*-script.py を実行するために使用するのだと思うが、shebang は絶対パスでないと動かない。
ここで諦めようとも思ったが、何か悔しいので「起動時に毎回 sphinx-*-script.py を書き換える」という力技に出てみた。
$SPHINX_HOME/sphinx.bat:
@echo off
setlocal
set _dp0=%~dp0
set SPHINX_HOME=%_dp0:~0,-1%
set _dp0=
set PYTHON_HOME=%SPHINX_HOME%\python
set PATH=%PATH%;%SPHINX_HOME%\bin;%PYTHON_HOME%;%PYTHON_HOME%\Scripts
python.exe "%SPHINX_HOME%\bin\setup-portable.py" -x >NUL
if errorlevel 1 (
echo ERROR: failed to setup
pause
exit 1
)
title PortableSphinx
echo *** Welcome to PortableSphinx! ***
echo.
cmd.exe /k
$SPHINX_HOME/bin/setup-portable.py:
#!python.exe
__doc__ = """\
Usage:
python %s -x
Description:
Setup PortableSphinx. This should be executed first whenever the
Sphinx directory is moved on your PC.
""" % __file__
import filecmp
import glob
import os
import re
import shutil
import sys
import tempfile
_sphinx_home = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _create_tempfile(lines=None):
path = ''
try:
with tempfile.NamedTemporaryFile(delete=False) as fh:
path = fh.name
if lines:
fh.writelines(lines)
return path
except StandardError:
_remove_file(path)
raise
def _remove_file(path):
try:
os.remove(path)
except StandardError:
pass
def _read_file(path):
with open(path) as fh:
return fh.readlines()
def _write_file(path, lines):
tmp_path = _create_tempfile(lines)
try:
if not filecmp.cmp(tmp_path, path):
shutil.copyfile(tmp_path, path)
finally:
_remove_file(tmp_path)
def _convert_script(lines):
lines = iter(lines)
for line in lines:
yield '#!"%s"\n' % os.path.join(_sphinx_home, 'python', 'python.exe')
break
sphinx_home = _sphinx_home.replace('\\', '\\\\\\\\')
re_library = re.compile(r"^(\s*').+?(?=\\\\(eggs|python)\\\\)", re.I)
for line in lines:
yield re_library.sub(r'\1' + sphinx_home, line)
def _setup_bin():
script_glob = os.path.join(_sphinx_home, 'bin', 'sphinx-*-script.py')
script_paths = glob.glob(script_glob)
if not script_paths:
raise RuntimeError('not found: %s' % script_glob)
for script_path in script_paths:
lines = _read_file(script_path)
_write_file(script_path, _convert_script(lines))
def main():
status = 1
args = sys.argv[1:]
if args and args[0] == '-x':
_setup_bin()
print 'OK'
status = 0
else:
print __doc__,
return status
if __name__ == '__main__':
sys.exit(main())
スタンドアロン版 Sphinx のインストール先に上記を突っ込んで、毎回 sphinx.bat から起動するようにすれば、擬似 PortableSphinx の出来上がり。後はディレクトリを zip で固めればそのままプロジェクトメンバーに配布できる。今のところ、私が使う範囲で不具合は出ていない。(不具合がないとは言っていない)
さて、実際に Sphinx を使ってみると、確かにこれはなかなか良い。懸念していた日本語ファイル名(CP932)も問題ないし、印刷時の改頁も(組み込み機能にはないものの)制御可能。簡単なブロック図であれば Sphinx 上で生成できる。正に、至れり尽くせり。うん、きっとこれはハヤル。(実際にはずっと前から流行っている)
Sphinx の「ソースを編集 → make → 動作確認」という流れは、昨今のプログラマーには受け入れ難いだろうが、古き良き時代のプログラム開発みたいで老年プログラマーには逆に心地良い。私も最初はウキウキしながら make していたが、デザイン調整などで CSS を書き換える度に make するのは流石に面倒になってきた。人間って本当に我がままだと思う。
ということで、(誰もが考え付くであろう)変更を自動で検知して make してみる。
$SPHINX_HOME/bin/makebot.bat:
@echo off
setlocal
:loop
echo [%~n0] watching... press Ctrl-C to abort
python.exe "%~dp0notify-changed.py" .
if errorlevel 1 exit /b 1
echo [%~n0] detected
call make clean && call make html
echo.
goto loop
$SPHINX_HOME/bin/notify-changed.py:
#!python.exe
__doc__ = """\
Usage:
python %s DIRECTORY
Description:
Watch and wait for changes in a DIRECTORY and ends with status 0.
""" % __file__
import fnmatch
import os
import sys
import time
_include_patterns = ('*.conf', '*.css', '*.css_t', '*.html', '*.py', '*.rst')
_exclude_patterns = ('_*',)
def _fnmatch_any(name, patterns):
for pattern in patterns:
if fnmatch.fnmatch(name, pattern):
return True
return False
def _filter_name(names, includes=None, excludes=None):
if includes:
names = [x for x in names if _fnmatch_any(x, includes)]
if excludes:
names = [x for x in names if not _fnmatch_any(x, excludes)]
return names
def _filter_dir(names):
return _filter_name(names, excludes=_exclude_patterns)
def _filter_file(names):
return _filter_name(names,
includes=_include_patterns,
excludes=_exclude_patterns)
def _find_files(path):
for dirpath, dirnames, filenames in os.walk(path):
dirnames[:] = _filter_dir(dirnames)
for filename in _filter_file(filenames):
yield os.path.join(dirpath, filename)
def _iter_filestats(path):
for filepath in _find_files(path):
yield filepath, int(os.stat(filepath).st_mtime)
def _diff_filestats(before, after):
for filepath, mtime in after.iteritems():
if mtime != before.get(filepath, 0):
return True
return False
def _watch_directory(path, interval=1):
if not os.path.isdir(path):
return "ERROR: directory not found: `%s'" % path
before = dict(_iter_filestats(path))
while True:
time.sleep(interval)
after = dict(_iter_filestats(path))
if _diff_filestats(before, after):
break
before = after
return 0
def main():
status = 1
args = sys.argv[1:]
if args:
try:
status = _watch_directory(args[0])
except KeyboardInterrupt:
pass
else:
print __doc__,
return status
if __name__ == '__main__':
sys.exit(main())
pywin32 辺りを使えばファイル変更を検知する API が使えたりするのかも知れないが、生憎 Sphinx には pywin32 が添付されていないので、力技で実装。数千くらいのファイルなら問題ないはず(数万とかは知らない)。後は Ctrl-C で終了すると「バッチ ジョブを終了しますか (Y/N)?」とか聞かれるのがウザいが、これは Windows BAT のクソ仕様の問題だし、基本 makebot しっ放しなので我慢する。
取りあえず、これで私の Sphinx on Windows 環境は幸せになった。
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。