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

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

このコミットは、Go言語のビルドシステムにおける回帰バグを修正するものです。具体的には、gccgoツールチェインのサポートが追加された際に発生した、外部コマンド(Goツリーに含まれないコマンド)のインストールパスが誤って上書き(clobber)される問題に対処しています。これにより、外部コマンドが本来インストールされるべきバイナリディレクトリではなく、パッケージディレクトリにインストールされてしまうという不具合が解消されます。

コミット

commit d2599b431e80a3824cc587f8b23e3783fd241f3a
Author: Anthony Martin <ality@pbrane.org>
Date:   Mon Jan 30 13:54:22 2012 -0500

    go: don't clobber command install paths
    
    This fixes a regression that was made when adding
    support for building with gccgo (in d6a14e6fac0c).
    
    External commands (those not from the Go tree) were
    being installed to the package directory instead of
    the binary directory.
    
    R=golang-dev, rsc, adg, remyoudompheng, rsc
    CC=golang-dev
    https://golang.org/cl/5564072

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

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

元コミット内容

このコミットは、Go言語のビルドシステムにおいて、gccgoツールチェインのサポート導入によって引き起こされた回帰バグを修正するものです。具体的には、Goのソースツリー外のコマンド(外部コマンド)が、本来インストールされるべきバイナリディレクトリではなく、誤ってパッケージディレクトリにインストールされてしまう問題に対処しています。この問題は、コミットd6a14e6fac0cgccgoサポートが追加された際に発生しました。

変更の背景

この変更の背景には、Go言語のビルドシステムが複数のコンパイラツールチェイン(標準のgcgccgo)をサポートするようになった経緯があります。

Go言語の初期のビルドシステムは、主にGoチームが開発した公式コンパイラであるgc(Go Compiler)を前提としていました。しかし、Go言語の普及に伴い、GCC(GNU Compiler Collection)をバックエンドとして利用するgccgoという代替コンパイラが開発されました。gccgoは、既存のGCCインフラストラクチャを活用することで、Goプログラムをより多くのプラットフォームでコンパイルできるようにすることを目指していました。

コミットメッセージに記載されているd6a14e6fac0cは、Goのビルドツール(goコマンド)にgccgoを統合するための変更でした。この統合自体は、Go言語の柔軟性と移植性を高める上で重要なステップでしたが、その過程で予期せぬ副作用が発生しました。

具体的には、gccgoのビルドパスの処理方法が、Goツリー外のコマンド(例えば、go getでインストールされるサードパーティ製のツールなど)のインストールパスの決定ロジックに影響を与えてしまいました。結果として、これらの外部コマンドが、ユーザーの$GOBIN(バイナリ実行ファイルが置かれるべき場所)ではなく、$GOPATH/pkg(コンパイル済みパッケージが置かれる場所)のようなパッケージディレクトリに誤って配置されるという回帰バグが発生しました。

この問題は、ユーザーがgo installgo getを使って外部ツールをインストールした際に、それらのツールが期待されるパスに存在せず、実行できないという形で現れました。このコミットは、この回帰バグを修正し、gccgoを使用している場合でも、外部コマンドが正しいバイナリディレクトリにインストールされるようにすることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語のビルドシステムと関連する概念についての知識が必要です。

  1. Go言語のワークスペースと環境変数:

    • GOPATH: Go 1.11以前のGoプロジェクトのワークスペースのルートディレクトリを指定する環境変数です。Goのソースコード、パッケージ、バイナリがこのディレクトリ構造内に配置されます。src(ソースコード)、pkg(コンパイル済みパッケージ)、bin(コンパイル済みバイナリ)の3つのサブディレクトリが慣例的に存在します。
    • GOBIN: コンパイルされたGoプログラムの実行可能ファイルがインストールされるディレクトリを指定する環境変数です。GOBINが設定されていない場合、実行可能ファイルはGOPATH/binにインストールされます。
    • go install: ソースコードをコンパイルし、その結果生成されたパッケージ(.aファイルなど)をGOPATH/pkgに、実行可能ファイル(コマンド)をGOPATH/binまたはGOBINにインストールするGoコマンドです。
    • go get: リモートリポジトリからGoパッケージをダウンロードし、ビルドしてインストールするGoコマンドです。
  2. Goのビルドツール (goコマンド):

    • Go言語のビルド、テスト、パッケージ管理などを行うための主要なコマンドラインツールです。src/cmd/goディレクトリにそのソースコードがあります。
    • このツールは、Goのソースコードをコンパイルし、実行可能ファイルを生成する際に、どのディレクトリに何を配置するかを決定するロジックを含んでいます。
  3. Goのパッケージとコマンド:

    • パッケージ: Goのコードの基本的な構成単位です。他のパッケージからインポートして利用されます。コンパイルされると、通常は.a(アーカイブ)ファイルとしてGOPATH/pkgに保存されます。
    • コマンド: mainパッケージを持ち、main関数を含むGoプログラムは、実行可能なコマンドとしてビルドされます。これらは通常、GOPATH/binまたはGOBINにインストールされます。
  4. Goのツールチェイン:

    • Goのソースコードをコンパイルし、実行可能ファイルを生成するための一連のツール(コンパイラ、アセンブラ、リンカなど)の集合です。
    • gc (Go Compiler): Goチームが開発した標準のGoコンパイラです。Goの公式リリースに同梱されています。
    • gccgo: GCCのフロントエンドとして実装されたGoコンパイラです。GCCの最適化やバックエンドのサポートを利用できます。gccgoは、gcとは異なる内部的なパス構造や命名規則を持つことがあります。
  5. build.Contextbuild.Tree:

    • Goのビルドツール内部で使用される構造体で、ビルド環境のコンテキスト(OS、アーキテクチャ、GOPATHなど)や、ビルドツリー(ソースディレクトリ、パッケージディレクトリ、バイナリディレクトリなど)の情報を保持します。
    • t.PkgDir(): build.Treeオブジェクトのメソッドで、コンパイル済みパッケージが置かれるべきディレクトリのパスを返します。
  6. p.target:

    • Goのビルドツール内部で、ビルド対象の最終的な出力ファイル(パッケージのアーカイブファイルや実行可能ファイル)のパスを保持するフィールドです。このフィールドの値が、最終的なファイルの配置場所を決定します。
  7. filepath.Joinfilepath.FromSlash:

    • Goの標準ライブラリpath/filepathパッケージの関数です。
    • filepath.Join: 複数のパス要素を結合して、OS固有のパス区切り文字(Windowsでは\、Unix系では/)を使用して正しいパスを生成します。
    • filepath.FromSlash: スラッシュ区切りのパスを、現在のOSのパス区切り文字に変換します。

