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

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

このコミットは、Go言語のビルドツール (cmd/go) において、go build -n コマンドがCgoを有効にしたパッケージに対して正しく動作しない問題を修正するものです。具体的には、libgcc のパスを決定する際に、go build -n が実際のコマンド実行を抑制しつつも、そのコマンドライン出力をキャプチャし、ビルドスクリプト内で $LIBGCC 変数として利用できるようにする変更が加えられています。

コミット

commit 6d2d2ae4d9af028ff13819497baeecfd984dc3e6
Author: Anthony Martin <ality@pbrane.org>
Date:   Wed Jan 30 15:09:34 2013 -0800

    cmd/go: fix build -n for cgo enabled packages
    
    R=golang-dev, minux.ma, rsc
    CC=golang-dev
    https://golang.org/cl/7095067

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

https://github.com/golang/go/commit/6d2d2ae4d9af028ff13819497baeecfd984dc3e6

元コミット内容

cmd/go: fix build -n for cgo enabled packages

R=golang-dev, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/7095067

変更の背景

Go言語のビルドコマンド go build には -n フラグがあります。このフラグは、実際にビルドを実行する代わりに、実行されるであろうコマンドラインを表示する機能を提供します。これは、デバッグやビルドプロセスの理解に非常に役立ちます。

しかし、Cgo(GoとC言語の相互運用機能)を有効にしたパッケージをビルドする際、go build -n は問題を抱えていました。Cgoパッケージのビルドプロセスでは、GCCコンパイラが使用され、その際に libgcc というライブラリのパスが必要になることがあります。libgcc のパスは通常、gcc -print-libgcc-file-name コマンドを実行することで取得されます。

従来の go build -n の実装では、この gcc -print-libgcc-file-name コマンドも表示対象となり、実際に実行はされませんでした。その結果、libgcc のパスが取得できず、ビルドスクリプトが不完全な状態になり、ユーザーがそのスクリプトをコピーして手動で実行しようとした場合に失敗するという問題がありました。

このコミットは、go build -n がCgoパッケージのビルドスクリプトを生成する際に、libgcc のパスを正しく解決し、生成されるスクリプトが $LIBGCC のような変数を使って、後で実行可能な形式になるように修正することを目的としています。これにより、go build -n の出力がより実用的で、デバッグや自動化のシナリオで役立つようになります。

前提知識の解説

Go言語のビルドシステム (cmd/go)

cmd/go は、Go言語のソースコードをコンパイル、テスト、インストールなどを行うための公式コマンドラインツールです。Goのモジュール管理、依存関係解決、クロスコンパイルなど、Go開発のほとんどの側面を管理します。

go build -n フラグ

go build -n は、Goのビルドコマンドに渡されるオプションの一つです。このフラグを使用すると、go build は実際にバイナリをコンパイルしたりリンクしたりする代わりに、ビルドプロセス中に実行されるであろうすべての外部コマンド(コンパイラ、リンカ、アセンブラなど)を標準出力に表示します。これは、ビルドがどのように行われるかを理解したり、特定のビルド問題をデバッグしたりする際に非常に有用です。

Cgo

Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。Cgoを使用すると、既存のCライブラリをGoプロジェクトに統合したり、パフォーマンスが重要な部分をCで記述したりすることができます。Cgoを使用するGoパッケージは、通常のGoコンパイラだけでなく、Cコンパイラ(通常はGCC)も必要とします。

libgcc

libgcc は、GCC (GNU Compiler Collection) のランタイムサポートライブラリです。これは、GCCが生成するコードが依存する低レベルのルーチン(例えば、整数除算、浮動小数点演算、例外処理など)を提供します。Cgoを通じてCコードをGoプログラムにリンクする場合、GCCが使用されるため、libgcc が必要になることがあります。gcc -print-libgcc-file-name コマンドは、システム上の libgcc ライブラリの絶対パスを出力するために使用されます。

builder 構造体と b.print, b.runOut

Goのビルドツール内部では、ビルドプロセスを管理するために builder のような構造体が使用されます。

  • b.print: これは、ビルドプロセス中に実行されるコマンドを標準出力(または -n フラグが指定された場合はキャプチャバッファ)に「表示」するための内部関数です。
  • b.runOut: これは、外部コマンドを実行し、その標準出力をキャプチャするための内部関数です。このコミットの文脈では、gcc -print-libgcc-file-name のようなコマンドを実行するために使用されます。

技術的詳細

このコミットの核心は、go build -n モードでの libgcc パスの取得方法の改善にあります。

libgcc のパスを取得する libgcc メソッドは、通常 b.runOut を使用して gcc -print-libgcc-file-name コマンドを実行し、その出力を直接取得します。しかし、buildN (つまり -n フラグが有効な場合) は、実際のコマンド実行を抑制します。

