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

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

このコミットは、Go言語のビルドツール (cmd/go) において、gccgo コンパイラを使用する際の外部テストファイルのリンク順序に関する問題を修正するものです。具体的には、以前の変更によって発生した、内部パッケージと外部テストパッケージのリンケージにおけるシンボル重複または不足のエラーを解決し、gc コンパイラの動作と整合性を持たせることを目的としています。

コミット

commit 3ce1677ad9f95fb0b1ede191f41996c451e054c1
Author: Dave Cheney <dave@cheney.net>
Date:   Wed Mar 26 15:30:55 2014 +1100

    cmd/go: ensure external test files are presented to the linker first
    
    Fixes #7627.
    
    CL 61970044 changed the order in which .a files are passed to gccgo's link phase. However by reversing the order it caused gccgo to complain if both internal (liba.a) and external (liba_test.a) versions of a package were presented as the former would not contain all the necessary symbols, and the latter would duplicate symbols already defined.
    
    This change ensures that all 'fake' targets remain at the top of the final link order which should be fine as a package compiled as an external test is a superset of its internal sibling.
    
    Looking at how gcToolchain links tests I think this change now accurately mirrors those actions which present $WORK/_test before $WORK in the link order.
    
    LGTM=iant
    R=rsc, iant
    CC=golang-codereviews, michael.hudson
    https://golang.org/cl/80300043

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

https://github.com/golang/go/commit/3ce1677ad9f95fb0b1ede191f41996c451e054c1

元コミット内容

このコミットは、cmd/go ツールにおいて、gccgo コンパイラが外部テストファイルをリンクする際に、それらのファイルをリンカに最初に渡すように変更します。

これは、Issue #7627 を修正するものです。

以前の変更である CL 61970044 は、.a (アーカイブ) ファイルが gccgo のリンクフェーズに渡される順序を変更しました。しかし、この順序を逆にしたことで、パッケージの内部バージョン (liba.a) と外部テストバージョン (liba_test.a) の両方がリンカに渡された場合に、gccgo がエラーを報告するようになりました。具体的には、内部バージョンが必要なシンボルをすべて含んでいないか、または外部テストバージョンが既に定義されているシンボルを重複して定義しているという問題が発生しました。

この変更は、「偽の (fake)」ターゲット(主に外部テストファイル)が最終的なリンク順序の最上位に維持されることを保証します。これは、外部テストとしてコンパイルされたパッケージが、その内部の兄弟パッケージのスーパーセットであるため、問題ないはずです。

gcToolchain がテストをリンクする方法を見ると、この変更は $WORK/_test$WORK の前にリンク順序で提示するそれらのアクションを正確に反映していると考えられます。

変更の背景

このコミットの背景には、Go言語のビルドシステム、特にgo testコマンドと、gcおよびgccgoという二つの主要なコンパイラの挙動の違いが深く関わっています。

Go言語では、テストコードは通常、対象となるパッケージと同じディレクトリに_test.goというサフィックスを持つファイルとして配置されます。これらのテストは大きく分けて二種類あります。

  1. 内部テスト (Internal Tests): package mypackage のように、テスト対象のパッケージと同じパッケージ名を持つテストファイルです。これらのテストは、パッケージの内部実装にアクセスできます。
  2. 外部テスト (External Tests): package mypackage_test のように、テスト対象のパッケージとは異なるパッケージ名を持つテストファイルです。これらのテストは、パッケージが外部からどのように見えるかをテストするために使用され、パッケージのエクスポートされたAPIのみにアクセスできます。

go testコマンドは、これらのテストファイルをコンパイルし、テスト実行可能なバイナリを生成します。この際、gc(標準のGoコンパイラ)とgccgo(GCCベースのGoコンパイラ)では、内部的なリンケージの挙動に違いがありました。

問題の発端は、CL 61970044という変更にあります。この変更は、gccgo.a(アーカイブ)ファイルをリンカに渡す順序を変更しました。リンカの動作において、ライブラリやオブジェクトファイルの順序は非常に重要です。シンボル(関数や変数など)の定義が複数のファイルに存在する場合、リンカは通常、コマンドラインで最初に見つかった定義を採用します。また、あるファイルが別のファイルで定義されたシンボルを参照する場合、参照するファイルが参照されるファイルよりも前にリンカに渡されると、シンボルが見つからないというエラーが発生する可能性があります。