これらの概念を理解することで、コミットがなぜ必要とされ、どのように問題を解決しているのかが明確になります。特に、gccgoが導入されたことで、従来のgcとは異なるパスの扱いが必要になった点が重要です。

技術的詳細

このコミットが修正している問題は、gccgoツールチェインが導入された際に、Goのビルドツール(goコマンド)が外部コマンドのインストールパスを誤って計算してしまうという回帰バグです。

問題の核心は、src/cmd/go/pkg.go内のscanPackage関数にあります。この関数は、Goパッケージのビルドターゲットパス(p.target)を決定する役割を担っています。

元のコードでは、p.targetの初期設定は以下のようになっていました。

// (省略)
} else {
    p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")
}

// For gccgo, rewrite p.target with the expected library name. We won't do
// that for the standard library for the moment.
if !p.Standard {
    dir := t.PkgDir()
    if _, ok := buildToolchain.(gccgoToolchain); ok {
        dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir))
    }
    p.target = buildToolchain.pkgpath(dir, p)
}

このロジックの問題点は以下の通りです。

  1. 初期のp.target設定: p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a") の行は、すべてのパッケージ(コマンドを含む)に対して、デフォルトでパッケージディレクトリ(t.PkgDir()、通常は$GOPATH/pkg)をベースとしたパスを設定していました。これは、コマンド(実行可能ファイル)が最終的に$GOBINまたは$GOPATH/binにインストールされるべきであるというGoの慣習と矛盾します。本来、コマンドのp.targetはバイナリディレクトリを指すべきです。

  2. gccgo特有のパス書き換えロジック: その後のif !p.Standardブロック内で、gccgoツールチェインが使用されている場合にp.targetを書き換えるロジックがありました。

    • dir := t.PkgDir(): ここで、パッケージディレクトリをベースとしています。
    • dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir)): gccgoは、標準のGoツールチェインとは異なるディレクトリ構造を使用することがあるため、gccgo固有のパッケージディレクトリパスを構築していました。例えば、$GOPATH/pkg/linux_amd64$GOPATH/pkg/gccgo/linux_amd64のようになることを意図していたと考えられます。
    • p.target = buildToolchain.pkgpath(dir, p): 最終的に、gccgoツールチェインのpkgpathメソッドを使って、gccgoの命名規則に沿ったパッケージパスを生成し、p.targetに設定していました。

この組み合わせにより、特にGoツリー外のコマンド(p.Standardfalseのケース)の場合に問題が発生しました。コマンドであっても、初期段階でp.targetがパッケージディレクトリを指すように設定され、その後のgccgo固有のパス書き換えもパッケージディレクトリをベースに行われるため、最終的にp.targetがバイナリディレクトリではなく、gccgoのパッケージディレクトリ(例: $GOPATH/pkg/gccgo/...)を指してしまい、実行可能ファイルが誤った場所にインストールされる結果となりました。

修正内容:

このコミットは、p.targetの初期設定ロジックを修正し、gccgoのパス処理をより適切に統合することで、この問題を解決しています。

修正後のコードは以下のようになります。

