Python : zip にアーカイブした pyz を直接実行できる専用 exe を作る

実行ファイルの置き換えに python を使用したときのメモです。Python 3 はプログラムコードを zip にアーカイブしたまま実行することができます。

●(1) Library として読み込む場合

テストコードを appmain.py, testmodule.py とします。

# appmain.py
import  testmodule

def main():
    testmodule.app()

if __name__ == '__main__':
    main()

# testmodule.py
def app():
    print( 'testmodule' )

そのまままとめて zip 圧縮可能で、Libpath に追加すると読み込むことができます。

### zip file を作る
import  zipfile

src_list= [
        'appmain.py',
        'testmodule.py',
    ]

with zipfile.ZipFile( 'applib.zip', 'w' ) as fo:
    for src in src_list:
        fo.write( src )


### zip file 内の code を実行する
import sys

sys.path.append( 'applib.zip' )

import  appmain
appmain.main()

embeddable 版の python3x.zip と同じように事前にコンパイルしておくこともできます。

●(2) zipapp (pyz) に変換してから実行する場合

zip ではなく zipapp (pyz) に変換すると script のように直接実行できるようになります。

予めサブフォルダ app を作って、その中に appmain.py, testmodule.py を入れておきます。下記のコードを実行すると pyz にまとめることができます。create_archive() 最初の引数 ‘app’ はフォルダ名です。

import	zipapp

zipapp.create_archive( 'app', 'app.pyz', '/usr/bin/env python3', 'appmain:main' )

変換出力した app.pyz はただの zip ですが、エントリポイントとして __main__.py が挿入されています。この pyz ファイルはそのまま python で実行できます。

python app.pyz

また unix 系 os の場合は実行属性をつけることで直接実行することができます。

$ chmod 755 app.pyz
$ ./app.pyz

Windows の場合そのまま実行することはできませんが、pyz 専用の exe を作る方法が zipapp のマニュアルで紹介されています。

●(3) pyz の起動用に exe を作る

zipapp — Manage executable Python zip archives

上記ページに載っている zastub.c をそのままコンパイルします。

// zastub.c
#define Py_LIMITED_API 1
#include "Python.h"

#define WIN32_LEAN_AND_MEAN
#include 

#ifdef WINDOWS
int WINAPI wWinMain(
    HINSTANCE hInstance,      /* handle to current instance */
    HINSTANCE hPrevInstance,  /* handle to previous instance */
    LPWSTR lpCmdLine,         /* pointer to command line */
    int nCmdShow              /* show state of window */
)
#else
int wmain()
#endif
{
    wchar_t **myargv = _alloca((__argc + 1) * sizeof(wchar_t*));
    myargv[0] = __wargv[0];
    memcpy(myargv + 1, __wargv, __argc * sizeof(wchar_t *));
    return Py_Main(__argc+1, myargv);
}

コンパイル方法もマニュアル通りです。下記のコードを実行してコンパイルできます。

# compile.py
from distutils.ccompiler import new_compiler
import distutils.sysconfig
import sys
import os
from pathlib import Path

def compile(src):
    src = Path(src)
    cc = new_compiler()
    exe = src.stem
    cc.add_include_dir(distutils.sysconfig.get_python_inc())
    cc.add_library_dir(os.path.join(sys.base_exec_prefix, 'libs'))
    # First the CLI executable
    objs = cc.compile([str(src)])
    cc.link_executable(objs, exe)
    # Now the GUI executable
    #cc.define_macro('WINDOWS')
    #objs = cc.compile([str(src)])
    #cc.link_executable(objs, exe + 'w')

if __name__ == "__main__":
    compile("zastub.c")

zastub.exe が作られるので、(2) で作った pyz とそのまま合成します。単純につなげるだけです。

copy /b zastub.exe + app.pyz app.exe

これで pyz を実行可能な app.exe ができました。そのまま実行できます。(python3.dll が必要)

app.exe

プログラムの内容によっては python の import でモジュールが見つからない場合があるかもしれません。その場合は予め検索パス設定しておいてください。環境変数 PYTHONPATH でも構いませんし、zastub.c の中で Py_SetPath() を使ってパスを与えることもできます。

zastub.c は python.exe 相当です。これが引数を何も加工していない状態になります。

int wmain()
{
    return  Py_Main( __argc, __wargv );
}

zastub.c では 1番目の引数に自分自身のパスを複製して挿入します。例えば引数が “arg1” なら「app.exe arg1」→ 「app.exe app.exe arg1」となります。自分自身である app.exe には app.pyz の内容がそのまま含まれているので、app.exe を pyz とみなしてそのまま実行できるわけです。

なお 1番目の引数を正しく渡すことができるなら、必ずしも exe に pyz を合成する必要がありません。例えば下のように直接 “app.pyz” を渡すことができます。

int wmain()
{
    wchar_t **myargv = _alloca((__argc + 1) * sizeof(wchar_t*));
    memcpy(myargv + 1, __wargv, __argc * sizeof(wchar_t *));
    myargv[0] = __wargv[0];
    myargv[1] = L"app.pyz";
    return Py_Main(__argc+1, myargv);
}

ただし若干問題もあります。上のコードはカレントディレクトリでなければ app.pyz を読み込むことができません。例えば他のフォルダからフルパスで app.exe を実行しようとすると app.pyz が見つからずにエラーになります。もし app.pyz を実行ファイルと同じ場所に置くなら、__wargv[0] や GetModuleFileNameW() を利用して app.pyz のパスを求めることができます。

↓ __wargv[0] から app.pyz のパスを求める。

#define Py_LIMITED_API 1
#include    

#define WIN32_LEAN_AND_MEAN
#include    

#ifdef WINDOWS
int WINAPI wWinMain(
    HINSTANCE hInstance,      /* handle to current instance */
    HINSTANCE hPrevInstance,  /* handle to previous instance */
    LPWSTR lpCmdLine,         /* pointer to command line */
    int nCmdShow              /* show state of window */
)
#else
int wmain()
#endif
{
    wchar_t *PYZ_NAME= L"app.pyz";      
    size_t  NAME_LENGTH= wcslen( PYZ_NAME ) + 1;
    size_t  length= wcslen( __wargv[0] ) + NAME_LENGTH;
    wchar_t *pyz_path= _alloca( (length + 2) * sizeof(wchar_t) );
    wcscpy_s( pyz_path, length, __wargv[0] );

    wchar_t **myargv = _alloca((__argc + 1) * sizeof(wchar_t*));
    myargv[0] = __wargv[0];
    memcpy(myargv + 1, __wargv, __argc * sizeof(wchar_t *));

    wchar_t *eptr= pyz_path;
    for( wchar_t *ptr= pyz_path ; *ptr ; ptr++ ){
        if( *ptr == L'/' || *ptr == L'\\' ){
            eptr= ptr + 1;
        }
    }
    wcscpy_s( eptr, NAME_LENGTH, PYZ_NAME );

    myargv[1]= pyz_path;
    return Py_Main(__argc+1, myargv);
}

同じようにして exe の場所からライブラリの位置を特定し、Py_SetPath() することもできます。例えば embeddable 版を使うなら、Py_SetPath() に python3x.zip と dll/pyd フォルダの 2つを与えれば OK です。