Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 19698] ファイルの概要

このコミットでは、Go言語の標準ライブラリテストをAndroidデバイス上で実行するためのビルドスクリプトとヘルパープログラムが追加されています。具体的には以下の3つのファイルが新規作成されました。

  • misc/android/README: Android上でのGo開発に関するドキュメントへのリンクと、標準ライブラリテストの実行方法に関する簡単な説明が含まれています。
  • misc/android/go_android_exec.go: GoツールチェインがAndroidデバイス上でバイナリを実行する際に使用するヘルパープログラムです。ADB(Android Debug Bridge)を介してバイナリをデバイスにプッシュし、実行し、その結果をホストに返します。
  • src/androidtest.bash: Androidデバイス上でGo標準ライブラリのテストを実行するためのシェルスクリプトです。Goのクロスコンパイル、GOROOTのデバイスへのプッシュ、テストの実行を自動化します。

コミット

commit a5f8e8f99cbac0cfecd3baa869d111bacfbaeac4
Author: David Crawshaw <david.crawshaw@zentus.com>
Date:   Wed Jul 9 06:56:49 2014 -0400

    androidtest.bash, misc/android: build scripts for android
    
    LGTM=minux
    R=minux
    CC=golang-codereviews
    https://golang.org/cl/107640044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a5f8e8f99cbac0cfecd3baa869d111bacfbaeac4

元コミット内容

androidtest.bash, misc/android: build scripts for android

LGTM=minux
R=minux
CC=golang-codereviews
https://golang.org/cl/107640044

変更の背景

このコミットは、Go言語がモバイルプラットフォーム、特にAndroidへの対応を強化する初期段階の一部として行われました。Go言語は元々サーバーサイドやツール開発に強みを持っていましたが、その軽量性、並行処理の容易さ、そしてクロスコンパイル能力から、モバイルアプリケーション開発への応用も期待されていました。

しかし、Goの標準ライブラリがAndroid環境で正しく動作するかを検証するためには、単にクロスコンパイルするだけでなく、実際にAndroidデバイス上でテストを実行できる仕組みが必要でした。当時のAndroid環境は、通常のデスクトップOSとは異なるファイルシステム構造、セキュリティ制約、そしてバイナリ実行のメカニズムを持っていました。特に、adb shellコマンドの挙動(終了コードの不正確さや、ファイルロックの問題)は、自動化されたテスト実行において大きな課題となっていました。

このコミットは、これらの課題を解決し、Goの標準ライブラリがAndroid環境で安定して動作することを保証するための基盤を構築することを目的としています。これにより、Go言語のAndroidサポートの品質向上と、将来的なGo Mobileプロジェクトの発展に貢献しました。

前提知識の解説

Go言語のクロスコンパイル

Go言語は、異なるオペレーティングシステムやアーキテクチャ向けのバイナリを簡単に生成できる強力なクロスコンパイル機能を内蔵しています。これは、GOOS(ターゲットOS)とGOARCH(ターゲットアーキテクチャ)という環境変数を設定するだけで実現できます。例えば、Linux上でAndroid ARM向けのバイナリをビルドするには、GOOS=android GOARCH=arm go buildのようにコマンドを実行します。この機能は、モバイル開発において非常に重要であり、開発者が特定のデバイス上で直接ビルド環境を構築することなく、ホストマシンで開発を進めることを可能にします。

Android NDK (Native Development Kit)

Android NDKは、Androidアプリケーションの一部をC/C++などのネイティブコードで記述できるようにするツールセットです。Go言語のランタイムはネイティブコードで実装されているため、Androidデバイス上でGoプログラムを実行するには、NDKが提供するツールチェイン(コンパイラ、リンカなど)やライブラリ(libcなど)との連携が必要になる場合があります。このコミットでは、直接NDKを使用するわけではありませんが、GoバイナリがAndroidのネイティブ環境で動作するための基盤を構築しています。

ADB (Android Debug Bridge)

ADBは、Androidデバイスとコンピュータ間で通信を行うための多機能なコマンドラインツールです。開発者はADBを使用して、デバイスへのファイルのプッシュ/プル、シェルコマンドの実行、アプリケーションのインストール/アンインストール、ログの取得など、様々な操作を行うことができます。このコミットでは、adb pushでGoバイナリをデバイスに転送し、adb shellでそのバイナリを実行するためにADBが中心的な役割を果たしています。

