since 2022-02-23
Python スクリプトから Windows 実行ファイルを作るツール。
NVDA の開発には以前から py2exe が使われていた。
NVDA が Python 3.7 に移行した際に(絶望的と思われた) py2exe for Python 3 が登場し、これに移行した。
32bit 版のサポート終了が NVDA の Python 3.11 移行における新たな懸念になっている。
GitHub
公式サイト
PyPI
概論
Python 3.11 Windows 64bit
hoge.py があるとする。
venv して pip install py2exe する。
setup.py
import py2exe py2exe.freeze(console=['hoge.py'])
> python setup.py
実行ファイル一式が dist にできる。
> dist\hoge.exe
distutils に依存しなくなった。
https://github.com/py2exe/py2exe/blob/master/docs/migration.md
ちょっと冒険して Python 3.10 で試そうか。。
> py -3.10-64 -m venv .env310w64 > .env310w64\scripts\activate > pip install py2exe > pip freeze cachetools==5.0.0 future==0.18.2 pefile==2021.9.3 py2exe==0.11.1.0
http://www.py2exe.org/index.cgi/Tutorial を見ながら、3.10 で動く書き方に直していく。
# hello.py print("Hello World!")
# setup.py from distutils.core import setup import py2exe setup(console=['hello.py'])
> python setup.py py2exe
> dir dist 2022/02/23 10:54 <DIR> . 2022/02/23 10:54 <DIR> .. 2022/02/23 10:54 36,352 hello.exe 2022/02/23 10:54 <DIR> lib 2021/12/06 19:29 3,429,624 libcrypto-1_1.dll 2021/12/06 19:28 32,792 libffi-7.dll 2022/02/23 10:54 5,747,745 library.zip 2021/12/06 19:29 695,032 libssl-1_1.dll 2021/12/06 19:28 191,728 pyexpat.pyd 2021/12/06 19:28 4,471,024 python310.dll 2021/12/06 19:28 25,320 select.pyd 2021/12/06 19:29 1,866,480 tcl86t.dll 2021/12/06 19:29 1,541,872 tk86t.dll 2021/12/06 19:28 1,117,936 unicodedata.pyd 2021/12/06 19:28 60,656 _asyncio.pyd 2021/12/06 19:28 80,112 _bz2.pyd 2021/12/06 19:28 119,024 _ctypes.pyd 2021/12/06 19:28 247,024 _decimal.pyd 2021/12/06 19:28 122,088 _elementtree.pyd 2021/12/06 19:28 59,120 _hashlib.pyd 2021/12/06 19:28 153,328 _lzma.pyd 2021/12/06 19:28 29,928 _multiprocessing.pyd 2021/12/06 19:28 45,296 _overlapped.pyd 2021/12/06 19:28 26,856 _queue.pyd 2021/12/06 19:28 74,480 _socket.pyd 2021/12/06 19:28 155,888 _ssl.pyd 2021/12/06 19:28 127,216 _testcapi.pyd 2021/12/06 19:28 27,368 _testinternalcapi.pyd 2021/12/06 19:28 61,160 _tkinter.pyd
> dist\hello.exe Hello World!
ということで Python スクリプトが exe ファイルになる。
http://www.py2exe.org/index.cgi/ListOfOptions
例えば生成されるファイルを減らしたい、もっとまとめたい、とする
bundle_files bundle dlls in the zipfile or the exe. Valid values for bundle_files are: 3 = don't bundle (default) 2 = bundle everything but the Python interpreter 1 = bundle everything, including the Python interpreter
まず dist を消す
> rmdir /s/q dist
setup.py をこうする
from distutils.core import setup import py2exe setup( console=["hello.py"], options={ "py2exe": { "bundle_files": 1, } }, )
再実行する。ファイルは減ったが、ちょっと中途半端な状況になった。
> python setup.py py2exe > dir dist 2022/02/23 11:05 <DIR> . 2022/02/23 11:05 <DIR> .. 2022/02/23 11:05 36,864 hello.exe 2022/02/23 11:05 <DIR> lib 2021/12/06 19:29 3,429,624 libcrypto-1_1.dll 2021/12/06 19:28 32,792 libffi-7.dll 2022/02/23 11:05 12,939,079 library.zip 2021/12/06 19:29 695,032 libssl-1_1.dll 2021/12/06 19:29 1,866,480 tcl86t.dll 2021/12/06 19:29 1,541,872 tk86t.dll > dir dist\lib 2022/02/23 11:05 <DIR> . 2022/02/23 11:05 <DIR> .. 2021/12/10 14:54 <DIR> tcl 2021/12/10 14:54 <DIR> tk
もちろん動く
> dist\hello.exe Hello World!
ようするに zip にいろいろ固めた状態。おお pyc なのか
> unzip -v dist\library.zip Archive: dist\library.zip Length Method Size Cmpr Date Time CRC-32 Name -------- ------ ------- ---- ---------- ----- -------- ---- 12463 Stored 12463 0% 2022-02-23 11:05 7f051780 email/_parseaddr.pyc 58909 Stored 58909 0% 2022-02-23 11:05 add4b8a5 difflib.pyc 2377 Stored 2377 0% 2022-02-23 11:05 d1541d09 encodings/mac_croatian.pyc 27358 Stored 27358 0% 2022-02-23 11:05 294c8927 platform.pyc 6600 Stored 6600 0% 2022-02-23 11:05 5526368d codeop.pyc 10567 Stored 10567 0% 2022-02-23 11:05 342016e4 email/feedparser.pyc 11479 Stored 11479 0% 2022-02-23 11:05 9969e788 multiprocessing/util.pyc 2338 Stored 2338 0% 2022-02-23 11:05 3adce6a2 encodings/iso8859_13.pyc 7370 Stored 7370 0% 2022-02-23 11:05 ae5f8866 email/contentmanager.pyc 289 Stored 289 0% 2022-02-23 11:05 dd9dc097 xml/parsers/__init__.pyc 9044 Stored 9044 0% 2022-02-23 11:05 a4c58fd8 encodings/cp864.pyc 2459 Stored 2459 0% 2022-02-23 11:05 7ab56894 encodings/cp874.pyc 2328 Stored 2328 0% 2022-02-23 11:05 928ae0dc encodings/cp875.pyc 1367 Stored 1367 0% 2022-02-23 11:05 b7254be1 encodings/shift_jis_2004.pyc 中略 -------- ------- --- ------- 8409429 8409429 0% 432 files
圧縮 0% であることに気づくわけだが、これをいじるオプションがある
compressed (boolean) create a compressed zipfile
使ってみる
from distutils.core import setup import py2exe setup( console=["hello.py"], options={ "py2exe": { "bundle_files": 1, "compressed": 1, } }, )
> rmdir /s/q dist > python setup.py py2exe > unzip -v dist\library.zip Archive: dist\library.zip Length Method Size Cmpr Date Time CRC-32 Name -------- ------ ------- ---- ---------- ----- -------- ---- 35669 Defl:N 13709 62% 2022-02-23 11:13 abf2128f importlib/metadata/__init__.pyc 4191 Defl:N 2358 44% 2022-02-23 11:13 b6ca3e31 getpass.pyc 36746 Defl:N 14138 62% 2022-02-23 11:13 fb94af1b statistics.pyc 53449 Defl:N 23489 56% 2022-02-23 11:13 eb024af0 http/cookiejar.pyc 中略 26520 Defl:N 11683 56% 2022-02-23 11:13 a93cb3a1 dataclasses.pyc 1949 Defl:N 1133 42% 2022-02-23 11:13 8ccb6674 email/iterators.pyc 4011 Defl:N 2222 45% 2022-02-23 11:13 4b10538d fnmatch.pyc -------- ------- --- ------- 8409429 3527556 58% 432 files > dist\hello.exe Hello World!
たぶん起動がちょっと遅くなるので、目的によるのでは。
さて、なぜか zip に入ってくれないファイルがあったり、そもそも tcl tk いらない、とか、GUI アプリはどうするんだ、とか、いろいろ気になる (夢がふくらむ)
> pip install numpy Collecting numpy Downloading numpy-1.22.2-cp310-cp310-win_amd64.whl (14.7 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14.7/14.7 MB 462.5 kB/s eta 0:00:00 Installing collected packages: numpy Successfully installed numpy-1.22.2
# hello.py import numpy as np rng = np.random.default_rng() samples = rng.normal(size=10) for i in range(samples.shape[0]): print(f"{samples[i]:+.3f}") print("Hello World!")
> python hello.py -0.103 -0.738 +1.558 -0.951 -2.200 +0.736 +1.098 +0.734 -0.374 -0.525 Hello World!
> rmdir /s/q dist > python setup.py py2exe
今回はログをちゃんとみるとこう言われている
running py2exe 222 missing Modules ------------------ ? Numeric imported from numpy.distutils.system_info ? __builtin__ imported from pkg_resources._vendor.pyparsing ? __main__ imported from bdb, pdb, pkg_resources ? _dummy_thread imported from numpy.core.arrayprint ? _frozen_importlib imported from importlib, importlib.abc, zipimport ? _frozen_importlib_external imported from importlib, importlib._bootstrap, importlib.abc, zipimport ? _posixshmem imported from multiprocessing.resource_tracker, multiprocessing.shared_memory ? _ufunc imported from numpy.typing ? _winreg imported from pkg_resources._vendor.appdirs, platform ? asyncio.DefaultEventLoopPolicy imported from - ? code_generators.genapi imported from numpy.core.cversions ? code_generators.numpy_api imported from numpy.core.cversions ? collections.Iterable imported from pkg_resources._vendor.pyparsing ? collections.MutableMapping imported from pkg_resources._vendor.pyparsing ? com.sun imported from pkg_resources._vendor.appdirs ? com.sun.jna imported from pkg_resources._vendor.appdirs ? com.sun.jna.platform imported from pkg_resources._vendor.appdirs ? core.abs imported from numpy ? core.max imported from numpy ? core.min imported from numpy ? core.round imported from numpy ? dummy.Process imported from multiprocessing.pool 以下略
だが library.zip には必要なものはちゃんと入っているようだ
> unzip -v dist\library.zip | grep numpy | head 10962 Defl:N 4840 56% 2022-02-23 15:43 645de53b numpy/typing/__init__.pyc 29563 Defl:N 8916 70% 2022-02-23 15:43 78438a2d numpy/matrixlib/defmatrix.pyc 498 Defl:N 341 32% 2022-02-23 15:43 d778966a numpy/core/cversions.pyc 7005 Defl:N 3427 51% 2022-02-23 15:43 1fc5701a numpy/typing/_generic_alias.pyc 48302 Defl:N 14321 70% 2022-02-23 15:43 e70155e0 numpy/polynomial/polynomial.pyc 6631 Defl:N 2321 65% 2022-02-23 15:43 e3da45f0 numpy/fft/helper.pyc 112640 Defl:N 52290 54% 2022-02-23 15:35 934aad89 numpy/fft/_pocketfft_internal.pyd 6968 Defl:N 3131 55% 2022-02-23 15:43 807b4153 numpy/lib/arrayterator.pyc 12177 Defl:N 5553 54% 2022-02-23 15:43 5294b305 numpy/distutils/npy_pkg_config.pyc 4479 Defl:N 2222 50% 2022-02-23 15:43 af596c8c numpy/distutils/__config__.pyc
venv 環境でないコマンドプロンプトで実行すると、ちゃんと動く
> dist\hello.exe +0.319 -0.218 +0.333 -0.231 +1.544 -2.628 +0.768 +0.710 -0.466 -0.262 Hello World!
この単純なケースでは、依存関係を自動的に判定してくれたようだ。
GUI アプリはどうするのか、ということで tkinter の過去記事で試したものを draw.py として置いてみる。
# setup.py from distutils.core import setup import py2exe setup( windows=["draw.py"], options={ "py2exe": { "bundle_files": 1, "compressed": 1, } }, )
コマンドプロンプトから dist\draw.exe で実行する。あるいは start dist でエクスプローラーを開いて draw.exe をダブルクリックすると実行。
venv 環境から numpy を削除したわけではないが、今回は library.zip には numpy は入っていない。
逆に dist\lib には tcl と tk のフォルダがあり、中にたくさんファイルがある。あまりソフトウェア配布方法としては好ましくない感じである。
数値を CSV ファイルで読み込むプログラムだったとする
> type data\sample.csv 0.319,-0.218,0.333 -0.231,1.544,-2.628 0.768,0.710,-0.466 > python data.py +0.319 -0.218 +0.333 -0.231 +1.544 -2.628 +0.768 +0.710 -0.466
csv モジュールでもよかったが、numpy でやってみた。
import numpy as np arr = np.loadtxt('data/sample.csv', delimiter=',') for y in range(arr.shape[1]): for x in range(arr.shape[0]): print(f"{arr[y][x]:+.3f} ", end='') print()
そして setup.py に data_files を指定する
from distutils.core import setup import py2exe setup( console=["data.py"], options={ "py2exe": { "bundle_files": 1, "compressed": 1, } }, data_files=[ ( "data", [ r"data\sample.csv", ], ), ], )
python setup.py py2exe を実行すると sample.csv が dist\data にコピーされる。
> dir dist\data 2022/02/23 16:16 <DIR> . 2022/02/23 16:16 <DIR> .. 2022/02/23 16:07 61 sample.csv
コマンドプロンプトで cd dist すればちゃんと動く
> data.exe +0.319 -0.218 +0.333 -0.231 +1.544 -2.628 +0.768 +0.710 -0.466
カレントディレクトリから相対パスで data/sample.csv を読んでいることについては、目的に応じて考慮する必要がある。
ここからは NVDA のビルドに使われている setup.py のテクニックをまとめる。
まず、複数の実行ファイルをひとつの setup.py で作ることができる。
from distutils.core import setup import py2exe setup( console=[ "hello.py", "data.py", ], windows=[ "draw.py", ], options={ "py2exe": { "bundle_files": 1, "compressed": 1, } }, data_files=[ ( "data", [ r"data\sample.csv", ], ), ], )
ただしぜんぶのプログラムで共通のひとつの dist になる。
いままで省略して書いていたが、下記のように書いても同じである。
from distutils.core import setup import py2exe setup( console=[ { "script": "hello.py" }, { "script": "data.py", }, ], windows=[ { "script": "draw.py", } ], options={ "py2exe": { "bundle_files": 1, "compressed": 1, } }, data_files=[ ( "data", [ r"data\sample.csv", ], ), ], )
プログラムごとにアイコンを指定する
list of module names to exclude
例えば NVDA では
"excludes": [ "tkinter",
list of packages to include with subpackages
script の依存パッケージ(ディレクトリ)を指定。
静的に解析できる場合は指定しなくてよいのかも知れない。
"packages": [ "NVDAObjects",
list of module names to include
script の依存モジュール(pyファイル)を指定。
"includes": [ "nvdaBuiltin",
NVDA こんなことやっている。
from py2exe import distutils_buildexe # 中略 class py2exe(distutils_buildexe.py2exe): # py2exe を独自に拡張 setup( # 中略 cmdclass={"py2exe": py2exe}, # 中略 )
Distutils の拡張 として紹介されている
ここに情報が隠れている
https://github.com/py2exe/py2exe/blob/master/README_ORIGINAL.rst
http://www.py2exe.org/index.cgi/HowToDetermineIfRunningFromExe
自分が exe なのかスクリプトなのか知りたいときに使う
if hasattr(sys, "frozen"): # 私は exe