背景
egg配布でPythonのソースコードを「含めない」という方法として、bdist_egg --exclude-source-filesと指定する方法があります。ありました。
例えば次のプロジェクトを例に。
python setup.py bdist_egg --exclude-source-files を実行するとeggが出来ます。
ただしPython 3系でこのeggをeasy_installしても動作しないのです。
--exclude-source-filesを指定しなければGreeting!という文字を出すだけのgreetingコマンドが手に入ります。
何故
要するに__pycache__下のpycだけではPython3のコードは動作しないのです。
以下の、特にPEP 3147と図を御覧ください。
図によれば.pyがあった上で__pycache__を引きます。
一方--exclude-source-filesを指定すると.pyだけ抜けるので、PEP 3147のフローから見るとImportError一直線です。
__init__.py相当のものもないのでモジュールとして認識しない( ー`дー´)
これを理解した上で、setuptoolsのbdist_egg実装をさっと読んでみますと、distutilsのバイトコンパイル機能を使ってるのです。が、これがlegacyオプション対応してない。固定で__pycache__にバイトコンパイル結果を突っ込んでしまうようです。compileall.compile_dirとか使ってくれてれば簡単な修正で済むんですががが
ワークアラウンド
bdist_egg --exclude-source-filesによって壊れた状態のeggファイルをzipファイルとして読みこんで、__pycache__/ 下にいるキャッシュとしてのpycを、レガシーな位置にむりやり引きずり出せば動作するようになります。
以下はPython 3.5.2上での実装例。importは省略。
RE_PYCACHE_FILE = re.compile('(.+/)__pycache__/(.+)\.cpython-3\d(.pyc)$')
with tempfile.TemporaryDirectory() as tmpdir:
names_lst = []
tmpdir_abs_path = os.path.abspath(tmpdir)
with ZipFile(egg_path, 'r') as zf:
for name in zf.namelist():
m = RE_PYCACHE_FILE.match(name)
if m:
w_name = m.group(1) + m.group(2) + m.group(3)
else:
w_name = name
w_path = os.path.join(tmpdir_abs_path, w_name)
dir_path = os.path.dirname(w_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(w_path, 'wb') as file_to_write:
with zf.open(name) as file_to_read:
file_to_write.write(file_to_read.read())
names_lst.append(w_name)
with ZipFile(egg_path, 'w', compression=ZIP_DEFLATED) as zf:
for name in names_lst:
r_path = os.path.join(tmpdir_abs_path, name)
zf.write(r_path, arcname=name)
おまけ
上のような事実に気づく前に果敢にバグ報告したら、意外なクローズのされ方をされてむしろ驚きました。
setuptoolsはここで報告しても意味ないのか。そっちかよ。
# しかし前回もそうだったけど、初動がめっちゃ早いなぁ……偉大だ。
出来れば上記を取りまとめて再度setuptools本家に報告する予定ですが、とりあえずそういうことがあったということで。
追記(2016-11-09 16:50): 報告しました。 https://github.com/pypa/setuptools/issues/844