CL 61970044による順序の変更は、gccgoがパッケージの内部バージョン(例: liba.a)と外部テストバージョン(例: liba_test.a)の両方を処理する際に問題を引き起こしました。

  • 内部バージョン (liba.a): これは通常のパッケージコードを含みます。
  • 外部テストバージョン (liba_test.a): これは外部テストコードを含み、通常は内部パッケージのシンボルをインポートして使用します。外部テストパッケージは、内部パッケージの「スーパーセット」と見なされることがあります。つまり、外部テストパッケージは内部パッケージのすべてのシンボルを含み、さらにテストに必要な追加のシンボルも持っている可能性があります。

CL 61970044によってリンカへの入力順序が逆になった結果、gccgoは以下のいずれかのエラーを報告するようになりました。

  1. シンボル不足: 内部パッケージ (liba.a) がリンカに先に渡された場合、外部テスト (liba_test.a) が必要とする特定のシンボルが内部パッケージには含まれていないため、リンカがシンボルを見つけられない。
  2. シンボル重複: 外部テストパッケージ (liba_test.a) が先に渡された場合、それが内部パッケージのシンボルを既に含んでいるため、後から渡される内部パッケージ (liba.a) の同じシンボルと重複してしまう。

この問題は、Goのビルドシステムが異なるコンパイラ(gcgccgo)間で一貫した動作を提供する必要があるという原則に反していました。特に、gcToolchaingcコンパイラを使用するツールチェーン)は、テストをリンクする際に、$WORK/_test(外部テストのビルド成果物)を$WORK(通常のパッケージのビルド成果物)よりも先にリンカに提示していました。これは、外部テストが内部パッケージのスーパーセットであるという性質を考慮した、正しいリンケージ順序です。

このコミットは、gccgoのリンケージ動作をgcToolchainの動作に合わせることで、この問題を解決し、go testコマンドの信頼性と一貫性を向上させることを目的としています。

前提知識の解説

1. Go言語のビルドシステム (go build, go test)

Go言語のビルドシステムは、goコマンドによって統合されています。

  • go build: ソースコードをコンパイルし、実行可能なバイナリやライブラリを生成します。
  • go test: テストコードをコンパイルし、テストを実行するための特別なバイナリを生成します。このコマンドは、テスト対象のパッケージとテストファイル(_test.go)を自動的に検出し、ビルドプロセスを管理します。

2. gcgccgo コンパイラ

Go言語には主に二つのコンパイラ実装が存在します。

  • gc (Go Compiler): Goプロジェクトの公式かつデフォルトのコンパイラです。Go言語で書かれており、高速なコンパイルと最適化が特徴です。
  • gccgo: GCC (GNU Compiler Collection) のフロントエンドとして実装されたGoコンパイラです。C/C++など他の言語と同じGCCのバックエンドを利用するため、既存のGCCツールチェーンとの統合が容易であり、特定のプラットフォームや最適化のニーズに対応できる場合があります。しかし、gcとは異なるビルド・リンケージの挙動を示すことがあります。

3. 内部テストと外部テスト

Goのテストファイルは、ファイル名が_test.goで終わることで識別されます。これらのテストは、パッケージの構造によって「内部テスト」と「外部テスト」に分類されます。

  • 内部テスト: package mypackage のように、テスト対象のパッケージと同じパッケージ名を持つテストファイルです。これにより、テストコードはパッケージ内の非エクスポート(プライベート)な関数や変数にもアクセスできます。
  • 外部テスト: package mypackage_test のように、テスト対象のパッケージとは異なるパッケージ名を持つテストファイルです。これにより、テストコードはパッケージのエクスポートされた(パブリックな)APIのみにアクセスでき、パッケージが外部からどのように利用されるかをより正確にシミュレートできます。go testコマンドは、外部テストをコンパイルする際に、元のパッケージとは別の「偽の (fake)」パッケージとして扱います。

4. リンカの動作とライブラリのリンク順序

