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

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

このコミットは、Go言語の公式ツールチェインにおいて、go buildgo installgo run コマンドの内部実装を大幅に刷新し、統合されたビルドシステムを導入するものです。これにより、従来の goinstall コマンドが置き換えられ、ビルドプロセスにおける中間ファイルの管理が改善され、一時ディレクトリが活用されるようになります。また、go clean コマンドが廃止され、ビルドのたびに一時ファイルが自動的にクリーンアップされる設計へと変更されました。

コミット

commit 2ad8a9c507ede9621bb1cd1f8d02f6cdac7a9e88
Author: Russ Cox <rsc@golang.org>
Date:   Wed Dec 14 22:42:42 2011 -0500

    go: implement build, install, run
    
    clean is gone; all the intermediate files are created
    in a temporary tree that is wiped when the command ends.
    
    Not using go/build's Script because it is not well aligned
    with this API.  The various builder methods are copied from
    go/build and adapted.  Probably once we delete goinstall
    we can delete the Script API too.
    
    R=rogpeppe, adg, adg
    CC=golang-dev
    https://golang.org/cl/5483069

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

https://github.com/golang/go/commit/2ad8a9c507ede9621bb1cd1f8d02f6cdac7a9e88

元コミット内容

go: implement build, install, run

clean is gone; all the intermediate files are created
in a temporary tree that is wiped when the command ends.

Not using go/build's Script because it is not well aligned
with this API.  The various builder methods are copied from
go/build and adapted.  Probably once we delete goinstall
we can delete the Script API too.

R=rogpeppe, adg, adg
CC=golang-dev
https://golang.org/cl/5483069

変更の背景

このコミットが行われた2011年当時、Go言語のビルドシステムは進化の途上にありました。初期のGoツールチェインには、goinstall というコマンドが存在し、パッケージの取得、ビルド、インストールを一手に担っていました。しかし、goinstall はその機能が多岐にわたり、柔軟性に欠ける部分がありました。

また、Goのビルドプロセスでは、コンパイル済みオブジェクトファイルやCgoによって生成される中間ファイルなど、多くの「中間ファイル」が生成されます。これらのファイルは通常、ビルドが完了すれば不要になりますが、明示的に削除しない限りディスク上に残り続ける可能性がありました。従来の go clean コマンドはこれらのファイルを削除する役割を担っていましたが、ユーザーが手動で実行する必要がありました。

このコミットの主な動機は以下の点に集約されます。

  1. ビルドプロセスの統合と合理化: goinstall の機能を go buildgo installgo run というより明確な役割を持つコマンドに分割し、それぞれのコマンドが内部で共通のビルドロジックを使用するように再設計すること。
  2. 中間ファイルの自動クリーンアップ: ビルドプロセス中に生成される一時ファイルを、コマンドの終了時に自動的に削除するメカニズムを導入し、ディスクスペースの管理を簡素化すること。これにより、go clean コマンドの必要性をなくすことが目指されました。
  3. go/build パッケージの Script APIからの脱却: 既存の go/build パッケージが提供する Script APIが、新しいビルドシステムの要件と合致しないと判断され、より柔軟で制御性の高い内部ビルダの実装へと移行すること。これは、Goツールチェインが自身のビルドプロセスをより細かく制御し、将来的な拡張性を確保するための重要なステップでした。

前提知識の解説

