VSCodeで2段階リモート接続時の設定JSONの場所

毎回忘れて困るのでメモ。

  • Windows環境にVSCodeが入っていて
  • 以下どちらかのルートで2段階でremote接続している状況(VSCode経由ではコンテナを起動していない)
    • WSL > 既存のDockerコンテナにアタッチ
    • SSH > 既存のDockerコンテナにアタッチ

リモート接続の設定JSONは以下の場所に入っている。

C:\Users\<ユーザー名>\AppData\Roaming\Code\User\globalStorage\ms-vscode-remote.remote-containers\imageConfigs

Dev Containersでosrf/ros:humble-desktopと表示されているコンテナの設定ファイル名は「osrf%2fros%3ahumble-desktop.json」

どうしてもWSLg環境でAWSIMを動かしたい時に見る記事

注意事項

今回のやり方は不具合が発生する可能性が高いため自力で問題切り分け・解決が可能な方に向けた情報共有です。広くおススメできる方法ではありません

達成したこと

Windows環境でAWSIMを起動し自動運転AIチャレンジのサンプルがそれらしく動くことを確認した。

WSLg上でAWSIMを起動する様子

※公式サイトの推奨環境ページに記載されている通り自動運転AIチャレンジの環境はUbuntu22.04を前提としている。今回は無理やりWindowsに環境作ったという報告

環境の構成

自動運転AIチャレンジの環境はシミュレータであるAWSIMと制御系のAutowareからなっている。(と思う。まだ始めたばかりでよく分かっていない)

Autowareの方はWindowsのWSL2環境でUbuntu22.04を用意して大会公式が用意してくれている環境構築手順そのままで構築すればOK。

問題はAWSIMの方でVulkanを使っているのでWSLg環境でDockerコンテナ内から実行することができない(エラーになる)。
※大会公式の環境ではUbuntu22.04上にDockerコンテナを作る形になっている

対策としてWSLgのIssueにあるようにkisak-mesaのドライバを入れてVulkanをD3D12に読み替えてバイパスしてもらうようにすれば、とりあえずたいていのVulkanアプリは動く。
ただし、自分が以前試した限りUbuntu24.04しかこの方法は使えなかった。参考

ただし、AWSIMはUbuntu22.04, ROS2 Humbleを前提にビルドされているので無理やり何とかする必要がある。

以上をまとめると下図のようにWindows上で22.04と24.04の2つのWSL環境を動かしAutowareはDockerコンテナ内、AWSIMはコンテナを使わず24.04上で直接実行する。

構成図

AWSIMを無理やり動かすための対策内容

WSL2上のUbuntu24.04でVulkanアプリが動くようにする

上述したWSLgのIssueの通りでOK。

sudo add-apt-repository ppa:kisak/kisak-mesa
sudo apt update
sudo apt upgrade

依存ライブラリを無理やりインストール

AWSIMの依存ライブラリをUbuntu22.04、ROS2 humbleからUbuntu24.04環境に持ってくる。

※バージョンが一致しないものを無理やり持ってくるので動く保証はない点に注意。一見動いているように見えても正しく動いていない可能性もある

Ubuntu22.04 から

足りないのは以下のファイル。/usr/lib/x86_64-linux-gnu ディレクトリに入っているのでWSL2のUbuntu22.04環境からコピーしてくる。

  • libconsole_bridge.so.1.0
  • libfmt.so.8.1.1
  • liborocos-kdl.so.1.5.1
  • libspdlog.so.1.9.2

24.04環境にコピーしたら以下のシンボリックリンクを張る。

libfmt.so.8 -> libfmt.so.8.1.1
liborocos-kdl.so.1.5 -> liborocos-kdl.so.1.5.1
libspdlog.so.1 -> libspdlog.so.1.9.2

ROS2 humble から

公式のリリースページからros2-humble-日付-linux-jammy-amd64.tar.bz2をダウンロードしてきてtar.bz2ファイルの中から以下のファイルをUbuntu24.04環境に入れる。

  • libclass_loader.so
  • librcl_action.so
  • librclcpp_action.so