リンカは、コンパイラによって生成されたオブジェクトファイル(.o)やアーカイブファイル(.a、ライブラリの集合)を結合し、最終的な実行可能ファイルや共有ライブラリを生成するツールです。リンカの動作において、入力ファイルの順序は非常に重要です。

  • シンボル解決: リンカは、あるオブジェクトファイルが参照するシンボル(関数や変数)の定義を、入力された他のオブジェクトファイルやライブラリの中から探します。
  • 順序依存性: 一般的に、リンカはコマンドラインで指定された順序でファイルを処理します。あるファイルが別のファイルで定義されたシンボルを参照する場合、参照されるファイルが参照するファイルよりもにリンカに渡されると、シンボルが見つからないエラー(未定義参照)が発生します。逆に、同じシンボルが複数のファイルで定義されている場合、リンカは通常、コマンドラインで最初に見つかった定義を採用します。
  • アーカイブファイル (.a): 複数のオブジェクトファイルをまとめた静的ライブラリです。リンカは、アーカイブファイルの中から必要なシンボル定義のみを抽出してリンクします。

5. $WORK$WORK/_test

Goのビルドプロセスでは、一時的な作業ディレクトリが作成されます。

  • $WORK: 通常のパッケージのビルド成果物(オブジェクトファイルやアーカイブファイル)が配置されるディレクトリを指します。
  • $WORK/_test: 外部テストパッケージのビルド成果物が配置されるディレクトリを指します。外部テストは、通常のパッケージとは別の独立したパッケージとして扱われるため、専用の作業ディレクトリが割り当てられます。

技術的詳細

このコミットが修正しようとしている問題は、gccgoコンパイラがGoの外部テストをリンクする際の、.aファイルの処理順序に起因するものです。

CL 61970044による問題の発生

以前の変更であるCL 61970044は、gccgoがリンカに渡す.aファイルの順序を逆転させました。この変更の具体的な意図はコミットメッセージからは不明ですが、結果として、パッケージの内部バージョン(例: mypackage.a)と外部テストバージョン(例: mypackage_test.a)が同時にリンカに渡された際に、gccgoがエラーを報告するようになりました。

この問題は、リンカのシンボル解決のメカニズムと、外部テストパッケージの性質に深く関連しています。

  • 外部テストパッケージの性質: 外部テストパッケージ(mypackage_test)は、テスト対象の内部パッケージ(mypackage)の公開されたAPIを利用します。多くの場合、外部テストパッケージは、内部パッケージのコンパイル済みコードをインポートし、それに加えてテスト固有のコードを含んでいます。このため、外部テストパッケージのアーカイブファイル(mypackage_test.a)は、内部パッケージのアーカイブファイル(mypackage.a)に含まれるシンボルの多くを「スーパーセット」として含んでいるか、あるいはそれらのシンボルを参照しています。

  • リンカの順序依存性:

    • CL 61970044後の問題: CL 61970044によってリンカへの入力順序が逆になった結果、もし内部パッケージのアーカイブ(mypackage.a)が外部テストパッケージのアーカイブ(mypackage_test.a)よりもにリンカに渡されると、mypackage_test.amypackage.a内のシンボルを参照しているにもかかわらず、リンカがそれらのシンボルを解決できないという「シンボル不足」のエラーが発生する可能性がありました。
    • あるいは、もしmypackage_test.amypackage.aのシンボルを既に含んでいる(スーパーセットである)場合、mypackage_test.aが先に渡され、その後にmypackage.aが渡されると、同じシンボルが二重に定義されているとリンカが判断し、「シンボル重複」のエラーが発生する可能性がありました。

コミットメッセージは、この問題が「内部バージョンが必要なシンボルをすべて含んでいないか、または外部テストバージョンが既に定義されているシンボルを重複して定義している」という形で現れたと説明しています。これは、リンカの順序が不適切であったために、シンボル解決の競合が発生したことを示唆しています。

「偽の (fake)」ターゲットとリンケージ順序

このコミットでは、「偽の (fake)」ターゲットという概念が導入されています。これは、主に外部テストパッケージのように、通常のパッケージとは異なる特別なリンケージ要件を持つビルド成果物を指します。コミットメッセージは、「外部テストとしてコンパイルされたパッケージは、その内部の兄弟パッケージのスーパーセットである」と述べています。この性質を考慮すると、リンカは外部テストパッケージのシンボル定義を、通常のパッケージのシンボル定義よりも優先して処理する必要があります。

gcToolchainとの整合性