GOROOTGOPATH

  • GOROOT: Goのインストールディレクトリを指します。Goの標準ライブラリのソースコードやツールチェインが含まれています。
  • GOPATH: ユーザーのGoワークスペースを指します。ユーザーが開発するGoプロジェクトのソースコード、コンパイルされたパッケージ、実行可能バイナリが配置されます。

このコミットでは、テスト対象のGo標準ライブラリがGOROOT内に存在するため、デバイス上にもGOROOTに相当するディレクトリ構造を再現し、テストバイナリが適切なパスでライブラリを参照できるようにする工夫が凝らされています。

adb shell の挙動に関する課題

コミットのコードコメントには、adb shellコマンドの2つの主要な問題が記載されています。

  1. 終了コードの不正確さ: adb shellコマンドは、実行された内部コマンドが失敗しても、常にホスト側で終了コード0を返してしまうという問題がありました(例: adb shell falseを実行しても、echo $?は0を返す)。これは、スクリプトが内部コマンドの成功/失敗を正確に判断できないことを意味します。
  2. "text file busy" エラー: adb pushでバイナリをデバイスに転送した後、すぐにadb shellでそのバイナリを実行しようとすると、"text file busy"エラーが発生することがありました。これは、adb pushがファイルを完全に解放する前にadb shellがアクセスしようとすることで生じる競合状態です。

これらの問題は、自動テストの信頼性と安定性を確保する上で克服すべき重要な技術的課題でした。

技術的詳細

このコミットで追加されたスクリプトとプログラムは、Goの標準ライブラリテストをAndroidデバイス上で実行するための複雑なワークフローを自動化します。

  1. androidtest.bash の役割:

    • このシェルスクリプトは、テスト実行のオーケストレーターとして機能します。
    • まず、ホスト環境でGoのビルドツールチェインを構築し、go_android_execバイナリをクロスコンパイルします。go_android_execは、ホストのGOOSGOARCHでビルドされるため、ホスト上で実行可能です。
    • 次に、GoのGOROOT(標準ライブラリのソースコードやテストデータを含む)をAndroidデバイスに転送します。この際、adb syncコマンドを利用しますが、adb sync/systemまたは/dataディレクトリの特定の構造に同期するため、一時的なシンボリックリンク構造(ANDROID_PRODUCT_OUT以下にdata/local/tmp/gorootを模倣)を作成して、GOROOT全体を/data/local/tmp/gorootにプッシュします。これにより、デバイス上でもGoのソースツリーが再現されます。
    • 最後に、./all.bash --no-cleanを実行して、Goの標準テストスイートを起動します。このテストスイートは、内部的にgo_android_execを呼び出して、個々のテストバイナリをAndroidデバイス上で実行します。
  2. go_android_exec.go の役割:

    • このGoプログラムは、Goツールチェインによってgo_android_GOARCH_execという名前で呼び出されることを想定しています。これは、Goのビルドシステムが特定のターゲットOS/アーキテクチャ向けのバイナリを実行する際に、カスタムの実行ラッパーを使用できる機能を利用しています。
    • バイナリのプッシュと実行:
      • adb pushコマンドを使用して、テスト対象のGoバイナリをホストからAndroidデバイス上の一時的な場所(/data/local/tmp/goroot/以下)に転送します。
      • 前述の"text file busy"問題を回避するため、バイナリをまず一時ファイル名(例: deviceBin-tmp)でプッシュし、その後adb shell cpコマンドで正しいファイル名にコピーし、元のtmpファイルをadb shell rmで削除するという3段階のプロセスを踏んでいます。これにより、adb pushがファイルを完全に解放するのを待ってから実行に移ることができます。
      • adb shellコマンドを使って、デバイス上でGoバイナリを実行します。実行時には、TMPDIRGOROOT環境変数をデバイス上で設定し、カレントディレクトリをテスト対象のパッケージのパスに移動してからバイナリを実行します。
    • 終了コードの処理:
      • adb shellの終了コードが常に0になる問題を回避するため、実行するコマンドの最後に; echo -n "exitcode=$?"という文字列を付加しています。これにより、実際のコマンドの終了コードが標準出力の末尾にexitcode=Nという形式で出力されます。
      • go_android_exec.goは、adb shellの出力全体をキャプチャし、その末尾からexitcode=の文字列を探し、そこから実際の終了コードをパースして、自身のプロセス(ホスト上のgo_android_exec)の終了コードとして返します。これにより、ホスト側のテストスクリプトがAndroidデバイス上でのテストの成否を正確に判断できるようになります。