入れる場所は分かりやすいように /opt/ros/humble/lib に入れた。

※Ubuntu24.04に対応するのはROS2 jazzyなので24.04環境に本来humbleディレクトリは存在しない

libpython3.10.so.1.0

Python3.10はPPAがあるので以下の手順ですんなり入る。

sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt install libpython3.10

AWSIMファイル転送

Ubuntu22.04側で大会公式の環境構築手順を実施すればAWSIMのファイルもダウンロードされているので以下のファイルをUbuntu24.04側へコピーする。

  • aichallenge-racingkart/aichallenge/simulator/AWSIM
    • AWSIMのファイル一式。ディレクトリごとコピーする
  • aichallenge-racingkart/vehicle/cyclonedds.xml
    • CycloneDDSの設定ファイル。Autoware側でも同じファイルを使うので22.04側から削除しないように注意

ファイルの置き場所は任意で問題ないはず。今回はユーザのホームディレクトリ直下に置いた。

実行手順

AWSIM起動

WSL2インスタンスを起動するたびに以下を1回実行する。(Autoware側はDockerコンテナで実行してくれている)

# ROS2のディスカバリー通信が届くようにloデバイスをマルチキャストOnに設定する
sudo ip link set multicast on lo
# CycloneDDSでエラーにならないようにバッファサイズを増やす
sudo sysctl -w net.core.rmem_max=2147483647 >/dev/null

make dev手順の代わりに以下のコマンドで同等の手順になる。

CYCLONEDDS_URI="file://$HOME/cyclonedds.xml" \
ROS_LOCALHOST_ONLY=0 \
RMW_IMPLEMENTATION=rmw_cyclonedds_cpp \
ROS_DOMAIN_ID=1 \
LD_LIBRARY_PATH="$HOME/AWSIM/AWSIM_Data/Plugins:/opt/ros/humble/lib" \
$HOME/AWSIM/AWSIM.x86_64 \
  --start-mode off \
  --vehicles 1 \
  --laps 600 \
  --timeout 60000000

LD_LIBRARY_PATHにはAWSIMのPluginsディレクトリと無理やり持ってきたHumbleのライブラリ置き場を指定する。

AWSIMのコマンドライン引数に何を指定すれば良いのかは aichallenge-racingkart/aichallenge/run_simulator.bash を見れば分かる。

Autoware起動

make dev手順の代わりに以下のコマンドで同等の手順になる。

make autoware-simulator

Appendix デバッグ・問題切り分け作業内容

AWSIM エラー内容の確認

AWSIM実行中のエラーは以下のログファイルに出力されている。

/$HOME/.config/unity3d/TIERIV/AWSIM/Player.log

以下のように問題のあるPluginがログに残っている。

Unable to preload the following plugins:
    libfastrtps.so
    libfastrtps.so.2.6.3
    librmw_fastrtps_cpp.so
    librmw_fastrtps_shared_cpp.so

※自動運転AIチャレンジではCycloneDDSが使われているので問題ないが、今回の対策を実行しても上記のFastRTPS関連のエラーは残ったままなので注意

ROS2通信が上手くいかない場合は先にログを見た方がよさそう。

足りないライブラリの探し方

今回AWSIMのPluginがロードできない問題は依存ライブラリが足りないケースのみだったためlddスクリプトでロードできていない.soを1つずつ調べて足りないライブラリを探した。

# libRobotecGPULidar.soの依存関係を調べる例
LD_LIBRARY_PATH="$HOME/AWSIM/AWSIM_Data/Plugins:/opt/ros/humble/lib" \
ldd AWSIM/AWSIM_Data/Plugins/libRobotecGPULidar.so

結論

コンテナを使わないUbuntu24.04環境に無理やり依存ライブラリを持ってくることでWindows環境でも自動運転AIチャレンジの環境を作ることができた。
ただし、ライブラリの互換性問題で一見動いているように見えても正しく動かない可能性があるため広くおススメできる状態にはならなかった。
この課題は自動運転AIチャレンジの環境がUbuntu24.04に移行すれば解消しそうに思える(※)ので現状は別途Ubuntu22.04環境を用意するのが良いと思わる。