// (省略)
} else {
    // 変更点1: p.targetの初期設定が削除された
    // p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")
}

// 変更点2: dirの初期化がif/elseブロックの外に移動し、常にt.PkgDir()から始まる
dir := t.PkgDir()
// For gccgo, rewrite p.target with the expected library name.
if _, ok := buildToolchain.(gccgoToolchain); ok {
    dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir))
}
p.target = buildToolchain.pkgpath(dir, p)

// 変更点3: 標準ライブラリに対するgccgoの特別な処理が追加された
// NB. Currently we have gccgo install the standard libraries
// in the "usual" location, where the Go toolchain puts them.
if p.Standard {
    if _, ok := buildToolchain.(gccgoToolchain); ok {
        p.target = goToolchain{}.pkgpath(dir, p)
    }
}

主要な変更点は以下の通りです。

  1. p.targetの初期設定の削除: elseブロック内のp.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")という行が削除されました。これにより、コマンドの場合に誤ってパッケージディレクトリを指す初期値が設定されることがなくなりました。
  2. dirの初期化とp.targetの計算ロジックの変更:
    • dir := t.PkgDir()が、if !p.Standardブロックの外に移動しました。これにより、すべてのパッケージ(標準ライブラリと外部パッケージの両方)に対して、t.PkgDir()をベースとしたdirが初期化されます。
    • その後のgccgo固有のdirの書き換えロジックはそのまま残っています。
    • そして、p.target = buildToolchain.pkgpath(dir, p)が、if !p.Standardの条件なしに実行されるようになりました。これは、buildToolchain.pkgpathが、現在のツールチェイン(gcまたはgccgo)に応じて適切なパスを生成することを期待しているためです。gcツールチェインの場合、pkgpathはコマンドに対してはバイナリディレクトリを、パッケージに対してはパッケージディレクトリを返すように実装されているはずです。
  3. 標準ライブラリに対するgccgoの特別な扱い: 新たにif p.Standardブロックが追加されました。
    • gccgoを使用している場合でも、標準ライブラリはGo標準ツールチェイン(gc)が配置する「通常の場所」にインストールされるように、p.targetgoToolchain{}.pkgpath(dir, p)で上書きしています。これは、gccgoが標準ライブラリを独自のgccgo固有のパスではなく、gcと同じパスに配置することで、互換性を保つための措置と考えられます。

この修正により、p.targetの計算が、パッケージの種類(コマンドかライブラリか)と使用しているツールチェイン(gcgccgoか)に応じて、より正確に行われるようになりました。特に、外部コマンドの場合、buildToolchain.pkgpathが最終的に正しいバイナリディレクトリを指すように動作することで、回帰バグが解消されます。

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

変更はsrc/cmd/go/pkg.goファイルに集中しています。

--- a/src/cmd/go/pkg.go
+++ b/src/cmd/go/pkg.go
@@ -279,17 +279,20 @@ func scanPackage(ctxt *build.Context, t *build.Tree, arg, importPath, dir string
 		p.target += ".exe"
 	} else {
-		p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")
-	}
-
-	// For gccgo, rewrite p.target with the expected library name. We won't do
-	// that for the standard library for the moment.
-	if !p.Standard {
 		dir := t.PkgDir()
+		// For gccgo, rewrite p.target with the expected library name.
 		if _, ok := buildToolchain.(gccgoToolchain); ok {
 			dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir))
 		}
 		p.target = buildToolchain.pkgpath(dir, p)
+
+		// NB. Currently we have gccgo install the standard libraries
+		// in the "usual" location, where the Go toolchain puts them.
+		if p.Standard {
+			if _, ok := buildToolchain.(gccgoToolchain); ok {
+				p.target = goToolchain{}.pkgpath(dir, p)
+			}
+		}
 	}
 
 	var built time.Time

コアとなるコードの解説

変更されたsrc/cmd/go/pkg.goscanPackage関数内のコードブロックを詳細に解説します。

元のコード:

} else {
    p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")
}

// For gccgo, rewrite p.target with the expected library name. We won't do
// that for the standard library for the moment.
if !p.Standard {
    dir := t.PkgDir()
    if _, ok := buildToolchain.(gccgoToolchain); ok {
        dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir))
    }
    p.target = buildToolchain.pkgpath(dir, p)
}

修正後のコード:

} else {
    // 変更点1: p.targetの初期設定が削除された
    // p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")

    // 変更点2: dirの初期化とp.targetの計算ロジックがこのブロック内に移動し、
    //         if !p.Standard の条件がなくなった
    dir := t.PkgDir()
    // For gccgo, rewrite p.target with the expected library name.
    if _, ok := buildToolchain.(gccgoToolchain); ok {
        dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir))
    }
    p.target = buildToolchain.pkgpath(dir, p)

    // 変更点3: 標準ライブラリに対するgccgoの特別な処理が追加された
    // NB. Currently we have gccgo install the standard libraries
    // in the "usual" location, where the Go toolchain puts them.
    if p.Standard {
        if _, ok := buildToolchain.(gccgoToolchain); ok {
            p.target = goToolchain{}.pkgpath(dir, p)
        }
    }
}