このコミットを理解するためには、以下のGo言語のビルドシステムに関する基本的な知識が必要です。

  • go build: Goソースコードを実行可能なバイナリにコンパイルするコマンドです。通常、カレントディレクトリのパッケージをコンパイルし、実行ファイルを同じディレクトリに生成します。
  • go install: go build と同様にGoパッケージをコンパイルしますが、生成された実行ファイルやパッケージアーカイブを $GOPATH/bin (または $GOBIN) や $GOPATH/pkg (または $GOPKG) にインストールします。これにより、システム全体のPATHから実行可能になります。
  • go run: 指定されたGoソースファイルをコンパイルし、すぐに実行するコマンドです。一時的な実行やスクリプトのような利用に適しています。
  • go clean: 以前は、Goのビルドプロセスによって生成された中間ファイルやキャッシュファイルを削除するためのコマンドでした。このコミットによってその役割が変更され、最終的には廃止されました。
  • go/build パッケージ: Go言語の標準ライブラリの一部で、Goのビルド環境に関する情報(GOPATH、GOROOTなど)を提供し、パッケージの解決やソースファイルの走査を行うためのAPIを提供します。このコミット以前は、ビルドスクリプトの生成など、より高レベルなビルド操作も担っていました。
  • Cgo: GoプログラムからC言語のコードを呼び出すためのメカニズムです。Cgoを使用すると、GoとCの間のインターフェースコードや、Cコードのコンパイル済みオブジェクトファイルが生成されます。
  • 一時ディレクトリ (Temporary Directory): オペレーティングシステムが提供する、一時的なファイルを保存するためのディレクトリです。通常、システムのリブート時や、アプリケーションの終了時に自動的にクリーンアップされます。このコミットでは、ビルドプロセス中に生成されるすべての中間ファイルをこの一時ディレクトリに配置し、コマンド終了時に自動的に削除する戦略が採用されています。

技術的詳細

このコミットの核心は、src/cmd/go/build.go に導入された新しいビルドエンジンです。主要な概念とメカニズムは以下の通りです。

  1. builder 構造体:

    • ビルドプロセス全体のグローバルな状態を保持します。これには、一時作業ディレクトリ (work)、各種フラグ (aflag, nflag, vflag)、アーキテクチャ情報 (arch)、そしてアクションキャッシュ (actionCache) が含まれます。
    • init メソッドで初期化され、一時ディレクトリの作成と、コマンド終了時のクリーンアップ (atexit を使用) を設定します。
  2. action 構造体とアクショングラフ:

    • ビルドプロセスにおける単一の操作(例:パッケージのビルド、インストール)を表します。
    • f: 実行される関数((*builder, *action) error 型)。
    • p: このアクションが対象とする *Package
    • deps: このアクションが実行される前に完了する必要がある依存アクションのリスト。
    • done, failed: アクションの状態を示すフラグ。
    • pkgobj, pkgbin: ビルド結果のファイルパス(.a ファイルや実行可能バイナリ)。
    • ビルドプロセスは、これらの action オブジェクトが相互に依存関係を持つ「アクショングラフ」として表現されます。builder.do メソッドがこのグラフを深さ優先探索のように実行し、依存関係が解決された順にアクションを実行します。
  3. ビルドモード (buildMode):

    • modeBuild: パッケージをビルドするが、インストールはしないモード。
    • modeInstall: パッケージをビルドし、インストールも行うモード。
    • builder.action メソッドは、これらのモードとパッケージ情報に基づいて、適切なアクションを生成し、キャッシュします。
  4. 一時ディレクトリの活用:

    • builder.work には、ビルド中に生成されるすべての中間ファイル(Cgoの出力、コンパイル済みオブジェクト、アーカイブなど)が格納される一時ディレクトリのパスが設定されます。
    • コマンド終了時に atexit フックによってこの一時ディレクトリ全体が削除されるため、明示的な go clean コマンドが不要になります。
  5. ビルドステップの抽象化:

    • builder 構造体には、cgo (Cgoファイルの処理)、gc (Goコンパイラ)、asm (アセンブラ)、gopack (アーカイブ作成)、ld (リンカ)、cc (Cコンパイラ)、gcc (GCCコンパイラ)、gccld (GCCリンカ) といった、Goのビルドプロセスを構成する各ステップを実行するためのメソッドが用意されています。
    • これらのメソッドは、適切なコマンドライン引数を構築し、builder.run メソッドを通じて外部コマンド(6g, 6a, 6l, cgo, gcc など)を実行します。
  6. go/build パッケージとの関係:

    • コミットメッセージにあるように、この新しいビルドシステムは go/build パッケージの Script APIを直接使用していません。代わりに、go/build パッケージはパッケージ情報のスキャン (build.ScanDir) や環境情報の取得 (build.DefaultContext) など、より低レベルな情報提供の役割に特化しています。
    • builder 内の多くのメソッドは、go/build の内部ロジックをコピーし、新しいAPIに合わせて適応させたものです。これは、Goツールチェインが自身のビルドプロセスを完全に制御するための設計判断です。
  7. go run の実装:

    • src/cmd/go/run.go に新しく追加された go run コマンドは、内部的にこの新しい builder を利用して、指定されたGoファイルをビルドし、その場で実行します。実行後、生成されたバイナリは一時ディレクトリと共に削除されます。

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