※AWSIMがUbuntu24.04前提ならmesaドライバのVulkan対応のみで動くため

以上

専用ライブラリを使わずに点群をglTFファイルに書き出してみた

CARLAシミュレータで出したLiDAR点群を可視化したいなぁと思ってnumpy.ndarrayの3次元座標をglTFファイルに出力するPythonスクリプトを書いてみた。

glTFファイルはテキスト形式だとJSONフォーマットらしくて、ざっくりした説明は公式のAPIリファレンスガイドPDFに書いてある。

今回は点群1つ1つに色を付けるのではなく、種類ごとに色を分けるだけにとどめた。 そうするとmeshesが1個で中に複数のprimitives[]を持てばよいらしい。

meshes[]
  ┣ primitives[0]
  ┃    ┣ attributes.POSITION  # accessors[]のインデックス番号(0始まり)を入れる
  ┃    ┗ material  # materials[]のインデックス番号(0始まり)を入れる
  ┗ primitives[1]

座標データは accessors > bufferViews > buffers と参照関係になるらしい。そしてbuffers[]のuriにはbase64エンコードした座標値を入れる。テキストのリストを書けないのは残念。テキストエディタで座標値変更できないね。手作業で座標値を更新できないならライブラリでglTF出力すれば良いのでは

ソースコード

from __future__ import annotations
import copy
import json
import base64
import numpy as np


GLTF_BASE_DICT = {
    'asset': {
        'version': '2.0',
        'generator': 'glTF Point Cloud from python script'
    },
    'scene': 0,
    'scenes': [{'nodes': [0]}],
    'nodes': [{'mesh': 0}],
    'meshes': [{'primitives': []}],  # ここに複数入る
    'materials': [],                 # ここに複数入る
    'accessors': [],                 # ここに複数入る
    'bufferViews': [],               # ここに複数入る
    'buffers': [],                   # ここに複数入る
}


def _get_material(color: tuple[int,int,int]) -> dict:
    """
    {
      "pbrMetallicRoughness": {
        "baseColorFactor": [color[0]/255, color[1]/255, color[2]/255, 1.0],
        "metallicFactor": 0.0,
        "roughnessFactor": 1.0
      }
    }
    """
    rgba = [value / 255 for value in color] + [1.0]
    return dict(pbrMetallicRoughness=dict(baseColorFactor=rgba, metallicFactor=0.0, roughnessFactor=1.0))


def _get_buffer(points: np.ndarray) -> dict:
    """
    {
      "byteLength": ...,
      "uri": "data:application/octet-stream;base64,..."
    }
    """
    binary_data = points.astype(np.float32).tobytes()
    base64_str = base64.b64encode(binary_data).decode('utf-8')
    uri = 'data:application/gltf-buffer;base64,' + base64_str
    return dict(byteLength=len(binary_data), uri=uri)


def _get_view(points: np.ndarray, buffer_index) -> dict:
    """
    {
      "buffer": buffer_index,
      "byteOffset": 0,
      "byteLength": ...,
      "target": 34962  # OpenGL buffer target?
    }
    """
    return dict(buffer=buffer_index, byteOffset=0, byteLength=len(points.astype(np.float32).tobytes()), target=34962)


def _get_accessor(points: np.ndarray, view_index) -> dict:
    """
    {
      "bufferView": view_index,
      "byteOffset": 0,
      "componentType": 5126,  # GL_FLOAT
      "count": 3,
      "type": "VEC3",
      "max": [1.0, 1.0, 0.0],
      "min": [0.0, 0.0, 0.0]
    }
    """
    count = points.shape[0]  # pointsのshapeは(N, 3)
    max_value = np.max(points, axis=0)
    min_value = np.min(points, axis=0)
    return dict(bufferView=view_index, byteOffset=0, componentType=5126, count=count, type='VEC3', max=max_value.tolist(), min=min_value.tolist())