変更点1: p.targetの初期設定の削除

  • 削除された行: - p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")
  • この行は、p.target(ビルド出力ファイルのパス)を、デフォルトでパッケージディレクトリ(t.PkgDir())とインポートパスを結合した.aファイル(アーカイブファイル)として設定していました。
  • 問題は、これがコマンド(実行可能ファイル)に対しても適用されてしまう点でした。コマンドは.aファイルではなく、実行可能ファイルとしてバイナリディレクトリにインストールされるべきです。
  • この行を削除することで、p.targetの初期値が誤ってパッケージディレクトリを指すことを防ぎ、後続のロジックで適切なパスが設定されるようにします。

変更点2: dirの初期化とp.targetの計算ロジックの移動と変更

  • 元のコードでは、if !p.Standard(標準ライブラリではない場合)という条件ブロックの中にdirの初期化とp.targetの計算ロジックがありました。
  • 修正後、このロジックはelseブロック(p.target.exeで終わらない場合、つまりコマンドではないパッケージの場合)の直下に移動し、if !p.Standardの条件がなくなりました。
  • dir := t.PkgDir(): まず、現在のビルドツリーのパッケージディレクトリを取得します。これは、Goのパッケージが通常配置される場所のベースとなります。
  • if _, ok := buildToolchain.(gccgoToolchain); ok { ... }: ここで、現在使用されているビルドツールチェインがgccgoToolchainであるかどうかをチェックします。
    • もしgccgoであれば、dirのパスをgccgo固有の構造に調整します。具体的には、filepath.Dir(dir)で親ディレクトリを取得し、その中に"gccgo"というサブディレクトリを追加し、元のdirのベース名(例: linux_amd64)を結合します。これにより、$GOPATH/pkg/gccgo/linux_amd64のようなパスが生成され、gccgoがパッケージを配置する場所を正確に反映します。
  • p.target = buildToolchain.pkgpath(dir, p): 最後に、現在のbuildToolchaingcまたはgccgo)のpkgpathメソッドを呼び出して、最終的なp.targetを計算します。
    • このpkgpathメソッドは、ツールチェインの種類、計算されたdir、およびパッケージ情報pに基づいて、適切な出力パス(パッケージの場合は.aファイル、コマンドの場合は実行可能ファイル)を返します。
    • この変更により、標準ライブラリではないパッケージ(外部コマンドを含む)に対しても、ツールチェインに応じた正しいp.targetが設定されるようになります。

変更点3: 標準ライブラリに対するgccgoの特別な処理の追加

  • 追加されたブロック:
    // NB. Currently we have gccgo install the standard libraries
    // in the "usual" location, where the Go toolchain puts them.
    if p.Standard {
        if _, ok := buildToolchain.(gccgoToolchain); ok {
            p.target = goToolchain{}.pkgpath(dir, p)
        }
    }
    
  • このブロックは、現在処理しているパッケージがGoの標準ライブラリ(p.Standardtrue)であり、かつgccgoツールチェインを使用している場合にのみ実行されます。
  • p.target = goToolchain{}.pkgpath(dir, p): ここで重要なのは、goToolchain{}というGo標準ツールチェインのインスタンスのpkgpathメソッドを呼び出している点です。
  • これは、gccgoを使用している場合でも、Goの標準ライブラリは、Go標準ツールチェイン(gc)が通常配置する場所(例: $GOROOT/pkg/linux_amd64)にインストールされるように、p.targetを上書きすることを意味します。
  • この措置は、gccgogcの間で標準ライブラリのパスに互換性を持たせるためのもので、ユーザーがどちらのツールチェインを使っても標準ライブラリが同じ場所から参照できるようにするためのものです。

これらの変更により、scanPackage関数は、Goのパッケージとコマンドのビルドターゲットパスを、使用されているツールチェイン(gcまたはgccgo)とパッケージの種類(標準ライブラリ、外部パッケージ、コマンド)に応じて、より正確かつ一貫性のある方法で決定できるようになりました。これにより、外部コマンドが誤ったディレクトリにインストールされるという回帰バグが修正されました。

関連リンク

参考にした情報源リンク

  • コミットのChange-Id: https://golang.org/cl/5564072 (GoのGerritコードレビューシステムへのリンク)
  • Go言語のソースコードリポジトリ: https://github.com/golang/go
  • Go言語のビルドシステムに関する一般的な情報 (Goの公式ドキュメントやブログ記事など)
  • d6a14e6fac0c コミットの検索結果 (このコミットが引き起こした回帰の元となったコミット):
    • https://github.com/golang/go/commit/d6a14e6fac0c
    • このコミットは "go: add gccgo support" というメッセージで、gccgoのサポートを追加したことが確認できます。# [インデックス 11476] ファイルの概要

