純規の暇人趣味ブログ

首を突っ込んで足を洗う

Docker + WSL2でmozjpegをWindows用にクロスコンパイルする

      2021/01/16    HimaJyun

mozjpegのWindows用バイナリ(exe)を、WSL2上で動くDockerを使ってMinGWでクロスコンパイルして作ります。

「は?」と思った方、おそらくあなたの感覚は正しい。

スポンサーリンク

お急ぎの方

gistにこの記事の内容を自動化するためのスクリプトを置いてあります。これをダウンロードしてdockerに食わせれば簡単に動きます。

# PowerShellでの例
Invoke-WebRequest ( `
  "https://gist.githubusercontent.com/" `
  + "HimaJyun/27540844dc325b01a30354dbe59d898f/" `
  + "raw/ec848b33bff4845639e62027fc1c65d55d5c6f13/" `
  + "mozjpeg.sh" `
) -OutFile mozjpeg.sh
docker run --rm `
  -e "MOZJPEG_VERSION=v4.0.0" `
  -v ((Get-Location).Path + "\mozjpeg:/exe") `
  -v ((Get-Location).Path + "\mozjpeg.sh:/build.sh") `
  --entrypoint "/build.sh" `
  debian:10.7 `
  -DWITH_ARITH_DEC=1 -DWITH_ARITH_ENC=1

Dockerfileにしていないのは、できるだけ新しいMinGWやGCCを使いたいから。(Dockerfileだとビルド時のバージョンで固定されてしまうから)

動機

Web用の画像、ちゃんと最適化しようと思うとPhotoshopやLightroomだけでは力不足だったりする。(Lightroomはプログレッシブjpegの出力ができないし、Photoshopもクイック書き出しでプログレッシブが選べないし……)

というわけで僕はmozjpegを併用しているのですが、これ、基本的にソースコードしか提供されていないので実行可能なバイナリを得るのが割と面倒だったりします😓

多くの人はネット上の誰かがコンパイルしてくれたバイナリを拾ってきたりするのでしょうが、僕は疑り深いのでそれらのバイナリの安全性を確認できないことを懸念🤔していたりする

その辺りの事を考えて、VisualStudioでコンパイルするのは以前やりました

mozjpegをWindowsでビルドする

この方法は手動での作業が多くて面倒な上に、VisualStudio、CMake、nasmなどを入れる必要がありお手軽さに欠ける。

自動化可能で環境を汚さない方法はないかと考えた結果、「そうだ!WSL2のDockerでWindows用にクロスコンパイルすれば良いんじゃね!!」というアイデアが思い付いたので、それを実施した……というわけです。

前提条件

WSL2でDockerを動かしているからおかしな事をやってる感が出てるだけで、要するにLinuxでWindows用のバイナリをクロスコンパイルしているだけです。それをDockerコンテナに閉じ込めただけ。

そういう訳なので、前提条件は「Dockerが動くこと」です。そこさえ満たせれば後は何でもいい。(WSLや実機のLinuxにMinGWを入れても良いですが今回は環境を汚したくないのでDockerに閉じ込めます)

今回の記事の内容は以下のバージョンで確認しています。

  • Windows 10 Pro 20H2 (x64)
  • Docker 20.10.0 (WSL2で動作)
  • mozjpeg 4.0.0

MinGWとかNASMとかその辺のバージョンもありますが、今回はaptでインストールします。

コンテナにはDebian 10.7を使用。

ホストのWindowsがx64なので64bit用のバイナリを作ります、きょうび32bitなんて使ってる人居ないよね?

(確認はしてませんが、32bitバイナリが必要ならコマンドの随所で出てくるx86_64-w64-mingw32i686-w64-migw32に変更すれば行けると思います)

Dockerでクロスコンパイル

今回の目的を再確認🎈しましょう、mozjpegのWindows用バイナリをDockerで作成する。レッツゴー!

基本的にはmozjpegのドキュメントのやり方そのままですが、MinGWでWindows用バイナリを作る時だけは書いてある通りでは出来ません。

主な手順は以下の通り。

  1. コンテナに入る
  2. ツールのインストール
  3. libpngのコンパイル
  4. mozjpegのコンパイル
  5. コンテナから出る

ここでは手動で作業する事を前提にコマンドを解説していますが、やってることは順にコマンドを投入しているだけなのでDockerfileやシェルスクリプトで自動化するのも簡単だと思います。

コンテナに入る

コンテナに入りましょう。

docker run -it --rm -v C:\mozjpeg:/exe debian:10.7

記事の内容を再現可能にするためにdebian:10.7を指定してますが、たぶんlatestでも大丈夫です。

Ubuntuとかでも行けるはず、要するにMinGWとNASMとCMakeが使えればいいので。

-itはシェルに入る用、自動で処理するシェルスクリプトとかを使うのであれば不要です。

作業が終わればコンテナは要らないので--rmで消えるようにしておきます。

成果物を回収するために-vでディレクトリをマウントしておきましょう。この例ではC:\mozjpegをコンテナの/exeにマウントしています。(WSL2のDockerだとフルパスで指定する必要がある点は要注意)

ツールのインストール

コンパイルに必要なものはMinGW、CMake、NASMです。その他ソースの取得などにgitとcurlを使います。

といっても、コンテナの中でaptするだけです。

apt update
apt install nasm cmake curl git mingw-w64

ちなみに、Dockerにはgccの公式コンテナがありますがaptでインストールするMinGWには関係ないっぽい。

libpngのコンパイル

(この機能はオプションで無効化できます、無効化する場合はこの手順はスキップ可能)

mozjpegがlibpngを要求します、そしてlibpngはzlibを要求するので、この2つをコンパイルします。

使用するディストリによってはMinGW用のコンパイル済みライブラリとヘッダが提供されている場合もあります。あるならそれを使った方が早い。

DebianではzlibのMinGW用コンパイル済みライブラリが提供されているのでそれを使います。

apt install libz-mingw-w64-dev

自分でコンパイルするのであれば次のようにすればいける。

curl -LO https://zlib.net/zlib-1.2.11.tar.gz
tar xvf zlib-1.2.11.tar.gz
cd zlib-1.2.11
make -f win32/Makefile.gcc \
  PREFIX=x86_64-w64-mingw32- \ 
  BINARY_PATH=/usr/x86_64-w64-mingw32/bin \
  INCLUDE_PATH=/usr/x86_64-w64-mingw32/include \
  LIBRARY_PATH=/usr/x86_64-w64-mingw32/lib \
  install

続いて、libpngをコンパイルします。こちらはDebianではコンパイル済みの物は用意されていないようなので自分でやる必要があります。

curl -LO https://jaist.dl.sourceforge.net/project/libpng/libpng16/1.6.37/libpng-1.6.37.tar.gz
tar xvf libpng-1.6.37.tar.gz
cd libpng-1.6.37
./configure --host=x86_64-w64-mingw32 \
  --prefix=/usr/x86_64-w64-mingw32 \
  CPPFLAGS=-I/usr/x86_64-w64-mingw32/include \
  LDFLAGS=-L/usr/x86_64-w64-mingw32/lib
make
make install

zlibやlibpngにより新しいバージョンがあるのならそれを使う方が良いでしょう。

mozjpegのコンパイル

長く辛く苦しく険しい道のりを忍耐強く乗り越えて来ました、我々はそろそろ救われるべきです。この長い旅路の途中でzlibとlibpngという武器を手に入れました、さぁリスクを承知で大胆に挑戦しましょう。

というわけで、まずはmozjpegをgit cloneして使いたいバージョンのtagをチェックアウトします。

git clone https://github.com/mozilla/mozjpeg.git
cd mozjpeg
git checkout refs/tags/v4.0.0
cd ../

続いて作業用のディレクトリを作ります。名前は何でも良いです。

mkdir build
cd build

cmakeします。

mozjpegのドキュメントではtoolchain.cmakeファイルを作る例が掲載されていますが、Docker環境だとエディタ類がなくてファイル編集がつらいので引数で渡します。

cmake -G"Unix Makefiles" \
  -DCMAKE_SYSTEM_NAME=Windows \
  -DCMAKE_SYSTEM_PROCESSOR=AMD64 \
  -DCMAKE_C_COMPILER=/usr/bin/x86_64-w64-mingw32-gcc \
  -DCMAKE_RC_COMPILER=/usr/bin/x86_64-w64-mingw32-windres \
  -DCMAKE_INCLUDE_PATH=/usr/x86_64-w64-mingw32/include \
  -DCMAKE_LIBRARY_PATH=/usr/x86_64-w64-mingw32/lib \
  -DWITH_ARITH_DEC=1 -DWITH_ARITH_ENC=1 \
  ../mozjpeg

各変数の細かい解説はもう少し後でやります。

正常に成功していればこんな感じの表示が出ます。

-- Configuring done
-- Generating done
-- Build files have been written to: /build

ここまでくれば後はmakeするだけです。make🤗

make

眺めましょう。めにぃこあのパソコンなら早いかも知れない💨

Scanning dependencies of target simd
[  1%] Building ASM_NASM object simd/CMakeFiles/simd.dir/x86_64/jsimdcpu.asm.obj
~~~ 省略 ~~~
[100%] Linking C executable md5cmp.exe
[100%] Built target md5cmp

できました 👍👍👍💪💪💪

コンテナから出る

せっかく作ったexeを忘れてそのままexitしてしまうと--rmの効果で消えてかなしいきもち😢になります。

忘れずに回収しておきましょう。必要なのはexeとdllだけなので、それらをマウントしておいたディレクトリにコピーしましょう。

cp /build/*.{exe,dll} /exe
# static版を使うなら不要
#cp /libpng-1.6.37/.libs/libpng16-16.dll /exe

もしくは、docker cpを使用したのでも良い、要するに作ったものをコピーできればいいので。

-staticの付いているファイルがDLL不要で動く奴です。よく使うのはjpegtranとcjpegあたり。

はまりどころ

いくつか意識しておいた方がいいポイント。

変数の位置

makeやcmakeで使う変数……

make -f win32/Makefile.gcc PREFIX=x86_64-w64-mingw32-……
cmake -G"Unix Makefiles" -DCMAKE_SYSTEM_NAME=Windows……

これ、位置が大切みたいで、GOでクロスコンパイルする時みたいに変数を頭に持ってくるとコケます。

PREFIX=x86_64-w64-mingw32- make -f win32/Makefile.gcc……
CMAKE_SYSTEM_NAME=Windows cmake -G"Unix Makefiles"……

どうやら単なる環境変数って訳ではない様子。

make系のお作法は良く分かりませんが、変数の位置に注意しましょう。記事に書いてある通りにやれば行けるはず。

変数の解説

mozjpegをcmakeする時の変数を解説します。

cmake -G"Unix Makefiles" \
  -DCMAKE_SYSTEM_NAME=Windows \
  -DCMAKE_SYSTEM_PROCESSOR=AMD64 \
  -DCMAKE_C_COMPILER=/usr/bin/x86_64-w64-mingw32-gcc \
  -DCMAKE_RC_COMPILER=/usr/bin/x86_64-w64-mingw32-windres \
  -DCMAKE_INCLUDE_PATH=/usr/x86_64-w64-mingw32/include \
  -DCMAKE_LIBRARY_PATH=/usr/x86_64-w64-mingw32/lib \
  -DWITH_ARITH_DEC=1 -DWITH_ARITH_ENC=1 \
  ../mozjpeg

CMakeの構文は-D{変数名}=値という感じです、Javaのシステムプロパティと似たようなもの。

CMAKE_SYSTEM_NAMEWindowsで固定。

CMAKE_SYSTEM_PROCESSORは64bitならAMD64、32bitならX86です。32bitの場合はx86_64-w64-mingw32の部分をi686-w64-migw32にするのもお忘れなく。

CMAKE_C_COMPILERCMAKE_RC_COMPILERはMinGWのgccとwindresを指定しています。

CMAKE_INCLUDE_PATHCMAKE_LIBRARY_PATH、これ、mozjpegのドキュメントに記載されていないですが必須です。MinGWのincludeディレクトリとlibディレクトリを指定しています。

これを忘れるとこんな感じのエラーが出ます。

-- Could NOT find ZLIB (missing: ZLIB_LIBRARY ZLIB_INCLUDE_DIR)
CMake Error at /usr/share/cmake-3.13/Modules/FindPackageHandleStandardArgs.cmake:137 (message):
  Could NOT find PNG (missing: PNG_LIBRARY PNG_PNG_INCLUDE_DIR) (Required is
  at least version "1.6")
Call Stack (most recent call first):
  /usr/share/cmake-3.13/Modules/FindPackageHandleStandardArgs.cmake:378 (_FPHSA_FAILURE_MESSAGE)
  /usr/share/cmake-3.13/Modules/FindPNG.cmake:142 (find_package_handle_standard_args)
  sharedlib/CMakeLists.txt:97 (find_package)

僕はこれが分からなくて1日潰しました。

WITH_ARITH_DECWITH_ARITH_ENCはmozjpegの機能を切り替えています。

../mozjpegの部分はgit cloneしてきたmozjpegのディレクトリを指定します。

機能の切り替え

切り替えできる機能はcmakeする時に表示されます。

-- Shared libraries enabled (ENABLE_SHARED = 1)
-- Static libraries enabled (ENABLE_STATIC = 1)
-- 12-bit JPEG support disabled (WITH_12BIT = 0)
-- Arithmetic decoding support enabled (WITH_ARITH_DEC = 1)
-- Arithmetic encoding support enabled (WITH_ARITH_ENC = 1)

1で有効、0で無効です。

ENABLE_SHAREDENABLE_STATICはバイナリの種類を指定します。

SHAREDの方は実行にdllが必要なので個人的にはstaticバイナリを使ってます。パソコンが非力なら要らない方を無効化するとコンパイルが高速化できるかもしれません。

WITH_12BITを有効にすると12bit jpegが扱えるようになります。

「12bit jpegが扱えるようになる」というより「12bit jpegしか扱えなくなる」と表現する方が正しいかも知れません。このオプションを有効にすると普通の8bit jpegが扱えなくなります。

画質は向上するらしいのですが、対応したソフトウェアやサービスが皆無なので無効のままで良いと思います。

一部のオプションと排他らしく、これを有効にすると一部のオプションが無効になります。

WITH_ARITH_DECWITH_ARITH_ENCはそれぞれ算術符号のデコードとエンコードの有効化です。

算術符号を使ってエンコードすると容量をさらに削減できます……が!特許の関係から(すでに特許は切れているのですが)対応したソフトウェアやサービスが皆無です。

WITH_12BITと違ってオプションが1個増えるだけでなので有効にしても害はない。

PNG_SUPPORTEDはPNG対応機能です、デフォルトで有効。

前述のとおり、PNGを有効にするにはlibpngが必要になります。

有効だと主にcjpegを使用したpng→jpegの変換が出来るようになります。逆に言えばそれくらいなので無効でも大した問題はないはず。

他にもありますが、メインで使われるのはこれくらいかな?

リビルド

mozjpegをcmakeし直す時にはビルドディレクトリの中身を消す必要があります。

rm -rfv build/*

cmakeのオプションを変えた時は忘れずにやりましょう。

WSLとDockerの活用

こうやって記事にすると簡単ですが、この答えにたどり着くのに1日かかりました。CMAKE_INCLUDE_PATHCMAKE_LIBRARY_PATHの事に気付かなくて……

その辺の試行錯誤は必要になりますが、Docker上でクロスコンパイルというアプローチ自体は環境依存を減らせるので割とありだと思います。

Windows用バイナリのコンパイルが面倒なプロジェクトってチラホラあるので、WSL+Dockerの組み合わせはそういう状況で強い。

rust-embedded/crossも似たようなことやってますし、なによりも自動化できるのが嬉しいね!

 - プログラミング , ,