Cythonを使用して、複数ファイルにまたがったPythonコードから実行ファイルを生成（Mac：成功、Linux：失敗、Windows：成功）

# 記事の内容

Pythonのプロジェクトで、ソースコードを納品しないケースに遭遇しました。
Pyinstallerを使用してバイナリ化する手順を別記事に書きましたが、Cythonも試しましたので手順を記録しておきます。
MacとLinuxで試して、Macは成功、Linuxは失敗という結果になりました。
いずれWindowsでも試す予定です。

追記（2020-01-24）記事の最後にWindows編（成功）を追加しました。

## サンプルコード

Pythonコードは以下の3ファイルを使用し、foo.pyをバイナリにしたものを起動することにします。

```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__}")
```

```mymod1.py

def bar(s):
    print(f"bar: {s}")
```

```mymod2.py

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を準備します。

```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を使用しています。

```bash

pipenv install setuptools cython pandas
```

現段階で存在するファイルは以下の通りです。

```bash

$ ls
Pipfile
foo.py
mymod1.py
mymod2.py
setup.py
```

生成されたPipfileは以下の通り。

```Pipfile

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
setuptools = "*"
cython = "*"
pandas = "*"

[requires]
python_version = "3.7"
```

端末で以下を実行して、.soファイルをビルドします。

```bash

pipenv run python setup.py build_ext --inplace
```

現段階で存在するファイルは以下の通りです。マイモジュールの.cと.soが生成されています。

```bash

$ 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を動作確認してみます。
端末で以下を実行します。

```bash

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オプションを使用しています。

```bash

pipenv run cython foo.py --embed
```

現段階で存在するファイルは以下の通りです。foo.cが生成されています。

```bash

$ 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の場所：

```bash

$ find $HOME -type f -name 'Python.h' 2> /dev/null
/Users/username/.pyenv/versions/3.7.4/include/python3.7m/Python.h
```

ライブラリの場所とライブラリファイル名：

```bash

$ 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
```

これらの情報を与えて、以下のようにコンパイルします。

```bash

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が生成されています。

```bash

$ 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
```

#### 実行ファイルの動作確認

生成された実行ファイルを動かします。

```bash

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言語のソースコードを生成します。

```bash

pipenv run cython foo.py --embed
```

現段階で存在するファイルは以下の通りです。foo.cが生成されています。

```bash

$ 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の場所：

```bash

$ find $HOME -type f -name 'Python.h' 2> /dev/null
/home/laradock/.anyenv/envs/pyenv/versions/3.7.5/include/python3.7m/Python.h
```

ライブラリの場所とライブラリファイル名：

```bash

$ 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
```

これらの情報を与えて、以下のようにコンパイルします。
リンクエラーが出たら、適宜必要なライブラリを加えます。

```bash

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が生成されています。

```bash

$ 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
```

#### 実行ファイルの動作確認

生成された実行ファイルを動かします。

```bash

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を準備します。以下に再掲します。

```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編と同じです。