Windows 11でROS 2を動かそうとしたら_rclpyのNoModuleErrorに無限にハマった話

また無限に時間を溶かしてしまいました

## 概要
とある試みのため，WSLではなくWindows上でROS 2を入れて動かしたかったんですが，
Pythonのノードを立ち上げようとすると`NoModuleError`が出て立ち上がりませんでした．
ごちょごちょやってる間に動くようになったので備忘録を兼ねて解決方法を書きます
（Windows11で，と書いてますがたぶんWim10でも同様だと思います）

##環境
* AMD Ryzen 7 Pro 4750U
* Windows 11 ※1
* Visual Studio 2019 Community
* ROS 2 foxy ※2
* Python 3.8.3

※1: WSLgを使いたくて，プレビュー版のWin11を入れて試しています．Win10でも同様になると思っていますが未検証です
※2: ROS 2のインストールは[https://docs.ros.org/en/foxy/Installation/Windows-Install-Binary.html#downloading-ros-2](https://docs.ros.org/en/foxy/Installation/Windows-Install-Binary.html#downloading-ros-2)にあるとおり，githubのreleasesからzipをダウンロード・展開して行いました
## 起こった現象
注意：以下コマンドの入出力はVS2019 Developer Command Promptで行ってます．

* 以下の記事の通りに，Pythonで実装するパッケージを作成しました  
[ROS2で自分で作成したpythonプログラムを動かす方法](https://qiita.com/inomu/items/5bd47b4d223434d57615)  
※ros-windowsはC直下，（あまりよくないと思いますが）ros2_wsはros-windows内に作りました
* `ros2 run my_python my_listener`したところ，以下のエラーが出てノードが立ち上がりませんでした  
(`talker`も同様）

```
C:\ros2-windows\ros2_ws>ros2 run my_python my_listener
Traceback (most recent call last):
  File "C:\ros2-windows\ros2_ws\install\my_python\lib\my_python\my_listener-script.py", line 33, in <module>
    sys.exit(load_entry_point('my-python==0.0.0', 'console_scripts', 'my_listener')())
  File "C:\ros2-windows\ros2_ws\install\my_python\lib\my_python\my_listener-script.py", line 25, in importlib_load_entry_point
    return next(matches).load()
  File "c:\python38\lib\importlib\metadata.py", line 77, in load
    module = import_module(match.group('module'))
  File "c:\python38\lib\importlib\__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "C:\ros2-windows\ros2_ws\install\my_python\Lib\site-packages\python_programs\listener.py", line 2, in <module>
    from rclpy.node import Node
  File "C:\ros2-windows\Lib\site-packages\rclpy\node.py", line 41, in <module>
    from rclpy.client import Client
  File "C:\ros2-windows\Lib\site-packages\rclpy\client.py", line 22, in <module>
    from rclpy.impl.implementation_singleton import rclpy_implementation as _rclpy
  File "C:\ros2-windows\Lib\site-packages\rclpy\impl\implementation_singleton.py", line 31, in <module>
    rclpy_implementation = _import('._rclpy')
  File "C:\ros2-windows\Lib\site-packages\rclpy\impl\__init__.py", line 28, in _import
    return importlib.import_module(name, package='rclpy')
  File "c:\python38\lib\importlib\__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
ModuleNotFoundError: No module named 'rclpy._rclpy'
The C extension 'C:\ros2-windows\Lib\site-packages\rclpy\_rclpy.pyd' isn't present on the system. Please refer to 'https://index.ros.org/doc/ros2/Troubleshooting/Installation-Troubleshooting/#import-failing-without-library-present-on-the-system' for possible solutions
```

* rclpyは`C:\ros2-windows\Lib\site-packages\rclpy`を参照しており，同パスは`PYTHONPATH`にも登録されていました
* `C:\ros2-windows\Lib\site-packages\rclpy`には`_rclpy.lib`と`_rclpy_d.pyd`があるものの，`_rclpy.pyd`はありませんでした

## 結論
* Pythonは3.8系（公式ページでは3.8.3が示されています）を使う必要がありました
* 通常のPython（python.exe）ではなく，**Windows デバッグ版（python_d.exe）**を使う必要がありました
* デバッグ版Pythonを使う場合，`numpy`など一部モジュールはそれ用にインストールしなおす必要がありました
* デバッグ版Pythonを使う場合，colcon buildの時にそのためのコマンドを使う必要がありました

## 解決策
### Pythonインストール
上述のとおり，デバッグ版の`python_d.exe`を使う必要があります．
この実行ファイルは，インストールの時にオプションで選択しないと入らないようになっています．
[Python公式サイト> Downloads > Windows](https://www.python.org/downloads/windows/)からPython**3.8.3**のインストーラをダウンロード・実行し，以下の画像のようにAdvanced Optionsから`debugging symbols`と`debug binaries`を入れてやるとよいです．
![スクリーンショット 2021-07-24 130517.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166634/39c1e586-5e4b-78ca-d84b-59d1f915ba02.png)
なお，ROS 2はPythonのインストールフォルダが`C:\Python38\`であると想定していますが，インストーラのデフォルト設定では`Program Files`以下になっているので，そこも修正が必要です．
また，`choco`や`anaconda`等からすでにPythonをインストールしている場合，インストーラが動作しません．
(私は`choco`で入れたpythonをuninstallしてインストーラから入れなおしました)

インストールがうまくて来ていると，Pythonのインストールフォルダ(`C:\Python38`)に，`python_d.exe`が存在します．
また，コマンドプロンプトから`python_d`コマンドでPythonの立ち上げが行えます

```
C:\ros2-windows\ros2_ws>python_d
Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:20:16) [MSC v.1925 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
```

### Debug版Python向けの環境構築
`python_d`を利用する場合に必要な依存パッケージ類ですが，ROS 2ドキュメントのBuilding ROS 2 on Windowsページに記載があります
[Extra stuff for Debug mode - Building ROS 2 on Windows, ROS 2 Documentation](https://docs.ros.org/en/rolling/Installation/Windows-Development-Setup.html#extra-stuff-for-debug-mode)
このページにあるとおり，chocoを使って依存パッケージをインストールする必要があります．
（なお，インストールには管理者権限が必要です．

```
choco install -y peazip
choco install -y svn hg
```

また，pythonパッケージも一部再インストールが必要です

```
python_d -m pip install --force-reinstall https://github.com/ros2/ros2/releases/download/numpy-archives/numpy-1.18.4-cp38-cp38d-win_amd64.whl
python_d -m pip install --force-reinstall https://github.com/ros2/ros2/releases/download/lxml-archives/lxml-4.5.1-cp38-cp38d-win_amd64.whl
```
#### 注：AMDのCPUを使用している場合
ここで，僕の環境(AMD)では以下のエラーのためインストールできませんでした．
(Intelの場合どうなるかは未検証です)

```
C:\ros2-windows\ros2_ws>python_d -m pip install --force-reinstall https://github.com/ros2/ros2/releases/download/numpy-archives/numpy-1.18.4-cp38-cp38d-win_amd64.whl
ERROR: numpy-1.18.4-cp38-cp38d-win_amd64.whl is not a supported wheel on this platform.
```
pipは.whlファイルのファイル名からバージョンを認識しているようで，次の手順のようにリネームしてやると解決します（ええんやろか）

1. `https://github.com/ros2/ros2/releases/download/numpy-archives/numpy-1.18.4-cp38-cp38d-win_amd64.whl`および` https://github.com/ros2/ros2/releases/download/lxml-archives/lxml-4.5.1-cp38-cp38d-win_amd64.whl`にアクセスして，`.whl`ファイルをダウンロードする
1. **それぞれのファイルについて，`...win_amd64.whl`とあるところを`...win32.whl`にリネームする**
1. 通常通り，`python_d -m pip install --force-reinstall <ダウンロードした.whlファイル>`を実行してインストールする

インストールがうまくいけば，エラーが出ずにそれぞれのパッケージをimportできるはずです

```python
C:\ros2-windows\ros2_ws>python_d
Python 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:20:16) [MSC v.1925 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from lxml import etree
>>> import numpy
#何もエラーが出ない
```

参考：
[【Python】pipでwhlファイルをインストールするときのエラー - 俺言語．](https://oregengo.hatenablog.com/entry/2016/12/20/175835)
[numpyインストールで四苦八苦 - Hajime Satoさんのnote記事](https://note.com/replicorn/n/nc9570c79358c)

## パッケージのビルド・実行
あとは，インストールした`python_d`でパッケージをビルドし，実行します．
[Extra stuff for Debug mode - Building ROS 2 on Windows, ROS 2 Documentation](https://docs.ros.org/en/rolling/Installation/Windows-Development-Setup.html#extra-stuff-for-debug-mode)にもあるように，`python_d`に`colcon`の実行ファイルのパスを与えます．
僕の場合，pythonを`C:\Python38`にインストールしていたので，コマンドは以下のようになります．
（なお，すでに`colcon build`でビルドしたものがある場合，`--cmake-clean-cache`で消去してから実行する必要があります．）

```
C:\ros2-windows\ros2_ws>colcon build --cmake-clean-cache #すでにcolcon buildを走らしていた場合
C:\ros2-windows\ros2_ws>python_d C:\Python38\Scripts\colcon.exe build
```

ビルド後実行してやるとうまく動きました．

```
C:\ros2-windows\ros2_ws>ros2 run my_python my_talker
[INFO] [1627127739.241812500] [talker]: Publishing: "Hello World: 0"
[INFO] [1627127740.085704200] [talker]: Publishing: "Hello World: 1"
```
コマンドプロンプトを2つ立ち上げてtalkerとlistenerを同時に実行してみたところ，メッセージの送受信が行われていることも確認できました．めでたしめでたし．


## 補足
"_rclpy not found"みたいなので調べていると，以下のように`colcon build`時にオプションをつけるといいよという記述がありました．
[ROS2: DebugModuleNotFoundError: No module named ‘rclpy._rclpy’ - ROS Discourse](https://discourse.ros.org/t/ros2-debugmodulenotfounderror-no-module-named-rclpy--rclpy/2401/3)

こんな感じです

```
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Debug
```
ただ，`python_d colcon`した場合とこれとは振る舞いが違うみたいで，僕はこのargsをつけるやり方では`_rclpy not found`エラーは解消できませんでした．
python_dを利用してbuildする場合でも，このオプションをつけるとうまくいかなかったです．
ちなみに，`--cmake-args`をつけてビルドした場合，同様に`cmake-clean-cache`する必要があるのですが，その時にも`--camke-args`オプションは必要です．

```
colcon build --cmake-cache-clean --cmake-args -DCMAKE_BUILD_TYPE=Debug
```

##おわりに
僕が調べた範囲だと日本語の記事等出てこなくて，結構苦労しました．
~~ほとんどの人WSLとかDocker使うやろうしWindows上で頑張ってもしゃあないってことですかね~~

公式ドキュメントのインストールガイドだと，動作確認として`demo_nodes_py`を用いるんですが，これは`python_d`を使わなくても動いちゃうんですよね．
なので，実はビルドしたPythonパッケージは動かない状態なのに，exampleが通ったので環境構築の時にはそれでオッケーにしちゃって，あとでハマるみたいなことになるじゃないかなと思いました．というかなりました．