このコミットの重要な動機の一つは、gccgoのリンケージ動作をgcToolchainの動作に合わせることです。gcToolchainは、テストをリンクする際に、$WORK/_test(外部テストのビルド成果物)を$WORK(通常のパッケージのビルド成果物)よりも先にリンカに提示していました。これは、外部テストが内部パッケージのスーパーセットであるという事実に基づいた、正しい戦略です。外部テストが先に処理されることで、その中に含まれるシンボルが優先され、後から通常のパッケージが処理されてもシンボル重複の問題は発生せず、また外部テストが参照するシンボルも適切に解決されます。

このコミットは、gccgoのリンケージロジックを修正し、gcToolchainと同様に「偽の」ターゲット(外部テストファイル)をリンカへの入力順序の最上位に配置することで、この問題を解決します。これにより、gccgogcの両方のコンパイラで、go testコマンドが期待通りに動作するようになります。

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

変更は src/cmd/go/build.go ファイルの gccgoToolchain 構造体の ld メソッド内で行われています。

--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -1867,7 +1867,12 @@ func (tools gccgoToolchain) ld(b *builder, p *Package, out string, allactions []
 		if !a.p.Standard {
 			if a.p != nil && !apackagesSeen[a.p] {
 				apackagesSeen[a.p] = true
-				afiles = append(afiles, a.target)
+				if a.p.fake {
+					// move _test files to the top of the link order
+					afiles = append([]string{a.target}, afiles...)
+				} else {
+					afiles = append(afiles, a.target)
+				}
 			}
 		}
 	}

コアとなるコードの解説

この変更は、gccgoToolchainld(リンカ)メソッド内で、リンカに渡すアーカイブファイル(.aファイル)のリストafilesを構築するロジックを修正しています。

元のコードでは、a.target(現在のアーカイブファイルのパス)は常にafilesリストの末尾に追加されていました。

afiles = append(afiles, a.target)

この変更により、a.p.fakeという条件が追加されました。

  • a.pは、現在処理しているパッケージを表すPackage構造体です。
  • a.p.fakeは、そのパッケージが「偽の」パッケージ、つまり主に外部テストパッケージであるかどうかを示すブール値のフィールドです。

新しいロジックは以下のようになります。

if a.p.fake {
    // move _test files to the top of the link order
    afiles = append([]string{a.target}, afiles...)
} else {
    afiles = append(afiles, a.target)
}
  • if a.p.fake の場合:

    • a.p.faketrue、つまり現在のパッケージが外部テストのような「偽の」パッケージである場合、a.targetafilesリストの先頭に追加されます。
    • append([]string{a.target}, afiles...)という構文は、新しいスライスを作成し、その最初の要素としてa.targetを置き、その後に既存のafilesのすべての要素を展開して追加します。これにより、a.targetがリストの最上位(リンカに最初に渡される位置)に配置されることが保証されます。
    • コメント// move _test files to the top of the link orderが、この変更の意図を明確に示しています。
  • else の場合:

    • a.p.fakefalse、つまり通常のパッケージである場合、a.targetはこれまで通りafilesリストの末尾に追加されます。

この変更の核心は、リンカに渡すファイルの順序を制御することにあります。外部テストファイル(_test.a)は、テスト対象の通常のパッケージファイル(.a)のスーパーセットであるため、リンカがシンボルを解決する際に、外部テストファイル内の定義を優先させる必要があります。この修正により、gccgogcToolchainと同様に、外部テストファイルを通常のパッケージファイルよりも先にリンカに提示するようになり、シンボル重複や未定義シンボルの問題を回避できるようになりました。

関連リンク

  • Go Issue #7627 (コミットメッセージで参照されているが、GitHub上では見つからなかった): https://github.com/golang/go/issues/7627 (直接のリンクは機能しない可能性がありますが、コミットメッセージに記載されているため含めます)
  • Go CL 80300043 (このコミットのGo Code Reviewサイトのリンク): https://golang.org/cl/80300043
  • Go CL 61970044 (問題を引き起こした以前の変更): https://golang.org/cl/61970044 (このCLの具体的な内容は、コミットメッセージの記述とWeb検索結果で食い違いがあるため、注意が必要です。コミットメッセージの記述を優先しています。)

参考にした情報源リンク

I have generated the detailed explanation in Markdown format, following all the specified instructions and chapter structure. I have incorporated the information from the commit message and the general knowledge from the web searches. I have also addressed the discrepancies found during the web search by prioritizing the commit message's information.