def _get_primitive(attr_index, material_index) -> dict:
    """
    {
        "attributes": {
        "POSITION": attr_index
        },
        "mode": 0,  # POINTS
        "material": material_index
    }
    """
    return dict(attributes=dict(POSITION=attr_index), mode=0, material=material_index)


def add_points(gltf_dict: dict, points: np.ndarray, color: tuple[int,int,int]):
    materials: list = gltf_dict['materials']
    material_index = len(materials)
    materials.append(_get_material(color))

    buffers: list = gltf_dict['buffers']
    buffer_index = len(buffers)
    buffers.append(_get_buffer(points))

    bufferViews: list = gltf_dict['bufferViews']
    view_index = len(bufferViews)
    bufferViews.append(_get_view(points, buffer_index=buffer_index))

    accessors: list = gltf_dict['accessors']
    accessor_index = len(accessors)
    accessors.append(_get_accessor(points, view_index=view_index))

    primitives: list = gltf_dict['meshes'][0]['primitives']
    primitives.append(_get_primitive(attr_index=accessor_index, material_index=material_index))


def write_gltf(point_list, color_list, output_filepath):
    gltf_dict = copy.deepcopy(GLTF_BASE_DICT)
    for point, color in zip(point_list, color_list):
        add_points(gltf_dict, point, color)

    with open(output_filepath, mode='w', encoding='utf-8', newline='') as f:
        json.dump(gltf_dict, f, indent=4)


if __name__ == '__main__':
    # お試し
    r = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]])
    g = np.array([[0.5, 0, 0.125], [0, 0.5, 0.125], [0.5, 0.5, 0.125], [0, 0, 0.125]])
    b = np.array([[0.25, 0, 0.25], [0, 0.25, 0.25], [0.25, 0.25, 0.25], [0, 0, 0.25]])
    write_gltf([r, g, b], [(255, 0, 0), (0, 255, 0), (0, 0, 255)], 'test.gltf')

お試し実行結果

glTFビューアで見たらそれっぽい位置関係で表示できたのでたぶん大丈夫?

ビューアで閲覧した画像

点が小さくて視力検査みたいになっちゃった。

PyAVで動画切り出し

PyAVでmp4ファイルの一部を切り出した(例えば開始40秒から20秒間の動画を切り出す)新しい動画を作ろうとしたけど、なかなかうまくいかなかったのでソースコードを残しておく。

使用したバージョン

  • av 16.1.0

ポイント

VideoFrameの以下のattributeを再設定しないと狙った通りの動画が生成されなかった。

attribute 設定値
dts None
pts None
time_base Fraction(1, rate)

rateは動画のフレームレート。30fpsならrate=30

ソースコード

import av
from av.container import InputContainer
from av.video import VideoFrame
from fractions import Fraction


def clip(in_file, out_file, start, end):  # in_fileのstart秒からend秒までをout_fileへ切り出す
    rate = 30  # 本来はin_stream.codec_context.framerate か in_stream.codec_context.rate から自動設定したかったが今回試した環境だとどちらもNoneが返るためハードコードした
    in_container: InputContainer
    with av.open(in_file) as in_container, av.open(out_file, mode='w') as out_conteiner:
        in_stream = in_container.streams.video[0]
        # out_stream = out_conteiner.add_stream_from_template(in_stream)  # 自分の環境だと UnknownCodecError になるのでadd_stream_from_template()メソッドではダメだった
        out_stream = out_conteiner.add_stream('h264', rate, width=in_stream.width, height=in_stream.height)
        in_container.seek(int(start / in_stream.time_base), stream=in_stream)
        for pkt in in_container.demux(in_stream):
            frame: VideoFrame
            for frame in pkt.decode():
                if frame.time < start:
                    continue
                if frame.time > end:
                    break
                frame.dts = None
                frame.pts = None
                frame.time_base = Fraction(1, rate)
                for packet in out_stream.encode(frame):
                    out_conteiner.mux(packet)
        # Flush stream
        for packet in out_stream.encode():
            out_conteiner.mux(packet)

