C++14, C++17 Python C/C++ モジュールを cmake build しパッケージングして pypi にアップロードするメモ(2022 年夏版)

## とりあえずのまとめ

scikit-build を使い `setup.py` を記述.

* C++14 : `manylinux2014` or `manylinux_2_24` ターゲットにして作る
  * `manylinux2014` がより広く使えるが, コンパイラ環境はやや古い 
* C++17 : `manylinux_2_28` ターゲットにして作る

`manylinux_X_Y` の場合, インストール側は pip 20.3 必要です!

## 背景

今まで?だと `setup.py` で Python モジュールをビルドとパッケージングしていましたが, 最近は pyproject.toml で `build` みたいな流れがあります. しかし試したところ微妙な感じでした.

結局は今までどおり `setup.py` でやりますが, `scikit-build` を使ってよろしくやります.

例は tinyusdz を参照ください.

https://github.com/syoyo/tinyusdz

## build?

C/C++ で cmake ベースで Python のモジュールビルドするものは, 最近は `build` というモジュールでビルド環境整えるのが流行りのようです.

https://qiita.com/ieiringoo/items/4bef4fc9975803b08671

https://packaging.python.org/en/latest/tutorials/packaging-projects/

ただ, verbose モードがなかったり, 

基本的には backend(e.g. `setup.py`) へ処理を delegate しているだけっぽい

https://github.com/pypa/build/issues/206

のと, ドキュメントも微妙

https://pypa-build.readthedocs.io/en/latest/

なので, 基本は `python setup.py` とかでビルド確認して, 最後 `python -m build` で処理するようにしたほうがよさそうです!

### build はここがよくない

`python-build` は venv 環境を作ってビルドするので, 最初の起動でいくらか時間かかります. またなにかビルド中にエラーがあったときに原因を突き止めるのが面倒いです.

↑にあるように clean コマンドが無いので, backend の生成したファイルが残っているとビルドが期待しないときがあります. 
著者の場合は MANIFEST ファイル関連でいろいろハマりました. repo にファイルを追加して MANIFEST を更新しても(中間生成されたファイルを見てしまうため)反映されませんでした.

`python -m build` ではキャッシュしたのを見に行くのがデフォルトのようで, 一度 `python -m build` するまえに, backend 側でいろいろ中間で生成されたファイルは先に消しておく必要がありました.

また, `-n` で venv 環境ではない環境でビルドすることもできますので, 開発中はこちらを使ったほうがよいでしょう.

## scikit-build

cmake ビルドがやりやすくなるっぽい scikit-build を使います.

https://scikit-build.readthedocs.io/en/latest/usage.html

scikit-build では `setup.py` もまだ必要でした. 
cmake ビルドについては, デフォルトでは `pyproject.toml` のあるディレクトリの `CMakeLists.txt` を見にいきます.

### ファイルが足りない...?

`python setup.py sdist` で sdist 作っても, あとから git repo にファイルを追加すると何故か見つけてくれないエラーに遭遇しました.

原因としては, ソースパッケージに含めるファイルは `MANIFEST.in` で管理しますが, これがないと git archive あたりから自動生成していたためでした...

`MANIFEST.in` 消したり(自動生成で良い場合) or `MANIFEST.in` 用意して対応します!

### pybind11, nanobind?

https://qiita.com/termoshtt/items/28c2784d71d5692e8ca3

pybind11, nanobind の場合でも, cmake で install ターゲットを記述しておけば `.so` 含んでパッケージングしてくれるようです.

```cmake
# CMakeLists.txt

nanobind_add_module(${BUILD_TARGET_PY} ${TINYUSDZ_PYTHON_BINDING_SOURCES})
target_link_libraries(${BUILD_TARGET_PY} PRIVATE ${TINYUSDZ_TARGET_STATIC})

# strip "py" prefix
set_target_properties(${BUILD_TARGET_PY} PROPERTIES OUTPUT_NAME "tinyusdz") 

# For pypi packaging
install(TARGETS ${BUILD_TARGET_PY} LIBRARY DESTINATION tinyusdz)

```



`__init__.py` を提供しておくとより確実なのかしらん?


https://github.com/pybind/scikit_build_example

## ファイル構成

python build 関連のファイルは, 最低限以下のような感じになります.

```
MANIFEST.in
pyproject.toml
setup.py
CMakeLists.txt
<python_folder>
```

`<python_folder>` は, package に含めたい native ではない Python コードを含んだフォルダです.

### manylinux でのビルド

PyPI へのバイナリパッケージ(whl)のアップロードは manylinux 構成でビルドが必要です.
(glibc ランタイムとか合わせていろいろな Linux ディストリビューション環境で動かせるようにするため)

https://scikit-build.readthedocs.io/en/latest/generators.html?highlight=manylinux

docker 経由でビルドが推奨のようです.

ただ, 手元でビルドと wheel 生成確認したいとき, グラフィックス系アプリで Docker と相性よくない(ネイティブで GL 関連のライブラリと一緒にビルドしたいとか)や, WSL みたいに Docker 使えない(使えるようにセットアップがめんどくさすぎる)環境でビルドする場合, `manylinux` 環境を最新に絞って対応でとりあえずいけます.
(Ubuntu 20.04 gcc-9 では `manylinux_2_24` はいけた)

#### C++14, C++17?