コアとなるコードの変更箇所

misc/android/go_android_exec.go

func run(args ...string) string {
	buf := new(bytes.Buffer)
	cmd := exec.Command("adb", args...)
	cmd.Stdout = io.MultiWriter(os.Stdout, buf)
	cmd.Stderr = os.Stderr
	log.Printf("adb %s", strings.Join(args, " "))
	err := cmd.Run()
	if err != nil {
		log.Fatalf("adb %s: %v", strings.Join(args, " "), err)
	}
	return buf.String()
}

func main() {
    // ... (cwd, subdir の決定) ...

	binName := filepath.Base(os.Args[1])
	deviceGoroot := "/data/local/tmp/goroot"
	deviceBin := fmt.Sprintf("%s/%s-%d", deviceGoroot, binName, os.Getpid())

	// The push of the binary happens in parallel with other tests.
	// Unfortunately, a simultaneous call to adb shell hold open
	// file descriptors, so it is necessary to push then move to
	// avoid a "text file busy" error on execution.
	// https://code.google.com/p/android/issues/detail?id=65857
	run("push", os.Args[1], deviceBin+"-tmp")
	run("shell", "cp '"+deviceBin+"-tmp' '"+deviceBin+"'")
	run("shell", "rm '"+deviceBin+"-tmp'")

	// The adb shell command will return an exit code of 0 regardless
	// of the command run. E.g.
	//	$ adb shell false
	//	$ echo $?
	//	0
	// https://code.google.com/p/android/issues/detail?id=3254
	// So we append the exitcode to the output and parse it from there.
	const exitstr = "exitcode="
	cmd := `export TMPDIR="/data/local/tmp"` +
		`; export GOROOT="` + deviceGoroot + `"` +
		`; cd "` + deviceGoroot + `/` + subdir + `"` +
		`; '" + deviceBin + "' " + strings.Join(os.Args[2:], " ") +
		`; echo -n "` + exitstr + "$?"`
	output := run("shell", cmd)
	run("shell", "rm '"+deviceBin+"'") // cleanup
	output = output[strings.LastIndex(output, "\n")+1:]
	if !strings.HasPrefix(output, exitstr) {
		log.Fatalf("no exit code: %q", output)
	}
	code, err := strconv.Atoi(output[len(exitstr):])
	if err != nil {
		log.Fatalf("bad exit code: %v", err)
	}
	os.Exit(code)
}

src/androidtest.bash

# ... (初期チェック、GOOS/CGO_ENABLEDの設定) ...

# Run the build for the host bootstrap, so we can build go_android_exec.
# Also lets us fail early before the (slow) adb push if the build is broken.
./make.bash
export GOROOT=$(dirname $(pwd))
export PATH=$GOROOT/bin:$PATH
GOOS=$GOHOSTOS GOARCH=$GOHOSTARCH go build \
	-o ../bin/go_android_${GOARCH}_exec \
	../misc/android/go_android_exec.go

# Push GOROOT to target device.
#
# The adb sync command will sync either the /system or /data
# directories of an android device from a similar directory
# on the host. So we fake one with symlinks to push the GOROOT
# into a subdirectory of /data.
export ANDROID_PRODUCT_OUT=/tmp/androidtest-$$
FAKE_GOROOT=$ANDROID_PRODUCT_OUT/data/local/tmp/goroot
mkdir -p $FAKE_GOROOT/src
ln -s $GOROOT/src/cmd $FAKE_GOROOT/src/cmd
ln -s $GOROOT/src/pkg $FAKE_GOROOT/src/pkg
ln -s $GOROOT/test $FAKE_GOROOT/test
ln -s $GOROOT/lib $FAKE_GOROOT/lib
adb sync data
rm -rf "$ANDROID_PRODUCT_OUT"

