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

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

このコミットは、Go言語のビルドコマンド (cmd/go) の中核をなす src/cmd/go/build.go ファイルに対する変更です。build.go は、Goパッケージのコンパイル、アセンブル、アーカイブ化、リンクといったビルドプロセス全体をオーケストレーションする役割を担っています。具体的には、ソースファイルの依存関係解決、適切なツールチェイン(コンパイラ、アセンブラ、リンカなど)の選択と実行、中間ファイルの管理、最終的な実行可能ファイルやライブラリの生成といった複雑なロジックが含まれています。

コミット

commit c86fc68ac6f95b2a50abe419dce6d1649adf4cd9
Author: Russ Cox <rsc@golang.org>
Date:   Tue Dec 17 21:44:36 2013 -0500

    cmd/go: avoid use of 'go tool pack'
    
    All packages now use the -pack option to the compiler.
    For a pure Go package, that's enough.
    For a package with additional C and assembly files, the extra
    archive entries can be added directly (by concatenation)
    instead of by invoking go tool pack.
    
    These changes make it possible to rewrite cmd/pack in Go.
    
    R=iant, r
    CC=golang-dev
    https://golang.org/cl/42910043

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

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

元コミット内容

このコミットの目的は、cmd/gogo tool pack の使用を避けるようにすることです。

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

  • すべてのパッケージは、コンパイラに -pack オプションを使用するようになりました。
  • 純粋なGoパッケージの場合、これだけで十分です。
  • Cやアセンブリファイルなどの追加ファイルを持つパッケージの場合、追加のアーカイブエントリは go tool pack を呼び出す代わりに、直接(連結によって)追加できるようになりました。
  • これらの変更により、cmd/pack をGo言語で書き直すことが可能になります。

変更の背景

このコミットの背景には、Go言語のツールチェインをよりGo言語自身で完結させるという設計思想があります。初期のGoツールチェインには、C言語で書かれたり、既存のUnixツール(例: ar アーカイバ)をラップしたりする部分がいくつか存在しました。go tool pack もその一つで、GoのオブジェクトファイルやC/アセンブリのオブジェクトファイルをまとめて .a (アーカイブ) ファイルを作成・操作するためのユーティリティでした。

go tool pack は、Goのビルドプロセスにおいて、コンパイルされたオブジェクトファイルをライブラリとしてまとめるために使用されていました。しかし、外部ツールへの依存は、ビルドプロセスの複雑性を増し、クロスコンパイルの難易度を上げ、Goツールチェイン全体のポータビリティを低下させる可能性がありました。

このコミットの主な動機は以下の通りです。

  1. 外部依存の削減: go tool pack への依存を減らすことで、Goツールチェインをより自己完結型にする。
  2. Go言語でのツールチェイン実装の推進: cmd/pack をGo言語で書き直すための基盤を整える。これにより、ツールチェインの保守性、理解しやすさ、そして将来的な機能拡張が容易になる。
  3. ビルドプロセスの効率化: go tool pack の呼び出しを減らすことで、ビルド時間の短縮や、より直接的なアーカイブ操作を可能にする。

特に、Goコンパイラが直接アーカイブを生成する機能(-packオプション)と、Go以外のオブジェクトファイルを既存のアーカイブに直接追記する機能の実装は、この目標達成のための重要なステップでした。

前提知識の解説

このコミットを理解するためには、以下のGo言語のビルドプロセスと関連ツールの知識が必要です。

  1. Goのビルドプロセス:

    • Goのソースコード (.go ファイル) は、Goコンパイラ (go tool compile または gc) によってオブジェクトファイル (.o または .a ファイル) にコンパイルされます。
    • C言語やアセンブリ言語のソースコード (.c, .s ファイル) は、Cコンパイラ (go tool cgogo tool asm) によってオブジェクトファイルにコンパイルされます。
    • これらのオブジェクトファイルは、最終的にリンカ (go tool link または ld) によって結合され、実行可能ファイルや共有ライブラリが生成されます。
    • 中間段階で、複数のオブジェクトファイルを一つにまとめる「アーカイブ」というステップがあります。これは通常、静的ライブラリ (.a ファイル) の形式で行われます。
  2. go tool pack:

    • Go 1.0の時代から存在したユーティリティで、Goのビルドシステムの一部として、オブジェクトファイルをアーカイブ (.a ファイル) にまとめる役割を担っていました。これはUnix系のシステムにおける ar コマンドに似た機能を提供していました。
    • pack は、オブジェクトファイルをアーカイブに追加したり、アーカイブから抽出したりする機能を持っていました。
  3. .a ファイル (アーカイブファイル):

    • Unix系システムで一般的な静的ライブラリの形式です。複数のオブジェクトファイル (.o ファイル) を一つにまとめたものです。
    • リンカは、これらのアーカイブファイルから必要なオブジェクトコードを抽出し、最終的な実行可能ファイルに結合します。
    • 内部的には、各オブジェクトファイルのメタデータ(ファイル名、サイズ、パーミッションなど)と実際のファイル内容が連続して格納されています。このフォーマットはAR (Archive) フォーマットとして知られています。
  4. cmd/go:

    • Goコマンドラインツールの中核であり、go build, go run, go test などのサブコマンドを実行します。
    • 内部的には、Goのビルドプロセスを管理し、適切なコンパイラ、アセンブラ、リンカ、そして以前は pack などのツールを呼び出して、ユーザーの要求に応じたビルドを実行します。

このコミットは、cmd/gogo tool pack を呼び出す代わりに、Goコンパイラ自身にアーカイブ生成の一部を担わせ、残りの部分を cmd/go 内部で直接処理するように変更することで、外部ツールへの依存を減らそうとしています。

技術的詳細

このコミットの技術的な核心は、Goのビルドプロセスにおけるアーカイブファイルの生成方法を変更し、go tool pack への依存を最小限に抑えることです。

1. Goコンパイラによるアーカイブ生成 (-pack オプション)

  • Goコンパイラ (gc) に -pack オプションが追加されました。このオプションが指定されると、コンパイラはGoのオブジェクトファイルを生成するだけでなく、そのオブジェクトファイルを直接 .a (アーカイブ) ファイルとして出力するようになります。
  • これにより、純粋なGoパッケージ(Cやアセンブリファイルを含まないパッケージ)の場合、コンパイラが生成した .a ファイルがそのまま最終的なパッケージアーカイブとなり、別途 go tool pack を呼び出す必要がなくなります。