このコミットによる主要なコード変更は以下のファイルに集中しています。

  • src/cmd/cgo/gcc.go: gccTmp 変数が関数 gccTmp() に変更され、一時オブジェクトファイルのパスが動的に生成されるようになりました。これにより、Cgoが生成する中間ファイルも一時ディレクトリの管理下に置かれるようになります。
  • src/cmd/cgo/main.go: objDir フラグが導入され、Cgoが中間ファイルを書き込むディレクトリを外部から指定できるようになりました。これにより、GoツールチェインのビルドシステムがCgoの出力先を制御できるようになります。
  • src/cmd/cgo/out.go: objDir 変数が *objDir フラグを参照するように変更され、Cgoが生成する各種ファイル(_cgo_gotypes.go, _cgo_defun.c など)の出力先が、Goツールチェインによって指定された一時ディレクトリになるように修正されました。また、dynimport 関数において、dynout フラグが指定された場合に標準出力ではなくファイルに書き出す機能が追加されました。
  • src/cmd/go/Makefile: clean.go がビルド対象から削除され、run.go が追加されました。これは go clean コマンドの廃止と go run コマンドの導入を反映しています。
  • src/cmd/go/build.go: このコミットの最も重要な変更点です。 go buildgo install コマンドの新しい実装が追加されました。builder 構造体、action 構造体、そしてビルドの各ステップ(Cgoの実行、Goコンパイル、アセンブル、アーカイブ作成、リンクなど)を管理する多数のヘルパー関数が定義されています。これにより、Goのビルドプロセスが完全に内部で制御されるようになります。
  • src/cmd/go/clean.go: ファイル自体が削除されました。 これは go clean コマンドが廃止されたことを意味します。
  • src/cmd/go/list.go: go list -json の出力が json.MarshalIndent を使用して整形されるように変更されました。
  • src/cmd/go/main.go: commands リストから cmdClean が削除され、cmdRun が追加されました。また、fatalfexitIfErrors といったエラー処理関数が atexit 関数(プログラム終了時に実行されるクリーンアップ関数を登録するメカニズム)を利用するように変更され、一時ディレクトリの自動削除を保証するようになりました。allPackages 関数が追加され、go build all のようなコマンドで全てのパッケージを対象にできるようになりました。
  • src/cmd/go/pkg.go: loadPackage および scanPackage 関数が変更され、パッケージのターゲットパス (p.targ) の決定ロジックが追加されました。これにより、ビルドされたパッケージや実行ファイルの出力先がより正確に管理されるようになります。
  • src/cmd/go/run.go: 新しく追加されたファイルです。 go run コマンドの実装が含まれており、builder を利用してGoソースファイルをビルドし、実行するロジックが記述されています。

コアとなるコードの解説

このコミットの核となるのは、src/cmd/go/build.go に実装された builder 構造体とその関連メソッドです。

builder 構造体

type builder struct {
	work        string               // the temporary work directory (ends in filepath.Separator)
	aflag       bool                 // the -a flag
	nflag       bool                 // the -n flag
	vflag       bool                 // the -v flag
	arch        string               // e.g., "6"
	actionCache map[cacheKey]*action // a cache of already-constructed actions
}