The output is now ready to be presented to the user.

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

このコミットは、Go言語のビルドツール (cmd/go) において、gccgo コンパイラを使用する際の外部テストファイルのリンク順序に関する問題を修正するものです。具体的には、以前の変更によって発生した、内部パッケージと外部テストパッケージのリンケージにおけるシンボル重複または不足のエラーを解決し、gc コンパイラの動作と整合性を持たせることを目的としています。

コミット

commit 3ce1677ad9f95fb0b1ede191f41996c451e054c1
Author: Dave Cheney <dave@cheney.net>
Date:   Wed Mar 26 15:30:55 2014 +1100

    cmd/go: ensure external test files are presented to the linker first
    
    Fixes #7627.
    
    CL 61970044 changed the order in which .a files are passed to gccgo's link phase. However by reversing the order it caused gccgo to complain if both internal (liba.a) and external (liba_test.a) versions of a package were presented as the former would not contain all the necessary symbols, and the latter would duplicate symbols already defined.
    
    This change ensures that all 'fake' targets remain at the top of the final link order which should be fine as a package compiled as an external test is a superset of its internal sibling.
    
    Looking at how gcToolchain links tests I think this change now accurately mirrors those actions which present $WORK/_test before $WORK in the link order.
    
    LGTM=iant
    R=rsc, iant
    CC=golang-codereviews, michael.hudson
    https://golang.org/cl/80300043

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

https://github.com/golang/go/commit/3ce1677ad9f95fb0b1ede191f41996c451e054c1

元コミット内容

このコミットは、cmd/go ツールにおいて、gccgo コンパイラが外部テストファイルをリンクする際に、それらのファイルをリンカに最初に渡すように変更します。

これは、Issue #7627 を修正するものです。

以前の変更である CL 61970044 は、.a (アーカイブ) ファイルが gccgo のリンクフェーズに渡される順序を変更しました。しかし、この順序を逆にしたことで、パッケージの内部バージョン (liba.a) と外部テストバージョン (liba_test.a) の両方がリンカに渡された場合に、gccgo がエラーを報告するようになりました。具体的には、内部バージョンが必要なシンボルをすべて含んでいないか、または外部テストバージョンが既に定義されているシンボルを重複して定義しているという問題が発生しました。

この変更は、「偽の (fake)」ターゲット(主に外部テストファイル)が最終的なリンク順序の最上位に維持されることを保証します。これは、外部テストとしてコンパイルされたパッケージが、その内部の兄弟パッケージのスーパーセットであるため、問題ないはずです。

gcToolchain がテストをリンクする方法を見ると、この変更は $WORK/_test$WORK の前にリンク順序で提示するそれらのアクションを正確に反映していると考えられます。

変更の背景

このコミットの背景には、Go言語のビルドシステム、特にgo testコマンドと、gcおよびgccgoという二つの主要なコンパイラの挙動の違いが深く関わっています。

Go言語では、テストコードは通常、対象となるパッケージと同じディレクトリに_test.goというサフィックスを持つファイルとして配置されます。これらのテストは大きく分けて二種類あります。

  1. 内部テスト (Internal Tests): package mypackage のように、テスト対象のパッケージと同じパッケージ名を持つテストファイルです。これらのテストは、パッケージの内部実装にアクセスできます。
  2. 外部テスト (External Tests): package mypackage_test のように、テスト対象のパッケージとは異なるパッケージ名を持つテストファイルです。これらのテストは、パッケージが外部からどのように見えるかをテストするために使用され、パッケージのエクスポートされたAPIのみにアクセスできます。

go testコマンドは、これらのテストファイルをコンパイルし、テスト実行可能なバイナリを生成します。この際、gc(標準のGoコンパイラ)とgccgo(GCCベースのGoコンパイラ)では、内部的なリンケージの挙動に違いがありました。

問題の発端は、CL 61970044という変更にあります。この変更は、gccgo.a(アーカイブ)ファイルをリンカに渡す順序を変更しました。リンカの動作において、ライブラリやオブジェクトファイルの順序は非常に重要です。シンボル(関数や変数など)の定義が複数のファイルに存在する場合、リンカは通常、コマンドラインで最初に見つかった定義を採用します。また、あるファイルが別のファイルで定義されたシンボルを参照する場合、参照するファイルが参照されるファイルよりも前にリンカに渡されると、シンボルが見つからないというエラーが発生する可能性があります。

