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

[インデックス 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 ツールに対する一連の修正を導入しています。具体的には以下の点が変更されました。

  1. -work オプションの追加: 一時ファイルを保存するための -work オプションが go build および go install コマンドに追加されました。これにより、ビルドプロセス中に生成される一時ディレクトリが終了時に削除されなくなり、デバッグや調査が容易になります。(Issue 2980の修正)
  2. go test -i と cgo パッケージの連携修正: go test -i コマンドが cgo パッケージで正しく動作しない問題を修正しました。これにより、cgo を使用するパッケージのテストがよりスムーズに行えるようになります。(Issue 2936の修正)
  3. ビルド時のファイル上書き/削除の挙動改善: ビルドプロセス中に、既存の空のディレクトリやオブジェクトファイルではないファイルを誤って上書きしたり削除したりしないように修正されました。これにより、ビルドの堅牢性が向上し、意図しないファイルシステムの変更を防ぎます。(Issue 2829の修正)
  4. ディレクトリごとの単一パッケージ制約の導入: package mainpackage non-main のヒューリスティックが削除され、1つのディレクトリには1つのパッケージのみが含まれるという厳格なルールが導入されました。これにより、パッケージ構造の明確化とビルドの予測可能性が向上します。(Issue 2864の修正)
  5. コマンドラインで指定されたファイルに対する +build タグの無視: 上記の単一パッケージ制約を機能させるため、go build x.go のようにコマンドラインで直接指定されたファイルに対しては、// +build タグ(特に // +build ignore)が無視されるようになりました。これにより、ヘルパープログラムなど、通常はビルド対象外とされるファイルも個別にビルドできるようになります。
  6. ヘルパープログラムへの // +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 コマンドの内部動作、特にビルドコンテキストの管理、ファイルシステム操作、およびパッケージスキャンロジックに深く関わっています。

  1. -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 フラグが指定された場合は一時ディレクトリが保持されます。
    • また、builderinstall メソッド内でも、ビルド成果物(オブジェクトディレクトリ a1.objdir とターゲットファイル a1.target)の遅延削除が !buildWork の条件付きになりました。
  2. ビルド時のファイル上書き/削除の挙動改善 (src/cmd/go/build.go):

    • buildercopyFile メソッド(ビルド成果物をコピーする際に使用される)に、コピー先のファイル (dst) が既に存在する場合のチェックが追加されました。
    • 新しいロジックでは、dst がディレクトリである場合、または isObject(dst) 関数が false を返す(つまりオブジェクトファイルではない)場合にエラーを返します。これにより、既存のディレクトリや非オブジェクトファイルを誤って上書き・削除するのを防ぎます。
    • isObject 関数は新しく追加され、ファイルの先頭数バイトを読み込み、Goが生成するオブジェクトファイル(アーカイブ、ELF、Mach-O、PEなど)の既知のマジックバイトと比較することで、そのファイルがオブジェクトファイルであるかどうかを判定します。
  3. ディレクトリごとの単一パッケージ制約と +build タグの無視 (src/cmd/go/pkg.go, src/pkg/go/build/dir.go):

    • src/cmd/go/pkg.goscanPackage 関数に useAllFiles という新しいブール型引数が追加されました。この引数は、go build x.go のようにコマンドラインで特定のファイルが指定された場合に true に設定されます。
    • src/pkg/go/build/dir.gobuild.Context 構造体に UseAllFiles というフィールドが追加されました。これは scanPackage から渡される useAllFiles の値を受け取ります。
    • Context.ScanDir メソッド内で、ファイルのフィルタリングロジックが変更されました。
      • 以前は !ctxt.goodOSArchFile(name) でOS/アーキテクチャに合致しないファイルをスキップしていましたが、これが !ctxt.UseAllFiles && !ctxt.goodOSArchFile(name) に変更されました。つまり、UseAllFilestrue の場合はOS/アーキテクチャのチェックをスキップします。
      • 同様に、!ctxt.shouldBuild(data)+build タグに基づいてファイルをスキップしていましたが、これも !ctxt.UseAllFiles && !ctxt.shouldBuild(data) に変更されました。これにより、UseAllFilestrue の場合は +build タグのチェックをスキップします。
    • Context.ScanDir 内のパッケージ名チェックロジックが大幅に変更されました。
      • 以前は package main とそれ以外のパッケージが混在するディレクトリを特別扱いするヒューリスティックがありましたが、これが削除されました。
      • 新しいロジックでは、di.Package が空の場合に最初に発見されたパッケージ名を記録し、それ以降に異なるパッケージ名を持つファイルが見つかった場合はエラー (found packages %s (%s) and %s (%s)) を返すようになりました。これにより、1つのディレクトリには1つのパッケージのみというルールが厳格に適用されます。
    • src/cmd/go/main.goallPackages 関数で、build.ScanDir のエラーハンドリングが strings.Contains(err.Error(), "no Go source files") でフィルタリングされるようになりました。これは、ディレクトリにGoソースファイルがない場合のエラーを無視するためです。
    • src/cmd/go/pkg.goloadPackage 関数で、isCmd というフラグが導入され、標準コマンド(cmd/ 以下にあるもの)をロードする際に true に設定されます。ロードされたパッケージがコマンドであるにもかかわらず package main でない場合、エラーを生成するようになりました。これは、コマンドは必ず package main であるべきというルールを強制するためです。
  4. go test -i と cgo パッケージの連携修正 (src/cmd/go/test.go):

    • runTest 関数内で、テストの依存関係を解決する際に、擬似パッケージである "C""unsafe" が依存関係リストから削除されるようになりました。これらのパッケージは実際のファイルとしては存在せず、cgo や Goの内部機能に関連する特別な識別子であるため、依存関係解決の対象から外すことで、go test -i が cgo パッケージで正しく動作するようになります。
  5. ヘルパープログラムへの // +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 buildgo 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言語のコマンドラインツール 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 ツールに対する一連の修正を導入しています。具体的には以下の点が変更されました。

  1. -work オプションの追加: 一時ファイルを保存するための -work オプションが go build および go install コマンドに追加されました。これにより、ビルドプロセス中に生成される一時ディレクトリが終了時に削除されなくなり、デバッグや調査が容易になります。(Issue 2980の修正)
  2. go test -i と cgo パッケージの連携修正: go test -i コマンドが cgo パッケージで正しく動作しない問題を修正しました。これにより、cgo を使用するパッケージのテストがよりスムーズに行えるようになります。(Issue 2936の修正)
  3. ビルド時のファイル上書き/削除の挙動改善: ビルドプロセス中に、既存の空のディレクトリやオブジェクトファイルではないファイルを誤って上書きしたり削除したりしないように修正されました。これにより、ビルドの堅牢性が向上し、意図しないファイルシステムの変更を防ぎます。(Issue 2829の修正)
  4. ディレクトリごとの単一パッケージ制約の導入: package mainpackage non-main のヒューリスティックが削除され、1つのディレクトリには1つのパッケージのみが含まれるという厳格なルールが導入されました。これにより、パッケージ構造の明確化とビルドの予測可能性が向上します。(Issue 2864の修正)
  5. コマンドラインで指定されたファイルに対する +build タグの無視: 上記の単一パッケージ制約を機能させるため、go build x.go のようにコマンドラインで直接指定されたファイルに対しては、// +build タグ(特に // +build ignore)が無視されるようになりました。これにより、ヘルパープログラムなど、通常はビルド対象外とされるファイルも個別にビルドできるようになります。
  6. ヘルパープログラムへの // +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 コマンドの内部動作、特にビルドコンテキストの管理、ファイルシステム操作、およびパッケージスキャンロジックに深く関わっています。

  1. -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 フラグが指定された場合は一時ディレクトリが保持されます。
    • また、builderinstall メソッド内でも、ビルド成果物(オブジェクトディレクトリ a1.objdir とターゲットファイル a1.target)の遅延削除が !buildWork の条件付きになりました。
  2. ビルド時のファイル上書き/削除の挙動改善 (src/cmd/go/build.go):

    • buildercopyFile メソッド(ビルド成果物をコピーする際に使用される)に、コピー先のファイル (dst) が既に存在する場合のチェックが追加されました。
    • 新しいロジックでは、dst がディレクトリである場合、または isObject(dst) 関数が false を返す(つまりオブジェクトファイルではない)場合にエラーを返します。これにより、既存のディレクトリや非オブジェクトファイルを誤って上書き・削除するのを防ぎます。
    • isObject 関数は新しく追加され、ファイルの先頭数バイトを読み込み、Goが生成するオブジェクトファイル(アーカイブ、ELF、Mach-O、PEなど)の既知のマジックバイトと比較することで、そのファイルがオブジェクトファイルであるかどうかを判定します。
  3. ディレクトリごとの単一パッケージ制約と +build タグの無視 (src/cmd/go/pkg.go, src/pkg/go/build/dir.go):

    • src/cmd/go/pkg.goscanPackage 関数に useAllFiles という新しいブール型引数が追加されました。この引数は、go build x.go のようにコマンドラインで特定のファイルが指定された場合に true に設定されます。
    • src/pkg/go/build/dir.gobuild.Context 構造体に UseAllFiles というフィールドが追加されました。これは scanPackage から渡される useAllFiles の値を受け取ります。
    • Context.ScanDir メソッド内で、ファイルのフィルタリングロジックが変更されました。
      • 以前は !ctxt.goodOSArchFile(name) でOS/アーキテクチャに合致しないファイルをスキップしていましたが、これが !ctxt.UseAllFiles && !ctxt.goodOSArchFile(name) に変更されました。つまり、UseAllFilestrue の場合はOS/アーキテクチャのチェックをスキップします。
      • 同様に、!ctxt.shouldBuild(data)+build タグに基づいてファイルをスキップしていましたが、これも !ctxt.UseAllFiles && !ctxt.shouldBuild(data) に変更されました。これにより、UseAllFilestrue の場合は +build タグのチェックをスキップします。
    • Context.ScanDir 内のパッケージ名チェックロジックが大幅に変更されました。
      • 以前は package main とそれ以外のパッケージが混在するディレクトリを特別扱いするヒューリスティックがありましたが、これが削除されました。
      • 新しいロジックでは、di.Package が空の場合に最初に発見されたパッケージ名を記録し、それ以降に異なるパッケージ名を持つファイルが見つかった場合、即座にエラーを返すようになりました。これにより、1つのディレクトリには1つのパッケージのみというルールが厳格に適用されます。
    • src/cmd/go/main.goallPackages 関数で、build.ScanDir のエラーハンドリングが strings.Contains(err.Error(), "no Go source files") でフィルタリングされるようになりました。これは、ディレクトリにGoソースファイルがない場合のエラーを無視するためです。
    • src/cmd/go/pkg.goloadPackage 関数で、isCmd というフラグが導入され、標準コマンド(cmd/ 以下にあるもの)をロードする際に true に設定されます。ロードされたパッケージがコマンドであるにもかかわらず package main でない場合、エラーを生成するようになりました。これは、コマンドは必ず package main であるべきというルールを強制するためです。
  4. go test -i と cgo パッケージの連携修正 (src/cmd/go/test.go):

    • runTest 関数内で、テストの依存関係を解決する際に、擬似パッケージである "C""unsafe" が依存関係リストから削除されるようになりました。これらのパッケージは実際のファイルとしては存在せず、cgo や Goの内部機能に関連する特別な識別子であるため、依存関係解決の対象から外すことで、go test -i が cgo パッケージで正しく動作するようになります。
  5. ヘルパープログラムへの // +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 を使用するパッケージの依存関係を正しく処理し、テストを正常に実行できるようになります。

関連リンク

参考にした情報源リンク