Docker のクロスコンパイルの処理を追う


Git ([docker/docker](https://github.com/docker/docker)) から clone をしてはみたけれど、何だかよく分からない。とりあえず「[Docker のソースの構成についてのメモ - Qiita](http://qiita.com/knaka/items/2ddaf55ca13db5ad4079)」で、トップディレクトリの分類はしてみた。

そして、docker/ が CLI プログラムのメインと見うけられる。そもそも Go 言語のビルドシステムや作法が分からないので、いろいろと分からない。

まあ、やってみるか。

# Docker を Docker 上でビルド

Docker のビルドは、Docker 上で行う。この点は簡単で助かる。依存地獄知らず。

ビルド用の Dockerfile が用意されているので、とりあえずそれを build してみる。右記を参考にした。 ∥ [Etsukata blog: Docker のビルド方法に見る Golang の利点](http://blog.etsukata.com/2014/05/docker-golang.html)

`docker run` のオプションについては、Dockerfile 内に注記がある。これ、なんで `--privileged` が要るのかな？

~~~~console:
$ git clone https://github.com/docker/docker.git
$ cd docker/
$ docker build -t docker .
$ docker run -it --rm -v $PWD:/go/src/github.com/docker/docker --privileged docker bash
# pwd
/go/src/github.com/docker/docker
# hack/make.sh
~~~~

なんと、この Docker 環境はクロスコンパイルをするよ、すごいね。いや、Go 言語がすごいのか。

~~~~console:
# ls -l /usr/local/go/bin/
total 12576
drwxr-xr-x 2 root root    4096 Jan 11 09:56 darwin_386
drwxr-xr-x 2 root root    4096 Jan 11 09:56 darwin_amd64
drwxr-xr-x 2 root root    4096 Jan 11 09:56 freebsd_386
drwxr-xr-x 2 root root    4096 Jan 11 09:56 freebsd_amd64
drwxr-xr-x 2 root root    4096 Jan 11 09:56 freebsd_arm
-rwxr-xr-x 1 root root 9350136 Jan 11 09:56 go
-rwxr-xr-x 1 root root 3496688 Jan 11 09:56 gofmt
drwxr-xr-x 2 root root    4096 Jan 11 09:55 linux_386
drwxr-xr-x 2 root root    4096 Jan 11 09:55 linux_arm
# hack/make.sh cross
~~~~

すると、以下のように。ちなみに環境としては、Mac 上の Boot2docker で Docker している。コンテナとしては、Dockerfile に `FROM ubuntu:14.04` とあるので、Ubuntu 14.04 TLS ですね。

~~~~console:
# ls -d bundles/1.3.2-dev/cross/*/* | cat
bundles/1.3.2-dev/cross/darwin/386
bundles/1.3.2-dev/cross/darwin/amd64
bundles/1.3.2-dev/cross/freebsd/386
bundles/1.3.2-dev/cross/freebsd/amd64
bundles/1.3.2-dev/cross/freebsd/arm
bundles/1.3.2-dev/cross/linux/386
bundles/1.3.2-dev/cross/linux/arm
# bundles/1.3.2-dev/cross/darwin/386/docker version
Client version: 1.3.2-dev
Client API version: 1.16
Go version (client): go1.3.3
Git commit (client): 353ff40
OS/Arch (client): darwin/386
FATA[0000] Get http:///var/run/docker.sock/v1.16/version: dial unix /var/run/docker.sock: no such file or directory. Are you trying to connect to a TLS-enabled daemon without TLS?
# bundles/1.3.2-dev/cross/darwin/amd64/docker version
Client version: 1.3.2-dev
Client API version: 1.16
Go version (client): go1.3.3
Git commit (client): 353ff40
OS/Arch (client): darwin/amd64
FATA[0000] Get http:///var/run/docker.sock/v1.16/version: dial unix /var/run/docker.sock: no such file or directory. Are you trying to connect to a TLS-enabled daemon without TLS?
# bundles/1.3.2-dev/cross/darwin/amd64/docker version
bash: bundles/1.3.2-dev/cross/darwin/amd64/docker: cannot execute binary file: Exec format error
# bundles/1.3.2-dev/cross/freebsd/arm/docker version
bash: bundles/1.3.2-dev/cross/freebsd/arm/docker: cannot execute binary file: Exec format error
~~~~

当然のことながら、Docker ホスト（というか、Boot2docker ホスト）である Mac（vboxsf で、VirtualBox の仮想マシンとフォルダを共有している）では、普通に Darwin のバイナリが実行できる。

~~~~console:
$ bundles/1.3.2-dev/cross/darwin/amd64/docker version
Client version: 1.3.2-dev
Client API version: 1.16
Go version (client): go1.3.3
Git commit (client): 353ff40
OS/Arch (client): darwin/amd64
FATA[0000] Get http:///var/run/docker.sock/v1.16/version: dial unix /var/run/docker.sock: no such file or directory. Are you trying to connect to a TLS-enabled daemon without TLS?
~~~~

# クロスコンパイルと、`daemon` ビルドタグの扱い

hack/make.sh が、引数に指定されたバンドル（＝ターゲット？）をターゲットとしてビルドする。無指定であれば、全部やるっぽい。

後に `cross` を見るが、これは `binary` を、ターゲットの OS/arch 指定で呼んでいる。無指定ならば `linux/amd64` になるってことなんだろう。

~~~~sh:hack/make.sh
...

DEFAULT_BUNDLES=(
	validate-dco
	validate-gofmt

	binary

	test-unit
	test-integration
	test-integration-cli
	test-docker-py

	dynbinary
	dyntest-unit
	dyntest-integration

	cover
	cross
	tgz
	ubuntu
)

...

bundle() {
	bundlescript=$1
	bundle=$(basename $bundlescript)
	echo "---> Making bundle: $bundle (in bundles/$VERSION/$bundle)"
	mkdir -p bundles/$VERSION/$bundle
	source "$bundlescript" "$(pwd)/bundles/$VERSION/$bundle"
}

main() {
	# We want this to fail if the bundles already exist and cannot be removed.
	# This is to avoid mixing bundles from different versions of the code.
	mkdir -p bundles
	if [ -e "bundles/$VERSION" ]; then
		echo "bundles/$VERSION already exists. Removing."
		rm -fr bundles/$VERSION && mkdir bundles/$VERSION || exit 1
		echo
	fi
	SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
	if [ $# -lt 1 ]; then
		bundles=(${DEFAULT_BUNDLES[@]})
	else
		bundles=($@)
	fi
	for bundle in ${bundles[@]}; do
		bundle $SCRIPTDIR/make/$bundle
		echo
	done
}

main "$@"
~~~~

以下では、`cross` バンドルに注目する。

$SCRIPTDIR を見ると、hack/make/ の下から、バンドルごとにバンドル名と同名のスクリプトを `source` する。よって、環境変数（export していないけれど）は、ここ（上）で指定したまま、下位のスクリプトに受け渡される。

下記が `cross` bundle のスクリプト。デーモンをサポートしているのは linux/amd64 のみであることが分かる。下記に、「remove the "daemon" build tag from platforms that aren't supported」とあるとおり。

~~~~bash:hack/make/cross
...
daemonSupporting=(
	[linux/amd64]=1
)
...
for platform in $DOCKER_CROSSPLATFORMS; do
	(
		mkdir -p "$DEST/$platform" # bundles/VERSION/cross/GOOS/GOARCH/docker-VERSION
		export GOOS=${platform%/*}
		export GOARCH=${platform##*/}
		if [ -z "${daemonSupporting[$platform]}" ]; then
			export LDFLAGS_STATIC_DOCKER="" # we just need a simple client for these platforms
			export BUILDFLAGS=( "${ORIG_BUILDFLAGS[@]/ daemon/}" ) # remove the "daemon" build tag from platforms that aren't supported
		fi
		source "$(dirname "$BASH_SOURCE")/binary" "$DEST/$platform"
	)
done
~~~~

> これがどう効いてくれかと言うと、たとえばプラットフォームによって、クライアント・Engine 両対応のバイナリを出力するか、あるいはクライアント専用のバイナリを出力するかの条件ビルドをする場合など。
> 
> Docker で言うと、main.mainDaemon() の定義が、docker/client.go と docker/daemon.go の二箇所にある。client.go は、クライアントオンリーバイナリの場合に結合されるソースだ。
> 
> ~~~~go:docker/client.go
> // +build !daemon
> 
> ...
> 
> func mainDaemon() {
> 	log.Fatal("This is a client-only binary - running the Docker daemon is not supported.")
> }
> ...
> ~~~~
> 
> Mac で実行してみる。
> 
> ~~~~console:
> $ docker -d
> 2015/01/11 23:54:30 This is a client-only binary - running the Docker daemon is not supported.
> ~~~~
>
> 詳しくは、「[build - The Go Programming Language](http://golang-jp.org/pkg/go/build/) 」あたりを参照。
>
>> ビルド制約はビルドタグとしても知られており、 以下に示す文字列で始まる行コメントです。
>> 
>> ~~~~go:
>> // +build
>> ~~~~
>>
>> この後にそのファイルがパッケージに含められる条件を列挙します。

ビルドできる OS/プラットフォームのリストである $DOCKER_CROSSPLATFORMS は、下記のように Dockerfile でコンテナイメージを作る際に指定している。

~~~~text:Dockerfile
...
# Compile Go for cross compilation
ENV DOCKER_CROSSPLATFORMS \
	linux/386 linux/arm \
	darwin/amd64 darwin/386 \
	freebsd/amd64 freebsd/386 freebsd/arm \
	windows/amd64 windows/386
...
~~~~

では、hack/make.sh では、`daemon` フラグはどう扱われているか。

~~~~sh:project/make.sh
if [ -z "$DOCKER_CLIENTONLY" ]; then
	DOCKER_BUILDTAGS+=" daemon"
fi
...
ORIG_BUILDFLAGS=( -a -tags "netgo static_build $DOCKER_BUILDTAGS" )
BUILDFLAGS=( $BUILDFLAGS "${ORIG_BUILDFLAGS[@]}" )
~~~~

上記のように、フラグ（「ビルドタグ」）を `-tags "aaa bbb ..."` と設定する。

project/make.sh → project/make/cross → project/make/binary と `source` されていって、最終的には、下記でビルドされる。

~~~~sh:project/make/binary
...
go build \
	-o "$DEST/$BINARY_FULLNAME" \
	"${BUILDFLAGS[@]}" \
	-ldflags "
		$LDFLAGS
		$LDFLAGS_STATIC_DOCKER
	" \
	./docker
...
~~~~

なるほどね。

# ところで、差分ビルドってできるの？

部分ビルドのしかたは分からないが（たぶん `go biuld` が差分ビルドをしてくれている）、コンテナ内の「docker/」で `go build` すれば、CLI ツールをコンパイルして出力してくれる。

~~~~bash:-
# cd docker/
# vi flags.go
~~~~

で、

~~~~bash:-
# go build
~~~~