🍓

google/gousbを用いたソフトウェアをRaspberry Pi向けにクロスコンパイルする

2022/12/02に公開

この記事は 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イメージは公式は提供していませんが、自前で作ることが出来ます。

参考

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