このコミットは、Go言語のビルドシステムにおける回帰バグを修正するものです。具体的には、gccgoツールチェインのサポートが追加された際に発生した、外部コマンド(Goツリーに含まれないコマンド)のインストールパスが誤って上書き(clobber)される問題に対処しています。これにより、外部コマンドが本来インストールされるべきバイナリディレクトリではなく、パッケージディレクトリにインストールされてしまうという不具合が解消されます。

コミット

commit d2599b431e80a3824cc587f8b23e3783fd241f3a
Author: Anthony Martin <ality@pbrane.org>
Date:   Mon Jan 30 13:54:22 2012 -0500

    go: don't clobber command install paths
    
    This fixes a regression that was made when adding
    support for building with gccgo (in d6a14e6fac0c).
    
    External commands (those not from the Go tree) were
    being installed to the package directory instead of
    the binary directory.
    
    R=golang-dev, rsc, adg, remyoudompheng, rsc
    CC=golang-dev
    https://golang.org/cl/5564072

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

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

元コミット内容

このコミットは、Go言語のビルドシステムにおいて、gccgoツールチェインのサポート導入によって引き起こされた回帰バグを修正するものです。具体的には、Goのソースツリー外のコマンド(外部コマンド)が、本来インストールされるべきバイナリディレクトリではなく、誤ってパッケージディレクトリにインストールされてしまう問題に対処しています。この問題は、コミットd6a14e6fac0cgccgoサポートが追加された際に発生しました。

変更の背景

この変更の背景には、Go言語のビルドシステムが複数のコンパイラツールチェイン(標準のgcgccgo)をサポートするようになった経緯があります。

Go言語の初期のビルドシステムは、主にGoチームが開発した公式コンパイラであるgc(Go Compiler)を前提としていました。しかし、Go言語の普及に伴い、GCC(GNU Compiler Collection)をバックエンドとして利用するgccgoという代替コンパイラが開発されました。gccgoは、既存のGCCインフラストラクチャを活用することで、Goプログラムをより多くのプラットフォームでコンパイルできるようにすることを目指していました。

コミットメッセージに記載されているd6a14e6fac0cは、Goのビルドツール(goコマンド)にgccgoを統合するための変更でした。この統合自体は、Go言語の柔軟性と移植性を高める上で重要なステップでしたが、その過程で予期せぬ副作用が発生しました。

具体的には、gccgoのビルドパスの処理方法が、Goツリー外のコマンド(例えば、go getでインストールされるサードパーティ製のツールなど)のインストールパスの決定ロジックに影響を与えてしまいました。結果として、これらの外部コマンドが、ユーザーの$GOBIN(バイナリ実行ファイルが置かれるべき場所)ではなく、$GOPATH/pkg(コンパイル済みパッケージが置かれる場所)のようなパッケージディレクトリに誤って配置されるという回帰バグが発生しました。

この問題は、ユーザーがgo installgo getを使って外部ツールをインストールした際に、それらのツールが期待されるパスに存在せず、実行できないという形で現れました。このコミットは、この回帰バグを修正し、gccgoを使用している場合でも、外部コマンドが正しいバイナリディレクトリにインストールされるようにすることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語のビルドシステムと関連する概念についての知識が必要です。

  1. Go言語のワークスペースと環境変数:

    • GOPATH: Go 1.11以前のGoプロジェクトのワークスペースのルートディレクトリを指定する環境変数です。Goのソースコード、パッケージ、バイナリがこのディレクトリ構造内に配置されます。src(ソースコード)、pkg(コンパイル済みパッケージ)、bin(コンパイル済みバイナリ)の3つのサブディレクトリが慣例的に存在します。
    • GOBIN: コンパイルされたGoプログラムの実行可能ファイルがインストールされるディレクトリを指定する環境変数です。GOBINが設定されていない場合、実行可能ファイルはGOPATH/binにインストールされます。
    • go install: ソースコードをコンパイルし、その結果生成されたパッケージ(.aファイルなど)をGOPATH/pkgに、実行可能ファイル(コマンド)をGOPATH/binまたはGOBINにインストールするGoコマンドです。
    • go get: リモートリポジトリからGoパッケージをダウンロードし、ビルドしてインストールするGoコマンドです。
  2. Goのビルドツール (goコマンド):

    • Go言語のビルド、テスト、パッケージ管理などを行うための主要なコマンドラインツールです。src/cmd/goディレクトリにそのソースコードがあります。
    • このツールは、Goのソースコードをコンパイルし、実行可能ファイルを生成する際に、どのディレクトリに何を配置するかを決定するロジックを含んでいます。
  3. Goのパッケージとコマンド:

    • パッケージ: Goのコードの基本的な構成単位です。他のパッケージからインポートして利用されます。コンパイルされると、通常は.a(アーカイブ)ファイルとしてGOPATH/pkgに保存されます。
    • コマンド: mainパッケージを持ち、main関数を含むGoプログラムは、実行可能なコマンドとしてビルドされます。これらは通常、GOPATH/binまたはGOBINにインストールされます。
  4. Goのツールチェイン:

    • Goのソースコードをコンパイルし、実行可能ファイルを生成するための一連のツール(コンパイラ、アセンブラ、リンカなど)の集合です。
    • gc (Go Compiler): Goチームが開発した標準のGoコンパイラです。Goの公式リリースに同梱されています。
    • gccgo: GCCのフロントエンドとして実装されたGoコンパイラです。GCCの最適化やバックエンドのサポートを利用できます。gccgoは、gcとは異なる内部的なパス構造や命名規則を持つことがあります。
  5. build.Contextbuild.Tree:

    • Goのビルドツール内部で使用される構造体で、ビルド環境のコンテキスト(OS、アーキテクチャ、GOPATHなど)や、ビルドツリー(ソースディレクトリ、パッケージディレクトリ、バイナリディレクトリなど)の情報を保持します。
    • t.PkgDir(): build.Treeオブジェクトのメソッドで、コンパイル済みパッケージが置かれるべきディレクトリのパスを返します。
  6. p.target:

    • Goのビルドツール内部で、ビルド対象の最終的な出力ファイル(パッケージのアーカイブファイルや実行可能ファイル)のパスを保持するフィールドです。このフィールドの値が、最終的なファイルの配置場所を決定します。
  7. filepath.Joinfilepath.FromSlash:

    • Goの標準ライブラリpath/filepathパッケージの関数です。
    • filepath.Join: 複数のパス要素を結合して、OS固有のパス区切り文字(Windowsでは\、Unix系では/)を使用して正しいパスを生成します。
    • filepath.FromSlash: スラッシュ区切りのパスを、現在のOSのパス区切り文字に変換します。

