google/gousbを用いたソフトウェアをRaspberry Pi向けにクロスコンパイルする
この記事は UMITRON Advent Calendar 2022 2日目の記事です。
弊社では自社プロダクトにRaspberry Piを用いており、そのRaspberry PiでUSB機器を扱うことがあります。またRaspberry Pi内で動くソフトウェア開発にGoを用いています。
そこでUSB機器をGoで扱う上で google/gousb を利用しようとしたんですが、 google/gousb はlibusbに依存しており、macOSでRaspberry Pi向けにクロスコンパイルするのにひと手間必要で、ググっても出てこなかったので、この記事ではこのやり方を共有しようと思います。
クロスコンパイルする例として google/gousb のサンプルプログラムである lsusb をコンパイルします。またDockerを用いて、32-bitのRaspberry Pi OS用にコンパイルします。私の検証環境はApple SiliconのmacOSですが、他のOSでも同様に可能だと思います。
ベースのDockerイメージ
公式のGoのイメージを用います。OSはDebianにします。
依存のインストール
公式のREADMEにもある通り、 libusb-1.0 をインストールする必要があります。
You must first install libusb-1.0
ただし、クロスコンパイルするため、32-bit Raspberry Pi OS、つまりarmhfのlibusb-1.0をインストールする必要があります。
Debianで別archのパッケージをインストールする場合、以下のようにします。
RUN dpkg --add-architecture armhf
RUN apt-get update
RUN apt-get install -y libusb-1.0-0-dev:armhf
ホスト側のlibusbも必要だったのでインストールします。
RUN apt-get install -y libusb-1.0-0-dev
また、cgoの仕組みを用いてコンパイルするわけですが、cgoでクロスコンパイルする場合Cのクロスコンパイラを指定する必要があります。
When cross-compiling, you must specify a C cross-compiler for cgo to use.
ので、クロスコンパイラをインストールします。
RUN apt-get install -y gcc-arm-linux-gnueabihf
環境変数の設定
普通のクロスコンパイルのターゲットを指定する変数を設定します。32-bit Raspberry Piはarmv7なのでGOARMを7にします。
ENV GOOS linux
ENV GOARCH arm
ENV GOARM 7
また、cgoは普通のコンパイルだとデフォルトで有効ですが、クロスコンパイルの場合デフォルトで無効になります。
The cgo tool is enabled by default for native builds on systems where it is expected to work. It is disabled by default when cross-compiling.
ので、有効にします。
ENV CGO_ENABLED 1
また前述の通りクロスコンパイラを明示的に指定する必要があるので、指定します。
ENV CC arm-linux-gnueabihf-gcc
Wrap Up
あとは google/gousb をインストールして普通にgoによるコンパイルするだけです。
これらの設定をまとめてDockerfileに記述するとこうなります。
FROM golang:1.19.3-bullseye
RUN dpkg --add-architecture armhf
RUN apt-get update
RUN apt-get install -y gcc-arm-linux-gnueabihf libusb-1.0-0-dev libusb-1.0-0-dev:armhf
ENV GOOS linux
ENV GOARCH arm
ENV GOARM 7
ENV CGO_ENABLED 1
ENV CC arm-linux-gnueabihf-gcc
ARG REPO_DIR=/app
WORKDIR $REPO_DIR
ADD go.mod go.sum main.go $REPO_DIR
RUN go mod download
RUN go build -o lsusb
go.mod, go.sumは普通に google/gousbの依存を書いてるだけで、main.goがlsusb本体です。
これらのコードは以下においてあります。
これらを1つのディレクトリにおいてdocker buildでコンパイルできます。
$ docker build -t gousb-cross-compile:latest .
これで出来たDockerイメージの中の /app/lsusb
にlsusb.goのRaspberry Pi OS用実行ファイルが出来上がります。ので、これを取り出します。
$ docker run --rm -v $(pwd):/host gousb-cross-compile:latest cp /app/lsusb /host
これで出来た実行ファイルをRaspberry Piに転送して実行してみます。
$ ./lsusb
001.003 0424:ec00 SMSC9512/9514 Fast Ethernet Adapter (Standard Microsystems Corp.)
Protocol: Vendor Specific Class
Configuration 1:
--------------
Interface 0 alternate setting 0 (available endpoints: [0x02(2,OUT) 0x81(1,IN) 0x83(3,IN)])
Vendor Specific Class
ep #1 IN (address 0x81) bulk [512 bytes]
ep #2 OUT (address 0x02) bulk [512 bytes]
ep #3 IN (address 0x83) interrupt - undefined usage [16 bytes]
--------------
001.002 0424:9514 SMC9514 Hub (Standard Microsystems Corp.)
Protocol: Hub (Unused) TT per port
Configuration 1:
--------------
Interface 0 alternate setting 0 (available endpoints: [0x81(1,IN)])
Hub (Unused) Single TT
ep #1 IN (address 0x81) interrupt - undefined usage [1 bytes]
Interface 0 alternate setting 1 (available endpoints: [0x81(1,IN)])
Hub (Unused) TT per port
ep #1 IN (address 0x81) interrupt - undefined usage [1 bytes]
--------------
001.001 1d6b:0002 2.0 root hub (Linux Foundation)
Protocol: Hub (Unused) Single TT
Configuration 1:
--------------
Interface 0 alternate setting 0 (available endpoints: [0x81(1,IN)])
Hub (Unused) Full speed (or root) hub
ep #1 IN (address 0x81) interrupt - undefined usage [4 bytes]
--------------
期待通り動きました。
その他の手段
以上で普通のやり方は終わりですが、 google/gousb のコンパイルに限らず、時間がかかってもいいならRaspberry Pi OS自体をDockerで動かして、その中でコンパイルする方法もあります。
Raspberry Pi OSのDockerイメージは公式は提供していませんが、自前で作ることが出来ます。
参考
- https://qiita.com/yuyakato/items/5dd06fb179922206044d
- https://qiita.com/moritalous/items/e37719362346c06a453c
Docker for Macの場合、QEMUが中に入っていて、違うarchのイメージであってもQEMUが自動で機能して動かすことが出来ます。QEMUを挟む分だけ時間がかかりますが、代わりにクロスコンパイルの複雑な事情を無視してコンパイルすることが可能です。
やってみます。
$ docker run --platform linux/arm/v7 --rm -it -v $(pwd):/app -w /app takc923/raspbian:bullseye-2021-10-30 bash
root@4cb1e1a51704:/app# apt-get update && apt-get install -y libusb-1.0-0-dev
# ログ省略
# 公式レポジトリのGoが1.15でgousbは1.16以上が必要だったため手動でインストール
root@4cb1e1a51704:/app# wget -q https://go.dev/dl/go1.19.3.linux-armv6l.tar.gz
root@4cb1e1a51704:/app# tar -C /usr/local -xzf go1.19.3.linux-armv6l.tar.gz
root@4cb1e1a51704:/app# /usr/local/go/bin/go build -o lsusb.1
go: downloading github.com/google/gousb v1.1.2
これもRaspberry Piで試してちゃんと動きました。
$ ./lsusb.1
001.003 0424:ec00 SMSC9512/9514 Fast Ethernet Adapter (Standard Microsystems Corp.)
Protocol: Vendor Specific Class
Configuration 1:
--------------
Interface 0 alternate setting 0 (available endpoints: [0x02(2,OUT) 0x81(1,IN) 0x83(3,IN)])
Vendor Specific Class
ep #1 IN (address 0x81) bulk [512 bytes]
ep #2 OUT (address 0x02) bulk [512 bytes]
ep #3 IN (address 0x83) interrupt - undefined usage [16 bytes]
--------------
001.002 0424:9514 SMC9514 Hub (Standard Microsystems Corp.)
Protocol: Hub (Unused) TT per port
Configuration 1:
--------------
Interface 0 alternate setting 0 (available endpoints: [0x81(1,IN)])
Hub (Unused) Single TT
ep #1 IN (address 0x81) interrupt - undefined usage [1 bytes]
Interface 0 alternate setting 1 (available endpoints: [0x81(1,IN)])
Hub (Unused) TT per port
ep #1 IN (address 0x81) interrupt - undefined usage [1 bytes]
--------------
001.001 1d6b:0002 2.0 root hub (Linux Foundation)
Protocol: Hub (Unused) Single TT
Configuration 1:
--------------
Interface 0 alternate setting 0 (available endpoints: [0x81(1,IN)])
Hub (Unused) Full speed (or root) hub
ep #1 IN (address 0x81) interrupt - undefined usage [4 bytes]
--------------
ちなみに、armhfのGo処理系が配布されておらずarmv6lを使ったためか、全く同じバイナリにはなりませんでした。
$ md5sum lsusb lsusb.1
a6fd2c36e4c5e67286ed815d700e9afa lsusb
8e08e2dd273a27573c9da8e99f4e3899 lsusb.1
Discussion