VSCodeからAWSのSSM接続経由でSSHする時にインスタンスID指定しない方法

たぶんできそうな案が出来たのでめも。動作確認できたらタイトルから「案」を削除予定 動作確認できた。プロファイル指定漏れがあった個所を修正済み。

結論

以下のモチベーションで色々試したりGoogle Geminiにアドバイスを求めたりしてたどり着いた方法。

以降のawsコマンドに--profile my_profile引数を渡している箇所は自分のプロファイル名に読み替えること。

# C:\Users\ユーザ名\.ssh\config
Host my_auto_ec2
    # EC2インスタンスのNameタグに付けた名前
    HostName my-instance-name
    # EC2インスタンスへログインするユーザ名
    User my-ec2-user
    ProxyCommand C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -Command "$id = (aws ec2 describe-instances --profile my_profile | jq -r --arg name Name --arg value %h '.Reservations[].Instances[] | select(.Tags[].Key==$name) | select(.Tags[].Value==$value) | .InstanceId') ; aws ssm start-session --profile my_profile --target $id --document-name AWS-StartSSHSession --parameters portNumber=%p"

この設定でssh my_auto_ec2で接続できる。

ProxyCommand に書いたPowerShellコマンドの簡単な解説

  • powershell.exe -Command "コマンド1 ; コマンド2"形式でPowerShellにコマンドを2つ続けて実行させる
  • "$id = (コマンド)"でコマンド実行結果を変数idに入れる
  • jqlang/jqは--arg 変数名 変数の内容形式のコマンドライン引数でクエリ文字列の内部で変数を使える(複雑なエスケープ指定を避けられる)

おまけ

表題と関係ないがjqlang/jqの変数を活用すると以下のような複雑な処理も書けた。

# Nameタグが「my-instance」から始まるインスタンスの名前, インスタンスタイプ, 状態をそれぞれカンマ区切りで表示する
aws ec2 describe-instances --profile my_profile | jq -r --arg name Name --arg value 'my-instance' '.Reservations[].Instances[] | select(.Tags[].Key==$name) | select(.Tags[].Value | startswith($value)) | [(.Tags[] | select(.Key==$name).Value), .InstanceType, .State.Name]' | jq -r --arg sep ', ' 'join($sep)'

出力例

my-instance-micro, t3.nano, running
my-instance-large, t3.large, stopped
my-instance-xlarge, t3a.xlarge, stopping

CMakeLists.txt自分用書き方メモ2025年版

CMakeLists.txt、たまにしか書かないからすぐ書き方を忘れるし、cmakeはすぐに新しいより良い書き方が出ているので調べた範囲での2025年時点の書き方をメモ。

未来の自分へ

毎回名前を忘れちゃうアレ、「ジェネレータ式」だよ。

# こういうの
target_compile_options(a.out PUBLIC
  $<$<PLATFORM_ID:Linux>: -Wall>
  $<$<PLATFORM_ID:Windows>: /Wall>
)

今回作ったもの

OpenCLの習作でA * B + Cを計算するDLLを作った。cmakeの勉強が主目的。リポジトリは以下。

github.com

今回やりたかったこととしてDLLの利用者(呼び出し側)にはOpenCLのI/Fを見せない(依存させない)ようにした。

ビルドしたものをインストールするとインストール先に以下の内容が生成されるようにした。

┣- bin
┃     ┣- fma_opencl.dll                 # 今回作ったDLL
┃     ┣- OpenCL.dll                     # OpenCLのDLL。依存している外部DLLも一緒に配布
┣- cmake                                # find_package()してもらうためのファイル(お試しで出してみた)
┃     ┣- fma_ocl_exports-release.cmake
┃     ┣- fma_ocl_exports.cmake
┣- include
┃     ┣- fma_opencl.hpp                 # 今回作ったDLLのI/Fを定義したヘッダファイル
┃     ┣- fma_opencl_export.h            # dllexportなどが書かれたファイル。自動生成したもの
┣- lib
      ┣- fma_opencl.lib                 # DLLリンク用。Windowsのみ