これらの概念を理解することで、コミットがなぜ必要とされ、どのように問題を解決しているのかが明確になります。特に、gccgoが導入されたことで、従来のgcとは異なるパスの扱いが必要になった点が重要です。

技術的詳細

このコミットが修正している問題は、gccgoツールチェインが導入された際に、Goのビルドツール(goコマンド)が外部コマンドのインストールパスを誤って計算してしまうという回帰バグです。

問題の核心は、src/cmd/go/pkg.go内のscanPackage関数にあります。この関数は、Goパッケージのビルドターゲットパス(p.target)を決定する役割を担っています。

元のコードでは、p.targetの初期設定は以下のようになっていました。

// (省略)
} else {
    p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")
}

// For gccgo, rewrite p.target with the expected library name. We won't do
// that for the standard library for the moment.
if !p.Standard {
    dir := t.PkgDir()
    if _, ok := buildToolchain.(gccgoToolchain); ok {
        dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir))
    }
    p.target = buildToolchain.pkgpath(dir, p)
}

このロジックの問題点は以下の通りです。

  1. 初期のp.target設定: p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a") の行は、すべてのパッケージ(コマンドを含む)に対して、デフォルトでパッケージディレクトリ(t.PkgDir()、通常は$GOPATH/pkg)をベースとしたパスを設定していました。これは、コマンド(実行可能ファイル)が最終的に$GOBINまたは$GOPATH/binにインストールされるべきであるというGoの慣習と矛盾します。本来、コマンドのp.targetはバイナリディレクトリを指すべきです。

  2. gccgo特有のパス書き換えロジック: その後のif !p.Standardブロック内で、gccgoツールチェインが使用されている場合にp.targetを書き換えるロジックがありました。

    • dir := t.PkgDir(): ここで、パッケージディレクトリをベースとしています。
    • dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir)): gccgoは、標準のGoツールチェインとは異なるディレクトリ構造を使用することがあるため、gccgo固有のパッケージディレクトリパスを構築していました。例えば、$GOPATH/pkg/linux_amd64$GOPATH/pkg/gccgo/linux_amd64のようになることを意図していたと考えられます。
    • p.target = buildToolchain.pkgpath(dir, p): 最終的に、gccgoツールチェインのpkgpathメソッドを使って、gccgoの命名規則に沿ったパッケージパスを生成し、p.targetに設定していました。

この組み合わせにより、特にGoツリー外のコマンド(p.Standardfalseのケース)の場合に問題が発生しました。コマンドであっても、初期段階でp.targetがパッケージディレクトリを指すように設定され、その後のgccgo固有のパス書き換えもパッケージディレクトリをベースに行われるため、最終的にp.targetがバイナリディレクトリではなく、gccgoのパッケージディレクトリ(例: $GOPATH/pkg/gccgo/...)を指してしまい、実行可能ファイルが誤った場所にインストールされる結果となりました。

修正内容:

このコミットは、p.targetの初期設定ロジックを修正し、gccgoのパス処理をより適切に統合することで、この問題を解決しています。

修正後のコードは以下のようになります。

