記事の内容
Pythonのプロジェクトで、ソースコードを納品しないケースに遭遇しました。
Pyinstallerを使用してバイナリ化する手順を別記事に書きましたが、Cythonも試しましたので手順を記録しておきます。
MacとLinuxで試して、Macは成功、Linuxは失敗という結果になりました。
いずれWindowsでも試す予定です。
追記(2020-01-24)記事の最後にWindows編(成功)を追加しました。
サンプルコード
Pythonコードは以下の3ファイルを使用し、foo.pyをバイナリにしたものを起動することにします。
from mymod1 import bar
from mymod2 import hoge
import sys
bar("Hello!")
hoge("Hi!")
print("-----------------")
print(sys.path)
print(f"__name__ = {__name__}")
print(f"__file__ = {__file__}")
def bar(s):
print(f"bar: {s}")
import pandas as pd
def hoge(s):
print(f"hoge: {s}")
df = pd.DataFrame(index=[])
print(df)
Mac編(成功の記録)
Python 3.7.4を使用しました。
【Mac編】ステップ1: マイモジュール(mymod1.py、mymod2.py)から共有ライブラリ(.so)を生成する
以下のsetup.pyを準備します。
from setuptools import setup, Extension
from Cython.Build import cythonize
setup(
ext_modules=cythonize([
Extension(
"mymod1",
sources=["mymod1.py"],
),
Extension(
"mymod2",
sources=["mymod2.py"],
),
]),
)
端末で以下を実行して、モジュールをインストールします。pipenvを使用しています。
pipenv install setuptools cython pandas
現段階で存在するファイルは以下の通りです。
$ ls
Pipfile
foo.py
mymod1.py
mymod2.py
setup.py
生成されたPipfileは以下の通り。
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
setuptools = "*"
cython = "*"
pandas = "*"
[requires]
python_version = "3.7"
端末で以下を実行して、.soファイルをビルドします。
pipenv run python setup.py build_ext --inplace
現段階で存在するファイルは以下の通りです。マイモジュールの.cと.soが生成されています。
$ ls
Pipfile
build
foo.py
mymod1.c
mymod1.cpython-37m-darwin.so
mymod1.py
mymod2.c
mymod2.cpython-37m-darwin.so
mymod2.py
setup.py
生成された共有ライブラリのファイル名がlibで始まっていません。
どうやらこれらの.soファイルは、実行ファイルのビルド時に人がリンクの指定をするものではなく、実行時にdlopen関数で動的にロードされて使われるようです。
最初はそのことに気づきませんでしたが、C言語でdlopen関数を使った遠い記憶があったので、ようやく気づいた(^^;
共有ライブラリ(.so)の動作確認
foo.pyの実行ファイルを生成する前に、先ほど生成された.soを動作確認してみます。
端末で以下を実行します。
mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py
pipenv run python foo.py
mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py
以下のように表示され、正常に動くことが確認できました。
bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['/Users/username/PycharmProjects/foo_project', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python37.zip', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7/lib-dynload', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7/site-packages']
__name__ = __main__
__file__ = foo.py
【Mac編】ステップ2: foo.pyから実行ファイルを生成する
端末で以下を実行して、C言語のソースコードを生成します。
main関数にするために、--embedオプションを使用しています。
pipenv run cython foo.py --embed
現段階で存在するファイルは以下の通りです。foo.cが生成されています。
$ ls
Pipfile
build
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-darwin.so
mymod1.py
mymod2.c
mymod2.cpython-37m-darwin.so
mymod2.py
setup.py
foo.cをビルドするために、Python.hとPythonライブラリが必要ですので、探しておきます。
Python.hの場所:
$ find $HOME -type f -name 'Python.h' 2> /dev/null
/Users/username/.pyenv/versions/3.7.4/include/python3.7m/Python.h
ライブラリの場所とライブラリファイル名:
$ cd /Users/username/.pyenv/versions/3.7.4
$ ls
Python.framework
bin
bin.orig
include
lib
share
$ cd lib/
$ ls
lib
libpython3.7m.a
libpython3.7m.dylib
pkgconfig
python3.7
$ pwd
/Users/username/.pyenv/versions/3.7.4/lib
これらの情報を与えて、以下のようにコンパイルします。
gcc foo.c -o foo -I$HOME/.pyenv/versions/3.7.4/include/python3.7m -L$HOME/.pyenv/versions/3.7.4/lib -lpython3.7m
現段階で存在するファイルは以下の通りです。実行ファイルfooが生成されています。
$ ls
Pipfile
build
foo
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-darwin.so
mymod1.py
mymod2.c
mymod2.cpython-37m-darwin.so
mymod2.py
setup.py
実行ファイルの動作確認
生成された実行ファイルを動かします。
mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py
./foo
mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py
以下のようにエラーメッセージが表示されました。
Traceback (most recent call last):
File "foo.py", line 2, in init foo
from mymod2 import hoge
File "mymod2.py", line 1, in init mymod2
import pandas as pd
ModuleNotFoundError: No module named 'pandas'
PYTHONPATHを設定して、やり直します。
pipenvを使用している場合の実行例:
mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py
PYTHONPATH=`pipenv --venv`/lib/python3.7/site-packages ./foo
mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py
以下のように、foo.pyの__name__を表示する行まで成功しました。
__file__を表示する行で落ちていますから、Pythonコードと同じ動きをするとは限らないようです。
ともかく、Macでは成功しました。
bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['/Users/username/PycharmProjects/foo_project', '/Users/username/.local/share/virtualenvs/foo_project-M0RfnekH/lib/python3.7/site-packages', '/Users/username/.pyenv/versions/3.7.4/lib/python37.zip', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7/lib-dynload', '/Users/username/.pyenv/versions/3.7.4/lib/python3.7/site-packages']
__name__ = __main__
Traceback (most recent call last):
File "foo.py", line 11, in init foo
print(f"__file__ = {__file__}")
NameError: name '__file__' is not defined
Linux編(失敗の記録)
Ubuntu 18.04、Python 3.7.5を使用しました。
【Linux編】ステップ1: マイモジュール(mymod1.py、mymod2.py)から共有ライブラリ(.so)を生成する
ステップ1はMac編と全く同じ手順で成功します。
以下、動作確認した部分のみ貼り付けておきます。
$ pipenv run python foo.py
/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7/site-packages/pandas/compat/__init__.py:85: UserWarning: Could not import the lzma module. Your installed Python is incomplete. Attempting to use lzma compression will result in a RuntimeError.
warnings.warn(msg)
bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['/var/www/foo_project', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python37.zip', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7/lib-dynload', '/home/laradock/.anyenv/envs/pyenv/versions/3.7.5/lib/python3.7', '/home/laradock/.local/share/virtualenvs/foo_project-pKqtKQTe/lib/python3.7/site-packages']
__name__ = __main__
__file__ = foo.py
【Linux編】ステップ2: foo.pyから実行ファイルを生成する
Mac編と同様に、端末で以下を実行して、C言語のソースコードを生成します。
pipenv run cython foo.py --embed
現段階で存在するファイルは以下の通りです。foo.cが生成されています。
$ ls
Pipfile
Pipfile.lock
build
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-x86_64-linux-gnu.so
mymod1.py
mymod2.c
mymod2.cpython-37m-x86_64-linux-gnu.so
mymod2.py
setup.py
foo.cをビルドするために、Python.hとPythonライブラリが必要ですので、探しておきます。
Python.hの場所:
$ find $HOME -type f -name 'Python.h' 2> /dev/null
/home/laradock/.anyenv/envs/pyenv/versions/3.7.5/include/python3.7m/Python.h
ライブラリの場所とライブラリファイル名:
$ cd /home/laradock/.anyenv/envs/pyenv/versions/3.7.5
$ ls
bin
include
lib
share
$ cd lib/
$ ls
libpython3.7m.a
pkgconfig
python3.7
$ pwd
/home/laradock/.anyenv/envs/pyenv/versions/3.7.5/lib
これらの情報を与えて、以下のようにコンパイルします。
リンクエラーが出たら、適宜必要なライブラリを加えます。
gcc foo.c -o foo -I$HOME/.anyenv/envs/pyenv/versions/3.7.5/include/python3.7m -L$HOME/.anyenv/envs/pyenv/versions/3.7.5/lib -lpython3.7m -lm -lpthread -ldl -lutil
現段階で存在するファイルは以下の通りです。実行ファイルfooが生成されています。
$ ls
Pipfile
Pipfile.lock
build
foo
foo.c
foo.py
mymod1.c
mymod1.cpython-37m-x86_64-linux-gnu.so
mymod1.py
mymod2.c
mymod2.cpython-37m-x86_64-linux-gnu.so
mymod2.py
setup.py
実行ファイルの動作確認
生成された実行ファイルを動かします。
mv mymod1.py _mymod1.py
mv mymod2.py _mymod2.py
PYTHONPATH=`pipenv --venv`/lib/python3.7/site-packages ./foo
mv _mymod1.py mymod1.py
mv _mymod2.py mymod2.py
以下のようにエラーメッセージが表示されました。
$ ./foo
Traceback (most recent call last):
File "foo.py", line 1, in init foo
from mymod1 import bar
ImportError: /var/www/foo_project/mymod1.cpython-37m-x86_64-linux-gnu.so: undefined symbol: PyExc_SystemError
このエラーを回避する方法はまだ発見できておりません(誰か教えて)。
追記(2020-01-24)Windows編(成功の記録)
Windows10でも試して成功しましたので、手順を記録しておきます。
Pythonは3.7.6を、CコンパイラはVisual Studio 2019 Communityを使用しました。
【Windows編】ステップ1: マイモジュール(mymod1.py、mymod2.py)から共有ライブラリ(.pyd)を生成する
Mac編と同内容のsetup.pyを準備します。以下に再掲します。
from setuptools import setup, Extension
from Cython.Build import cythonize
setup(
ext_modules=cythonize([
Extension(
"mymod1",
sources=["mymod1.py"],
),
Extension(
"mymod2",
sources=["mymod2.py"],
),
]),
)
「x64 Native Tools Command Prompt for VS 2019」で以下を実行して、モジュールをインストールします。pipenvを使用しています。
pipenv install setuptools cython pandas
現段階で存在するファイルは以下の通りです。
> dir
foo.py
mymod1.py
mymod2.py
Pipfile
setup.py
「x64 Native Tools Command Prompt for VS 2019」で以下を実行して、.pydファイルをビルドします。
pipenv run python setup.py build_ext --inplace
現段階で存在するファイルは以下の通りです。マイモジュールの.cと.pydが生成されています。
> dir
build
foo.py
mymod1.c
mymod1.cp37-win_amd64.pyd
mymod1.py
mymod2.c
mymod2.cp37-win_amd64.pyd
mymod2.py
Pipfile
setup.py
共有ライブラリ(.pyd)の動作確認
foo.pyの実行ファイルを生成する前に、先ほど生成された.pydを動作確認してみます。
「x64 Native Tools Command Prompt for VS 2019」で以下を実行します。
move mymod1.py _mymod1.py
move mymod2.py _mymod2.py
pipenv run python foo.py
move _mymod1.py mymod1.py
move _mymod2.py mymod2.py
以下のように表示され、正常に動くことが確認できました。
bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['C:\\Users\\username\\PycharmProjects\\foo_project', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\Lib\\site-packages', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\Scripts\\python37.zip', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\DLLs', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\lib', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\Scripts', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\Lib', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\DLLs', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS']
__name__ = __main__
__file__ = foo.py
【Windows編】ステップ2: foo.pyから実行ファイルを生成する
「x64 Native Tools Command Prompt for VS 2019」で以下を実行して、C言語のソースコードを生成します。
main関数にするために、--embedオプションを使用しています。
pipenv run cython foo.py --embed
現段階で存在するファイルは以下の通りです。foo.cが生成されています。
>dir
build
foo.c
foo.py
mymod1.c
mymod1.cp37-win_amd64.pyd
mymod1.py
mymod2.c
mymod2.cp37-win_amd64.pyd
mymod2.py
Pipfile
setup.py
foo.cをビルドするために、Python.hとPythonライブラリが必要ですので、何らかの方法で探しておきます。
私の環境では、以下の場所にありました。
Python.hの場所:
>dir C:\Users\username\AppData\Local\Programs\Python\Python37\include\Python.h
C:\Users\username\AppData\Local\Programs\Python\Python37\include のディレクトリ
2019/12/18 23:41 3,713 Python.h
ライブラリの場所:
>dir C:\Users\username\AppData\Local\Programs\Python\Python37\libs
C:\Users\username\AppData\Local\Programs\Python\Python37\libs のディレクトリ
2019/12/19 00:45 1,232,690 libpython37.a
2019/12/19 00:43 170,564 python3.lib
2019/12/19 00:42 342,420 python37.lib
2019/12/19 00:43 1,750 _tkinter.lib
これらの情報を与えて、以下のようにコンパイルします。
cl foo.c /IC:\Users\username\AppData\Local\Programs\Python\Python37\include /link C:\Users\username\AppData\Local\Programs\Python\Python37\libs\python37.lib
現段階で存在するファイルは以下の通りです。実行ファイルfoo.exe等が生成されています。
>dir
build
foo.c
foo.exe
foo.exp
foo.lib
foo.obj
foo.py
mymod1.c
mymod1.cp37-win_amd64.pyd
mymod1.py
mymod2.c
mymod2.cp37-win_amd64.pyd
mymod2.py
Pipfile
setup.py
実行ファイルの動作確認
生成された実行ファイルを動かします。
move mymod1.py _mymod1.py
move mymod2.py _mymod2.py
.\foo.exe
move _mymod1.py mymod1.py
move _mymod2.py mymod2.py
以下のようにエラーメッセージが表示されました。
Traceback (most recent call last):
File "foo.py", line 2, in init foo
from mymod2 import hoge
File "mymod2.py", line 1, in init mymod2
ModuleNotFoundError: No module named 'pandas'
PYTHONPATHを設定して、やり直します。
pipenvを使用している場合の実行例:
(PYTHONPATHの設定を一時的にしたいので、コマンドプロンプト上で更にコマンドプロンプトを起動し、処理終了後に抜けています)
>cmd
Microsoft Windows [Version 10.0.18362.592]
(c) 2019 Microsoft Corporation. All rights reserved.
>pipenv --venv
C:\Users\username\.virtualenvs\foo_project-HtF6OWVS
>set PYTHONPATH=C:\Users\username\.virtualenvs\foo_project-HtF6OWVS\Lib\site-packages
>move mymod1.py _mymod1.py
1 個のファイルを移動しました。
>move mymod2.py _mymod2.py
1 個のファイルを移動しました。
>.\foo.exe
bar: Hello!
hoge: Hi!
Empty DataFrame
Columns: []
Index: []
-----------------
['C:\\Users\\username\\PycharmProjects\\foo_project', 'C:\\Users\\username\\.virtualenvs\\foo_project-HtF6OWVS\\Lib\\site-packages', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\python37.zip', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\Lib', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\DLLs', 'C:\\Users\\username\\PycharmProjects\\foo_project', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37', 'C:\\Users\\username\\AppData\\Local\\Programs\\Python\\Python37\\lib\\site-packages']
__name__ = __main__
Traceback (most recent call last):
File "foo.py", line 11, in init foo
print(f"__file__ = {__file__}")
NameError: name '__file__' is not defined
>move _mymod1.py mymod1.py
1 個のファイルを移動しました。
>move _mymod2.py mymod2.py
1 個のファイルを移動しました。
>exit
Mac編と同様に、foo.pyの__name__を表示する行まで成功しました。
__file__を表示する行で落ちているのも、Mac編と同じです。