py2exe

since 2022-02-23

Python スクリプトから Windows 実行ファイルを作るツール。

NVDA の開発には以前から py2exe が使われていた。

NVDA が Python 3.7 に移行した際に(絶望的と思われた) py2exe for Python 3 が登場し、これに移行した。

32bit 版のサポート終了が NVDA の Python 3.11 移行における新たな懸念になっている。

GitHub

公式サイト

  • ちなみに上記サイトで使われている MoinMoinWiki は Python で作られた Wiki エンジン

PyPI

概論

  • インストーラーを作るツールは別の話

チュートリアル 2023年9月

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

チュートリアル 2022

ちょっと冒険して 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 アプリはどうするんだ、とか、いろいろ気になる (夢がふくらむ)

numpy

> 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!

この単純なケースでは、依存関係を自動的に判定してくれたようだ。

tkinter

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 のフォルダがあり、中にたくさんファイルがある。あまりソフトウェア配布方法としては好ましくない感じである。

data_files

数値を 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 を読んでいることについては、目的に応じて考慮する必要がある。

setup.py あれこれ

ここからは 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",
            ],
        ),
    ],
)

プログラムごとにアイコンを指定する

excludes

list of module names to exclude

例えば NVDA では

"excludes": [
    "tkinter",

packages

list of packages to include with subpackages

script の依存パッケージ(ディレクトリ)を指定。

静的に解析できる場合は指定しなくてよいのかも知れない。

"packages": [
    "NVDAObjects",

includes

list of module names to include

script の依存モジュール(pyファイル)を指定。

"includes": [
    "nvdaBuiltin",

cmdclass

NVDA こんなことやっている。

from py2exe import distutils_buildexe
 
# 中略
 
 
class py2exe(distutils_buildexe.py2exe):
    # py2exe を独自に拡張
 
 
setup(
    # 中略
    cmdclass={"py2exe": py2exe},
    # 中略
)

Distutils の拡張 として紹介されている

https://docs.python.org/ja/3/distutils/extending.html

README_ORIGINAL

ここに情報が隠れている

https://github.com/py2exe/py2exe/blob/master/README_ORIGINAL.rst

  • build_exe という CLI がある
  • bundle_files 0 というモードがある
  • そもそも bundle_files 1 と 0 ではすべてのプログラムが動くとは限らない
  • 前述の例は 0 だと全滅
  • 動かないときにコンソールにエラーは出ない。

frozen

http://www.py2exe.org/index.cgi/HowToDetermineIfRunningFromExe

自分が exe なのかスクリプトなのか知りたいときに使う

if hasattr(sys, "frozen"):
    # 私は exe
py2exe.txt · 最終更新: 2023/09/29 10:01 by Takuya Nishimoto
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0