# Run standard build and tests.
./all.bash --no-clean

コアとなるコードの解説

go_android_exec.go の解説

  • run 関数: このヘルパー関数は、adbコマンドを実行し、その標準出力と標準エラー出力をキャプチャしつつ、ホストのコンソールにも表示します。エラーが発生した場合は、致命的なエラーとしてプログラムを終了させます。
  • バイナリのプッシュと移動:
    • run("push", os.Args[1], deviceBin+"-tmp"): ホストから実行対象のGoバイナリ(os.Args[1])を、デバイス上の一時的な名前(deviceBin-tmp)でプッシュします。
    • run("shell", "cp '"+deviceBin+"-tmp' '"+deviceBin+"'"): デバイス上で、一時ファイルから最終的な実行パスにバイナリをコピーします。
    • run("shell", "rm '"+deviceBin+"-tmp'"): 一時ファイルを削除します。
    • この3ステップは、adb pushadb shellの同時実行による"text file busy"エラーを回避するための重要なワークアラウンドです。
  • 終了コードの取得と処理:
    • cmd := ... ; echo -n " + exitstr + "$?": デバイス上で実行されるシェルコマンド文字列を構築しています。注目すべきは、Goバイナリの実行コマンドの直後に; echo -n "exitcode=$?"を追加している点です。これにより、Goバイナリの実際の終了コードがexitcode=N`という形式で標準出力の末尾に出力されます。
    • output = output[strings.LastIndex(output, "\n")+1:]: adb shellの出力から、最後の改行以降の部分(つまり、exitcode=Nの部分)を抽出します。
    • strconv.Atoi(output[len(exitstr):]): 抽出した文字列からexitcode=の部分を除去し、残りの数値部分を整数に変換します。
    • os.Exit(code): 変換した終了コードを、ホスト上のgo_android_execプロセスの終了コードとして設定します。これにより、androidtest.bashなどの上位のスクリプトが、Androidデバイス上でのテストの成否を正確に判断できるようになります。

src/androidtest.bash の解説

  • go_android_exec のビルド:
    • GOOS=$GOHOSTOS GOARCH=$GOHOSTARCH go build ... go_android_exec.go: ホストのOSとアーキテクチャ向けにgo_android_exec.goをビルドし、../bin/go_android_${GOARCH}_execとして配置します。これにより、GoツールチェインがAndroid向けのテストを実行する際に、このカスタム実行ラッパーを自動的に使用するようになります。
  • GOROOT のデバイスへのプッシュ:
    • export ANDROID_PRODUCT_OUT=/tmp/androidtest-$$: 一時的なディレクトリを定義します。
    • FAKE_GOROOT=$ANDROID_PRODUCT_OUT/data/local/tmp/goroot: adb sync dataが期待するパス構造に合わせて、偽のGOROOTパスを定義します。
    • mkdir -p $FAKE_GOROOT/srcln -s ...: ホストの実際のGOROOT内のsrc, pkg, test, libディレクトリへのシンボリックリンクを、偽のGOROOT構造内に作成します。これにより、adb sync dataがこれらのシンボリックリンクをたどって、実際のGOROOTの内容をデバイスの/data/local/tmp/gorootに転送します。
    • adb sync data: ホストのANDROID_PRODUCT_OUT/dataディレクトリの内容を、デバイスの/dataディレクトリに同期します。この際、シンボリックリンクが指す実際のファイルが転送されます。
    • rm -rf "$ANDROID_PRODUCT_OUT": 一時的に作成したシンボリックリンク構造をクリーンアップします。
  • 標準テストの実行:
    • ./all.bash --no-clean: Goの標準テストスイートを実行します。このコマンドは、内部的にGoツールチェインを呼び出し、Android向けにクロスコンパイルされたテストバイナリを、先ほど配置したgo_android_GOARCH_execラッパーを介してAndroidデバイス上で実行します。--no-cleanオプションは、ビルド成果物やテスト結果を削除しないことを意味します。

関連リンク