ソースコードディレクトリ構成は以下の感じ。

┣- include
┣- src
┣- test
┃     ┣- CMakeLists.txt
┣- CMakeLists.txt

以降にCMakeLists.txtの書き方について今回分かったつもりになったことをメモ(間違いやもっと良い書き方はあるはず)。

外部ライブラリの取り込み : FetchContent

  1. 今回作ったDLL
  2. OpenCL-SDK
  3. OpenCL-CLHPP, OpenCL-Headers, OpenCL-ICD-Loader

1 > 2 > 3の方向に依存関係がある。gitのサブモジュールとして1のexternalディレクトリに2を置く方法もあるが今回はCMakeLists.txtに書く方法を使ってみた。

公式のドキュメント。FetchContent_Declareでどんなことが指定できるかはExternalProject_Addのドキュメントを見ればOK。

ダウンロード+unzipからのfind_package

今回はWindowsでのcmake時が該当。(OpenCL-SDKをgit cloneしようとしたがビルドが大変すぎて諦めてビルド済みzipを使うようにした)

include(FetchContent)

FetchContent_Declare(OpenCL_SDK
  URL https://github.com/KhronosGroup/OpenCL-SDK/releases/download/v2025.07.23/OpenCL-SDK-v2025.07.23-Win-x64.zip
)

FetchContent_MakeAvailable(OpenCL_SDK)  # ダウンロードしてunzip

find_package(OpenCL REQUIRED
  HINTS ${opencl_sdk_SOURCE_DIR}
)

FetchContentのinclude > FetchContent_Declare() > FetchContent_MakeAvailable()の順番。

FetchContent_Declare()で指定したzipファイルがビルドディレクトリ配下の _deps/opencl_sdk-src にunzipされる。 変数opencl_sdk_SOURCE_DIRに_deps/opencl_sdk-srcが入っているのでfind_package()に指定した。

※zipファイルはビルド済みのファイルが入っていてそのままfind_package()できる形式だったため上記の書き方になった

git cloneからのadd_subdirectory

今回はLinuxでのcmake時が該当。

include(FetchContent)

FetchContent_Declare(OpenCL_SDK
  GIT_REPOSITORY https://github.com/KhronosGroup/OpenCL-SDK.git
  GIT_TAG v2025.07.23
  GIT_SUBMODULES_RECURSE TRUE   # --recursive 相当
  GIT_SHALLOW TRUE              # --depth 1 相当
)

set(OPENCL_SDK_BUILD_SAMPLES OFF)  # FetchContent_DeclareでCMAKE_ARGSが効かないのでワークアラウンド

FetchContent_MakeAvailable(OpenCL_SDK)  # git cloneしてadd_subdirectory

ビルドディレクトリ配下の _deps/opencl_sdk-src にgit cloneしてadd_subdirectory()相当っぽいことをやってくれるっぽい。 OpenCL-SDKのexternalに依存ライブラリがサブモジュールとして入っているので再帰的なクローンを有効にしている。

なお、CMAKE_ARGSは効かないらしいのでset()で代用した。 (数年後にはしれっと対応してそうな気もする)

ソースファイル

export用ヘッダファイル自動生成

include(GenerateExportHeader)

generate_export_header(fma_opencl)

これでビルドディレクト直下fma_opencl_export.hが生成される(ビルドディレクトリはCMAKE_CURRENT_BINARY_DIRで参照可能)。

DLLのI/Fを定義するヘッダファイルでfma_opencl_export.hをincludeしてI/F関数やクラスに FMA_OPENCL_EXPORT を付けるとdllexportなどに読み替えてくれる(マクロになっている)。

名前のルールとかは公式のドキュメント参照

ソースファイル一式の指定

cmake 3.23 以降は以下の書き方ができそう。target_sources()のドキュメント

