Pyinstallerを使って外部ライブラリ以外をexe化する。~外部ライブラリはクライアントに任せてライセンス回避~

こんにちは、Rcatです。
今回はPythonのPyinstallerを使用してスクリプトを実行ファイルにしてみようと思います。
ただし、普通のやり方と違ってサードパーティーライブラリは一切含まないような方法を行います。
目的は個別のライブラリのライセンスを確認するのが面倒くさいからです。
そして、その場合の起動方法について考えていきます。


なぜそんなめんどくさいことをするのか

それは大量のライブラリを使った場合、全てのライセンスを確認するのが面倒くさいからです。
また、コンパイルする目的がソースコードを隠蔽するためなので、そもそもダブルクリックですぐ実行してもらうことを前提としているわけではないからです。
つまり、環境構築は普通にやってもらいます。
普通にやってもらうと言っても以下の記事で紹介しているように、ダブルクリックで全自動環境構築ができるような環境を既に整えてあるので、敷居はかなり低くなっています。

標準ライブラリ以外を全て外す

pyinstallerの使い方についてはいろんなところで語られているので割愛します。

ライブラリの除外方法

とりあえず標準ライブラリとインタープリタ以外を全て除外してコンパイルする方法だけ書きます。

まず最初に次のコードで1回コンパイルします。

pyinstaller -F pyinstallertest.py

すると作業ディレクトリにSPECファイルが出来上がるのでこちらを開きます。

中身が以下のような感じなのですが、excludesの部分に除外するライブラリのリストを代入します。この例ではすでに省いています。

# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
    ['pyinstallertest.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=['altgraph', 'certifi', 'charset-normalizer', 'idna', 'packaging', 'pefile', 'pip', 'pyinstaller', 'pyinstaller-hooks-contrib', 'pywin32-ctypes', 'requests', 'setuptools', 'urllib3'],
    noarchive=False,
    optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='pyinstallertest',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=True,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)

ライブラリの列挙はめんどくさいので、以下のような感じでやるのがいいかと思います。

(venv) pyinstaller>pip list
Package                   Version
------------------------- ---------
altgraph                  0.17.4
certifi                   2024.8.30
charset-normalizer        3.3.2
idna                      3.10
packaging                 24.1
pefile                    2024.8.26
pip                       24.2
pyinstaller               6.10.0
pyinstaller-hooks-contrib 2024.8
pywin32-ctypes            0.2.3
requests                  2.32.3
setuptools                75.1.0
urllib3                   2.2.3

(venv) pyinstaller>py
Python 3.11.6 (tags/v3.11.6:8b6ee5b, Oct  2 2023, 14:57:12) [MSC v.1935 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> a = '''altgraph
... certifi
... charset-normalizer
... idna
... packaging
... pefile
... pip
... pyinstaller
... pyinstaller-hooks-contrib
... pywin32-ctypes
... requests
... setuptools
... urllib3                  '''
>>> [i.strip() for i in a.split("\n")]
['altgraph', 'certifi', 'charset-normalizer', 'idna', 'packaging', 'pefile', 'pip', 'pyinstaller', 'pyinstaller-hooks-contrib', 'pywin32-ctypes', 'requests', 'setuptools', 'urllib3']
>>>

コマンドプロンプト上でALTキーを押しながらだとボックス選択ができるので、ライブラリの名前のところだけ四角が囲んでコピペ。
Pythonを起動して文字列をして変数に代入した後、スプリットして空白を削除してリスト化する。出てきたリストをコピーして、さっきのテキストに貼り付けるだけ。
こんなもんでしょうか。

最後に引数をspecファイルの名前に変更して再度コンパイルします。

pyinstaller>pyinstaller pyinstallertest.spec

こうすると、この環境にインストールされている全ての外部パッケージが除外されます。

実行してみる

実行してみると以下のようなエラーが出ましたrequestsライブラリが見つからないというエラーです。
これは狙い通りなのでうまく除外することができたようです。

pyinstaller\dist>pyinstallertest.exe
Traceback (most recent call last):
  File "pyinstallertest.py", line 47, in <module>
ModuleNotFoundError: No module named 'requests'
[PYI-10632:ERROR] Failed to execute script 'pyinstallertest' due to unhandled exception!

これで作った実行ファイルの中には、Pythonのインタプリタ及び標準パッケージ、私のスクリプト以外は入っていません。


外部パッケージをユーザーにインストールしてもらって、起動する環境を作る

さて、目的は達成しました。
起動できなくなってしまいましたね。どうしましょうか?

というわけで、次のような方法を考えました。

  • いつも通り仮想環境を作成してrequirementsを使ったパッケージの自動インストールを行う。

  • その環境を参照してライブラリを読み込む

この手順で起動できれば目的を達成することができそうです。

環境変数を変更して、ライブラリの読み込み先を変える

さて、みんな大好きPYTHONPATHを変更しましょう。
通常はPythonのインストールディレクトリや仮想環境のライブラリ保存先が指定されているのですが、コンパイル後は次のようになっていました。
コンパイル前のスクリプトに"print(sys.path)"と書いてあります。

pyinstaller>pyinstallertest.exe
['C:\\Users\\\\AppData\\Local\\Temp\\_MEI195282\\base_library.zip', 'C:\\Users\\\\AppData\\Local\\Temp\\_MEI195282\\lib-dynload', 'C:\\Users\\\\AppData\\Local\\Temp\\_MEI195282']

なるほど。一時フォルダに何かしら展開してそこから引っ張ってきてる感じなんですね。で全部除外したから、この中にライブラリがないからエラーで落ちるということですね。

というわけで、コンパイル前のソースコードの先頭を以下のように変更しました。事前に仮想環境のPYTHONPATHを出力しておき、実行時にそのまま設定し直すということです。簡単ですね。
これを実行するために標準ライブラリ3ライブラリをimportしています。

ちなみに読み込み元のファイルは以下のようにして作られています。
こちらは私が配布している全自動環境構築用スクリプトを一部改造したところです。

仮想環境が起動した後、パスのリストをテキストとして出力しておく行を追加しました。これで上書き用のパスを出力できます。

この状態で再度実行してみます。

pyinstaller>pyinstallertest.exe
PYTHONPATHを更新します
やっほー

はい、うまく起動しました。
これでソースコードの秘匿という目的であればライセンスを気にせず、大量のライブラリを使った実行ファイルを配布できるようになりました。


まとめ

Python自体は様々な製品の中に組み込まれており、調べてみるとPython自体を変更したとしても、ソースコードをオープンにしなくても良いPython Software Foundation Licenseというライセンスのようですので、exe化はソースコード秘匿に持ってこいだなと思ってました。
しかし、様々なライブラリに頼って物を作る。Pythonでは個別のライブラリのライセンスは無視できません。しかし、大量のライブラリを使い始めると個人には面倒でやってられません
そこで思いついたのが今回の方法、自分のソースコードをそのまま配布した時と同じように各自で環境構築をしてもらえばいいのです。
今後お試し版を配布しようと考えていて、その時に知識があれば書き換えられてしまうソース配布だと不都合なので、今回のやり方を検討しました。なので、ユーザーはPythonを自分で実行する前提です。
この前提であれば、この方法はかなり有用な方法であると考えられます。
それではまたお会いしましょう。

バージョン違っても行けるのかは…そのうち試しましょう


情報が役に立ったと思えば、僅かでも投げ銭していただけるとありがたいです。