参考にした情報源リンク

このコミットでは、Go言語の標準ライブラリテストをAndroidデバイス上で実行するためのビルドスクリプトとヘルパープログラムが追加されています。具体的には以下の3つのファイルが新規作成されました。

  • misc/android/README: Android上でのGo開発に関するドキュメントへのリンクと、標準ライブラリテストの実行方法に関する簡単な説明が含まれています。
  • misc/android/go_android_exec.go: GoツールチェインがAndroidデバイス上でバイナリを実行する際に使用するヘルパープログラムです。ADB(Android Debug Bridge)を介してバイナリをデバイスにプッシュし、実行し、その結果をホストに返します。
  • src/androidtest.bash: Androidデバイス上でGo標準ライブラリのテストを実行するためのシェルスクリプトです。Goのクロスコンパイル、GOROOTのデバイスへのプッシュ、テストの実行を自動化します。

コミット

commit a5f8e8f99cbac0cfecd3baa869d111bacfbaeac4
Author: David Crawshaw <david.crawshaw@zentus.com>
Date:   Wed Jul 9 06:56:49 2014 -0400

    androidtest.bash, misc/android: build scripts for android
    
    LGTM=minux
    R=minux
    CC=golang-codereviews
    https://golang.org/cl/107640044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a5f8e8f99cbac0cfecd3baa869d111bacfbaeac4

元コミット内容

androidtest.bash, misc/android: build scripts for android

LGTM=minux
R=minux
CC=golang-codereviews
https://golang.org/cl/107640044

変更の背景

このコミットは、Go言語がモバイルプラットフォーム、特にAndroidへの対応を強化する初期段階の一部として行われました。Go言語は元々サーバーサイドやツール開発に強みを持っていましたが、その軽量性、並行処理の容易さ、そしてクロスコンパイル能力から、モバイルアプリケーション開発への応用も期待されていました。

しかし、Goの標準ライブラリがAndroid環境で正しく動作するかを検証するためには、単にクロスコンパイルするだけでなく、実際にAndroidデバイス上でテストを実行できる仕組みが必要でした。当時のAndroid環境は、通常のデスクトップOSとは異なるファイルシステム構造、セキュリティ制約、そしてバイナリ実行のメカニズムを持っていました。特に、adb shellコマンドの挙動(終了コードの不正確さや、ファイルロックの問題)は、自動化されたテスト実行において大きな課題となっていました。

このコミットは、これらの課題を解決し、Goの標準ライブラリがAndroid環境で安定して動作することを保証するための基盤を構築することを目的としています。これにより、Go言語のAndroidサポートの品質向上と、将来的なGo Mobileプロジェクトの発展に貢献しました。

前提知識の解説

Go言語のクロスコンパイル

Go言語は、異なるオペレーティングシステムやアーキテクチャ向けのバイナリを簡単に生成できる強力なクロスコンパイル機能を内蔵しています。これは、GOOS(ターゲットOS)とGOARCH(ターゲットアーキテクチャ)という環境変数を設定するだけで実現できます。例えば、Linux上でAndroid ARM向けのバイナリをビルドするには、GOOS=android GOARCH=arm go buildのようにコマンドを実行します。この機能は、モバイル開発において非常に重要であり、開発者が特定のデバイス上で直接ビルド環境を構築することなく、ホストマシンで開発を進めることを可能にします。

Android NDK (Native Development Kit)

Android NDKは、Androidアプリケーションの一部をC/C++などのネイティブコードで記述できるようにするツールセットです。Go言語のランタイムはネイティブコードで実装されているため、Androidデバイス上でGoプログラムを実行するには、NDKが提供するツールチェイン(コンパイラ、リンカなど)やライブラリ(libcなど)との連携が必要になる場合があります。このコミットでは、直接NDKを使用するわけではありませんが、GoバイナリがAndroidのネイティブ環境で動作するための基盤を構築しています。

ADB (Android Debug Bridge)

ADBは、Androidデバイスとコンピュータ間で通信を行うための多機能なコマンドラインツールです。開発者はADBを使用して、デバイスへのファイルのプッシュ/プル、シェルコマンドの実行、アプリケーションのインストール/アンインストール、ログの取得など、様々な操作を行うことができます。このコミットでは、adb pushでGoバイナリをデバイスに転送し、adb shellでそのバイナリを実行するためにADBが中心的な役割を果たしています。