// (省略)
} else {
    // 変更点1: p.targetの初期設定が削除された
    // p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")
}

// 変更点2: dirの初期化がif/elseブロックの外に移動し、常にt.PkgDir()から始まる
dir := t.PkgDir()
// For gccgo, rewrite p.target with the expected library name.
if _, ok := buildToolchain.(gccgoToolchain); ok {
    dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir))
}
p.target = buildToolchain.pkgpath(dir, p)

// 変更点3: 標準ライブラリに対するgccgoの特別な処理が追加された
// NB. Currently we have gccgo install the standard libraries
// in the "usual" location, where the Go toolchain puts them.
if p.Standard {
    if _, ok := buildToolchain.(gccgoToolchain); ok {
        p.target = goToolchain{}.pkgpath(dir, p)
    }
}

主要な変更点は以下の通りです。

  1. p.targetの初期設定の削除: elseブロック内のp.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")という行が削除されました。これにより、コマンドの場合に誤ってパッケージディレクトリを指す初期値が設定されることがなくなりました。
  2. dirの初期化とp.targetの計算ロジックの変更:
    • dir := t.PkgDir()が、if !p.Standardブロックの外に移動しました。これにより、すべてのパッケージ(標準ライブラリと外部パッケージの両方)に対して、t.PkgDir()をベースとしたdirが初期化されます。
    • その後のgccgo固有のdirの書き換えロジックはそのまま残っています。
    • そして、p.target = buildToolchain.pkgpath(dir, p)が、if !p.Standardの条件なしに実行されるようになりました。これは、buildToolchain.pkgpathが、現在のツールチェイン(gcまたはgccgo)に応じて適切なパスを生成することを期待しているためです。gcツールチェインの場合、pkgpathはコマンドに対してはバイナリディレクトリを、パッケージに対してはパッケージディレクトリを返すように実装されているはずです。
  3. 標準ライブラリに対するgccgoの特別な扱い: 新たにif p.Standardブロックが追加されました。
    • gccgoを使用している場合でも、標準ライブラリはGo標準ツールチェイン(gc)が配置する「通常の場所」にインストールされるように、p.targetgoToolchain{}.pkgpath(dir, p)で上書きしています。これは、gccgoが標準ライブラリを独自のgccgo固有のパスではなく、gcと同じパスに配置することで、互換性を保つための措置と考えられます。

この修正により、p.targetの計算が、パッケージの種類(コマンドかライブラリか)と使用しているツールチェイン(gcgccgoか)に応じて、より正確に行われるようになりました。特に、外部コマンドの場合、buildToolchain.pkgpathが最終的に正しいバイナリディレクトリを指すように動作することで、回帰バグが解消されます。

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

変更はsrc/cmd/go/pkg.goファイルに集中しています。

--- a/src/cmd/go/pkg.go
+++ b/src/cmd/go/pkg.go
@@ -279,17 +279,20 @@ func scanPackage(ctxt *build.Context, t *build.Tree, arg, importPath, dir string
 		p.target += ".exe"
 	} else {
-		p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")
-	}
-
-	// For gccgo, rewrite p.target with the expected library name. We won't do
-	// that for the standard library for the moment.
-	if !p.Standard {
 		dir := t.PkgDir()
+		// For gccgo, rewrite p.target with the expected library name.
 		if _, ok := buildToolchain.(gccgoToolchain); ok {
 			dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir))
 		}
 		p.target = buildToolchain.pkgpath(dir, p)
+
+		// NB. Currently we have gccgo install the standard libraries
+		// in the "usual" location, where the Go toolchain puts them.
+		if p.Standard {
+			if _, ok := buildToolchain.(gccgoToolchain); ok {
+				p.target = goToolchain{}.pkgpath(dir, p)
+			}
+		}
 	}
 
 	var built time.Time

コアとなるコードの解説

変更されたsrc/cmd/go/pkg.goscanPackage関数内のコードブロックを詳細に解説します。

元のコード:

} else {
    p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")
}

// For gccgo, rewrite p.target with the expected library name. We won't do
// that for the standard library for the moment.
if !p.Standard {
    dir := t.PkgDir()
    if _, ok := buildToolchain.(gccgoToolchain); ok {
        dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir))
    }
    p.target = buildToolchain.pkgpath(dir, p)
}

修正後のコード:

} else {
    // 変更点1: p.targetの初期設定が削除された
    // p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")

    // 変更点2: dirの初期化とp.targetの計算ロジックがこのブロック内に移動し、
    //         if !p.Standard の条件がなくなった
    dir := t.PkgDir()
    // For gccgo, rewrite p.target with the expected library name.
    if _, ok := buildToolchain.(gccgoToolchain); ok {
        dir = filepath.Join(filepath.Dir(dir), "gccgo", filepath.Base(dir))
    }
    p.target = buildToolchain.pkgpath(dir, p)

    // 変更点3: 標準ライブラリに対するgccgoの特別な処理が追加された
    // NB. Currently we have gccgo install the standard libraries
    // in the "usual" location, where the Go toolchain puts them.
    if p.Standard {
        if _, ok := buildToolchain.(gccgoToolchain); ok {
            p.target = goToolchain{}.pkgpath(dir, p)
        }
    }
}

