[インデックス 11905] ファイルの概要
このコミットは、Go言語のコマンドラインツール cmd/go
における複数の重要な修正と改善をまとめています。主にビルド、テスト、パッケージ管理の挙動に関する問題に対処し、開発者の利便性とツールの堅牢性を向上させています。
コミット
commit 9f333170bf4d8d15b6f9c53caf9a44ef00758ea6
Author: Russ Cox <rsc@golang.org>
Date: Tue Feb 14 16:39:20 2012 -0500
cmd/go: a raft of fixes
* add -work option to save temporary files (Fixes issue 2980)
* fix go test -i to work with cgo packages (Fixes issue 2936)
* do not overwrite/remove empty directories or non-object
files during build (Fixes issue 2829)
* remove package main vs package non-main heuristic:
a directory must contain only one package (Fixes issue 2864)
* to make last item workable, ignore +build tags for files
named on command line: go build x.go builds x.go even
if it says // +build ignore.
* add // +build ignore tags to helper programs
R=golang-dev, r, r
CC=golang-dev
https://golang.org/cl/5674043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9f333170bf4d8d15b6f9c53caf9a44ef00758ea6
元コミット内容
このコミットは、cmd/go
ツールに対する一連の修正を導入しています。具体的には以下の点が変更されました。
-work
オプションの追加: 一時ファイルを保存するための-work
オプションがgo build
およびgo install
コマンドに追加されました。これにより、ビルドプロセス中に生成される一時ディレクトリが終了時に削除されなくなり、デバッグや調査が容易になります。(Issue 2980の修正)go test -i
と cgo パッケージの連携修正:go test -i
コマンドが cgo パッケージで正しく動作しない問題を修正しました。これにより、cgo を使用するパッケージのテストがよりスムーズに行えるようになります。(Issue 2936の修正)- ビルド時のファイル上書き/削除の挙動改善: ビルドプロセス中に、既存の空のディレクトリやオブジェクトファイルではないファイルを誤って上書きしたり削除したりしないように修正されました。これにより、ビルドの堅牢性が向上し、意図しないファイルシステムの変更を防ぎます。(Issue 2829の修正)
- ディレクトリごとの単一パッケージ制約の導入:
package main
とpackage non-main
のヒューリスティックが削除され、1つのディレクトリには1つのパッケージのみが含まれるという厳格なルールが導入されました。これにより、パッケージ構造の明確化とビルドの予測可能性が向上します。(Issue 2864の修正) - コマンドラインで指定されたファイルに対する
+build
タグの無視: 上記の単一パッケージ制約を機能させるため、go build x.go
のようにコマンドラインで直接指定されたファイルに対しては、// +build
タグ(特に// +build ignore
)が無視されるようになりました。これにより、ヘルパープログラムなど、通常はビルド対象外とされるファイルも個別にビルドできるようになります。 - ヘルパープログラムへの
// +build ignore
タグの追加: ビルドシステムがこれらのファイルをデフォルトで無視するように、いくつかのヘルパープログラムに// +build ignore
タグが追加されました。
変更の背景
このコミットは、Go言語の開発初期段階における go
コマンドの成熟度を高めるために行われました。当時の go
コマンドはまだ開発途上にあり、ユーザーからのフィードバックやバグレポートに基づいて、多くの改善が必要とされていました。
具体的には、以下のような問題が背景にありました。
- デバッグの困難さ: ビルド中に生成される一時ファイルが自動的に削除されるため、ビルドエラーの原因究明や、ビルドプロセスの詳細な理解が困難でした。開発者は、ビルドがどのように行われているかを詳細に確認したい場合がありました。
- cgo パッケージのテストの不便さ: C言語との連携を可能にする cgo パッケージは、Go言語の重要な機能の一つですが、
go test -i
コマンドが cgo パッケージの依存関係を正しく処理できず、テストの実行が妨げられることがありました。 - ビルドの堅牢性の不足: ビルドプロセスが既存のファイルやディレクトリを意図せず変更・削除してしまう可能性があり、これは開発者の作業環境に予期せぬ影響を与えるリスクがありました。特に、ビルド出力先が既存の重要なディレクトリであった場合、データ損失につながる可能性も考えられました。
- パッケージ管理の曖昧さ: 1つのディレクトリ内に複数のパッケージが存在しうるという曖昧なルールは、Goのパッケージシステムを理解しにくくし、予期せぬビルドエラーや依存関係の問題を引き起こす原因となっていました。Goの設計思想として、シンプルで明確なパッケージ構造が求められていました。
+build
タグの柔軟性の欠如:// +build ignore
のようなビルドタグは、特定のファイルをビルドから除外するために使用されますが、コマンドラインで明示的にビルドしたい場合でも、これらのタグが適用されてしまうため、ヘルパープログラムなどの個別のビルドが困難でした。
これらの問題に対処することで、go
コマンドの使いやすさ、信頼性、そしてGo言語全体の開発体験を向上させることが、このコミットの主要な目的でした。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびビルドシステムに関する基本的な知識が必要です。
go
コマンド: Go言語の公式ツールチェーンの主要なコマンドで、ソースコードのビルド、テスト、インストール、フォーマットなど、様々な開発タスクを実行します。go build
: Goのソースファイルをコンパイルして実行可能バイナリを生成するコマンドです。go install
:go build
と同様にコンパイルを行いますが、生成されたバイナリやパッケージアーカイブを$GOPATH/bin
や$GOPATH/pkg
などの標準的な場所にインストールします。go test
: Goのテストを実行するコマンドです。-i
オプションは、テストに必要な依存関係をインストール(ビルド)してからテストを実行します。- パッケージ (Package): Go言語におけるコードの組織化の単位です。関連するGoソースファイルは同じパッケージに属し、通常は同じディレクトリに配置されます。パッケージは、他のパッケージからインポートして再利用できます。
package main
: 実行可能プログラムのエントリポイントとなるパッケージです。main
パッケージにはmain
関数が含まれ、これがプログラムの実行開始点となります。cgo
: GoプログラムからC言語のコードを呼び出すためのメカニズムです。cgo
を使用するGoパッケージは、Cコンパイラやリンカのサポートを必要とします。// +build
タグ (Build Tags): Goソースファイルの先頭に記述される特殊なコメントで、ファイルのビルド条件を指定します。例えば、// +build linux,amd64
は、そのファイルがLinuxかつAMD64アーキテクチャの場合にのみビルドされることを意味します。// +build ignore
は、そのファイルをGoツールが通常は無視することを示します。これは、コード生成ツールやヘルパープログラムなど、直接ビルドされることを意図しないファイルによく使われます。- 一時ディレクトリ (Temporary Directory): ビルドプロセス中に中間ファイルや一時的な成果物を保存するために作成されるディレクトリです。通常、ビルドが完了すると自動的に削除されます。
- オブジェクトファイル (Object File): コンパイラによって生成される中間ファイルで、機械語コードが含まれていますが、まだ実行可能形式にはリンクされていません。Goの文脈では、コンパイルされたパッケージアーカイブ(
.a
ファイルなど)や、リンカが処理する前のバイナリ断片を指すことがあります。
技術的詳細
このコミットの技術的詳細は、go
コマンドの内部動作、特にビルドコンテキストの管理、ファイルシステム操作、およびパッケージスキャンロジックに深く関わっています。
-
-work
オプションの実装 (src/cmd/go/build.go
):buildWork
という新しいブール型フラグが導入され、go build
およびgo install
コマンドのフラグセットに追加されました。- ビルドプロセスを管理する
builder
構造体のinit
メソッド内で、一時ディレクトリ (b.work
) の作成と削除ロジックが変更されました。 - 以前は
buildX
(コマンド実行を表示するフラグ) が設定されている場合にのみ一時ディレクトリのパスが表示され、常にatexit(func() { os.RemoveAll(b.work) })
で終了時に削除されていました。 - 変更後、
buildX
またはbuildWork
が設定されている場合に一時ディレクトリのパスが表示されるようになりました。 - そして、
!buildWork
の場合にのみatexit
を使用して一時ディレクトリが削除されるようになりました。これにより、-work
フラグが指定された場合は一時ディレクトリが保持されます。 - また、
builder
のinstall
メソッド内でも、ビルド成果物(オブジェクトディレクトリa1.objdir
とターゲットファイルa1.target
)の遅延削除が!buildWork
の条件付きになりました。
-
ビルド時のファイル上書き/削除の挙動改善 (
src/cmd/go/build.go
):builder
のcopyFile
メソッド(ビルド成果物をコピーする際に使用される)に、コピー先のファイル (dst
) が既に存在する場合のチェックが追加されました。- 新しいロジックでは、
dst
がディレクトリである場合、またはisObject(dst)
関数がfalse
を返す(つまりオブジェクトファイルではない)場合にエラーを返します。これにより、既存のディレクトリや非オブジェクトファイルを誤って上書き・削除するのを防ぎます。 isObject
関数は新しく追加され、ファイルの先頭数バイトを読み込み、Goが生成するオブジェクトファイル(アーカイブ、ELF、Mach-O、PEなど)の既知のマジックバイトと比較することで、そのファイルがオブジェクトファイルであるかどうかを判定します。
-
ディレクトリごとの単一パッケージ制約と
+build
タグの無視 (src/cmd/go/pkg.go
,src/pkg/go/build/dir.go
):src/cmd/go/pkg.go
のscanPackage
関数にuseAllFiles
という新しいブール型引数が追加されました。この引数は、go build x.go
のようにコマンドラインで特定のファイルが指定された場合にtrue
に設定されます。src/pkg/go/build/dir.go
のbuild.Context
構造体にUseAllFiles
というフィールドが追加されました。これはscanPackage
から渡されるuseAllFiles
の値を受け取ります。Context.ScanDir
メソッド内で、ファイルのフィルタリングロジックが変更されました。- 以前は
!ctxt.goodOSArchFile(name)
でOS/アーキテクチャに合致しないファイルをスキップしていましたが、これが!ctxt.UseAllFiles && !ctxt.goodOSArchFile(name)
に変更されました。つまり、UseAllFiles
がtrue
の場合はOS/アーキテクチャのチェックをスキップします。 - 同様に、
!ctxt.shouldBuild(data)
で+build
タグに基づいてファイルをスキップしていましたが、これも!ctxt.UseAllFiles && !ctxt.shouldBuild(data)
に変更されました。これにより、UseAllFiles
がtrue
の場合は+build
タグのチェックをスキップします。
- 以前は
Context.ScanDir
内のパッケージ名チェックロジックが大幅に変更されました。- 以前は
package main
とそれ以外のパッケージが混在するディレクトリを特別扱いするヒューリスティックがありましたが、これが削除されました。 - 新しいロジックでは、
di.Package
が空の場合に最初に発見されたパッケージ名を記録し、それ以降に異なるパッケージ名を持つファイルが見つかった場合はエラー (found packages %s (%s) and %s (%s)
) を返すようになりました。これにより、1つのディレクトリには1つのパッケージのみというルールが厳格に適用されます。
- 以前は
src/cmd/go/main.go
のallPackages
関数で、build.ScanDir
のエラーハンドリングがstrings.Contains(err.Error(), "no Go source files")
でフィルタリングされるようになりました。これは、ディレクトリにGoソースファイルがない場合のエラーを無視するためです。src/cmd/go/pkg.go
のloadPackage
関数で、isCmd
というフラグが導入され、標準コマンド(cmd/
以下にあるもの)をロードする際にtrue
に設定されます。ロードされたパッケージがコマンドであるにもかかわらずpackage main
でない場合、エラーを生成するようになりました。これは、コマンドは必ずpackage main
であるべきというルールを強制するためです。
-
go test -i
と cgo パッケージの連携修正 (src/cmd/go/test.go
):runTest
関数内で、テストの依存関係を解決する際に、擬似パッケージである"C"
と"unsafe"
が依存関係リストから削除されるようになりました。これらのパッケージは実際のファイルとしては存在せず、cgo や Goの内部機能に関連する特別な識別子であるため、依存関係解決の対象から外すことで、go test -i
が cgo パッケージで正しく動作するようになります。
-
ヘルパープログラムへの
// +build ignore
タグの追加:src/pkg/crypto/tls/generate_cert.go
src/pkg/encoding/gob/dump.go
src/pkg/exp/norm/maketables.go
src/pkg/exp/norm/maketesttables.go
src/pkg/exp/norm/normregtest.go
src/pkg/exp/norm/triegen.go
src/pkg/go/doc/headscan.go
src/pkg/net/http/triv.go
src/pkg/unicode/maketables.go
これらのファイルに// +build ignore
が追加されました。これにより、これらのファイルは通常go build
やgo install
の対象から外れ、特定の目的(例: コード生成)のためにのみ明示的にビルドされるようになります。
これらの変更は、Goのビルドシステムがより予測可能で、堅牢で、デバッグしやすいものになるように設計されています。特に、パッケージの単一ディレクトリ制約は、Goのモジュールシステムとパッケージ解決の基盤を強化する上で重要なステップでした。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
src/cmd/go/build.go
:cmdBuild
およびcmdInstall
コマンドのUsageLine
に-work
オプションが追加。buildWork
フラグの定義とaddBuildFlags
での追加。builder.init()
メソッドで、一時ディレクトリの削除ロジックがbuildWork
フラグに基づいて条件付けされるように変更。builder.install()
メソッドで、ビルド成果物の遅延削除が!buildWork
に基づいて条件付けされるように変更。builder.copyFile()
メソッドに、コピー先のファイルが既存のディレクトリや非オブジェクトファイルである場合にエラーを返すロジックを追加。isObject()
関数(オブジェクトファイルのマジックバイトをチェック)の新規追加。
src/cmd/go/pkg.go
:scanPackage
関数のシグネチャにuseAllFiles
ブール型引数を追加。loadPackage
関数内で、scanPackage
の呼び出しにuseAllFiles
を渡すように変更。isCmd
フラグを導入し、コマンドとしてロードされたパッケージがmain
でない場合にエラーを生成するロジックを追加。
src/pkg/go/build/dir.go
:Context
構造体にUseAllFiles
フィールドを追加。Context.ScanDir
メソッド内で、goodOSArchFile
およびshouldBuild
のチェックが!ctxt.UseAllFiles
に基づいて条件付けされるように変更。Context.ScanDir
メソッド内のパッケージ名チェックロジックを修正し、1つのディレクトリに複数のパッケージが存在する場合にエラーを返すように変更。
src/cmd/go/test.go
:runTest
関数内で、テストの依存関係から"C"
と"unsafe"
パッケージを削除するロジックを追加。
- 複数の
src/pkg/...
ファイル:src/pkg/crypto/tls/generate_cert.go
src/pkg/encoding/gob/dump.go
src/pkg/exp/norm/maketables.go
src/pkg/exp/norm/maketesttables.go
src/pkg/exp/norm/normregtest.go
src/pkg/exp/norm/triegen.go
src/pkg/go/doc/headscan.go
src/pkg/net/http/triv.go
src/pkg/unicode/maketables.go
これらのファイルの先頭に// +build ignore
タグが追加されました。
コアとなるコードの解説
src/cmd/go/build.go
における -work
オプションとファイル保護
// build.go (抜粋)
// ...
var buildWork bool // -work flag
// ...
func (b *builder) init() {
// ...
if err != nil {
fatalf("%s", err)
}
if buildX || buildWork { // -x または -work が指定された場合に WORK ディレクトリを表示
fmt.Printf("WORK=%s\n", b.work)
}
if !buildWork { // -work が指定されていない場合にのみ、終了時に一時ディレクトリを削除
atexit(func() { os.RemoveAll(b.work) })
}
}
// ...
func (b *builder) install(a *action) error {
// ...
if !buildWork { // -work が指定されていない場合にのみ、中間オブジェクトとターゲットを削除
defer os.RemoveAll(a1.objdir)
defer os.Remove(a1.target)
}
// ...
}
// ...
func (b *builder) copyFile(dst, src string, perm os.FileMode) error {
// ...
// Be careful about removing/overwriting dst.
// Do not remove/overwrite if dst exists and is a directory
// or a non-object file.
if fi, err := os.Stat(dst); err == nil {
if fi.IsDir() {
return fmt.Errorf("build output %q already exists and is a directory", dst)
}
if !isObject(dst) { // オブジェクトファイルではない場合
return fmt.Errorf("build output %q already exists and is not an object file", dst)
}
}
// ...
}
// isObject はファイルがGoのオブジェクトファイルであるかどうかをマジックバイトで判定する
var objectMagic = [][]byte{
{'!', '<', 'a', 'r', 'c', 'h', '>', '\n'}, // Package archive
{0x7F, 'E', 'L', 'F'}, // ELF
{0xFE, 0xED, 0xFA, 0xCE}, // Mach-O big-endian 32-bit
{0xFE, 0xED, 0xFA, 0xCF}, // Mach-O big-endian 64-bit
{0xCE, 0xFA, 0xED, 0xFE}, // Mach-O little-endian 32-bit
{0xCF, 0xFA, 0xED, 0xFE}, // Mach-O little-endian 64-bit
{0x4d, 0x5a, 0x90, 0x00, 0x03, 0x00, 0x04, 0x00}, // PE (Windows) as generated by 6l/8l
}
func isObject(s string) bool {
f, err := os.Open(s)
if err != nil {
return false
}
defer f.Close()
buf := make([]byte, 64)
io.ReadFull(f, buf)
for _, magic := range objectMagic {
if bytes.HasPrefix(buf, magic) {
return true
}
}
return false
}
build.go
の変更は、主にビルドプロセスの一時ファイルの管理と、ビルド出力先での既存ファイル保護に関するものです。
-work
フラグが導入され、これが true
の場合、ビルド終了時に一時ディレクトリや中間成果物が削除されなくなります。これにより、デバッグやビルドプロセスの詳細な調査が可能になります。
copyFile
関数では、ビルド出力先が既存のディレクトリであったり、Goのオブジェクトファイルではない場合に、上書きを防ぐためのチェックが追加されました。isObject
関数は、ファイルの内容を読み取り、Goが生成するバイナリ形式のマジックバイトと照合することで、そのファイルがGoのオブジェクトファイルであるかを判定します。これにより、重要なファイルが誤って上書きされるリスクが軽減されます。
src/pkg/go/build/dir.go
におけるパッケージスキャンロジックの変更
// dir.go (抜粋)
type Context struct {
// ...
BuildTags []string // additional tags to recognize in +build lines
UseAllFiles bool // use files regardless of +build lines, file names
}
// ...
func (ctxt *Context) ScanDir(dir string) (info *DirInfo, err error) {
// ...
var firstFile string // 最初に発見されたパッケージのファイル名
// ...
for _, de := range dir {
name := de.Name()
// ...
if !ctxt.UseAllFiles && !ctxt.goodOSArchFile(name) { // UseAllFiles が true の場合は OS/Arch チェックをスキップ
continue
}
// ...
// Look for +build comments to accept or reject the file.
if !ctxt.UseAllFiles && !ctxt.shouldBuild(data) { // UseAllFiles が true の場合は +build タグチェックをスキップ
continue
}
// ...
pkg := string(pf.Name.Name)
// ...
if di.Package == "" {
di.Package = pkg
firstFile = name // 最初のパッケージ名とファイル名を記録
} else if pkg != di.Package {
// 異なるパッケージ名が見つかった場合、エラーを返す
return nil, fmt.Errorf("%s: found packages %s (%s) and %s (%s)", dir, di.Package, firstFile, pkg, name)
}
// ...
}
// ...
}
dir.go
の変更は、Goのパッケージスキャンロジックの核心部分に影響を与えます。
Context
構造体に UseAllFiles
フィールドが追加され、これが true
の場合(主に go build x.go
のように特定のファイルがコマンドラインで指定された場合)、OS/アーキテクチャのフィルタリングや +build
タグによるファイルの除外が行われなくなります。これにより、// +build ignore
が付いているファイルでも、明示的に指定すればビルドできるようになります。
最も重要な変更は、ScanDir
関数内のパッケージ名チェックです。以前は package main
とそれ以外のパッケージが混在するケースを許容するヒューリスティックがありましたが、これが削除されました。新しいロジックでは、ディレクトリ内で最初に発見されたパッケージ名が記録され、その後、異なるパッケージ名を持つファイルが見つかった場合、即座にエラーを返します。これにより、「1つのディレクトリには1つのパッケージのみ」というGoの厳格なルールが強制され、パッケージ構造の明確化とビルドの予測可能性が向上します。
src/cmd/go/test.go
における cgo パッケージのテスト修正
// test.go (抜粋)
func runTest(cmd *Command, args []string) {
// ...
// Ignore pseudo-packages.
delete(deps, "C")
delete(deps, "unsafe")
// ...
}
test.go
の変更はシンプルですが、go test -i
が cgo パッケージで動作しない問題を解決するために重要です。
テストに必要な依存関係を解決する際、擬似パッケージである "C"
と "unsafe"
が依存関係リストから明示的に削除されるようになりました。これらのパッケージは実際のGoソースファイルとしては存在せず、Goのコンパイラやランタイムが特別に扱う識別子です。これらを依存関係解決の対象から外すことで、go test -i
が cgo を使用するパッケージの依存関係を正しく処理し、テストを正常に実行できるようになります。
関連リンク
- Go Issue 2980:
cmd/go: add -work option to save temporary files
- https://github.com/golang/go/issues/2980 - Go Issue 2936:
cmd/go: go test -i fails for cgo packages
- https://github.com/golang/go/issues/2936 - Go Issue 2829:
cmd/go: build should not overwrite/remove empty directories or non-object files
- https://github.com/golang/go/issues/2829 - Go Issue 2864:
cmd/go: a directory must contain only one package
- https://github.com/golang/go/issues/2864 - Go Code Review 5674043:
cmd/go: a raft of fixes
- https://golang.org/cl/5674043 (これはコミットメッセージに記載されている Gerrit の変更リストへのリンクです)
参考にした情報源リンク
- Go言語公式ドキュメント:
go build
- https://pkg.go.dev/cmd/go#hdr-Build_packages - Go言語公式ドキュメント:
go test
- https://pkg.go.dev/cmd/go#hdr-Test_packages - Go言語公式ドキュメント:
cgo
- https://pkg.go.dev/cmd/cgo - Go言語公式ドキュメント: Build Constraints (
+build
tags) - https://pkg.go.dev/cmd/go#hdr-Build_constraints - Go言語公式ドキュメント: Packages - https://go.dev/doc/effective_go#packages
- ELF (Executable and Linkable Format) - https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
- Mach-O - https://en.wikipedia.org/wiki/Mach-O
- Portable Executable (PE) - https://en.wikipedia.org/wiki/Portable_Executable
- Unix ar (archive) format - https://en.wikipedia.org/wiki/Ar_(Unix)
- Goのソースコード (特に
src/cmd/go
とsrc/pkg/go/build
ディレクトリ) - GitHubのGoリポジトリのIssueトラッカー# [インデックス 11905] ファイルの概要
このコミットは、Go言語のコマンドラインツール cmd/go
における複数の重要な修正と改善をまとめています。主にビルド、テスト、パッケージ管理の挙動に関する問題に対処し、開発者の利便性とツールの堅牢性を向上させています。
コミット
commit 9f333170bf4d8d15b6f9c53caf9a44ef00758ea6
Author: Russ Cox <rsc@golang.org>
Date: Tue Feb 14 16:39:20 2012 -0500
cmd/go: a raft of fixes
* add -work option to save temporary files (Fixes issue 2980)
* fix go test -i to work with cgo packages (Fixes issue 2936)
* do not overwrite/remove empty directories or non-object
files during build (Fixes issue 2829)
* remove package main vs package non-main heuristic:
a directory must contain only one package (Fixes issue 2864)
* to make last item workable, ignore +build tags for files
named on command line: go build x.go builds x.go even
if it says // +build ignore.
* add // +build ignore tags to helper programs
R=golang-dev, r, r
CC=golang-dev
https://golang.org/cl/5674043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9f333170bf4d8d15b6f9c53caf9a44ef00758ea6
元コミット内容
このコミットは、cmd/go
ツールに対する一連の修正を導入しています。具体的には以下の点が変更されました。
-work
オプションの追加: 一時ファイルを保存するための-work
オプションがgo build
およびgo install
コマンドに追加されました。これにより、ビルドプロセス中に生成される一時ディレクトリが終了時に削除されなくなり、デバッグや調査が容易になります。(Issue 2980の修正)go test -i
と cgo パッケージの連携修正:go test -i
コマンドが cgo パッケージで正しく動作しない問題を修正しました。これにより、cgo を使用するパッケージのテストがよりスムーズに行えるようになります。(Issue 2936の修正)- ビルド時のファイル上書き/削除の挙動改善: ビルドプロセス中に、既存の空のディレクトリやオブジェクトファイルではないファイルを誤って上書きしたり削除したりしないように修正されました。これにより、ビルドの堅牢性が向上し、意図しないファイルシステムの変更を防ぎます。(Issue 2829の修正)
- ディレクトリごとの単一パッケージ制約の導入:
package main
とpackage non-main
のヒューリスティックが削除され、1つのディレクトリには1つのパッケージのみが含まれるという厳格なルールが導入されました。これにより、パッケージ構造の明確化とビルドの予測可能性が向上します。(Issue 2864の修正) - コマンドラインで指定されたファイルに対する
+build
タグの無視: 上記の単一パッケージ制約を機能させるため、go build x.go
のようにコマンドラインで直接指定されたファイルに対しては、// +build
タグ(特に// +build ignore
)が無視されるようになりました。これにより、ヘルパープログラムなど、通常はビルド対象外とされるファイルも個別にビルドできるようになります。 - ヘルパープログラムへの
// +build ignore
タグの追加: ビルドシステムがこれらのファイルをデフォルトで無視するように、いくつかのヘルパープログラムに// +build ignore
タグが追加されました。
変更の背景
このコミットは、Go言語の開発初期段階における go
コマンドの成熟度を高めるために行われました。当時の go
コマンドはまだ開発途上にあり、ユーザーからのフィードバックやバグレポートに基づいて、多くの改善が必要とされていました。
具体的には、以下のような問題が背景にありました。
- デバッグの困難さ: ビルド中に生成される一時ファイルが自動的に削除されるため、ビルドエラーの原因究明や、ビルドプロセスの詳細な理解が困難でした。開発者は、ビルドがどのように行われているかを詳細に確認したい場合がありました。
- cgo パッケージのテストの不便さ: C言語との連携を可能にする cgo パッケージは、Go言語の重要な機能の一つですが、
go test -i
コマンドが cgo パッケージの依存関係を正しく処理できず、テストの実行が妨げられることがありました。 - ビルドの堅牢性の不足: ビルドプロセスが既存のファイルやディレクトリを意図せず変更・削除してしまう可能性があり、これは開発者の作業環境に予期せぬ影響を与えるリスクがありました。特に、ビルド出力先が既存の重要なディレクトリであった場合、データ損失につながる可能性も考えられました。
- パッケージ管理の曖昧さ: 1つのディレクトリ内に複数のパッケージが存在しうるという曖昧なルールは、Goのパッケージシステムを理解しにくくし、予期せぬビルドエラーや依存関係の問題を引き起こす原因となっていました。Goの設計思想として、シンプルで明確なパッケージ構造が求められていました。
+build
タグの柔軟性の欠如:// +build ignore
のようなビルドタグは、特定のファイルをビルドから除外するために使用されますが、コマンドラインで明示的にビルドしたい場合でも、これらのタグが適用されてしまうため、ヘルパープログラムなどの個別のビルドが困難でした。
これらの問題に対処することで、go
コマンドの使いやすさ、信頼性、そしてGo言語全体の開発体験を向上させることが、このコミットの主要な目的でした。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびビルドシステムに関する基本的な知識が必要です。
go
コマンド: Go言語の公式ツールチェーンの主要なコマンドで、ソースコードのビルド、テスト、インストール、フォーマットなど、様々な開発タスクを実行します。go build
: Goのソースファイルをコンパイルして実行可能バイナリを生成するコマンドです。go install
:go build
と同様にコンパイルを行いますが、生成されたバイナリやパッケージアーカイブを$GOPATH/bin
や$GOPATH/pkg
などの標準的な場所にインストールします。go test
: Goのテストを実行するコマンドです。-i
オプションは、テストに必要な依存関係をインストール(ビルド)してからテストを実行します。- パッケージ (Package): Go言語におけるコードの組織化の単位です。関連するGoソースファイルは同じパッケージに属し、通常は同じディレクトリに配置されます。パッケージは、他のパッケージからインポートして再利用できます。
package main
: 実行可能プログラムのエントリポイントとなるパッケージです。main
パッケージにはmain
関数が含まれ、これがプログラムの実行開始点となります。cgo
: GoプログラムからC言語のコードを呼び出すためのメカニズムです。cgo
を使用するGoパッケージは、Cコンパイラやリンカのサポートを必要とします。// +build
タグ (Build Tags): Goソースファイルの先頭に記述される特殊なコメントで、ファイルのビルド条件を指定します。例えば、// +build linux,amd64
は、そのファイルがLinuxかつAMD64アーキテクチャの場合にのみビルドされることを意味します。// +build ignore
は、そのファイルをGoツールが通常は無視することを示します。これは、コード生成ツールやヘルパープログラムなど、直接ビルドされることを意図しないファイルによく使われます。- 一時ディレクトリ (Temporary Directory): ビルドプロセス中に中間ファイルや一時的な成果物を保存するために作成されるディレクトリです。通常、ビルドが完了すると自動的に削除されます。
- オブジェクトファイル (Object File): コンパイラによって生成される中間ファイルで、機械語コードが含まれていますが、まだ実行可能形式にはリンクされていません。Goの文脈では、コンパイルされたパッケージアーカイブ(
.a
ファイルなど)や、リンカが処理する前のバイナリ断片を指すことがあります。
技術的詳細
このコミットの技術的詳細は、go
コマンドの内部動作、特にビルドコンテキストの管理、ファイルシステム操作、およびパッケージスキャンロジックに深く関わっています。
-
-work
オプションの実装 (src/cmd/go/build.go
):buildWork
という新しいブール型フラグが導入され、go build
およびgo install
コマンドのフラグセットに追加されました。- ビルドプロセスを管理する
builder
構造体のinit
メソッド内で、一時ディレクトリ (b.work
) の作成と削除ロジックが変更されました。 - 以前は
buildX
(コマンド実行を表示するフラグ) が設定されている場合にのみ一時ディレクトリのパスが表示され、常にatexit(func() { os.RemoveAll(b.work) })
で終了時に削除されていました。 - 変更後、
buildX
またはbuildWork
が設定されている場合に一時ディレクトリのパスが表示されるようになりました。 - そして、
!buildWork
の場合にのみatexit
を使用して一時ディレクトリが削除されるようになりました。これにより、-work
フラグが指定された場合は一時ディレクトリが保持されます。 - また、
builder
のinstall
メソッド内でも、ビルド成果物(オブジェクトディレクトリa1.objdir
とターゲットファイルa1.target
)の遅延削除が!buildWork
の条件付きになりました。
-
ビルド時のファイル上書き/削除の挙動改善 (
src/cmd/go/build.go
):builder
のcopyFile
メソッド(ビルド成果物をコピーする際に使用される)に、コピー先のファイル (dst
) が既に存在する場合のチェックが追加されました。- 新しいロジックでは、
dst
がディレクトリである場合、またはisObject(dst)
関数がfalse
を返す(つまりオブジェクトファイルではない)場合にエラーを返します。これにより、既存のディレクトリや非オブジェクトファイルを誤って上書き・削除するのを防ぎます。 isObject
関数は新しく追加され、ファイルの先頭数バイトを読み込み、Goが生成するオブジェクトファイル(アーカイブ、ELF、Mach-O、PEなど)の既知のマジックバイトと比較することで、そのファイルがオブジェクトファイルであるかどうかを判定します。
-
ディレクトリごとの単一パッケージ制約と
+build
タグの無視 (src/cmd/go/pkg.go
,src/pkg/go/build/dir.go
):src/cmd/go/pkg.go
のscanPackage
関数にuseAllFiles
という新しいブール型引数が追加されました。この引数は、go build x.go
のようにコマンドラインで特定のファイルが指定された場合にtrue
に設定されます。src/pkg/go/build/dir.go
のbuild.Context
構造体にUseAllFiles
というフィールドが追加されました。これはscanPackage
から渡されるuseAllFiles
の値を受け取ります。Context.ScanDir
メソッド内で、ファイルのフィルタリングロジックが変更されました。- 以前は
!ctxt.goodOSArchFile(name)
でOS/アーキテクチャに合致しないファイルをスキップしていましたが、これが!ctxt.UseAllFiles && !ctxt.goodOSArchFile(name)
に変更されました。つまり、UseAllFiles
がtrue
の場合はOS/アーキテクチャのチェックをスキップします。 - 同様に、
!ctxt.shouldBuild(data)
で+build
タグに基づいてファイルをスキップしていましたが、これも!ctxt.UseAllFiles && !ctxt.shouldBuild(data)
に変更されました。これにより、UseAllFiles
がtrue
の場合は+build
タグのチェックをスキップします。
- 以前は
Context.ScanDir
内のパッケージ名チェックロジックが大幅に変更されました。- 以前は
package main
とそれ以外のパッケージが混在するディレクトリを特別扱いするヒューリスティックがありましたが、これが削除されました。 - 新しいロジックでは、
di.Package
が空の場合に最初に発見されたパッケージ名を記録し、それ以降に異なるパッケージ名を持つファイルが見つかった場合、即座にエラーを返すようになりました。これにより、1つのディレクトリには1つのパッケージのみというルールが厳格に適用されます。
- 以前は
src/cmd/go/main.go
のallPackages
関数で、build.ScanDir
のエラーハンドリングがstrings.Contains(err.Error(), "no Go source files")
でフィルタリングされるようになりました。これは、ディレクトリにGoソースファイルがない場合のエラーを無視するためです。src/cmd/go/pkg.go
のloadPackage
関数で、isCmd
というフラグが導入され、標準コマンド(cmd/
以下にあるもの)をロードする際にtrue
に設定されます。ロードされたパッケージがコマンドであるにもかかわらずpackage main
でない場合、エラーを生成するようになりました。これは、コマンドは必ずpackage main
であるべきというルールを強制するためです。
-
go test -i
と cgo パッケージの連携修正 (src/cmd/go/test.go
):runTest
関数内で、テストの依存関係を解決する際に、擬似パッケージである"C"
と"unsafe"
が依存関係リストから削除されるようになりました。これらのパッケージは実際のファイルとしては存在せず、cgo や Goの内部機能に関連する特別な識別子であるため、依存関係解決の対象から外すことで、go test -i
が cgo パッケージで正しく動作するようになります。
-
ヘルパープログラムへの
// +build ignore
タグの追加:src/pkg/crypto/tls/generate_cert.go
src/pkg/encoding/gob/dump.go
src/pkg/exp/norm/maketables.go
src/pkg/exp/norm/maketesttables.go
src/pkg/exp/norm/normregtest.go
src/pkg/exp/norm/triegen.go
src/pkg/go/doc/headscan.go
src/pkg/net/http/triv.go
src/pkg/unicode/maketables.go
これらのファイルの先頭に// +build ignore
タグが追加されました。
これらの変更は、Goのビルドシステムがより予測可能で、堅牢で、デバッグしやすいものになるように設計されています。特に、パッケージの単一ディレクトリ制約は、Goのモジュールシステムとパッケージ解決の基盤を強化する上で重要なステップでした。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
src/cmd/go/build.go
:cmdBuild
およびcmdInstall
コマンドのUsageLine
に-work
オプションが追加。buildWork
フラグの定義とaddBuildFlags
での追加。builder.init()
メソッドで、一時ディレクトリの削除ロジックがbuildWork
フラグに基づいて条件付けされるように変更。builder.install()
メソッドで、ビルド成果物の遅延削除が!buildWork
に基づいて条件付けされるように変更。builder.copyFile()
メソッドに、コピー先のファイルが既存のディレクトリや非オブジェクトファイルである場合にエラーを返すロジックを追加。isObject()
関数(オブジェクトファイルのマジックバイトをチェック)の新規追加。
src/cmd/go/pkg.go
:scanPackage
関数のシグネチャにuseAllFiles
ブール型引数を追加。loadPackage
関数内で、scanPackage
の呼び出しにuseAllFiles
を渡すように変更。isCmd
フラグを導入し、コマンドとしてロードされたパッケージがmain
でない場合にエラーを生成するロジックを追加。
src/pkg/go/build/dir.go
:Context
構造体にUseAllFiles
フィールドを追加。Context.ScanDir
メソッド内で、goodOSArchFile
およびshouldBuild
のチェックが!ctxt.UseAllFiles
に基づいて条件付けされるように変更。Context.ScanDir
メソッド内のパッケージ名チェックロジックを修正し、1つのディレクトリに複数のパッケージが存在する場合にエラーを返すように変更。
src/cmd/go/test.go
:runTest
関数内で、テストの依存関係から"C"
と"unsafe"
パッケージを削除するロジックを追加。
- 複数の
src/pkg/...
ファイル:src/pkg/crypto/tls/generate_cert.go
src/pkg/encoding/gob/dump.go
src/pkg/exp/norm/maketables.go
src/pkg/exp/norm/maketesttables.go
src/pkg/exp/norm/normregtest.go
src/pkg/exp/norm/triegen.go
src/pkg/go/doc/headscan.go
src/pkg/net/http/triv.go
src/pkg/unicode/maketables.go
これらのファイルの先頭に// +build ignore
タグが追加されました。
コアとなるコードの解説
src/cmd/go/build.go
における -work
オプションとファイル保護
// build.go (抜粋)
// ...
var buildWork bool // -work flag
// ...
func (b *builder) init() {
// ...
if err != nil {
fatalf("%s", err)
}
if buildX || buildWork { // -x または -work が指定された場合に WORK ディレクトリを表示
fmt.Printf("WORK=%s\n", b.work)
}
if !buildWork { // -work が指定されていない場合にのみ、終了時に一時ディレクトリを削除
atexit(func() { os.RemoveAll(b.work) })
}
}
// ...
func (b *builder) install(a *action) error {
// ...
if !buildWork { // -work が指定されていない場合にのみ、中間オブジェクトとターゲットを削除
defer os.RemoveAll(a1.objdir)
defer os.Remove(a1.target)
}
// ...
}
// ...
func (b *builder) copyFile(dst, src string, perm os.FileMode) error {
// ...
// Be careful about removing/overwriting dst.
// Do not remove/overwrite if dst exists and is a directory
// or a non-object file.
if fi, err := os.Stat(dst); err == nil {
if fi.IsDir() {
return fmt.Errorf("build output %q already exists and is a directory", dst)
}
if !isObject(dst) { // オブジェクトファイルではない場合
return fmt.Errorf("build output %q already exists and is not an object file", dst)
}
}
// ...
}
// isObject はファイルがGoのオブジェクトファイルであるかどうかをマジックバイトで判定する
var objectMagic = [][]byte{
{'!', '<', 'a', 'r', 'c', 'h', '>', '\n'}, // Package archive
{0x7F, 'E', 'L', 'F'}, // ELF
{0xFE, 0xED, 0xFA, 0xCE}, // Mach-O big-endian 32-bit
{0xFE, 0xED, 0xFA, 0xCF}, // Mach-O big-endian 64-bit
{0xCE, 0xFA, 0xED, 0xFE}, // Mach-O little-endian 32-bit
{0xCF, 0xFA, 0xED, 0xFE}, // Mach-O little-endian 64-bit
{0x4d, 0x5a, 0x90, 0x00, 0x03, 0x00, 0x04, 0x00}, // PE (Windows) as generated by 6l/8l
}
func isObject(s string) bool {
f, err := os.Open(s)
if err != nil {
return false
}
defer f.Close()
buf := make([]byte, 64)
io.ReadFull(f, buf)
for _, magic := range objectMagic {
if bytes.HasPrefix(buf, magic) {
return true
}
}
return false
}
build.go
の変更は、主にビルドプロセスの一時ファイルの管理と、ビルド出力先での既存ファイル保護に関するものです。
-work
フラグが導入され、これが true
の場合、ビルド終了時に一時ディレクトリや中間成果物が削除されなくなります。これにより、デバッグやビルドプロセスの詳細な調査が可能になります。
copyFile
関数では、ビルド出力先が既存のディレクトリであったり、Goのオブジェクトファイルではない場合に、上書きを防ぐためのチェックが追加されました。isObject
関数は、ファイルの内容を読み取り、Goが生成するバイナリ形式のマジックバイトと照合することで、そのファイルがGoのオブジェクトファイルであるかを判定します。これにより、重要なファイルが誤って上書きされるリスクが軽減されます。
src/pkg/go/build/dir.go
におけるパッケージスキャンロジックの変更
// dir.go (抜粋)
type Context struct {
// ...
BuildTags []string // additional tags to recognize in +build lines
UseAllFiles bool // use files regardless of +build lines, file names
}
// ...
func (ctxt *Context) ScanDir(dir string) (info *DirInfo, err error) {
// ...
var firstFile string // 最初に発見されたパッケージのファイル名
// ...
for _, de := range dir {
name := de.Name()
// ...
if !ctxt.UseAllFiles && !ctxt.goodOSArchFile(name) { // UseAllFiles が true の場合は OS/Arch チェックをスキップ
continue
}
// ...
// Look for +build comments to accept or reject the file.
if !ctxt.UseAllFiles && !ctxt.shouldBuild(data) { // UseAllFiles が true の場合は +build タグチェックをスキップ
continue
}
// ...
pkg := string(pf.Name.Name)
// ...
if di.Package == "" {
di.Package = pkg
firstFile = name // 最初のパッケージ名とファイル名を記録
} else if pkg != di.Package {
// 異なるパッケージ名が見つかった場合、エラーを返す
return nil, fmt.Errorf("%s: found packages %s (%s) and %s (%s)", dir, di.Package, firstFile, pkg, name)
}
// ...
}
// ...
}
dir.go
の変更は、Goのパッケージスキャンロジックの核心部分に影響を与えます。
Context
構造体に UseAllFiles
フィールドが追加され、これが true
の場合(主に go build x.go
のように特定のファイルがコマンドラインで指定された場合)、OS/アーキテクチャのフィルタリングや +build
タグによるファイルの除外が行われなくなります。これにより、// +build ignore
が付いているファイルでも、明示的に指定すればビルドできるようになります。
最も重要な変更は、ScanDir
関数内のパッケージ名チェックです。以前は package main
とそれ以外のパッケージが混在するケースを許容するヒューリスティックがありましたが、これが削除されました。新しいロジックでは、ディレクトリ内で最初に発見されたパッケージ名が記録され、その後、異なるパッケージ名を持つファイルが見つかった場合、即座にエラーを返します。これにより、「1つのディレクトリには1つのパッケージのみ」というGoの厳格なルールが強制され、パッケージ構造の明確化とビルドの予測可能性が向上します。
src/cmd/go/test.go
における cgo パッケージのテスト修正
// test.go (抜粋)
func runTest(cmd *Command, args []string) {
// ...
// Ignore pseudo-packages.
delete(deps, "C")
delete(deps, "unsafe")
// ...
}
test.go
の変更はシンプルですが、go test -i
が cgo パッケージで動作しない問題を解決するために重要です。
テストに必要な依存関係を解決する際、擬似パッケージである "C"
と "unsafe"
が依存関係リストから明示的に削除されるようになりました。これらのパッケージは実際のGoソースファイルとしては存在せず、Goのコンパイラやランタイムが特別に扱う識別子です。これらを依存関係解決の対象から外すことで、go test -i
が cgo を使用するパッケージの依存関係を正しく処理し、テストを正常に実行できるようになります。
関連リンク
- Go Issue 2980:
cmd/go: add -work option to save temporary files
- https://github.com/golang/go/issues/2980 - Go Issue 2936:
cmd/go: go test -i fails for cgo packages
- https://github.com/golang/go/issues/2936 - Go Issue 2829:
cmd/go: build should not overwrite/remove empty directories or non-object files
- https://github.com/golang/go/issues/2829 - Go Issue 2864:
cmd/go: a directory must contain only one package
- https://github.com/golang/go/issues/2864 - Go Code Review 5674043:
cmd/go: a raft of fixes
- https://golang.org/cl/5674043 (これはコミットメッセージに記載されている Gerrit の変更リストへのリンクです)
参考にした情報源リンク
- Go言語公式ドキュメント:
go build
- https://pkg.go.dev/cmd/go#hdr-Build_packages - Go言語公式ドキュメント:
go test
- https://pkg.go.dev/cmd/go#hdr-Test_packages - Go言語公式ドキュメント:
cgo
- https://pkg.go.dev/cmd/cgo - Go言語公式ドキュメント: Build Constraints (
+build
tags) - https://pkg.go.dev/cmd/go#hdr-Build_constraints - Go言語公式ドキュメント: Packages - https://go.dev/doc/effective_go#packages
- ELF (Executable and Linkable Format) - https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
- Mach-O - https://en.wikipedia.org/wiki/Mach-O
- Portable Executable (PE) - https://en.wikipedia.org/wiki/Portable_Executable
- Unix ar (archive) format - https://en.wikipedia.org/wiki/Ar_(Unix)
- Goのソースコード (特に
src/cmd/go
とsrc/pkg/go/build
ディレクトリ) - GitHubのGoリポジトリのIssueトラッカー