GOROOTGOPATH

  • GOROOT: Goのインストールディレクトリを指します。Goの標準ライブラリのソースコードやツールチェインが含まれています。
  • GOPATH: ユーザーのGoワークスペースを指します。ユーザーが開発するGoプロジェクトのソースコード、コンパイルされたパッケージ、実行可能バイナリが配置されます。

このコミットでは、テスト対象のGo標準ライブラリがGOROOT内に存在するため、デバイス上にもGOROOTに相当するディレクトリ構造を再現し、テストバイナリが適切なパスでライブラリを参照できるようにする工夫が凝らされています。

adb shell の挙動に関する課題

コミットのコードコメントには、adb shellコマンドの2つの主要な問題が記載されています。

  1. 終了コードの不正確さ: adb shellコマンドは、実行された内部コマンドが失敗しても、常にホスト側で終了コード0を返してしまうという問題がありました(例: adb shell falseを実行しても、echo $?は0を返す)。これは、スクリプトが内部コマンドの成功/失敗を正確に判断できないことを意味します。
  2. "text file busy" エラー: adb pushでバイナリをデバイスに転送した後、すぐにadb shellでそのバイナリを実行しようとすると、"text file busy"エラーが発生することがありました。これは、adb pushがファイルを完全に解放する前にadb shellがアクセスしようとすることで生じる競合状態です。

これらの問題は、自動テストの信頼性と安定性を確保する上で克服すべき重要な技術的課題でした。

技術的詳細

このコミットで追加されたスクリプトとプログラムは、Goの標準ライブラリテストをAndroidデバイス上で実行するための複雑なワークフローを自動化します。

  1. androidtest.bash の役割:

    • このシェルスクリプトは、テスト実行のオーケストレーターとして機能します。
    • まず、ホスト環境でGoのビルドツールチェインを構築し、go_android_execバイナリをクロスコンパイルします。go_android_execは、ホストのGOOSGOARCHでビルドされるため、ホスト上で実行可能です。
    • 次に、GoのGOROOT(標準ライブラリのソースコードやテストデータを含む)をAndroidデバイスに転送します。この際、adb syncコマンドを利用しますが、adb sync/systemまたは/dataディレクトリの特定の構造に同期するため、一時的なシンボリックリンク構造(ANDROID_PRODUCT_OUT以下にdata/local/tmp/gorootを模倣)を作成して、GOROOT全体を/data/local/tmp/gorootにプッシュします。これにより、デバイス上でもGoのソースツリーが再現されます。
    • 最後に、./all.bash --no-cleanを実行して、Goの標準テストスイートを起動します。このテストスイートは、内部的にgo_android_execを呼び出して、個々のテストバイナリをAndroidデバイス上で実行します。
  2. go_android_exec.go の役割:

    • このGoプログラムは、Goツールチェインによってgo_android_GOARCH_execという名前で呼び出されることを想定しています。これは、Goのビルドシステムが特定のターゲットOS/アーキテクチャ向けのバイナリを実行する際に、カスタムの実行ラッパーを使用できる機能を利用しています。
    • バイナリのプッシュと実行:
      • adb pushコマンドを使用して、テスト対象のGoバイナリをホストからAndroidデバイス上の一時的な場所(/data/local/tmp/goroot/以下)に転送します。
      • 前述の"text file busy"問題を回避するため、バイナリをまず一時ファイル名(例: deviceBin-tmp)でプッシュし、その後adb shell cpコマンドで正しいファイル名にコピーし、元のtmpファイルをadb shell rmで削除するという3段階のプロセスを踏んでいます。これにより、adb pushがファイルを完全に解放するのを待ってから実行に移ることができます。
      • adb shellコマンドを使って、デバイス上でGoバイナリを実行します。実行時には、TMPDIRGOROOT環境変数をデバイス上で設定し、カレントディレクトリをテスト対象のパッケージのパスに移動してからバイナリを実行します。
    • 終了コードの処理:
      • adb shellの終了コードが常に0になる問題を回避するため、実行するコマンドの最後に; echo -n "exitcode=$?"という文字列を付加しています。これにより、実際のコマンドの終了コードが標準出力の末尾にexitcode=Nという形式で出力されます。
      • go_android_exec.goは、adb shellの出力全体をキャプチャし、その末尾からexitcode=の文字列を探し、そこから実際の終了コードをパースして、自身のプロセス(ホスト上のgo_android_exec)の終了コードとして返します。これにより、ホスト側のテストスクリプトがAndroidデバイス上でのテストの成否を正確に判断できるようになります。