この修正では、buildNtrue の場合に以下のロジックが追加されています。

  1. b.print の一時的なスワップ: b.print 関数は、通常、実行されるコマンドを直接出力するために使用されます。しかし、-n モードでは、この出力を一時的に bytes.Buffer にリダイレクトします。これにより、gcc -print-libgcc-file-name コマンド自体が bytes.Buffer にキャプチャされ、標準出力には表示されなくなります。
  2. コマンドラインのキャプチャ: b.runOut は引き続き呼び出されますが、-n モードでは実際にはコマンドは実行されず、そのコマンドラインが b.print を通じて buf に書き込まれます。
  3. $LIBGCC 変数の生成: gcc -print-libgcc-file-name コマンドが buf にキャプチャされた後、buildNtrue の場合、buf に格納されたコマンドラインを使用して LIBGCC=$(...) という形式のシェル変数が生成されます。この $(...) はシェルスクリプトのコマンド置換構文であり、括弧内のコマンドの出力を変数に代入します。
  4. 元の b.print の復元と出力: 一時的にスワップされた b.print は元の関数に戻され、生成された LIBGCC=$(...) の行が b.print を通じて標準出力に書き込まれます。これにより、go build -n の出力には、libgcc のパスを動的に解決するためのシェルコマンドが含まれるようになります。
  5. $LIBGCC の返却: libgcc メソッドは、実際の libgcc パスではなく、シェル変数 $LIBGCC を返します。これにより、生成されるビルドスクリプトは、後で実行されたときに libgcc のパスを正しく解決できるようになります。

この変更により、go build -n はCgoパッケージに対しても、実行可能なビルドスクリプトを生成できるようになり、デバッグや自動化のワークフローが改善されます。

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

--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -1534,10 +1534,28 @@ func gccgoCleanPkgpath(p *Package) string {
 // libgcc returns the filename for libgcc, as determined by invoking gcc with
 // the -print-libgcc-file-name option.
 func (b *builder) libgcc(p *Package) (string, error) {
+	var buf bytes.Buffer
+
+	prev := b.print
+	if buildN {
+		// In -n mode we temporarily swap out the builder's
+		// print function to capture the command-line. This
+		// let's us assign it to $LIBGCC and produce a valid
+		// buildscript for cgo packages.
+		b.print = func(a ...interface{}) (n int, err error) {
+			return fmt.Fprint(&buf, a...)
+		}
+	}
 	f, err := b.runOut(p.Dir, p.ImportPath, b.gccCmd(p.Dir), "-print-libgcc-file-name")
 	if err != nil {
 		return "", fmt.Errorf("gcc -print-libgcc-file-name: %v (%s)", err, f)
 	}\n+	if buildN {
+		s := fmt.Sprintf("LIBGCC=$(%s)\\n", buf.Next(buf.Len()-1))
+		b.print = prev
+		b.print(s)
+		return "$LIBGCC", nil
+	}
 	return strings.Trim(string(f), "\\r\\n"), nil
 }
 

コアとなるコードの解説

src/cmd/go/build.go ファイル内の (*builder).libgcc メソッドが変更されています。

  1. var buf bytes.Buffer: bytes.Buffer 型の buf 変数が宣言されます。これは、-n モードでコマンドライン出力を一時的にキャプチャするためのバッファとして機能します。
  2. prev := b.print: 現在の b.print 関数(ビルドコマンドの出力を担当)が prev 変数に保存されます。これは、後で元の関数に戻すために必要です。
  3. if buildN { ... }: buildN グローバル変数(-n フラグが指定されているかどうかを示す)が true の場合にのみ、以下のロジックが実行されます。
    • b.print = func(a ...interface{}) (n int, err error) { return fmt.Fprint(&buf, a...) }: b.print 関数が一時的に上書きされます。新しい b.print は、引数を受け取り、それらを標準出力ではなく buf に書き込みます。これにより、gcc -print-libgcc-file-name コマンド自体が buf にキャプチャされます。
  4. f, err := b.runOut(p.Dir, p.ImportPath, b.gccCmd(p.Dir), "-print-libgcc-file-name"): b.runOut が呼び出され、gcc -print-libgcc-file-name コマンドを実行(または -n モードではそのコマンドラインを生成)します。-n モードでは、この呼び出しによってコマンドラインが一時的に上書きされた b.print を通じて buf に書き込まれます。
  5. if err != nil { ... }: エラーハンドリングは変更されていません。
  6. if buildN { ... }: 再び buildNtrue の場合にのみ、以下のロジックが実行されます。
    • s := fmt.Sprintf("LIBGCC=$(%s)\\n", buf.Next(buf.Len()-1)): buf にキャプチャされたコマンドライン(gcc -print-libgcc-file-name)を使用して、LIBGCC=$(...) という形式の文字列 s が生成されます。buf.Next(buf.Len()-1) は、バッファの最後の改行文字を除いた内容を取得します。
    • b.print = prev: b.print 関数が、ステップ2で保存しておいた元の関数に戻されます。
    • b.print(s): 生成された LIBGCC=$(...) の行が、元の b.print 関数を通じて標準出力に書き込まれます。これが go build -n の出力の一部となります。
    • return "$LIBGCC", nil: libgcc メソッドは、実際の libgcc パスではなく、シェル変数 $LIBGCC を返します。これにより、生成されるビルドスクリプトは、この変数を使用して libgcc のパスを参照するようになります。
  7. return strings.Trim(string(f), "\\r\\n"), nil: buildNfalse の場合(通常のビルドモード)、b.runOut から返された実際の libgcc パスがトリムされて返されます。

この変更により、go build -n はCgoパッケージのビルドスクリプトを生成する際に、libgcc のパスを動的に解決するためのシェルコマンドを埋め込むことができるようになり、生成されるスクリプトの実行可能性が向上します。

関連リンク

参考にした情報源リンク