2. Go以外のオブジェクトファイルの直接追記

  • Cやアセンブリファイルを含むパッケージの場合、GoコンパイラはGoのオブジェクトを含むアーカイブを生成します。その後、Cやアセンブリからコンパイルされたオブジェクトファイル (.o ファイル) を、この既存のアーカイブに追記する必要があります。
  • この追記処理は、go tool pack を呼び出す代わりに、src/cmd/go/build.go 内に新しく実装された packInternal 関数によって直接行われます。
  • packInternal は、既存のアーカイブファイルを開き、新しいオブジェクトファイルのヘッダ情報(ファイル名、サイズなど)と内容を、ARアーカイブフォーマットに従って直接追記します。これにより、外部の pack ツールを介さずに、Goのコード内でアーカイブ操作が完結します。

src/cmd/go/build.go の具体的な変更点

  • builder.build 関数の変更:
    • buildToolchain.gc の呼び出しに、出力先のアーカイブファイル名 (a.objpkg) が新しい引数として渡されるようになりました。これにより、コンパイラは直接そのファイルにアーカイブを書き込むことができます。
    • buildToolchain.pack の呼び出しが条件付きになりました。len(objects) > 0 の場合にのみ pack が呼び出されます。objects にはGo以外のソースから生成されたオブジェクトファイルが含まれます。つまり、Go以外のオブジェクトファイルが存在する場合にのみ、アーカイブへの追記処理が必要となるため、pack が呼び出されます。
  • toolchain インターフェースと gcToolchain の変更:
    • toolchain インターフェースの gc メソッドのシグネチャが変更され、archive という新しい引数が追加されました。
    • gcToolchain.gc の実装では、archive 引数が空でなければ、コンパイラの出力ファイル (ofile) をその archive に設定します。
    • ofilearchive と同じ場合(つまり、コンパイラが直接アーカイブを生成する場合)、コンパイラコマンドの引数に -pack が追加されます。
  • gcToolchain.pack メソッドの変更:
    • このメソッドは、アーカイブファイル (afile) が既に存在するかどうかを os.Stat で確認します。
    • もし存在する場合 (appending = true)、それはGoコンパイラによって既に作成されたアーカイブであり、Go以外のオブジェクトファイルを追記する必要があることを意味します。この場合、新しく追加された packInternal 関数が呼び出され、オブジェクトファイルが直接アーカイブに追記されます。
    • もしアーカイブファイルが存在しない場合(例: Goソースファイルが全くなく、C/アセンブリファイルのみで構成されるパッケージの場合)、従来の go tool pack コマンドが呼び出され、新規にアーカイブが作成されます。
  • packInternal 関数の新規追加:
    • この関数は、指定されたアーカイブファイル (afile) に、指定されたオブジェクトファイル (ofiles) を直接追記するロジックを実装しています。
    • アーカイブファイルを os.O_WRONLY|os.O_APPEND モードで開き、bufio.NewWriter を使用して効率的に書き込みます。
    • 各オブジェクトファイルについて、そのファイル名、サイズ、パーミッションなどのメタデータをARアーカイブフォーマットのヘッダとして書き込み、その後にオブジェクトファイルの内容をコピーします。
    • ARフォーマットの要件(例: 偶数バイトへのパディング)も考慮されています。
  • moveOrCopyFile 関数の導入:
    • これはビルドプロセスにおけるファイル操作の最適化です。os.Rename を試み、それが失敗した場合に copyFile にフォールバックします。os.Rename は通常、同じファイルシステム内であればアトミックで高速な操作であるため、パフォーマンス向上に寄与します。

これらの変更により、go tool pack の使用は、Goコンパイラがアーカイブを生成しない特殊なケース(例: Goソースファイルがないパッケージ)に限定され、ほとんどのケースでGoツールチェイン内部でアーカイブ操作が完結するようになりました。

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