コアとなるコードの変更箇所

misc/android/go_android_exec.go

func run(args ...string) string {
	buf := new(bytes.Buffer)
	cmd := exec.Command("adb", args...)
	cmd.Stdout = io.MultiWriter(os.Stdout, buf)
	cmd.Stderr = os.Stderr
	log.Printf("adb %s", strings.Join(args, " "))
	err := cmd.Run()
	if err != nil {
		log.Fatalf("adb %s: %v", strings.Join(args, " "), err)
	}
	return buf.String()
}

func main() {
    // ... (cwd, subdir の決定) ...

	binName := filepath.Base(os.Args[1])
	deviceGoroot := "/data/local/tmp/goroot"
	deviceBin := fmt.Sprintf("%s/%s-%d", deviceGoroot, binName, os.Getpid())

	// The push of the binary happens in parallel with other tests.
	// Unfortunately, a simultaneous call to adb shell hold open
	// file descriptors, so it is necessary to push then move to
	// avoid a "text file busy" error on execution.
	// https://code.google.com/p/android/issues/detail?id=65857
	run("push", os.Args[1], deviceBin+"-tmp")
	run("shell", "cp '"+deviceBin+"-tmp' '"+deviceBin+"'")
	run("shell", "rm '"+deviceBin+"-tmp'")

	// The adb shell command will return an exit code of 0 regardless
	// of the command run. E.g.
	//	$ adb shell false
	//	$ echo $?
	//	0
	// https://code.google.com/p/android/issues/detail?id=3254
	// So we append the exitcode to the output and parse it from there.
	const exitstr = "exitcode="
	cmd := `export TMPDIR="/data/local/tmp"` +
		`; export GOROOT="` + deviceGoroot + `"` +
		`; cd "` + deviceGoroot + `/` + subdir + `"` +
		`; '" + deviceBin + "' " + strings.Join(os.Args[2:], " ") +
		`; echo -n "` + exitstr + "$?"`
	output := run("shell", cmd)
	run("shell", "rm '"+deviceBin+"'") // cleanup
	output = output[strings.LastIndex(output, "\n")+1:]
	if !strings.HasPrefix(output, exitstr) {
		log.Fatalf("no exit code: %q", output)
	}
	code, err := strconv.Atoi(output[len(exitstr):])
	if err != nil {
		log.Fatalf("bad exit code: %v", err)
	}
	os.Exit(code)
}

src/androidtest.bash

# ... (初期チェック、GOOS/CGO_ENABLEDの設定) ...

# Run the build for the host bootstrap, so we can build go_android_exec.
# Also lets us fail early before the (slow) adb push if the build is broken.
./make.bash
export GOROOT=$(dirname $(pwd))
export PATH=$GOROOT/bin:$PATH
GOOS=$GOHOSTOS GOARCH=$GOHOSTARCH go build \
	-o ../bin/go_android_${GOARCH}_exec \
	../misc/android/go_android_exec.go

# Push GOROOT to target device.
#
# The adb sync command will sync either the /system or /data
# directories of an android device from a similar directory
# on the host. So we fake one with symlinks to push the GOROOT
# into a subdirectory of /data.
export ANDROID_PRODUCT_OUT=/tmp/androidtest-$$
FAKE_GOROOT=$ANDROID_PRODUCT_OUT/data/local/tmp/goroot
mkdir -p $FAKE_GOROOT/src
ln -s $GOROOT/src/cmd $FAKE_GOROOT/src/cmd
ln -s $GOROOT/src/pkg $FAKE_GOROOT/src/pkg
ln -s $GOROOT/test $FAKE_GOROOT/test
ln -s $GOROOT/lib $FAKE_GOROOT/lib
adb sync data
rm -rf "$ANDROID_PRODUCT_OUT"

