2020年2月8日土曜日

C++のビルド済みバイナリをpipでインストール

目的

C++ と cmake で作った実行ファイルを、pipでインストールできる形式にします。 加えて、pythonのライブラリも添付します。

なお、PyPIへのアップロード・配布は考慮していません。

作り方

pythonの準備

skbuildを使うので、pip install scikit-build でインストールしておきます。

python 3.8.1 で動作確認しています。

pipでインストールするC++のプログラムの準備

練習用C++プログラムを準備します。既存の、C++ & cmakeでビルドするプログラムがあれば、ここはスキップします。

まず、練習用C++プログラムの本体です。

[aaa.cpp]

1
2
3
4
5
6
7
8
9
#include <iostream>

int main(int argc, char *argv[]){
  std::cout << "aaa bbb ccc" << std::endl;
  for(int i=0; i<argc; ++i){
    std::cout << argv[i] << std::endl;
  }
  return 0;
}
文字列を表示するだけのプログラムです。

これをビルドするCMakeLists.txtを作成します。

[CMakeLists.txt]

1
2
3
4
cmake_minimum_required(VERSION 3.11.0)
project(projAAA)
add_executable(aaa aaa.cpp)
install(TARGETS aaa RUNTIME DESTINATION .)
ここでは aaa という名前のコマンドを作成しています。C++やcmakeの説明は省略します。

setup.py

配布パッケージを作成するためのPythonスクリプト setup.py を作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from skbuild import setup
setup(name="bdist-template", # パッケージの名前。pipでインストール後、pip list で表示される名前
      version="0.0.0",
      author="Your NAME",
      author_email="your@name.com",
      packages=["bdist_example"],  # importするときに使う名前。nameと一致してなくても良い。
                                   # 同じ方がユーザーには親切かも。
      cmake_install_dir="bdist_example/bin", # 最初のディレクトリ名はpackagesで指定した名前と同じにする
      entry_points={
          "console_scripts": [
              "cmdaaa=bdist_example:python_function_aaa"
          ]
          # cmdaaa がpip install後にシェルから使えるコマンド名になる
          # =と:の間は、packagesで指定した名前と同じにする
          # :の右側は後ほど説明
      },
      url="http://yourname.abc",
      download_url="https://yourname.abc/download",
      description="Test package", # このパッケージの短い説明文
      long_description="Test package !!!", # このパッケージの詳細な説明文
      classifiers=[
          "Programming Language :: C++",
          'Programming Language :: Python'
      ],
      license="???" # ライセンス名を記載(GPLとかApacheとか)
)

__init__.py

pipでインストールする場合、C++でビルドした実行ファイルをそのまま実行するのではなく、Pythonを一旦経由するので、 そのためのスクリプトを準備します。

まず、 setup.py のpackagesで指定した名前(ここではbdist_example)のディレクトリを作成します。 そこに次のような内容を含む __init__.py を作成します。

[bdist_example/__init__.py]

1
2
3
4
5
6
7
8
import os
import subprocess
import sys

CMAKE_BIN_DIR = os.path.join(os.path.dirname(__file__), "bin")

def python_function_aaa():
    raise SystemExit(subprocess.call([os.path.join(CMAKE_BIN_DIR, "aaa")] + sys.argv[1:]))
"aaa"の部分を CMakeLists.txt で書いたプログラム名と一致させます。

python_function_aaa は setup.py の console_scripts の部分の : の右側部分に書いた名前と一致させます。

同梱するスクリプト

次のスクリプトも同梱するようにします。バイナリのみの配布であれば、ここはスキップします。

[bdist_example/aaalib.py]

1
2
def print_aaaaa():
    print("aaaaa")

.whlファイルの作成

ここまでそろうとパッケージを作ることができます。

$ python setup.py bdist_wheel
を実行すると、distディレクトリの中に.whlファイルが作成されます。

インストール

作成した.whlを次のようにしてインストールします。

$ pip install dist/bdist_template-0.0.0-cp38-cp38-linux_x86_64.whl 
Processing ./dist/bdist_template-0.0.0-cp38-cp38-linux_x86_64.whl
Installing collected packages: bdist-template
Successfully installed bdist-template-0.0.0

インストールされたことの確認は、

$ pip list
Package        Version
-------------- -------
bdist-template 0.0.0  
packaging      20.1   
pip            20.0.2 
pyparsing      2.4.6  
scikit-build   0.10.0 
setuptools     41.2.0 
six            1.14.0 
wheel          0.34.2 
でできます。bdist-templateがインストールされていることがわかります。

動作確認

先程作成したコマンドが呼び出せることを確認します。

$ cmdaaa x y z
aaa bbb ccc
/home/user/.pyenv/versions/3.8.1/envs/py38/lib/python3.8/site-packages/bdist_example/bin/aaa
x
y
z
C++でビルドしたコマンドそのものは aaa でしたが、setup.pyで書いたコマンド名 cmdaaa が有効になっていることが確認できました。

同梱したスクリプトをpythonから実行してみます。

>>> import bdist_example.aaalib
>>> bdist_example.aaalib.print_aaaaa()
aaaaa

おまけ1

データを同梱したい場合は、次のようなコードを setup.py のsetup関数に渡す引数に追加します。

setup(...
      include_package_data=True,
      package_data={
          "bdist_example": ["data/*.dat"],
      }
)
ここでは、bdist_example/data/*.dat にデータがあると仮定しています。

おまけ2

.whlのファイル名でインストールできるpythonのバージョンが制限されるので、

$ mv bdist_template-0.0.0-{cp38-cp38,py3-none}-linux_x86_64.whl
のようにリネームしておくと、Python3ならインストールできるようになります。

ファイル名での制限を外すと、動作しないバージョンのPythonにインストールできてしまうので、代わりに、

setup(...
      python_requires='~=3.6'
)
のように指定しておきます。バージョン指定の書き方は
https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
が参考になります。

まとめ

skbuild により、 setup.py と __init__.py を準備するだけで .whl ファイルを作ることができました。

参考文献

scikit-build (skbuild) のドキュメント
https://scikit-build.readthedocs.io/en/latest/usage.html#basic-usage

cmake-python-distributions のコード
https://github.com/scikit-build/cmake-python-distributions

プロジェクトのパッケージングと配布
https://python-packaging-user-guide-ja.readthedocs.io/ja/latest/distributing.html
https://packaging.python.org/guides/distributing-packages-using-setuptools/

0 件のコメント :