target_sources(fma_opencl
  PRIVATE             # 実装コード(DLL利用者に見せないファイル)
    src/main.cpp
  INTERFACE           # DLL利用者に見せるファイル(呼び出しI/Fが書かれたファイルをこちらで指定する)
    FILE_SET HEADERS  # `HEADERS`という名前のFILE_SETを定義
      TYPE HEADERS    # `ヘッダファイル`種別。この行のHEADERSはFILE_SETとは別。ややこしいのでFILE_SET名の方は別名にした方が良いような
      BASE_DIRS
        include
        ${CMAKE_CURRENT_BINARY_DIR}
      FILES
        include/fma_opencl.hpp
        ${CMAKE_CURRENT_BINARY_DIR}/fma_opencl_export.h  # generate_export_header()で自動生成したファイル
)

# これがないとヘッダファイルをincludeできない
target_include_directories(fma_opencl PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
)

if (NOT WIN32)
  target_include_directories(fma_opencl PRIVATE
    ${opencl_sdk_SOURCE_DIR}/external/OpenCL-CLHPP/include
    ${opencl_sdk_SOURCE_DIR}/external/OpenCL-Headers
  )
endif ()

target_sources()でBASE_DIRSを指定しないと後述するinstall(EXPORT)を入れた時に以下のエラーになった。

must be in one of the file set's base directories

target_sources()だけだとヘッダファイルのインクルードパスが入らない?っぽくてtarget_include_directories()も必要だった(少なくとも試した範囲では)。

それからLinux(git cloneしている)の場合はexternal配下がインクルードパスに必要だった。今回書いたコードではOpenCL-HeadersとOpenCL-CLHPP配下のヘッダが必要だったのでそれらをtarget_include_directories()した。 ただし、PUBLIC指定だと以下のエラーになってしまう。

CMake Error in CMakeLists.txt: Target "fma_opencl" INTERFACE_INCLUDE_DIRECTORIES property contains path:

"/home/ubuntu/build/_deps/opencl_sdk-src/external/OpenCL-CLHPP/include"

which is prefixed in the build directory.

CMake Error in CMakeLists.txt: Target "fma_opencl" INTERFACE_INCLUDE_DIRECTORIES property contains path:

"/home/ubuntu/build/_deps/opencl_sdk-src/external/OpenCL-Headers"

which is prefixed in the build directory.

今回はOpenCLへの依存はDLL内に閉じたためPRIVATEに変更して対処した。PUBLICで追加する方法は不明。

インストール指定

installコマンドのドキュメントGNUInstallDirsモジュールのドキュメント

include(GNUInstallDirs)  # CMAKE_INSTALL_ほげほげ を使うためinclude

# インストールするファイルとインストール先の設定
install(TARGETS fma_opencl
  EXPORT fma_ocl_exports
  RUNTIME                     # デフォルトのDESTINATIONはbin。WindowsのDLL
  LIBRARY                     # デフォルトのDESTINATIONはlib。LinuxのDLL
  ARCHIVE                     # デフォルトのDESTINATIONはlib。静的ライブラリもしくはWindows版DLLに対するlib
  FILE_SET HEADERS            # デフォルトのDESTINATIONはinclude
)

# .cmakeファイルのエクスポート設定。install(TARGETS)にEXPORT指定が必要
install(EXPORT fma_ocl_exports DESTINATION cmake)

# 依存しているOpenCLのDLLをインストールする設定
install(FILES
  $<TARGET_RUNTIME_DLLS:fma_opencl>
  DESTINATION $<$<PLATFORM_ID:Windows>:${CMAKE_INSTALL_BINDIR}>$<$<PLATFORM_ID:Linux>:${CMAKE_INSTALL_LIBDIR}>
)

最後のinstall(FILES)は依存しているOpenCL-SDKのDLLを一緒にインストールするための設定。 恐らくcmakeの想定としては、DLLが依存するライブラリはfind_package()とかでDLLの利用者側で取り込む想定なんじゃないかと思う。今回はDLLの利用者にOpenCL依存させない方向にしたかったので一緒に配布するためにinstall(FILES)でインストール対象に追加した。