変更点1: p.targetの初期設定の削除

  • 削除された行: - p.target = filepath.Join(t.PkgDir(), filepath.FromSlash(importPath)+".a")
  • この行は、p.target(ビルド出力ファイルのパス)を、デフォルトでパッケージディレクトリ(t.PkgDir())とインポートパスを結合した.aファイル(アーカイブファイル)として設定していました。
  • 問題は、これがコマンド(実行可能ファイル)に対しても適用されてしまう点でした。コマンドは.aファイルではなく、実行可能ファイルとしてバイナリディレクトリにインストールされるべきです。
  • この行を削除することで、p.targetの初期値が誤ってパッケージディレクトリを指すことを防ぎ、後続のロジックで適切なパスが設定されるようにします。

変更点2: dirの初期化とp.targetの計算ロジックの移動と変更

  • 元のコードでは、if !p.Standard(標準ライブラリではない場合)という条件ブロックの中にdirの初期化とp.targetの計算ロジックがありました。
  • 修正後、このロジックはelseブロック(p.target.exeで終わらない場合、つまりコマンドではないパッケージの場合)の直下に移動し、if !p.Standardの条件がなくなりました。
  • dir := t.PkgDir(): まず、現在のビルドツリーのパッケージディレクトリを取得します。これは、Goのパッケージが通常配置される場所のベースとなります。
  • if _, ok := buildToolchain.(gccgoToolchain); ok { ... }: ここで、現在使用されているビルドツールチェインがgccgoToolchainであるかどうかをチェックします。
    • もしgccgoであれば、dirのパスをgccgo固有の構造に調整します。具体的には、filepath.Dir(dir)で親ディレクトリを取得し、その中に"gccgo"というサブディレクトリを追加し、元のdirのベース名(例: linux_amd64)を結合します。これにより、$GOPATH/pkg/gccgo/linux_amd64のようなパスが生成され、gccgoがパッケージを配置する場所を正確に反映します。
  • p.target = buildToolchain.pkgpath(dir, p): 最後に、現在のbuildToolchaingcまたはgccgo)のpkgpathメソッドを呼び出して、最終的なp.targetを計算します。
    • このpkgpathメソッドは、ツールチェインの種類、計算されたdir、およびパッケージ情報pに基づいて、適切な出力パス(パッケージの場合は.aファイル、コマンドの場合は実行可能ファイル)を返します。
    • この変更により、標準ライブラリではないパッケージ(外部コマンドを含む)に対しても、ツールチェインに応じた正しいp.targetが設定されるようになります。

変更点3: 標準ライブラリに対するgccgoの特別な処理の追加

  • 追加されたブロック:
    // NB. Currently we have gccgo install the standard libraries
    // in the "usual" location, where the Go toolchain puts them.
    if p.Standard {
        if _, ok := buildToolchain.(gccgoToolchain); ok {
            p.target = goToolchain{}.pkgpath(dir, p)
        }
    }
    
  • このブロックは、現在処理しているパッケージがGoの標準ライブラリ(p.Standardtrue)であり、かつgccgoツールチェインを使用している場合にのみ実行されます。
  • p.target = goToolchain{}.pkgpath(dir, p): ここで重要なのは、goToolchain{}というGo標準ツールチェインのインスタンスのpkgpathメソッドを呼び出している点です。
  • これは、gccgoを使用している場合でも、Goの標準ライブラリは、Go標準ツールチェイン(gc)が通常配置する場所(例: $GOROOT/pkg/linux_amd64)にインストールされるように、p.targetを上書きすることを意味します。
  • この措置は、gccgogcの間で標準ライブラリのパスに互換性を持たせるためのもので、ユーザーがどちらのツールチェインを使っても標準ライブラリが同じ場所から参照できるようにするためのものです。

これらの変更により、scanPackage関数は、Goのパッケージとコマンドのビルドターゲットパスを、使用されているツールチェイン(gcまたはgccgo)とパッケージの種類(標準ライブラリ、外部パッケージ、コマンド)に応じて、より正確かつ一貫性のある方法で決定できるようになりました。これにより、外部コマンドが誤ったディレクトリにインストールされるという回帰バグが修正されました。

関連リンク

参考にした情報源リンク

  • コミットのChange-Id: https://golang.org/cl/5564072 (GoのGerritコードレビューシステムへのリンク)
  • Go言語のソースコードリポジトリ: https://github.com/golang/go
  • Go言語のビルドシステムに関する一般的な情報 (Goの公式ドキュメントやブログ記事など)
  • d6a14e6fac0c コミットの検索結果 (このコミットが引き起こした回帰の元となったコミット):