src/cmd/go/build.go の主要な変更箇所は以下の通りです。

  1. import 文の追加:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -5,6 +5,7 @@
     package main
     
     import (
    +	"bufio"
     	"bytes"
     	"container/heap"
     	"errors"
    

    bufio パッケージが追加され、packInternal 関数での効率的なファイル書き込みに使用されます。

  2. builder.build 関数内の gc 呼び出しの変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -879,7 +880,7 @@ func (b *builder) build(a *action) (err error) {
     
     	// Compile Go.
     	if len(gofiles) > 0 {
    -		ofile, out, err := buildToolchain.gc(b, a.p, obj, inc, gofiles)
    +		ofile, out, err := buildToolchain.gc(b, a.p, a.objpkg, obj, inc, gofiles)
     		if len(out) > 0 {
     			b.showOutput(a.p.Dir, a.p.ImportPath, b.processOutput(out))
     			if err != nil {
    @@ -889,7 +890,9 @@ func (b *builder) build(a *action) (err error) {
     		if err != nil {
     			return err
     		}
    -		objects = append(objects, ofile)
    +		if ofile != a.objpkg {
    +			objects = append(objects, ofile)
    +		}
     	}
    

    buildToolchain.gca.objpkg (最終的なアーカイブファイル名) が渡されるようになりました。また、ofilea.objpkg と同じ場合は、objects リストに追加しないようになりました。これは、コンパイラが直接アーカイブを生成した場合、そのアーカイブ自体がGo以外のオブジェクトを追記する対象となるため、Goオブジェクトファイルとして別途リストに含める必要がないことを意味します。

  3. builder.build 関数内の pack 呼び出しの変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -952,9 +955,15 @@ func (b *builder) build(a *action) (err error) {
     		objects = append(objects, filepath.Join(a.p.Dir, syso))\n \t}\n \n-\t// Pack into archive in obj directory\n-\tif err := buildToolchain.pack(b, a.p, obj, a.objpkg, objects); err != nil {\n-\t\treturn err\n+\t// Pack into archive in obj directory.\n+\t// If the Go compiler wrote an archive, we only need to add the\n+\t// object files for non-Go sources to the archive.\n+\t// If the Go compiler wrote an archive and the package is entirely\n+\t// Go sources, there is no pack to execute at all.\n+\tif len(objects) > 0 {\n+\t\tif err := buildToolchain.pack(b, a.p, obj, a.objpkg, objects); err != nil {\n+\t\t\treturn err\n+\t\t}\n     }\n ```
    `pack` の呼び出しが `if len(objects) > 0` で囲まれました。これは、Go以外のオブジェクトファイルが存在する場合にのみ `pack` 処理が必要であることを示しています。
    
    
  4. install 関数内のファイル移動処理の変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1016,7 +1025,7 @@ func (b *builder) install(a *action) (err error) {
     		}\n \t}\n \n-\treturn b.copyFile(a, a.target, a1.target, perm)\n+\treturn b.moveOrCopyFile(a, a.target, a1.target, perm)\n     }\n ```
    `copyFile` の代わりに新しく追加された `moveOrCopyFile` が使用されるようになりました。
    
    
  5. moveOrCopyFile 関数の新規追加:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1062,6 +1071,27 @@ func (b *builder) includeArgs(flag string, all []*action) []string {\n     \treturn inc\n     }\n     \n    +// moveOrCopyFile is like 'mv src dst' or 'cp src dst'.\n    +func (b *builder) moveOrCopyFile(a *action, dst, src string, perm os.FileMode) error {\n    +\tif buildN {\n    +\t\tb.showcmd("", "mv %s %s", src, dst)\n    +\t\treturn nil\n    +\t}\n    +\n    +\t// If we can update the mode and rename to the dst, do it.\n    +\t// Otherwise fall back to standard copy.\n    +\tif err := os.Chmod(src, perm); err == nil {\n    +\t\tif err := os.Rename(src, dst); err == nil {\n    +\t\t\tif buildX {\n    +\t\t\t\tb.showcmd("", "mv %s %s", src, dst)\n    +\t\t\t}\n    +\t\t\treturn nil\n    +\t\t}\n    +\t}\n    +\n    +\treturn b.copyFile(a, dst, src, perm)\n    +}\n    +\n     // copyFile is like 'cp src dst'.\n     func (b *builder) copyFile(a *action, dst, src string, perm os.FileMode) error {\n     \tif buildN || buildX {
    

    ファイル移動の最適化のためのヘルパー関数です。

  6. toolchain.gc インターフェースのシグネチャ変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1432,7 +1462,7 @@ type toolchain interface {\n     	// gc runs the compiler in a specific directory on a set of files\n     	// and returns the name of the generated output file.\n     	// The compiler runs in the directory dir.\n    -	gc(b *builder, p *Package, obj string, importArgs []string, gofiles []string) (ofile string, out []byte, err error)\n    +	gc(b *builder, p *Package, archive, obj string, importArgs []string, gofiles []string) (ofile string, out []byte, err error)\n     	// cc runs the toolchain's C compiler in a directory on a C file\n     	// to produce an output file.\n     	cc(b *builder, p *Package, objdir, ofile, cfile string) error
    

    archive という新しい引数が追加されました。

  7. gcToolchain.gc の実装変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1505,9 +1535,14 @@ func (gcToolchain) linker() string {\n     	return tool(archChar + "l")\n     }\n     \n    -func (gcToolchain) gc(b *builder, p *Package, obj string, importArgs []string, gofiles []string) (ofile string, output []byte, err error) {\n    -	out := "_go_." + archChar\n    -	ofile = obj + out\n    +func (gcToolchain) gc(b *builder, p *Package, archive, obj string, importArgs []string, gofiles []string) (ofile string, output []byte, err error) {\n    +	if archive != "" {\n    +		ofile = archive\n    +	} else {\n    +		out := "_go_." + archChar\n    +		ofile = obj + out\n    +	}\n    +\n     	gcargs := []string{"-p", p.ImportPath}\n     	if p.Standard && p.ImportPath == "runtime" {\n     		// runtime compiles with a special 6g flag to emit\n    @@ -1534,6 +1569,9 @@ func (gcToolchain) gc(b *builder, p *Package, obj string, importArgs []string, g\n     	}\n     \n     	args := stringList(tool(archChar+"g"), "-o", ofile, buildGcflags, gcargs, "-D", p.localPrefix, importArgs)\n    +	if ofile == archive {\n    +		args = append(args, "-pack")\n    +	}\n     	for _, f := range gofiles {\n     		args = append(args, mkAbs(p.Dir, f))\n     	}\
    

    archive 引数に基づいて出力ファイル名 (ofile) を設定し、ofilearchive と同じ場合はコンパイラに -pack オプションを追加しています。

  8. gcToolchain.pack の実装変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1557,7 +1595,83 @@ func (gcToolchain) pack(b *builder, p *Package, objDir, afile string, ofiles []s\n     	for _, f := range ofiles {\n     		absOfiles = append(absOfiles, mkAbs(objDir, f))\n     	}\n    -	return b.run(p.Dir, p.ImportPath, nil, tool("pack"), "grcP", b.work, mkAbs(objDir, afile), absOfiles)\n    +	cmd := "grcP"\n    +	absAfile := mkAbs(objDir, afile)\n    +	appending := false\n    +	if _, err := os.Stat(absAfile); err == nil {\n    +		appending = true\n    +		cmd = "rqP"\n    +	}\n    +\n    +	cmdline := stringList("pack", cmd, b.work, absAfile, absOfiles)\n    +\n    +	if appending {\n    +		if buildN || buildX {\n    +			b.showcmd(p.Dir, "%s # internal", joinUnambiguously(cmdline))\n    +		}\n    +		if buildN {\n    +			return nil\n    +		}\n    +		if err := packInternal(b, absAfile, absOfiles); err != nil {\n    +			b.showOutput(p.Dir, p.ImportPath, err.Error()+"\n")\n    +			return errPrintedOutput\n    +		}\n    +		return nil\n    +	}\n    +\n    +	// Need actual pack.\n    +	cmdline[0] = tool("pack")\n    +	return b.run(p.Dir, p.ImportPath, nil, cmdline)\n     }\n    +\n    +func packInternal(b *builder, afile string, ofiles []string) error {\n    +	dst, err := os.OpenFile(afile, os.O_WRONLY|os.O_APPEND, 0)\n    +	if err != nil {\n    +		return err\n    +	}\n    +	defer dst.Close() // only for error returns or panics\n    +	w := bufio.NewWriter(dst)\n    +\n    +	for _, ofile := range ofiles {\n    +		src, err := os.Open(ofile)\n    +		if err != nil {\n    +			return err\n    +		}\n    +		fi, err := src.Stat()\n    +		if err != nil {\n    +			src.Close()\n    +			return err\n    +		}\n    +		// Note: Not using %-16.16s format because we care\n    +		// about bytes, not runes.\n    +		name := fi.Name()\n    +		if len(name) > 16 {\n    +			name = name[:16]\n    +		} else {\n    +			name += strings.Repeat(" ", 16-len(name))\n    +		}\n    +		size := fi.Size()\n    +		fmt.Fprintf(w, "%s%-12d%-6d%-6d%-8o%-10d`\\n",\n    +			name, 0, 0, 0, 0644, size)\n    +		n, err := io.Copy(w, src)\n    +		src.Close()\n    +		if err == nil && n < size {\n    +			err = io.ErrUnexpectedEOF\n    +		} else if err == nil && n > size {\n    +			err = fmt.Errorf("file larger than size reported by stat")\n    +		}\n    +		if err != nil {\n    +			return fmt.Errorf("copying %s to %s: %v", ofile, afile, err)\n    +		}\n    +		if size&1 != 0 {\n    +			w.WriteByte(0)\n    +		}\n    +	}\n    +\n    +	if err := w.Flush(); err != nil {\n    +		return err\n    +	}\n    +	return dst.Close()\n     }\n ```
    `pack` メソッドは、アーカイブファイルが既に存在するかどうかをチェックし、存在する場合は新しく追加された `packInternal` 関数を呼び出してオブジェクトファイルを追記します。存在しない場合は、引き続き `go tool pack` を呼び出して新規アーカイブを作成します。
    
    
  9. gccgoToolchain.gc および swigIntSizegc 呼び出しの変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1650,7 +1764,7 @@ func (gccgoToolchain) linker() string {\n     	return gccgoBin\n     }\n     \n    -func (gccgoToolchain) gc(b *builder, p *Package, obj string, importArgs []string, gofiles []string) (ofile string, output []byte, err error) {\n    +func (gccgoToolchain) gc(b *builder, p *Package, archive, obj string, importArgs []string, gofiles []string) (ofile string, output []byte, err error) {\n     	out := p.Name + ".o"\n     	ofile = obj + out\n     	gcargs := []string{"-g"}\
    
    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -2220,7 +2334,7 @@ func (b *builder) swigIntSize(obj string) (intsize string, err error) {\n     \n     	p := goFilesPackage(srcs)\n     \n    -	if _, _, e := buildToolchain.gc(b, p, obj, nil, srcs); e != nil {\n    +	if _, _, e := buildToolchain.gc(b, p, "", obj, nil, srcs); e != nil {\n     		return "32", nil\n     	}\n     	return "64", nil
    

    これらの箇所でも gc メソッドのシグネチャ変更に合わせて引数が更新されています。swigIntSize の場合は、アーカイブを生成しないため、空文字列 "" が渡されています。

コアとなるコードの解説

このコミットの最も重要な変更は、gcToolchain.pack メソッドのロジック変更と、新しく導入された packInternal 関数です。

gcToolchain.pack の新しいロジック

以前は、Go以外のオブジェクトファイル(Cやアセンブリ)をアーカイブに追加する際も、常に go tool pack を呼び出していました。このコミットでは、まず os.Stat(absAfile) を使って、最終的なアーカイブファイル (.a ファイル) が既に存在するかどうかを確認します。

  • アーカイブが既に存在する場合 (appending = true): これは、Goコンパイラが既に -pack オプションを使ってGoのオブジェクトを含むアーカイブを生成済みであることを意味します。この場合、go tool pack を呼び出す代わりに、新しく定義された packInternal 関数が呼び出されます。packInternal は、Go以外のオブジェクトファイル (ofiles) を、既存のアーカイブ (absAfile) に直接追記します。これにより、外部ツールへの依存が回避されます。

  • アーカイブがまだ存在しない場合: これは、Goソースファイルが全くなく、Cやアセンブリファイルのみで構成されるパッケージの場合などに発生します。この場合、Goコンパイラはアーカイブを生成していないため、go tool pack コマンドが呼び出され、新規にアーカイブが作成されます。

packInternal 関数の詳細

packInternal 関数は、Go言語でARアーカイブフォーマットの簡易的な追記処理を実装しています。

  1. アーカイブファイルのオープン:

    dst, err := os.OpenFile(afile, os.O_WRONLY|os.O_APPEND, 0)
    

    os.OpenFileos.O_APPEND フラグ付きで呼び出すことで、既存のアーカイブファイルの末尾にデータを追記できるようにします。

  2. オブジェクトファイルの追記ループ: for _, ofile := range ofiles ループで、追記対象の各オブジェクトファイルについて以下の処理を行います。

    • オブジェクトファイルのオープンと情報取得:

      src, err := os.Open(ofile)
      fi, err := src.Stat()
      

      追記するオブジェクトファイルを開き、os.Stat() でファイルサイズやファイル名などのメタデータを取得します。

    • ARヘッダの生成:

      name := fi.Name()
      if len(name) > 16 {
          name = name[:16]
      } else {
          name += strings.Repeat(" ", 16-len(name))
      }
      size := fi.Size()
      fmt.Fprintf(w, "%s%-12d%-6d%-6d%-8o%-10d`\\n",
          name, 0, 0, 0, 0644, size)
      

      ARアーカイブフォーマットでは、各ファイルの内容の前に固定長のヘッダが付きます。このヘッダには、ファイル名(16バイトにパディング)、ファイルサイズ、パーミッションなどの情報が含まれます。packInternal は、これらの情報を整形して書き込みます。特に、ファイル名が16バイトを超える場合は切り詰め、短い場合はスペースでパディングします。

    • オブジェクトファイル内容のコピー:

      n, err := io.Copy(w, src)
      

      io.Copy を使って、オブジェクトファイルの内容をアーカイブファイルに直接コピーします。

    • パディングの処理:

      if size&1 != 0 {
          w.WriteByte(0)
      }
      

      ARアーカイブフォーマットの慣例として、各ファイルのエントリは偶数バイト境界で終わる必要があります。オブジェクトファイルのサイズが奇数の場合、1バイトのヌル文字 (0) を追記してパディングします。

この packInternal 関数により、Goツールチェインは go tool pack に頼ることなく、Go以外のオブジェクトファイルを既存のアーカイブに効率的に追記できるようになりました。これは、Goツールチェインの自己完結性を高め、将来的な cmd/pack のGo言語での再実装への道を開く重要な一歩です。

関連リンク

  • Go Issue/Change List (CL):
    • https://golang.org/cl/42910043 - このコミットに対応するGoのChange Listページです。詳細なレビューコメントや変更履歴を確認できます。

参考にした情報源リンク

  • Go言語の公式ドキュメント: Goのビルドプロセスやツールチェインに関する一般的な情報。
  • AR (ファイルフォーマット): Wikipediaやその他の技術文書でARアーカイブフォーマットの仕様を確認しました。packInternal の実装がこのフォーマットに準拠していることを理解するために必要でした。
  • Goのソースコード: src/cmd/go/build.go および関連ファイルの変更履歴とコメント。
  • Goのメーリングリストやフォーラム: 過去の議論で go tool pack の役割やGoツールチェインの進化に関する情報。
  • Goのビルドシステムに関するブログ記事や解説: Goのビルドプロセスの内部動作を解説している記事。
    • 例: "Inside the Go build system" (特定の記事を指すものではなく、一般的な情報源として)

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

このコミットは、Go言語のビルドコマンド (cmd/go) の中核をなす src/cmd/go/build.go ファイルに対する変更です。build.go は、Goパッケージのコンパイル、アセンブル、アーカイブ化、リンクといったビルドプロセス全体をオーケストレーションする役割を担っています。具体的には、ソースファイルの依存関係解決、適切なツールチェイン(コンパイラ、アセンブラ、リンカなど)の選択と実行、中間ファイルの管理、最終的な実行可能ファイルやライブラリの生成といった複雑なロジックが含まれています。

コミット

commit c86fc68ac6f95b2a50abe419dce6d1649adf4cd9
Author: Russ Cox <rsc@golang.org>
Date:   Tue Dec 17 21:44:36 2013 -0500

    cmd/go: avoid use of 'go tool pack'
    
    All packages now use the -pack option to the compiler.
    For a pure Go package, that's enough.
    For a package with additional C and assembly files, the extra
    archive entries can be added directly (by concatenation)
    instead of by invoking go tool pack.
    
    These changes make it possible to rewrite cmd/pack in Go.
    
    R=iant, r
    CC=golang-dev
    https://golang.org/cl/42910043

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

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

元コミット内容

このコミットの目的は、cmd/gogo tool pack の使用を避けるようにすることです。

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

  • すべてのパッケージは、コンパイラに -pack オプションを使用するようになりました。
  • 純粋なGoパッケージの場合、これだけで十分です。
  • Cやアセンブリファイルなどの追加ファイルを持つパッケージの場合、追加のアーカイブエントリは go tool pack を呼び出す代わりに、直接(連結によって)追加できるようになりました。
  • これらの変更により、cmd/pack をGo言語で書き直すことが可能になります。

変更の背景

このコミットの背景には、Go言語のツールチェインをよりGo言語自身で完結させるという設計思想があります。初期のGoツールチェインには、C言語で書かれたり、既存のUnixツール(例: ar アーカイバ)をラップしたりする部分がいくつか存在しました。go tool pack もその一つで、Goのビルドプロセスにおいて、コンパイルされたオブジェクトファイルやC/アセンブリのオブジェクトファイルをまとめて .a (アーカイブ) ファイルを作成するためのユーティリティでした。

go tool pack は、Goのビルドプロセスにおいて、コンパイルされたオブジェクトファイルをライブラリとしてまとめるために使用されていました。しかし、外部ツールへの依存は、ビルドプロセスの複雑性を増し、クロスコンパイルの難易度を上げ、Goツールチェイン全体のポータビリティを低下させる可能性がありました。

このコミットの主な動機は以下の通りです。

  1. 外部依存の削減: go tool pack への依存を減らすことで、Goツールチェインをより自己完結型にする。
  2. Go言語でのツールチェイン実装の推進: cmd/pack をGo言語で書き直すための基盤を整える。これにより、ツールチェインの保守性、理解しやすさ、そして将来的な機能拡張が容易になる。
  3. ビルドプロセスの効率化: go tool pack の呼び出しを減らすことで、ビルド時間の短縮や、より直接的なアーカイブ操作を可能にする。

特に、Goコンパイラが直接アーカイブを生成する機能(-packオプション)と、Go以外のオブジェクトファイルを既存のアーカイブに直接追記する機能の実装は、この目標達成のための重要なステップでした。

前提知識の解説

このコミットを理解するためには、以下のGo言語のビルドプロセスと関連ツールの知識が必要です。

  1. Goのビルドプロセス:

    • Goのソースコード (.go ファイル) は、Goコンパイラ (go tool compile または gc) によってオブジェクトファイル (.o または .a ファイル) にコンパイルされます。
    • C言語やアセンブリ言語のソースコード (.c, .s ファイル) は、Cコンパイラ (go tool cgogo tool asm) によってオブジェクトファイルにコンパイルされます。
    • これらのオブジェクトファイルは、最終的にリンカ (go tool link または ld) によって結合され、実行可能ファイルや共有ライブラリが生成されます。
    • 中間段階で、複数のオブジェクトファイルを一つにまとめる「アーカイブ」というステップがあります。これは通常、静的ライブラリ (.a ファイル) の形式で行われます。
  2. go tool pack:

    • Go 1.0の時代から存在したユーティリティで、Goのビルドシステムの一部として、オブジェクトファイルをアーカイブ (.a ファイル) にまとめる役割を担っていました。これはUnix系のシステムにおける ar コマンドに似た機能を提供していました。
    • pack は、オブジェクトファイルをアーカイブに追加したり、アーカイブから抽出したりする機能を持っていました。
  3. .a ファイル (アーカイブファイル):

    • Unix系システムで一般的な静的ライブラリの形式です。複数のオブジェクトファイル (.o ファイル) を一つにまとめたものです。
    • リンカは、これらのアーカイブファイルから必要なオブジェクトコードを抽出し、最終的な実行可能ファイルに結合します。
    • 内部的には、各オブジェクトファイルのメタデータ(ファイル名、サイズ、パーミッションなど)と実際のファイル内容が連続して格納されています。このフォーマットはAR (Archive) フォーマットとして知られています。
  4. cmd/go:

    • Goコマンドラインツールの中核であり、go build, go run, go test などのサブコマンドを実行します。
    • 内部的には、Goのビルドプロセスを管理し、適切なコンパイラ、アセンブラ、リンカ、そして以前は pack などのツールを呼び出して、ユーザーの要求に応じたビルドを実行します。

このコミットは、cmd/gogo tool pack を呼び出す代わりに、Goコンパイラ自身にアーカイブ生成の一部を担わせ、残りの部分を cmd/go 内部で直接処理するように変更することで、外部ツールへの依存を減らそうとしています。

技術的詳細

このコミットの技術的な核心は、Goのビルドプロセスにおけるアーカイブファイルの生成方法を変更し、go tool pack への依存を最小限に抑えることです。

1. Goコンパイラによるアーカイブ生成 (-pack オプション)

  • Goコンパイラ (gc) に -pack オプションが追加されました。このオプションが指定されると、コンパイラはGoのオブジェクトファイルを生成するだけでなく、そのオブジェクトファイルを直接 .a (アーカイブ) ファイルとして出力するようになります。
  • これにより、純粋なGoパッケージ(Cやアセンブリファイルを含まないパッケージ)の場合、コンパイラが生成した .a ファイルがそのまま最終的なパッケージアーカイブとなり、別途 go tool pack を呼び出す必要がなくなります。

2. Go以外のオブジェクトファイルの直接追記

  • Cやアセンブリファイルを含むパッケージの場合、GoコンパイラはGoのオブジェクトを含むアーカイブを生成します。その後、Cやアセンブリからコンパイルされたオブジェクトファイル (.o ファイル) を、この既存のアーカイブに追記する必要があります。
  • この追記処理は、go tool pack を呼び出す代わりに、src/cmd/go/build.go 内に新しく実装された packInternal 関数によって直接行われます。
  • packInternal は、既存のアーカイブファイルを開き、新しいオブジェクトファイルのヘッダ情報(ファイル名、サイズなど)と内容を、ARアーカイブフォーマットに従って直接追記します。これにより、外部の pack ツールを介さずに、Goのコード内でアーカイブ操作が完結します。

src/cmd/go/build.go の具体的な変更点

  • builder.build 関数の変更:
    • buildToolchain.gc の呼び出しに、出力先のアーカイブファイル名 (a.objpkg) が新しい引数として渡されるようになりました。これにより、コンパイラは直接そのファイルにアーカイブを書き込むことができます。
    • buildToolchain.pack の呼び出しが条件付きになりました。len(objects) > 0 の場合にのみ pack が呼び出されます。objects にはGo以外のソースから生成されたオブジェクトファイルが含まれます。つまり、Go以外のオブジェクトファイルが存在する場合にのみ、アーカイブへの追記処理が必要となるため、pack が呼び出されます。
  • toolchain インターフェースと gcToolchain の変更:
    • toolchain インターフェースの gc メソッドのシグネチャが変更され、archive という新しい引数が追加されました。
    • gcToolchain.gc の実装では、archive 引数が空でなければ、コンパイラの出力ファイル (ofile) をその archive に設定します。
    • ofilearchive と同じ場合(つまり、コンパイラが直接アーカイブを生成する場合)、コンパイラコマンドの引数に -pack が追加されます。
  • gcToolchain.pack メソッドの変更:
    • このメソッドは、アーカイブファイル (afile) が既に存在するかどうかを os.Stat で確認します。
    • もし存在する場合 (appending = true)、それはGoコンパイラによって既に作成されたアーカイブであり、Go以外のオブジェクトファイルを追記する必要があることを意味します。この場合、新しく追加された packInternal 関数が呼び出され、オブジェクトファイルが直接アーカイブに追記されます。
    • もしアーカイブファイルが存在しない場合(例: Goソースファイルが全くなく、C/アセンブリファイルのみで構成されるパッケージの場合)、従来の go tool pack コマンドが呼び出され、新規にアーカイブが作成されます。
  • packInternal 関数の新規追加:
    • この関数は、指定されたアーカイブファイル (afile) に、指定されたオブジェクトファイル (ofiles) を直接追記するロジックを実装しています。
    • アーカイブファイルを os.O_WRONLY|os.O_APPEND モードで開き、bufio.NewWriter を使用して効率的に書き込みます。
    • 各オブジェクトファイルについて、そのファイル名、サイズ、パーミッションなどのメタデータをARアーカイブフォーマットのヘッダとして書き込み、その後にオブジェクトファイルの内容をコピーします。
    • ARフォーマットの要件(例: 偶数バイトへのパディング)も考慮されています。
  • moveOrCopyFile 関数の導入:
    • これはビルドプロセスにおけるファイル操作の最適化です。os.Rename を試み、それが失敗した場合に copyFile にフォールバックします。os.Rename は通常、同じファイルシステム内であればアトミックで高速な操作であるため、パフォーマンス向上に寄与します。

これらの変更により、go tool pack の使用は、Goコンパイラがアーカイブを生成しない特殊なケース(例: Goソースファイルがないパッケージ)に限定され、ほとんどのケースでGoツールチェイン内部でアーカイブ操作が完結するようになりました。

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

src/cmd/go/build.go の主要な変更箇所は以下の通りです。

  1. import 文の追加:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -5,6 +5,7 @@
     package main
     
     import (
    +	"bufio"
     	"bytes"
     	"container/heap"
     	"errors"
    

    bufio パッケージが追加され、packInternal 関数での効率的なファイル書き込みに使用されます。

  2. builder.build 関数内の gc 呼び出しの変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -879,7 +880,7 @@ func (b *builder) build(a *action) (err error) {
     
     	// Compile Go.
     	if len(gofiles) > 0 {
    -		ofile, out, err := buildToolchain.gc(b, a.p, obj, inc, gofiles)
    +		ofile, out, err := buildToolchain.gc(b, a.p, a.objpkg, obj, inc, gofiles)
     		if len(out) > 0 {
     			b.showOutput(a.p.Dir, a.p.ImportPath, b.processOutput(out))
     			if err != nil {
    @@ -889,7 +890,9 @@ func (b *builder) build(a *action) (err error) {
     		if err != nil {
     			return err
     		}
    -		objects = append(objects, ofile)
    +		if ofile != a.objpkg {
    +			objects = append(objects, ofile)
    +		}
     	}
    

    buildToolchain.gca.objpkg (最終的なアーカイブファイル名) が渡されるようになりました。また、ofilea.objpkg と同じ場合は、objects リストに追加しないようになりました。これは、コンパイラが直接アーカイブを生成した場合、そのアーカイブ自体がGo以外のオブジェクトを追記する対象となるため、Goオブジェクトファイルとして別途リストに含める必要がないことを意味します。

  3. builder.build 関数内の pack 呼び出しの変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -952,9 +955,15 @@ func (b *builder) build(a *action) (err error) {
     		objects = append(objects, filepath.Join(a.p.Dir, syso))\n \t}\n \n-\t// Pack into archive in obj directory\n-\tif err := buildToolchain.pack(b, a.p, obj, a.objpkg, objects); err != nil {\n-\t\treturn err\n+\t// Pack into archive in obj directory.\n+\t// If the Go compiler wrote an archive, we only need to add the\n+\t// object files for non-Go sources to the archive.\n+\t// If the Go compiler wrote an archive and the package is entirely\n+\t// Go sources, there is no pack to execute at all.\n+\tif len(objects) > 0 {\n+\t\tif err := buildToolchain.pack(b, a.p, obj, a.objpkg, objects); err != nil {\n+\t\t\treturn err\n+\t\t}\n     }\n ```
    `pack` の呼び出しが `if len(objects) > 0` で囲まれました。これは、Go以外のオブジェクトファイルが存在する場合にのみ `pack` 処理が必要であることを示しています。
    
    
  4. install 関数内のファイル移動処理の変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1016,7 +1025,7 @@ func (b *builder) install(a *action) (err error) {
     		}\n \t}\n \n-\treturn b.copyFile(a, a.target, a1.target, perm)\n+\treturn b.moveOrCopyFile(a, a.target, a1.target, perm)\n     }\n ```
    `copyFile` の代わりに新しく追加された `moveOrCopyFile` が使用されるようになりました。
    
    
  5. moveOrCopyFile 関数の新規追加:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1062,6 +1071,27 @@ func (b *builder) includeArgs(flag string, all []*action) []string {\n     \treturn inc\n     }\n     \n    +// moveOrCopyFile is like 'mv src dst' or 'cp src dst'.\n    +func (b *builder) moveOrCopyFile(a *action, dst, src string, perm os.FileMode) error {\n    +\tif buildN {\n    +\t\tb.showcmd("", "mv %s %s", src, dst)\n    +\t\treturn nil\n    +\t}\n    +\n    +\t// If we can update the mode and rename to the dst, do it.\n    +\t// Otherwise fall back to standard copy.\n    +\tif err := os.Chmod(src, perm); err == nil {\n    +\t\tif err := os.Rename(src, dst); err == nil {\n    +\t\t\tif buildX {\n    +\t\t\t\tb.showcmd("", "mv %s %s", src, dst)\n    +\t\t\t}\n    +\t\t\treturn nil\n    +\t\t}\n    +\t}\n    +\n    +\treturn b.copyFile(a, dst, src, perm)\n    +}\n    +\n     // copyFile is like 'cp src dst'.\n     func (b *builder) copyFile(a *action, dst, src string, perm os.FileMode) error {\n     \tif buildN || buildX {
    

    ファイル移動の最適化のためのヘルパー関数です。

  6. toolchain.gc インターフェースのシグネチャ変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1432,7 +1462,7 @@ type toolchain interface {\n     	// gc runs the compiler in a specific directory on a set of files\n     	// and returns the name of the generated output file.\n     	// The compiler runs in the directory dir.\n    -	gc(b *builder, p *Package, obj string, importArgs []string, gofiles []string) (ofile string, out []byte, err error)\n    +	gc(b *builder, p *Package, archive, obj string, importArgs []string, gofiles []string) (ofile string, out []byte, err error)\n     	// cc runs the toolchain's C compiler in a directory on a C file\n     	// to produce an output file.\n     	cc(b *builder, p *Package, objdir, ofile, cfile string) error
    

    archive という新しい引数が追加されました。

  7. gcToolchain.gc の実装変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1505,9 +1535,14 @@ func (gcToolchain) linker() string {\n     	return tool(archChar + "l")\n     }\n     \n    -func (gcToolchain) gc(b *builder, p *Package, obj string, importArgs []string, gofiles []string) (ofile string, output []byte, err error) {\n    -	out := "_go_." + archChar\n    -	ofile = obj + out\n    +func (gcToolchain) gc(b *builder, p *Package, archive, obj string, importArgs []string, gofiles []string) (ofile string, output []byte, err error) {\n    +	if archive != "" {\n    +		ofile = archive\n    +	} else {\n    +		out := "_go_." + archChar\n    +		ofile = obj + out\n    +	}\n    +\n     	gcargs := []string{"-p", p.ImportPath}\n     	if p.Standard && p.ImportPath == "runtime" {\n     		// runtime compiles with a special 6g flag to emit\n    @@ -1534,6 +1569,9 @@ func (gcToolchain) gc(b *builder, p *Package, obj string, importArgs []string, g\n     	}\n     \n     	args := stringList(tool(archChar+"g"), "-o", ofile, buildGcflags, gcargs, "-D", p.localPrefix, importArgs)\n    +	if ofile == archive {\n    +		args = append(args, "-pack")\n    +	}\n     	for _, f := range gofiles {\n     		args = append(args, mkAbs(p.Dir, f))\n     	}\
    

    archive 引数に基づいて出力ファイル名 (ofile) を設定し、ofilearchive と同じ場合はコンパイラに -pack オプションを追加しています。

  8. gcToolchain.pack の実装変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1557,7 +1595,83 @@ func (gcToolchain) pack(b *builder, p *Package, objDir, afile string, ofiles []s\n     	for _, f := range ofiles {\n     		absOfiles = append(absOfiles, mkAbs(objDir, f))\n     	}\n    -	return b.run(p.Dir, p.ImportPath, nil, tool("pack"), "grcP", b.work, mkAbs(objDir, afile), absOfiles)\n    +\tcmd := "grcP"\n    +\tabsAfile := mkAbs(objDir, afile)\n    +\tappending := false\n    +\tif _, err := os.Stat(absAfile); err == nil {\n    +\t\tappending = true\n    +\t\tcmd = "rqP"\n    +\t}\n    +\n    +\tcmdline := stringList("pack", cmd, b.work, absAfile, absOfiles)\n    +\n    +\tif appending {\n    +\t\tif buildN || buildX {\n    +\t\t\tb.showcmd(p.Dir, "%s # internal", joinUnambiguously(cmdline))\n    +\t\t}\n    +\t\tif buildN {\n    +\t\t\treturn nil\n    +\t\t}\n    +\t\tif err := packInternal(b, absAfile, absOfiles); err != nil {\n    +\t\t\tb.showOutput(p.Dir, p.ImportPath, err.Error()+"\n")\n    +\t\t\treturn errPrintedOutput\n    +\t\t}\n    +\t\treturn nil\n    +\t}\n    +\n    +\t// Need actual pack.\n    +\tcmdline[0] = tool("pack")\n    +\treturn b.run(p.Dir, p.ImportPath, nil, cmdline)\n     }\n    +\n    +func packInternal(b *builder, afile string, ofiles []string) error {\n    +	dst, err := os.OpenFile(afile, os.O_WRONLY|os.O_APPEND, 0)\n    +	if err != nil {\n    +		return err\n    +	}\n    +	defer dst.Close() // only for error returns or panics\n    +	w := bufio.NewWriter(dst)\n    +\n    +	for _, ofile := range ofiles {\n    +		src, err := os.Open(ofile)\n    +		if err != nil {\n    +			return err\n    +		}\n    +		fi, err := src.Stat()\n    +		if err != nil {\n    +			src.Close()\n    +			return err\n    +		}\n    +		// Note: Not using %-16.16s format because we care\n    +		// about bytes, not runes.\n    +		name := fi.Name()\n    +		if len(name) > 16 {\n    +			name = name[:16]\n    +		} else {\n    +			name += strings.Repeat(" ", 16-len(name))\n    +		}\n    +		size := fi.Size()\n    +		fmt.Fprintf(w, "%s%-12d%-6d%-6d%-8o%-10d`\\n",\n    +			name, 0, 0, 0, 0644, size)\n    +		n, err := io.Copy(w, src)\n    +		src.Close()\n    +		if err == nil && n < size {\n    +			err = io.ErrUnexpectedEOF\n    +		} else if err == nil && n > size {\n    +			err = fmt.Errorf("file larger than size reported by stat")\n    +		}\n    +		if err != nil {\n    +			return fmt.Errorf("copying %s to %s: %v", ofile, afile, err)\n    +		}\n    +		if size&1 != 0 {\n    +			w.WriteByte(0)\n    +		}\n    +	}\n    +\n    +	if err := w.Flush(); err != nil {\n    +		return err\n    +	}\n    +	return dst.Close()\n     }\n ```
    `pack` メソッドは、アーカイブファイルが既に存在するかどうかをチェックし、存在する場合は新しく追加された `packInternal` 関数を呼び出してオブジェクトファイルを追記します。存在しない場合は、引き続き `go tool pack` を呼び出して新規アーカイブを作成します。
    
    
  9. gccgoToolchain.gc および swigIntSizegc 呼び出しの変更:

    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -1650,7 +1764,7 @@ func (gccgoToolchain) linker() string {\n     	return gccgoBin\n     }\n     \n    -func (gccgoToolchain) gc(b *builder, p *Package, obj string, importArgs []string, gofiles []string) (ofile string, output []byte, err error) {\n    +func (gccgoToolchain) gc(b *builder, p *Package, archive, obj string, importArgs []string, gofiles []string) (ofile string, output []byte, err error) {\n     	out := p.Name + ".o"\n     	ofile = obj + out\n     	gcargs := []string{"-g"}\
    
    --- a/src/cmd/go/build.go
    +++ b/src/cmd/go/build.go
    @@ -2220,7 +2334,7 @@ func (b *builder) swigIntSize(obj string) (intsize string, err error) {\n     \n     	p := goFilesPackage(srcs)\n     \n    -	if _, _, e := buildToolchain.gc(b, p, obj, nil, srcs); e != nil {\n    +	if _, _, e := buildToolchain.gc(b, p, "", obj, nil, srcs); e != nil {\n     		return "32", nil\n     	}\n     	return "64", nil
    

    これらの箇所でも gc メソッドのシグネチャ変更に合わせて引数が更新されています。swigIntSize の場合は、アーカイブを生成しないため、空文字列 "" が渡されています。

コアとなるコードの解説

このコミットの最も重要な変更は、gcToolchain.pack メソッドのロジック変更と、新しく導入された packInternal 関数です。

gcToolchain.pack の新しいロジック

以前は、Go以外のオブジェクトファイル(Cやアセンブリ)をアーカイブに追加する際も、常に go tool pack を呼び出していました。このコミットでは、まず os.Stat(absAfile) を使って、最終的なアーカイブファイル (.a ファイル) が既に存在するかどうかを確認します。

  • アーカイブが既に存在する場合 (appending = true): これは、Goコンパイラが既に -pack オプションを使ってGoのオブジェクトを含むアーカイブを生成済みであることを意味します。この場合、go tool pack を呼び出す代わりに、新しく定義された packInternal 関数が呼び出されます。packInternal は、Go以外のオブジェクトファイル (ofiles) を、既存のアーカイブ (absAfile) に直接追記します。これにより、外部ツールへの依存が回避されます。

  • アーカイブがまだ存在しない場合: これは、Goソースファイルが全くなく、Cやアセンブリファイルのみで構成されるパッケージの場合などに発生します。この場合、Goコンパイラはアーカイブを生成していないため、go tool pack コマンドが呼び出され、新規にアーカイブが作成されます。

packInternal 関数の詳細

packInternal 関数は、Go言語でARアーカイブフォーマットの簡易的な追記処理を実装しています。

  1. アーカイブファイルのオープン:

    dst, err := os.OpenFile(afile, os.O_WRONLY|os.O_APPEND, 0)
    

    os.OpenFileos.O_APPEND フラグ付きで呼び出すことで、既存のアーカイブファイルの末尾にデータを追記できるようにします。

  2. オブジェクトファイルの追記ループ: for _, ofile := range ofiles ループで、追記対象の各オブジェクトファイルについて以下の処理を行います。

    • オブジェクトファイルのオープンと情報取得:

      src, err := os.Open(ofile)
      fi, err := src.Stat()
      

      追記するオブジェクトファイルを開き、os.Stat() でファイルサイズやファイル名などのメタデータを取得します。

    • ARヘッダの生成:

      name := fi.Name()
      if len(name) > 16 {
          name = name[:16]
      } else {
          name += strings.Repeat(" ", 16-len(name))
      }
      size := fi.Size()
      fmt.Fprintf(w, "%s%-12d%-6d%-6d%-8o%-10d`\\n",
          name, 0, 0, 0, 0644, size)
      

      ARアーカイブフォーマットでは、各ファイルの内容の前に固定長のヘッダが付きます。このヘッダには、ファイル名(16バイトにパディング)、ファイルサイズ、パーミッションなどの情報が含まれます。packInternal は、これらの情報を整形して書き込みます。特に、ファイル名が16バイトを超える場合は切り詰め、短い場合はスペースでパディングします。

    • オブジェクトファイル内容のコピー:

      n, err := io.Copy(w, src)
      

      io.Copy を使って、オブジェクトファイルの内容をアーカイブファイルに直接コピーします。

    • パディングの処理:

      if size&1 != 0 {
          w.WriteByte(0)
      }
      

      ARアーカイブフォーマットの慣例として、各ファイルのエントリは偶数バイト境界で終わる必要があります。オブジェクトファイルのサイズが奇数の場合、1バイトのヌル文字 (0) を追記してパディングします。

この packInternal 関数により、Goツールチェインは go tool pack に頼ることなく、Go以外のオブジェクトファイルを既存のアーカイブに効率的に追記できるようになりました。これは、Goツールチェインの自己完結性を高め、将来的な cmd/pack のGo言語での再実装への道を開く重要な一歩です。

関連リンク

  • Go Issue/Change List (CL):
    • https://golang.org/cl/42910043 - このコミットに対応するGoのChange Listページです。詳細なレビューコメントや変更履歴を確認できます。

参考にした情報源リンク

  • Go言語の公式ドキュメント: Goのビルドプロセスやツールチェインに関する一般的な情報。
  • AR (ファイルフォーマット): Wikipediaやその他の技術文書でARアーカイブフォーマットの仕様を確認しました。packInternal の実装がこのフォーマットに準拠していることを理解するために必要でした。
  • Goのソースコード: src/cmd/go/build.go および関連ファイルの変更履歴とコメント。
  • Goのメーリングリストやフォーラム: 過去の議論で go tool pack の役割やGoツールチェインの進化に関する情報。
  • Goのビルドシステムに関するブログ記事や解説: Goのビルドプロセスの内部動作を解説している記事。
    • 例: "Inside the Go build system" (特定の記事を指すものではなく、一般的な情報源として)