以下はOpenCL-SDKのDLLを一緒にインストールしようとして試した(ダメだった)こと。

# install(TARGETS)側にRUNTIME_DEPENDENCY_SETを入れておいて
install(TARGETS fma_opencl
  RUNTIME_DEPENDENCY_SET fma_ocl_dlls
  ...
)

# install(RUNTIME_DEPENDENCY_SET)を書く
install(RUNTIME_DEPENDENCY_SET fma_ocl_dlls)

その1。RUNTIME_DEPENDENCY_SETで解決しようとするとWindows環境でコンパイラに使っているVisual Studio関連DLLまで探そうとしてエラーになる...(エラーの原因はVisual Studio関連DLLが見つからない(C:\System32とかに入っていた)から) あー、確かに依存しているDLLではあるね。うん。

install(IMPORTED_RUNTIME_ARTIFACTS)

その2。IMPORTED_RUNTIME_ARTIFACTSで入ってくれないか確かめたが、fma_opencl.dll(今回作ったDLL自身)がインストールされてしまう(ように見える)。依存しているライブラリのDLLではなかった。

テスト

以下のようにOnの場合にテストコードを入れたディレクトリをadd_subdirectory()する形らしい。(テストディレクトリにもCMakeLists.txtを置く)

# オプションでOn/Off可能にしておいて
option(BUILD_TESTING "Enable testing and build tests" OFF)

# Onならenableしてからadd_subdirectory
if(BUILD_TESTING)
  enable_testing()
  add_subdirectory(test)
endif ()

テストディレクトリのCMakeLists.txtではテスト用の実行ファイルをadd_executableする。

# テスト用の実行ファイル
add_executable(test_fma fma.cpp)
target_link_libraries(test_fma gtest_main fma_opencl)

# CTestに登録?
include(GoogleTest)
gtest_discover_tests(test_fma)

google testはFetchContentでgit clone+add_subdirectoryするのが楽。

include(FetchContent)
FetchContent_Declare(googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        v1.17.0
  GIT_SHALLOW    TRUE
)
FetchContent_MakeAvailable(googletest)

あと、テスト実行時に依存するDLLが無いとエラーになるのでカスタムコマンドでコピーする設定を入れておく。

参考1 参考2

if (WIN32)
  add_custom_command(TARGET test_fma POST_BUILD
    COMMAND
      ${CMAKE_COMMAND} -E copy -t $<TARGET_FILE_DIR:test_fma> $<TARGET_RUNTIME_DLLS:test_fma>
    COMMAND_EXPAND_LISTS
  )
endif ()

PyTorchのCUDAExtensionがpipのビルド隔離環境と相性が最悪な件

忘れそうなのでメモ

発生条件

  • Pythonのライブラリ
  • setup.pyでインストール(ビルド済みのwheelではない)
  • pyproject.tomlがある
  • ビルド作業がPythonの環境に依存している(PyTorchのCUDAExtensionのようにインストール済みのPyTorchに依存する、など)

起きること

  • インストール済みのライブラリではなくビルド用の隔離環境に新たにインストールしたライブラリを使ってビルド作業が行われる
    • 例えばPyTorchならCPU版がインストールされてCUDA関連のビルドなどでエラー祭りになる

手っ取り早い解決策

  • pipコマンドの引数に--no-build-isolationを追加してビルド用の隔離環境を使わないようにする

pyproject.tomlに関するメモ

以下のような記述の場合pipによるビルド時の隔離環境にはsetuptools, wheel, torchのみがインストールされるらしい。

[build-system]
build-backend = "setuptools.build_meta"
requires = [
  "setuptools",
  "wheel",
  "torch",
]

※ちなみにGoogle Gemini君に質問したら「requiresで--index-urlや--extra-index-urlなどのオプションを渡すことができない」という回答だった(ウラは取っていないので真偽不明)