builder は、ビルドプロセス全体の状態を管理する中心的なオブジェクトです。work フィールドは、ビルド中に生成されるすべての中間ファイルが格納される一時ディレクトリのパスを保持します。このディレクトリは、ビルドコマンドの終了時に自動的に削除されます。aflag, nflag, vflag は、それぞれ go buildgo install コマンドに渡される -a (強制再ビルド)、-n (コマンド表示のみ)、-v (詳細表示) フラグの状態を保持します。actionCache は、既に構築されたビルドアクションをキャッシュし、重複する作業を避けるために使用されます。

action 構造体

type action struct {
	f func(*builder, *action) error // the action itself

	p      *Package  // the package this action works on
	deps   []*action // actions that must happen before this one
	done   bool      // whether the action is done (might have failed)
	failed bool      // whether the action failed

	// Results left for communication with other code.
	pkgobj string // the built .a file
	pkgbin string // the built a.out file, if one exists
}

action は、ビルドグラフのノードを表します。各 action は特定のパッケージ (p) に対して実行される関数 (f) を持ち、他の action (deps) に依存することができます。この依存関係によって、ビルドの順序が決定されます。pkgobjpkgbin は、ビルドされたパッケージアーカイブ (.a ファイル) や実行可能バイナリのパスを保持し、後続のアクションやユーザーへの結果として利用されます。

builder.do(a *action)

func (b *builder) do(a *action) {
	if a.done {
		return
	}
	for _, a1 := range a.deps {
		b.do(a1)
		if a1.failed {
			a.failed = true
			a.done = true
			return
		}
	}
	if err := a.f(b, a); err != nil {
		errorf("%s", err)
		a.failed = true
	}
	a.done = true
}

do メソッドは、アクショングラフを再帰的に実行する中心的なロジックです。まず、現在のアクションが既に完了しているか (a.done) を確認します。次に、全ての子依存アクション (a.deps) を再帰的に実行します。依存アクションのいずれかが失敗した場合、現在のアクションも失敗としてマークされ、処理を中断します。全ての依存アクションが成功した場合、現在のアクションの関数 a.f を実行します。これにより、ビルドの依存関係が適切に解決され、順序通りに処理が進められます。

builder.build(a *action) error

func (b *builder) build(a *action) error {
	obj := filepath.Join(b.work, filepath.FromSlash(a.p.ImportPath+"/_obj")) + string(filepath.Separator)
	a.pkgobj = filepath.Join(b.work, filepath.FromSlash(a.p.ImportPath+".a"))

	// make build directory
	if err := b.mkdir(obj); err != nil {
		return err
	}

	var objects []string
	var gofiles []string
	gofiles = append(gofiles, a.p.GoFiles...)

	// run cgo
	if len(a.p.CgoFiles) > 0 {
		outGo, outObj, err := b.cgo(a.p.Dir, obj, a.p.info)
		if err != nil {
			return err
		}
		objects = append(objects, outObj...)
		gofiles = append(gofiles, outGo...)
	}

	// prepare Go import path list
	var inc []string
	inc = append(inc, "-I", b.work)
	incMap := map[string]bool{}
	for _, a1 := range a.deps {
		p1 := a1.p
		if p1.t.Goroot {
			continue
		}
		pkgdir := p1.t.PkgDir()
		if !incMap[pkgdir] {
			incMap[pkgdir] = true
			inc = append(inc, "-I", pkgdir)
		}
	}

	// compile Go
	if len(gofiles) > 0 {
		out := "_go_.6"
		if err := b.gc(a.p.Dir, obj+out, a.p.ImportPath, inc, gofiles); err != nil {
			return err
		}
		objects = append(objects, out)
	}

	// assemble .s files
	if len(a.p.SFiles) > 0 {
		for _, sfile := range a.p.SFiles {
			out := sfile[:len(sfile)-len(".s")] + "." + b.arch
			if err := b.asm(a.p.Dir, obj+out, sfile); err != nil {
				return err
			}
			objects = append(objects, out)
		}
	}

	// pack into archive
	if err := b.gopack(obj, a.pkgobj, objects); err != nil {
		return err
	}

	if a.p.Name == "main" {
		// command.
		// import paths for compiler are introduced by -I.
		// for linker, they are introduced by -L.
		for i := 0; i < len(inc); i += 2 {
			inc[i] = "-L"
		}
		a.pkgbin = obj + "a.out"
		if err := b.ld(a.p.Dir, a.pkgbin, inc, a.pkgobj); err != nil {
			return err
		}
	}

	return nil
}