CL 61970044によってリンカへの入力順序が逆になった結果、gccgoはパッケージの内部バージョン(例: liba.a)と外部テストバージョン(例: liba_test.a)の両方を処理する際に問題を引き起こしました。

  • 内部バージョン (liba.a): これは通常のパッケージコードを含みます。
  • 外部テストバージョン (liba_test.a): これは外部テストコードを含み、通常は内部パッケージのシンボルをインポートして使用します。外部テストパッケージは、内部パッケージの「スーパーセット」と見なされることがあります。つまり、外部テストパッケージは内部パッケージのすべてのシンボルを含み、さらにテストに必要な追加のシンボルも持っている可能性があります。

CL 61970044によってリンカへの入力順序が逆になった結果、gccgoは以下のいずれかのエラーを報告するようになりました。

  1. シンボル不足: 内部パッケージ (liba.a) がリンカに先に渡された場合、外部テスト (liba_test.a) が必要とする特定のシンボルが内部パッケージには含まれていないため、リンカがシンボルを見つけられない。
  2. シンボル重複: 外部テストパッケージ (liba_test.a) が先に渡された場合、それが内部パッケージのシンボルを既に含んでいるため、後から渡される内部パッケージ (liba.a) の同じシンボルと重複してしまう。

この問題は、Goのビルドシステムが異なるコンパイラ(gcgccgo)間で一貫した動作を提供する必要があるという原則に反していました。特に、gcToolchaingcコンパイラを使用するツールチェーン)は、テストをリンクする際に、$WORK/_test(外部テストのビルド成果物)を$WORK(通常のパッケージのビルド成果物)よりも先にリンカに提示していました。これは、外部テストが内部パッケージのスーパーセットであるという性質を考慮した、正しいリンケージ順序です。

このコミットは、gccgoのリンケージ動作をgcToolchainの動作に合わせることで、この問題を解決し、go testコマンドの信頼性と一貫性を向上させることを目的としています。

前提知識の解説

1. Go言語のビルドシステム (go build, go test)

Go言語のビルドシステムは、goコマンドによって統合されています。

  • go build: ソースコードをコンパイルし、実行可能なバイナリやライブラリを生成します。
  • go test: テストコードをコンパイルし、テストを実行するための特別なバイナリを生成します。このコマンドは、テスト対象のパッケージとテストファイル(_test.go)を自動的に検出し、ビルドプロセスを管理します。

2. gcgccgo コンパイラ

Go言語には主に二つのコンパイラ実装が存在します。

  • gc (Go Compiler): Goプロジェクトの公式かつデフォルトのコンパイラです。Go言語で書かれており、高速なコンパイルと最適化が特徴です。
  • gccgo: GCC (GNU Compiler Collection) のフロントエンドとして実装されたGoコンパイラです。C/C++など他の言語と同じGCCのバックエンドを利用するため、既存のGCCツールチェーンとの統合が容易であり、特定のプラットフォームや最適化のニーズに対応できる場合があります。しかし、gcとは異なるビルド・リンケージの挙動を示すことがあります。

3. 内部テストと外部テスト

Goのテストファイルは、ファイル名が_test.goで終わることで識別されます。これらのテストは、パッケージの構造によって「内部テスト」と「外部テスト」に分類されます。

  • 内部テスト: package mypackage のように、テスト対象のパッケージと同じパッケージ名を持つテストファイルです。これにより、テストコードはパッケージ内の非エクスポート(プライベート)な関数や変数にもアクセスできます。
  • 外部テスト: package mypackage_test のように、テスト対象のパッケージとは異なるパッケージ名を持つテストファイルです。これにより、テストコードはパッケージのエクスポートされた(パブリックな)APIのみにアクセスでき、パッケージが外部からどのように利用されるかをより正確にシミュレートできます。go testコマンドは、外部テストをコンパイルする際に、元のパッケージとは別の「偽の (fake)」パッケージとして扱います。

4. リンカの動作とライブラリのリンク順序