https://github.com/pypa/manylinux

C++ 対応状況はこんな感じです.

* `manylinux_2_5_x86_64 (aliased by manylinux1_x86_64)`: だいたいの C++11 が使える(`gcc-4.8` ベースなので regex あたり微妙に使えないものがある)
* `manylinux_2_12_x86_64 (aliased by manylinux2010_x86_64)` : gcc-8. きちんと C++11 が使える(はず)
* `manylinux2014`(`manylinux_2_17`):  GCC-10. C++14 が使える
* `manylinux_2_24` : gcc-6 ベース. より確実に C++14 が使える
* `manylinux_2_28`: gcc-11 ベース. C++17 が使える(`filesystem` はきちんと使えるかは不明)

最近は `manylinux_X_Y` でバージョン付けするようになっています.

(`manylinux2014` が gcc-10 で `2_24` だと gcc-6 ですが, glibc ランタイムとしては `manylinux2014` のほうが古いディストリビューションをサポートしているため glibc バージョンは低いです)

2022/08 時点でよく使われているっぽい? である `manylinux_2_17`(`manylinux2014`)では C++14 まで(gcc-10)のサポートのため, nanobind(C++17)がコンパイルできないです!

https://phcerdan.github.io/dockcross/

http://catherineh.github.io/programming/2021/11/16/python-binary-distributions-whls-with-c17-cmake-auditwheel-and-manylinux

gcc-8 インストールし, `stdc++` を static link する手もありますが,
あんまり古い環境をサポートする必要がなければ, 

`2.28` (gcc-11 が使える)を使うのがよいかなと思います!
ただ, `2.28` の場合はクライアントにインストールするときに pip 20.3 以上である必要があります.

https://peps.python.org/pep-0600/#legacy-manylinux-tags


### auditwheel

ビルドしただけでは, PyPi は受け付けてくれず manylinux 環境に合うようにしないといけません.

`auditwheel repair` でビルドした binary wheel が manylinux 環境に合うように修正します. 


```
$ auditwheel repair -h
usage: auditwheel repair [-h] [--plat PLATFORM] [-L LIB_SDIR] [-w WHEEL_DIR] [--no-update-tags] [--strip]
                         [--only-plat]
                         WHEEL_FILE

Vendor in external shared library dependencies of a wheel.

positional arguments:
  WHEEL_FILE            Path to wheel file.

optional arguments:
  -h, --help            show this help message and exit
  --plat PLATFORM       Desired target platform. See the available platforms under the PLATFORMS section
                        below. (default: "manylinux_2_5_x86_64")
  -L LIB_SDIR, --lib-sdir LIB_SDIR
                        Subdirectory in packages to store copied libraries. (default: ".libs")
  -w WHEEL_DIR, --wheel-dir WHEEL_DIR
                        Directory to store delocated wheels (default: "wheelhouse/")
  --no-update-tags      Do not update the wheel filename tags and WHEEL info to match the repaired platform
                        tag.
  --strip               Strip symbols in the resulting wheel
  --only-plat           Do not check for higher policy compatibility

PLATFORMS:
These are the possible target platform tags, as specified by PEP 600.
Note that old, pre-PEP 600 tags are still usable and are listed as aliases
below.
- linux_x86_64
- manylinux_2_5_x86_64 (aliased by manylinux1_x86_64)
- manylinux_2_12_x86_64 (aliased by manylinux2010_x86_64)
- manylinux_2_17_x86_64 (aliased by manylinux2014_x86_64)
- manylinux_2_24_x86_64
- manylinux_2_27_x86_64
- manylinux_2_28_x86_64
- manylinux_2_31_x86_64
```

`auditwheel` は `manylinux1`(`manylinux_2_5`)がデフォルトになっていますので, `--plat` で manylinux バージョン指定しましょう.

`auditwheel -v` で 2_28 など表示してくれない場合, conda で最新 python 環境作るのが推奨ですが,

https://stackoverflow.com/questions/70419260/manylinux-wheel-creation-causes-cannot-repair-because-of-the-presence-of-too-re

`auditwheel-symbols` を使うのも手かもしれません.

また, Ubuntu 20.04 標準の gcc-9 では, `manylinux_2_24` は行けました.


### auditwheel を使わない方法

pytorch では auditwheel を使わず, 自前で処理しています.

https://github.com/pytorch/builder/issues/468#issuecomment-661943587


## CI + Pypi へのアップロード

twine つかっていつもどおりできます!

CI で一括ビルドする場合, cibuildwheel がよいでしょう.

cibuildwheel で python bwheel(C++ モジュール含む) を CI で一括ビルドし PyPI へアップロードするメモ 
https://qiita.com/syoyo/items/97f35b4d5c40761cc314

GitHub Actions と cibuildwheel を使ってarm64対応 wheel をビルドする
https://qiita.com/methane/items/41c7d92d603552f2ef33

を参照ください.

aarch64 対応も考えると github actions を使うのがいいかもしれません.

### manylinux_2_28 対応 :tada:

https://github.com/pypa/cibuildwheel#v270

cibuildwheel 最近(2022/07) `manylinux_2_28` 対応しました. これで C++17 アプリのパッケージングが楽になりますね!

## TODO

* setup.py とか MANIFEST.in とか, python build 関連を別フォルダにする方法を探す.