build メソッドは、単一のGoパッケージをビルドするための具体的な手順を定義しています。

  1. 一時ディレクトリの準備: パッケージ固有の一時オブジェクトディレクトリ (obj) を作成します。
  2. Cgoの実行: パッケージにCgoファイルが含まれている場合、builder.cgo メソッドを呼び出してCgoを実行し、GoとCの中間ファイルを生成します。
  3. Goコンパイル: builder.gc メソッドを呼び出してGoソースファイル(Cgoによって生成されたGoファイルも含む)をコンパイルし、オブジェクトファイルを生成します。依存パッケージのインポートパスも適切に設定されます。
  4. アセンブル: アセンブリ言語のソースファイル (.s ファイル) がある場合、builder.asm メソッドを呼び出してアセンブルし、オブジェクトファイルを生成します。
  5. アーカイブ作成: 生成された全てのオブジェクトファイルを builder.gopack メソッドでパッケージアーカイブ (.a ファイル) にまとめます。
  6. リンク (mainパッケージの場合): パッケージが main パッケージ(実行可能プログラム)の場合、builder.ld メソッドを呼び出して、アーカイブと依存ライブラリをリンクし、最終的な実行可能バイナリ (a.out) を生成します。

このメソッドは、Goのビルドプロセスにおける各段階を明確に分離し、それぞれを専用のヘルパーメソッドに委譲することで、コードの可読性と保守性を高めています。

builder.install(a *action) error

func (b *builder) install(a *action) error {
	if err := b.build(a); err != nil {
		return err
	}

	var src string
	var perm uint32
	if a.pkgbin != "" {
		src = a.pkgbin
		perm = 0777
	} else {
		src = a.pkgobj
		perm = 0666
	}

	// make target directory
	dst := a.p.targ
	dir, _ := filepath.Split(dst)
	if dir != "" {
		if err := b.mkdir(dir); err != nil {
			return err
		}
	}

	return b.copyFile(dst, src, perm)
}

install メソッドは、パッケージをビルドした後、それを適切なインストール先にコピーする役割を担います。まず builder.build を呼び出してパッケージをビルドします。その後、ビルドされたバイナリ (pkgbin) またはパッケージアーカイブ (pkgobj) を、パッケージのターゲットパス (a.p.targ) にコピーします。この際、必要に応じてターゲットディレクトリを作成し、適切なファイルパーミッションを設定します。

atexit とクリーンアップ

src/cmd/go/main.go に導入された atexit 関数と exit 関数は、プログラムが終了する際に登録されたクリーンアップ関数を確実に実行するためのメカニズムです。

var atexitFuncs []func()

func atexit(f func()) {
	atexitFuncs = append(atexitFuncs, f)
}

func exit() {
	for _, f := range atexitFuncs {
		f()
	}
	os.Exit(exitStatus)
}

builder.init メソッド内で atexit(func() { os.RemoveAll(b.work) }) が呼び出されることで、ビルドコマンドが正常終了するか、エラーで終了するかにかかわらず、一時作業ディレクトリが確実に削除されるようになります。これにより、go clean コマンドが不要となり、ビルドプロセスがよりクリーンになりました。

これらの変更により、Goツールチェインは、ビルドプロセスをより細かく制御し、中間ファイルの管理を自動化し、ユーザーエクスペリエンスを向上させるための強固な基盤を確立しました。

関連リンク

参考にした情報源リンク