PythonのuvをROS2で使う

## サンプルプロジェクト

今回の記事の内容からより利便性を向上させたサンプルプロジェクトはこちら

https://github.com/Geson-anko/ros2_uv_template

## はじめに

ROS2の標準的なPythonパッケージ`ament_python`は、`setup.py`形式を採用しておりますが、それは現在のPythonでは非推奨となっています。

Pythonコミュニティでは`pyproject.toml`を用いることが主流となっているため、本記事では、Rustで実装された高速なパッケージマネージャ[uv](https://docs.astral.sh/uv/)を用いて、ROS2のPython開発環境をモダンに構築する方法を紹介します。

## 環境

- OS: **Ubuntu 24.04.1 LTS**
- ROS2: **Jazzy**

## 1. 開発環境のセットアップ

### uvのインストール

```bash
# 通常のインストール
curl -LsSf https://astral.sh/uv/install.sh | sh
```
```Dockerfile
# Dockerの場合
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
```

### ROS2パッケージの作成

**重要：`ament_python`ではなく`ament_cmake` (デフォルト)を使用します。**

```bash
cd ~/ros2_ws/src
ros2 pkg create uv_sample_pkg --license MIT
cd uv_sample_pkg
```

生成されるパッケージの初期構造: 
```
uv_sample_pkg/
├── CMakeLists.txt
├── LICENSE
├── include
│   └── uv_sample_pkg
├── package.xml
└── src
```

### `uv`プロジェクトの設定

```bash
# プロジェクトの初期化
uv init . --lib --python-preference only-system

# 仮想環境の作成（ROS2パッケージにアクセス可能）
uv venv --system-site-packages
```

コマンドラインオプションの説明：

`uv init`のオプション：
- `--lib`: プロジェクトをライブラリとして初期化します。これにより`src`ディレクトリ内にPythonパッケージとしての構造が生成されます。
- `--python-preference only-system`: システムにインストールされているPythonを使用します。これはROS2のPythonパッケージとして使用する際に重要です。

`uv venv`のオプション：
- `--system-site-packages`: システムのPythonパッケージ（`rclpy`など）を仮想環境から利用可能にします。ROS2の開発には必須です。

初期化後の構造：
```diff
uv_sample_pkg/
  ├── CMakeLists.txt
  ├── LICENSE
+ ├── README.md
  ├── include
  │   └── uv_sample_pkg
  ├── package.xml
+ ├── pyproject.toml
  └── src
+    └── uv_sample_pkg
+         ├── __init__.py
+         └── py.typed
```

## 2. ROS2ノードの実装

### パッケージ構造の準備

今回はROSのノードを`scripts`ディレクトリに作成します。

```bash
# scriptsディレクトリの作成
mkdir scripts
```

### ライブラリコードの実装

`src/uv_sample_pkg/__init__.py`に以下のコードを記述します：

```python
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class MinimalPublisher(Node):
    def __init__(self):
        super().__init__("minimal_publisher")
        self.publisher_ = self.create_publisher(String, "topic", 10)
        timer_period = 0.5  # seconds
        self.timer = self.create_timer(timer_period, self.timer_callback)
        self.i = 0

    def timer_callback(self):
        msg = String()
        msg.data = f"Hello World: {self.i}"
        self.publisher_.publish(msg)
        self.get_logger().info(f'Publishing: "{msg.data}"')
        self.i += 1

def main(args=None):
    rclpy.init(args=args)
    minimal_publisher = MinimalPublisher()
    
    try:
        rclpy.spin(minimal_publisher)
    except KeyboardInterrupt:
        pass
    finally:
        minimal_publisher.destroy_node()
        rclpy.shutdown()
```

### エントリーポイントの作成

`scripts/publisher.py`を作成し、以下のコードを記述します：

```python
#!/usr/bin/env python3
from uv_sample_pkg import main

if __name__ == "__main__":
    main()
```

その後、実行権限の付与します：

```bash
chmod +x scripts/publisher.py
```

## 3. ビルド設定

### CMakeLists.txtの設定

`CMakeLists.txt`を以下のように編集します：

```cmake
cmake_minimum_required(VERSION 3.8)
project(uv_sample_pkg)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# 依存パッケージ
find_package(ament_cmake REQUIRED)
find_package(ament_cmake_python REQUIRED)
find_package(rclpy REQUIRED)

# Pythonパッケージのインストール
ament_python_install_package(${PROJECT_NAME} PACKAGE_DIR src/${PROJECT_NAME})

# スクリプトのインストール
install(PROGRAMS scripts/publisher.py DESTINATION lib/${PROJECT_NAME})

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  set(ament_cmake_copyright_FOUND TRUE)
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()
```

## 4. ビルドと実行

### ビルド

```bash
# ワークスペースのルートディレクトリで実行
cd ~/ros2_ws
colcon build --packages-select uv_sample_pkg --symlink-install
source ./install/setup.sh
```
:::note info
`--symlink-install`をつけることでシンボリックリンクとしてPythonパッケージがインストールされるため、ソースファイルの編集を直ちにノードに反映することができます。
:::


### 実行

```bash
# パッケージディレクトリで仮想環境を有効化
cd ~/ros2_ws/src/uv_sample_pkg
source .venv/bin/activate

# ノードの実行
ros2 run uv_sample_pkg publisher.py
```

別のターミナルで実行結果を確認：
```bash
ros2 topic echo /topic
```

## なぜuvを使うのか？

1. **高速な依存解決**
   - **Rust**による実装のため`poetry`などと比較して非常に高速です。

2. **モダンな開発環境**
   - `pyproject.toml`による管理
   - Python公式のパッケージングガイドに準拠しています。（`poetry`や`rye`は独自のスタイル）

3. **環境分離**
   - パッケージごとの独立した仮想環境


## uvの使い方

### 依存パッケージの追加

```bash
# 実行時依存の追加
uv add numpy

# 開発時依存の追加
uv add --dev pytest
```

## まとめ

`uv`を使用することで、厳密な依存関係の管理、モダンなPythonパッケージ管理、そして仮想環境によるパッケージ間の独立性の向上が図れられます。もし用途が合えば、ぜひ試してみてください！✨

## 参考文献

- [【ROS2/Python】RyeをROS2で使う](https://qiita.com/koichi_baseball/items/d6e54c4e727b3f303dee) 👈 この方の記事を大変参考にさせていただきました :bow: 
- [uv Documentation](https://docs.astral.sh/uv/)
- [Python Packaging Guide](https://packaging.python.org/ja/latest/)
- [ROS2 Documentation](https://docs.ros.org/en/rolling/)