リンカは、コンパイラによって生成されたオブジェクトファイル(.o)やアーカイブファイル(.a、ライブラリの集合)を結合し、最終的な実行可能ファイルや共有ライブラリを生成するツールです。リンカの動作において、入力ファイルの順序は非常に重要です。

  • シンボル解決: リンカは、あるオブジェクトファイルが参照するシンボル(関数や変数)の定義を、入力された他のオブジェクトファイルやライブラリの中から探します。
  • 順序依存性: 一般的に、リンカはコマンドラインで指定された順序でファイルを処理します。あるファイルが別のファイルで定義されたシンボルを参照する場合、参照されるファイルが参照するファイルよりもにリンカに渡されると、シンボルが見つからないエラー(未定義参照)が発生します。逆に、同じシンボルが複数のファイルで定義されている場合、リンカは通常、コマンドラインで最初に見つかった定義を採用します。
  • アーカイブファイル (.a): 複数のオブジェクトファイルをまとめた静的ライブラリです。リンカは、アーカイブファイルの中から必要なシンボル定義のみを抽出してリンクします。

5. $WORK$WORK/_test

Goのビルドプロセスでは、一時的な作業ディレクトリが作成されます。

  • $WORK: 通常のパッケージのビルド成果物(オブジェクトファイルやアーカイブファイル)が配置されるディレクトリを指します。
  • $WORK/_test: 外部テストパッケージのビルド成果物が配置されるディレクトリを指します。外部テストは、通常のパッケージとは別の独立したパッケージとして扱われるため、専用の作業ディレクトリが割り当てられます。

技術的詳細

このコミットが修正しようとしている問題は、gccgoコンパイラがGoの外部テストをリンクする際の、.aファイルの処理順序に起因するものです。

CL 61970044による問題の発生

以前の変更であるCL 61970044は、gccgoがリンカに渡す.aファイルの順序を逆転させました。この変更の具体的な意図はコミットメッセージからは不明ですが、結果として、パッケージの内部バージョン(例: mypackage.a)と外部テストバージョン(例: mypackage_test.a)が同時にリンカに渡された際に、gccgoがエラーを報告するようになりました。

この問題は、リンカのシンボル解決のメカニズムと、外部テストパッケージの性質に深く関連しています。

  • 外部テストパッケージの性質: 外部テストパッケージ(mypackage_test)は、テスト対象の内部パッケージ(mypackage)の公開されたAPIを利用します。多くの場合、外部テストパッケージは、内部パッケージのコンパイル済みコードをインポートし、それに加えてテスト固有のコードを含んでいます。このため、外部テストパッケージのアーカイブファイル(mypackage_test.a)は、内部パッケージのアーカイブファイル(mypackage.a)に含まれるシンボルの多くを「スーパーセット」として含んでいるか、あるいはそれらのシンボルを参照しています。

  • リンカの順序依存性:

    • CL 61970044後の問題: CL 61970044によってリンカへの入力順序が逆になった結果、もし内部パッケージのアーカイブ(mypackage.a)が外部テストパッケージのアーカイブ(mypackage_test.a)よりもにリンカに渡されると、mypackage_test.amypackage.a内のシンボルを参照しているにもかかわらず、リンカがそれらのシンボルを解決できないという「シンボル不足」のエラーが発生する可能性がありました。
    • あるいは、もしmypackage_test.amypackage.aのシンボルを既に含んでいる(スーパーセットである)場合、mypackage_test.aが先に渡され、その後にmypackage.aが渡されると、同じシンボルが二重に定義されているとリンカが判断し、「シンボル重複」のエラーが発生する可能性がありました。

コミットメッセージは、この問題が「内部バージョンが必要なシンボルをすべて含んでいないか、または外部テストバージョンが既に定義されているシンボルを重複して定義している」という形で現れたと説明しています。これは、リンカの順序が不適切であったために、シンボル解決の競合が発生したことを示唆しています。

「偽の (fake)」ターゲットとリンケージ順序

このコミットでは、「偽の (fake)」ターゲットという概念が導入されています。これは、主に外部テストパッケージのように、通常のパッケージとは異なる特別なリンケージ要件を持つビルド成果物を指します。コミットメッセージは、「外部テストとしてコンパイルされたパッケージは、その内部の兄弟パッケージのスーパーセットである」と述べています。この性質を考慮すると、リンカは外部テストパッケージのシンボル定義を、通常のパッケージのシンボル定義よりも優先して処理する必要があります。