# Run standard build and tests.
./all.bash --no-clean

コアとなるコードの解説

go_android_exec.go の解説

  • run 関数: このヘルパー関数は、adbコマンドを実行し、その標準出力と標準エラー出力をキャプチャしつつ、ホストのコンソールにも表示します。エラーが発生した場合は、致命的なエラーとしてプログラムを終了させます。
  • バイナリのプッシュと移動:
    • run("push", os.Args[1], deviceBin+"-tmp"): ホストから実行対象のGoバイナリ(os.Args[1])を、デバイス上の一時的な名前(deviceBin-tmp)でプッシュします。
    • run("shell", "cp '"+deviceBin+"-tmp' '"+deviceBin+"'"): デバイス上で、一時ファイルから最終的な実行パスにバイナリをコピーします。
    • run("shell", "rm '"+deviceBin+"-tmp'"): 一時ファイルを削除します。
    • この3ステップは、adb pushadb shellの同時実行による"text file busy"エラーを回避するための重要なワークアラウンドです。
  • 終了コードの取得と処理:
    • cmd := ... ; echo -n " + exitstr + "$?": デバイス上で実行されるシェルコマンド文字列を構築しています。注目すべきは、Goバイナリの実行コマンドの直後に; echo -n "exitcode=$?"を追加している点です。これにより、Goバイナリの実際の終了コードがexitcode=N`という形式で標準出力の末尾に出力されます。
    • output = output[strings.LastIndex(output, "\n")+1:]: adb shellの出力から、最後の改行以降の部分(つまり、exitcode=Nの部分)を抽出します。
    • strconv.Atoi(output[len(exitstr):]): 抽出した文字列からexitcode=の部分を除去し、残りの数値部分を整数に変換します。
    • os.Exit(code): 変換した終了コードを、ホスト上のgo_android_execプロセスの終了コードとして設定します。これにより、androidtest.bashなどの上位のスクリプトが、Androidデバイス上でのテストの成否を正確に判断できるようになります。

src/androidtest.bash の解説

  • go_android_exec のビルド:
    • GOOS=$GOHOSTOS GOARCH=$GOHOSTARCH go build ... go_android_exec.go: ホストのOSとアーキテクチャ向けにgo_android_exec.goをビルドし、../bin/go_android_${GOARCH}_execとして配置します。これにより、GoツールチェインがAndroid向けのテストを実行する際に、このカスタム実行ラッパーを自動的に使用するようになります。
  • GOROOT のデバイスへのプッシュ:
    • export ANDROID_PRODUCT_OUT=/tmp/androidtest-$$: 一時的なディレクトリを定義します。
    • FAKE_GOROOT=$ANDROID_PRODUCT_OUT/data/local/tmp/goroot: adb sync dataが期待するパス構造に合わせて、偽のGOROOTパスを定義します。
    • mkdir -p $FAKE_GOROOT/srcln -s ...: ホストの実際のGOROOT内のsrc, pkg, test, libディレクトリへのシンボリックリンクを、偽のGOROOT構造内に作成します。これにより、adb sync dataがこれらのシンボリックリンクをたどって、実際のGOROOTの内容をデバイスの/data/local/tmp/gorootに転送します。
    • adb sync data: ホストのANDROID_PRODUCT_OUT/dataディレクトリの内容を、デバイスの/dataディレクトリに同期します。この際、シンボリックリンクが指す実際のファイルが転送されます。
    • rm -rf "$ANDROID_PRODUCT_OUT": 一時的に作成したシンボリックリンク構造をクリーンアップします。
  • 標準テストの実行:
    • ./all.bash --no-clean: Goの標準テストスイートを実行します。このコマンドは、内部的にGoツールチェインを呼び出し、Android向けにクロスコンパイルされたテストバイナリを、先ほど配置したgo_android_GOARCH_execラッパーを介してAndroidデバイス上で実行します。--no-cleanオプションは、ビルド成果物やテスト結果を削除しないことを意味します。

関連リンク

参考にした情報源リンク