gcToolchainとの整合性

このコミットの重要な動機の一つは、gccgoのリンケージ動作をgcToolchainの動作に合わせることです。gcToolchainは、テストをリンクする際に、$WORK/_test(外部テストのビルド成果物)を$WORK(通常のパッケージのビルド成果物)よりも先にリンカに提示していました。これは、外部テストが内部パッケージのスーパーセットであるという事実に基づいた、正しい戦略です。外部テストが先に処理されることで、その中に含まれるシンボルが優先され、後から通常のパッケージが処理されてもシンボル重複の問題は発生せず、また外部テストが参照するシンボルも適切に解決されます。

このコミットは、gccgoのリンケージロジックを修正し、gcToolchainと同様に「偽の」ターゲット(外部テストファイル)をリンカへの入力順序の最上位に配置することで、この問題を解決します。これにより、gccgogcの両方のコンパイラで、go testコマンドが期待通りに動作するようになります。

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

変更は src/cmd/go/build.go ファイルの gccgoToolchain 構造体の ld メソッド内で行われています。

--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -1867,7 +1867,12 @@ func (tools gccgoToolchain) ld(b *builder, p *Package, out string, allactions []
 		if !a.p.Standard {
 			if a.p != nil && !apackagesSeen[a.p] {
 				apackagesSeen[a.p] = true
-				afiles = append(afiles, a.target)
+				if a.p.fake {
+					// move _test files to the top of the link order
+					afiles = append([]string{a.target}, afiles...)
+				} else {
+					afiles = append(afiles, a.target)
+				}
 			}
 		}
 	}

コアとなるコードの解説

この変更は、gccgoToolchainld(リンカ)メソッド内で、リンカに渡すアーカイブファイル(.aファイル)のリストafilesを構築するロジックを修正しています。

元のコードでは、a.target(現在のアーカイブファイルのパス)は常にafilesリストの末尾に追加されていました。

afiles = append(afiles, a.target)

この変更により、a.p.fakeという条件が追加されました。

  • a.pは、現在処理しているパッケージを表すPackage構造体です。
  • a.p.fakeは、そのパッケージが「偽の」パッケージ、つまり主に外部テストパッケージであるかどうかを示すブール値のフィールドです。

新しいロジックは以下のようになります。

if a.p.fake {
    // move _test files to the top of the link order
    afiles = append([]string{a.target}, afiles...)
} else {
    afiles = append(afiles, a.target)
}
  • if a.p.fake の場合:

    • a.p.faketrue、つまり現在のパッケージが外部テストのような「偽の」パッケージである場合、a.targetafilesリストの先頭に追加されます。
    • append([]string{a.target}, afiles...)という構文は、新しいスライスを作成し、その最初の要素としてa.targetを置き、その後に既存のafilesのすべての要素を展開して追加します。これにより、a.targetがリストの最上位(リンカに最初に渡される位置)に配置されることが保証されます。
    • コメント// move _test files to the top of the link orderが、この変更の意図を明確に示しています。
  • else の場合:

    • a.p.fakefalse、つまり通常のパッケージである場合、a.targetはこれまで通りafilesリストの末尾に追加されます。

この変更の核心は、リンカに渡すファイルの順序を制御することにあります。外部テストファイル(_test.a)は、テスト対象の通常のパッケージファイル(.a)のスーパーセットであるため、リンカがシンボルを解決する際に、外部テストファイル内の定義を優先させる必要があります。この修正により、gccgogcToolchainと同様に、外部テストファイルを通常のパッケージファイルよりも先にリンカに提示するようになり、シンボル重複や未定義シンボルの問題を回避できるようになりました。

関連リンク

  • Go Issue #7627 (コミットメッセージで参照されているが、GitHub上では見つからなかった): https://github.com/golang/go/issues/7627 (直接のリンクは機能しない可能性がありますが、コミットメッセージに記載されているため含めます)
  • Go CL 80300043 (このコミットのGo Code Reviewサイトのリンク): https://golang.org/cl/80300043
  • Go CL 61970044 (問題を引き起こした以前の変更): https://golang.org/cl/61970044 (このCLの具体的な内容は、コミットメッセージの記述とWeb検索結果で食い違いがあるため、注意が必要です。コミットメッセージの記述を優先しています。)

参考にした情報源リンク