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

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

このコミットは、Go言語の初期のビルドツールである gobuild の大幅な機能改善と、それに伴う標準ライブラリの Makefile 群の更新を目的としています。主な変更点は、gobuild がディレクトリ内の複数のパッケージを扱えるようになったこと、引数なしで実行された場合にソースファイルを自動的にスキャンするようになったこと、パッケージ名を自動で推論する機能が追加されたこと、そして gotest を呼び出すテストルールが組み込まれたことです。これにより、Goプロジェクトのビルドプロセスがより柔軟かつ自動化されました。

コミット

commit 360151d4e2b3990db67555a8c61b1e581294fc44
Author: Russ Cox <rsc@golang.org>
Date:   Tue Nov 18 17:11:56 2008 -0800

    gobuild changes.
            * handles multiple packages per directory
            * scans directory for files if given no arguments
            * infers package name
            * includes test rule invoking gotest
    
    R=r
    DELTA=746  (444 added, 150 deleted, 152 changed)
    OCL=19504
    CL=19521

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

https://github.com/golang/go/commit/360151d4e2b3990db67555a8c61b1e581294fc44

元コミット内容

gobuild changes. * handles multiple packages per directory * scans directory for files if given no arguments * infers package name * includes test rule invoking gotest

変更の背景

このコミットが行われた2008年11月は、Go言語がまだ一般に公開される前の初期開発段階でした。当時のGoのビルドシステムは現在とは大きく異なり、gobuild のようなツールが個々のパッケージのビルドを管理していました。

このコミットの背景には、以下のような課題があったと考えられます。

  1. ビルドの柔軟性の欠如: 以前の gobuild は、ビルド対象のソースファイルを明示的に指定する必要がありました。これは、特に多数のファイルや複数のパッケージを含むディレクトリを扱う際に、手動での管理が煩雑になる原因となっていました。
  2. パッケージ管理の非効率性: 1つのディレクトリに複数のGoパッケージが存在する場合、gobuild がそれらを適切に処理できない、または手動での介入が必要となる制約がありました。
  3. ビルドプロセスの自動化不足: パッケージ名の推論やテストの自動実行といった機能が不足しており、開発者が手動で多くのビルドステップを実行する必要がありました。
  4. Makefileの複雑性: 各パッケージの Makefilegobuild の呼び出し方やアーカイブの管理について多くの詳細を記述する必要があり、冗長性や保守性の問題が生じていました。

これらの課題を解決し、Go言語のビルドシステムをより堅牢で使いやすく、自動化されたものにするために、gobuild の機能拡張が不可欠でした。このコミットは、その初期の重要なステップの一つと言えます。

前提知識の解説

このコミットを理解するためには、以下の技術的背景知識が役立ちます。

  1. Go言語の初期のビルドシステム:

    • gobuild: Go言語の初期に存在したビルドツールの一つで、Goソースコードをコンパイルし、アーカイブ(.a ファイル)を作成する役割を担っていました。現在の go build コマンドの前身のようなものです。
    • gotest: Go言語の初期のテスト実行ツールで、現在の go test コマンドに相当します。テストファイルをコンパイルし、実行する機能を提供していました。
    • 6g, 6c, 6a, 6l: Go言語の初期のコンパイラ、アセンブラ、アーカイバ、リンカのコマンド名です。6 は当時のGoが主にターゲットとしていた64ビットアーキテクチャ(amd64)を指します。
      • 6g: Goコンパイラ
      • 6c: Cコンパイラ(Goのランタイムや一部のライブラリはCで書かれていたため)
      • 6a: アセンブラ
      • 6l: リンカ
      • 6ar: アーカイバ(ar コマンドのGo版)
    • Makefile: make ユーティリティが使用するビルドスクリプト。依存関係に基づいてコマンドを実行し、プロジェクトのビルドを自動化します。Goの初期では、各パッケージに Makefile が存在し、gobuild を呼び出してビルドを行っていました。
  2. Unix/Linuxの基本的なコマンドと概念:

    • fork() / exec() / waitfor(): プロセス管理のためのシステムコール。gobuild が外部コマンド(コンパイラ、アーカイバなど)を実行するために使用します。
    • dup(): ファイルディスクリプタを複製するシステムコール。標準入出力のリダイレクトなどに使用されます。
    • sysfatal(): エラーが発生した場合にプログラムを終了させるための関数。
    • Biobuf: バッファリングされたI/Oを扱うための構造体。
    • smprint(): 文字列をフォーマットして新しい文字列を返す関数。
    • getenv(): 環境変数の値を取得する関数。GOROOT, GOOS, GOARCH などのGo関連の環境変数が使用されます。
    • unlink(): ファイルを削除するシステムコール。
    • ar (archiver): オブジェクトファイルやライブラリファイルをアーカイブ(.a ファイル)にまとめるためのユーティリティ。
  3. Go言語のパッケージ構造:

    • Goのソースファイルは package 宣言によってどのパッケージに属するかを定義します。
    • Goのパッケージは通常、ディレクトリに対応しますが、このコミット以前は1つのディレクトリに複数のパッケージを置くことが gobuild で直接サポートされていなかった可能性があります。

これらの知識は、コミットのコード変更がGoのビルドプロセスにどのように影響するか、そしてなぜこれらの変更が必要とされたのかを深く理解する上で重要です。

技術的詳細

このコミットの技術的詳細は、主に src/cmd/gobuild/gobuild.c の変更に集約されています。

  1. gobuild の引数処理の変更:

    • 以前の gobuild は、gobuild [-m] packagename *.go *.c *.s のように、ビルド対象のパッケージ名とソースファイルを明示的に指定する必要がありました。
    • 変更後、usage メッセージが gobuild [-m] [packagename...] となり、引数なしで実行された場合(argc == 0 の場合)に sourcefilenames 関数を呼び出してカレントディレクトリ内の .go, .c, .s ファイルを自動的にスキャンするようになりました。これにより、ビルドの自動化が促進されます。
  2. メモリ管理ヘルパーの追加:

    • emallocerealloc という関数が追加されました。これらは mallocrealloc のラッパーで、メモリ確保に失敗した場合に sysfatal を呼び出してプログラムを終了させます。これにより、メモリ管理がより堅牢になります。
  3. ar (アーカイバ) 呼び出しの抽象化:

    • ar という新しい関数が追加されました。これは、指定されたパッケージ名とファイルリストを受け取り、6ar コマンドを呼び出してオブジェクトファイルをアーカイブ(.a ファイル)に追加する処理をカプセル化します。これにより、アーカイブ操作のコードが整理され、再利用性が向上します。
  4. パッケージ名の自動推論 (getpkg 関数):

    • getpkg という重要な関数が追加されました。この関数は .go ファイルを読み込み、その内容から package 宣言を解析してパッケージ名を抽出します。
    • 抽出されたパッケージ名は内部の pkg リストに保存され、重複がなければ追加されます。これにより、gobuild はソースファイルから自動的にパッケージ名を推論し、1つのディレクトリに複数のGoパッケージが存在する場合でも適切に処理できるようになりました。
  5. ビルドプロセスのリファクタリング:

    • main 関数内のビルドロジックが大幅に書き換えられました。以前は単純な「繰り返しパス」方式でしたが、新しい実装では pending, fail, success という3つのジョブリストを使用して、依存関係が解決されるまでコンパイルを試行し続ける、より洗練されたビルドスケジューリングが行われるようになりました。
    • 各パスでコンパイルが成功したファイルは、対応するパッケージのアーカイブに追加され、その後オブジェクトファイルは削除されます。
    • コンパイルが失敗したファイルは次のパスで再試行されます。これにより、循環依存などの問題がある場合でも、可能な限り多くのファイルをビルドしようとします。
  6. Makefile 生成ロジックの分離と強化 (writemakefile 関数):

    • Makefile の生成ロジックが main 関数から writemakefile という独立した関数に切り出されました。
    • 生成される Makefile は、複数のパッケージをサポートするように変更されました。
    • 新しい Makefile には以下の重要なルールが追加されました。
      • default: packages: デフォルトのターゲットが packages となり、すべてのパッケージをビルドするようになりました。
      • test: packages: gotest を呼び出す test ルールが追加されました。これにより、make test で簡単にテストを実行できるようになります。
      • install: packages: packages ターゲットに依存し、ビルドされたすべてのパッケージアーカイブを $(GOROOT)/pkg ディレクトリにコピーする install ルールが追加されました。
      • nuke: clean: clean に依存し、$(GOROOT)/pkg ディレクトリからすべてのパッケージアーカイブを削除する nuke ルールが追加されました。
    • オブジェクトファイルの依存関係 ($(O1): newpkg, $(O2): a1 など) も、複数パッケージとパスベースのビルドに対応するように更新されました。
  7. gotest スクリプトの改善:

    • src/cmd/gotest/gotest シェルスクリプトは、コマンドライン引数の解析をより堅牢に行うようになりました。
    • test*.go ファイルを自動的に検出するロジックが改善されました。
    • テスト実行後の一時ファイルのクリーンアップのために trap コマンドが追加され、スクリプトの堅牢性が向上しました。
  8. 標準ライブラリの Makefile の更新:

    • src/lib/*/Makefile ファイル群は、gobuild の変更に合わせて簡素化されました。
    • 以前は gobuild -m <pkgname> <files> のように gobuild の呼び出し方がコメントに記載されていましたが、これが gobuild -m >Makefile に変更されました。これは、gobuildMakefile 全体を生成するようになったことを示しています。
    • Makefile から PKG, PKGDIR, install, nuke の定義が削除され、gobuild が生成する共通のルールに依存するようになりました。
    • default: packagestest: packages ルールが追加され、ビルドとテストの実行方法が統一されました。
    • アーカイブ作成のルール (a1:, a2: など) も、gobuild が生成する形式に合わせて、$(AR) grc <pkgname>.a <object_files> のように変更されました。

これらの変更により、gobuild は単なるコンパイラのラッパーから、Goプロジェクトのビルドとテストを自動化する、よりインテリジェントなツールへと進化しました。

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

このコミットのコアとなる変更は、主に src/cmd/gobuild/gobuild.c に集中しています。

  1. src/cmd/gobuild/gobuild.c:

    • usage 関数の変更 (行 14-15): gobuild の使用方法の表示が簡素化され、引数なしでの実行を許容するようになりました。
    • 新しいヘルパー関数 emalloc, erealloc の追加 (行 29-45): メモリ確保の安全性を高めるためのユーティリティ。
    • 新しい ar 関数の追加 (行 94-115): アーカイブ操作を抽象化。
    • 新しい getpkg 関数の追加 (行 137-192): Goソースファイルからパッケージ名を自動推論するロジック。
    • writemakefile 関数の追加 (行 201-300): Makefile 生成ロジックを分離し、複数パッケージ対応、test ルール追加など。
    • sourcefilenames 関数の追加 (行 302-323): カレントディレクトリ内のソースファイルを自動検出。
    • main 関数の大幅な変更 (行 325-454):
      • 引数なしの場合のソースファイル自動検出。
      • ビルドジョブの管理 (job, pending, fail, success リスト)。
      • 複数パスでのコンパイルとアーカイブ処理のロジック。
      • makefile フラグが設定されている場合に writemakefile を呼び出す。
  2. src/cmd/gotest/gotest:

    • 引数解析ロジックの変更 (行 10-25): gofiles の設定方法が改善され、引数がない場合に test*.go を自動検出。
    • trap コマンドの追加 (行 38): 一時ファイルのクリーンアップ。
  3. src/lib/*/Makefile ファイル群:

    • src/lib/fmt/Makefile (行 3-60) を含む、Go標準ライブラリの各パッケージの Makefile が変更されています。
    • # gobuild -m ... コメントの変更 (例: gobuild -m fmt format.go print.go から gobuild -m >Makefile へ)。
    • PKG, PKGDIR, install, nuke ルールの削除または簡素化。
    • default: packages および test: packages ルールの追加。
    • アーカイブ作成ルール (a1:, a2: など) の変更。

これらの変更は、Goのビルドシステムの中核部分に影響を与え、その後のGo開発の基盤を築く上で重要な役割を果たしました。

コアとなるコードの解説

src/cmd/gobuild/gobuild.c の主要な変更点

getpkg 関数

char*
getpkg(char *file)
{
	Biobuf *b;
	char *p, *q;
	int i;

	if(!suffix(file, ".go"))
		return nil;
	if((b = Bopen(file, OREAD)) == nil)
		sysfatal("open %s: %r", file);
	while((p = Brdline(b, '\n')) != nil) {
		p[Blinelen(b)-1] = '\0';
		while(*p == ' ' || *p == '\t')
			p++;
		if(strncmp(p, "package", 7) == 0 && (p[7] == ' ' || p[7] == '\t')) {
			p+=7;
			while(*p == ' ' || *p == '\t')
				p++;
			q = p+strlen(p);
			while(q > p && (*(q-1) == ' ' || *(q-1) == '\t'))
				*--q = '\0';
			for(i=0; i<npkg; i++) {
				if(strcmp(pkg[i], p) == 0) {
					Bterm(b);
					return pkg[i];
				}
			}
			npkg++;
			pkg = erealloc(pkg, npkg*sizeof pkg[0]);
			pkg[i] = emalloc(strlen(p)+1);
			strcpy(pkg[i], p);
			Bterm(b);
			return pkg[i];
		}
	}
	Bterm(b);
	return nil;
}

この関数は、Goソースファイル (.go 拡張子を持つファイル) を読み込み、そのファイルが属するパッケージ名を特定します。

  1. ファイルが .go ファイルでない場合、または開けない場合は nil を返します。
  2. ファイルを1行ずつ読み込みます。
  3. 各行の先頭の空白をスキップします。
  4. 行が "package" で始まり、その後に空白が続く場合、それがパッケージ宣言であると判断します。
  5. "package" キーワードとそれに続く空白をスキップし、実際のパッケージ名を取得します。
  6. 取得したパッケージ名が既に pkg リスト(gobuild がこれまでに発見したパッケージ名のリスト)に存在するかを確認します。
  7. 存在すれば、既存のポインタを返します。
  8. 存在しなければ、新しいパッケージとして pkg リストに追加し、そのポインタを返します。 この機能により、gobuild は明示的な指定なしに、ディレクトリ内のGoソースファイルからパッケージ情報を自動的に抽出し、複数のパッケージを適切に管理できるようになりました。

writemakefile 関数

void
writemakefile(void)
{
	Biobuf bout;
	vlong o;
	int i, k, l, pass;
	char **obj;
	int nobj;

	// Write makefile.
	Binit(&bout, 1, OWRITE); // 標準出力に書き込むためのバッファを初期化
	Bprint(&bout, "# DO NOT EDIT.  Automatically generated by gobuild.\\n");
	// ... (コメント、preambleの出力) ...

	// O2=\\
	//	os_file.$O\\
	//	os_time.$O\\
	//
	obj = emalloc(njob*sizeof obj[0]);
	for(pass=0;; pass++) { // ビルドパスごとにオブジェクトファイルリストを生成
		nobj = 0;
		for(i=0; i<njob; i++)
			if(job[i].pass == pass)
				obj[nobj++] = goobj(job[i].name, "$O");
		if(nobj == 0)
			break;
		Bprint(&bout, "O%d=\\\\\n", pass+1); // O1, O2, ... 変数を定義
		for(i=0; i<nobj; i++)
			Bprint(&bout, "\\t%$\\\\\n", obj[i]);
		Bprint(&bout, "\\n");
	}

	// math.a: a1 a2
	for(i=0; i<npkg; i++) { // 各パッケージのアーカイブターゲットを定義
		Bprint(&bout, "%s.a:", pkg[i]);
		for(k=0; k<pass; k++)
			Bprint(&bout, " a%d", k+1);
		Bprint(&bout, "\\n");
	}
	Bprint(&bout, "\\n");

	// a1: $(O1)
	//	$(AR) grc $(PKG) $(O1)
	//	rm -f $(O1)
	for(k=0; k<pass; k++){ // 各パスのアーカイブ更新ルールを定義
		Bprint(&bout, "a%d:\\t$(O%d)\\n", k+1, k+1);
		for(i=0; i<npkg; i++) {
			nobj = 0;
			for(l=0; l<njob; l++)
				if(job[l].pass == k && job[l].pkg == pkg[i])
					obj[nobj++] = goobj(job[l].name, "$O");
			if(nobj > 0) {
				Bprint(&bout, "\\t$(AR) grc %s.a", pkg[i]); // 複数パッケージ対応
				for(l=0; l<nobj; l++)
					Bprint(&bout, " %$\", obj[l]);
				Bprint(&bout, "\\n");
			}
		}
		Bprint(&bout, "\\trm -f $(O%d)\\n", k+1);
		Bprint(&bout, "\\n");
	}

	// newpkg: clean
	//	6ar grc pkg.a
	Bprint(&bout, "newpkg: clean\\n"); // 新しいパッケージアーカイブを作成するルール
	for(i=0; i<npkg; i++)
		Bprint(&bout, "\\t$(AR) grc %s.a\\n", pkg[i]);
	Bprint(&bout, "\\n");

	// $(O1): newpkg
	// $(O2): a1
	Bprint(&bout, "$(O1): newpkg\\n"); // オブジェクトファイルの依存関係
	for(i=1; i<pass; i++)
		Bprint(&bout, "$(O%d): a%d\\n", i+1, i);
	Bprint(&bout, "\\n");

	// nuke: clean
	//	rm -f $(GOROOT)/pkg/xxx.a
	Bprint(&bout, "nuke: clean\\n"); // クリーンアップルール
	Bprint(&bout, "\\trm -f");
	for(i=0; i<npkg; i++)
		Bprint(&bout, " $(GOROOT)/pkg/%s.a", pkg[i]);
	Bprint(&bout, "\\n\\n");

	// packages: pkg.a
	//	rm -f $(GOROOT)/pkg/xxx.a
	Bprint(&bout, "packages:"); // すべてのパッケージをビルドするルール
	for(i=0; i<npkg; i++)
		Bprint(&bout, " %s.a", pkg[i]);
	Bprint(&bout, "\\n\\n");

	// install: packages
	//	cp xxx.a $(GOROOT)/pkg/xxx.a
	Bprint(&bout, "install: packages\\n"); // インストールルール
	for(i=0; i<npkg; i++)
		Bprint(&bout, "\\tcp %s.a $(GOROOT)/pkg/%s.a\\n", pkg[i], pkg[i]);
	Bprint(&bout, "\\n");

	Bterm(&bout);
}

この関数は、gobuild が実行時に動的に Makefile を生成するロジックを含んでいます。

  • # DO NOT EDIT. Automatically generated by gobuild. というコメントが先頭に付与され、手動編集を避けるよう促します。
  • ビルドのパス (pass) ごとにオブジェクトファイル (.O ファイル) のリスト (O1, O2, ...) を定義します。
  • 各パッケージのアーカイブファイル (.a ファイル) のターゲットを定義し、それがどのパスのオブジェクトファイルに依存するかを示します。
  • 各パスで生成されたオブジェクトファイルを対応するパッケージアーカイブに追加し、その後オブジェクトファイルを削除するルール (a1:, a2:, ...) を定義します。ここで $(AR) grc %s.a のように、複数パッケージに対応したアーカイブコマンドが生成されます。
  • newpkg ルールは、クリーンな状態から新しいパッケージアーカイブを作成します。
  • オブジェクトファイルの依存関係 ($(O1): newpkg, $(O2): a1 など) を定義し、ビルドの順序を制御します。
  • nuke ルールは、ビルドされたパッケージアーカイブを GOROOT から削除します。
  • packages ルールは、すべてのパッケージアーカイブをビルドする依存関係を定義します。
  • install ルールは、ビルドされたパッケージアーカイブを GOROOT/pkg にコピーします。
  • 特に重要なのは、default: packagestest: packages ルールが追加されたことです。これにより、生成される Makefile は、デフォルトでパッケージをビルドし、make test でテストを実行できるようになります。

この writemakefile 関数の導入により、各Goパッケージの Makefile は非常に簡素化され、gobuild -m >Makefile を実行するだけで、複雑なビルドロジックが自動的に生成されるようになりました。これは、Goのビルドシステムの保守性と自動化を大幅に向上させる変更です。

src/cmd/gotest/gotest の変更点

+gofiles=""
+loop=true
+while $loop; do
+	case "x$1" in
+	x-*)
+		loop=false
+		;;
+	x)
+		loop=false
+		;;
+	*)
+		gofiles="$gofiles $1"
+		shift
+		;;
+	esac
+done
+
+case "x$gofiles" in
+x)
+	gofiles=$(echo test*.go)
+esac
 
-gofiles=${*:-$(echo test*.go)}\

この部分では、gotest スクリプトがコマンドライン引数を解析し、テスト対象のGoファイルを決定するロジックが変更されています。

  • 以前は gofiles=${*:-$(echo test*.go)} というシェルスクリプトの機能を使っていましたが、これは引数がない場合に test*.go をデフォルトとしていました。
  • 新しいロジックでは、while ループと case ステートメントを使って、より明示的に引数を処理しています。これにより、- で始まるオプション引数とファイル名を区別できるようになり、スクリプトの堅牢性が向上しました。
  • 引数としてファイルが指定されなかった場合(x$gofilesx の場合)、$(echo test*.go) を実行してカレントディレクトリ内の test*.go ファイルを gofiles に設定します。
+set -e
 
+# They all compile; now generate the code to call them.
+trap "rm -f _testmain.go _testmain.6 6.out" 0 1 2 3 14 15
  • set -e: コマンドが失敗した場合にスクリプトを即座に終了させる設定が追加されました。これにより、エラーハンドリングが強化されます。
  • trap "rm -f _testmain.go _testmain.6 6.out" 0 1 2 3 14 15: スクリプトが終了する際(シグナル 0, 1, 2, 3, 14, 15 を受け取った場合を含む)に、一時的に生成されたテスト関連ファイル (_testmain.go, _testmain.6, 6.out) を削除する trap が設定されました。これにより、テスト実行後の一時ファイルのクリーンアップが自動化され、開発環境がクリーンに保たれます。

これらの gotest の変更は、テスト実行の信頼性と使いやすさを向上させるものです。

関連リンク

  • Go言語の初期のビルドシステムに関する議論やドキュメントは、現在のGoの公式ドキュメントからは見つけにくい場合があります。当時のメーリングリストのアーカイブや、Goの初期のソースコードリポジトリを直接参照することが、より深い理解につながります。
  • make ユーティリティの公式ドキュメント: https://www.gnu.org/software/make/manual/
  • ar コマンドのドキュメント (Unix/Linux): man ar で参照可能。

参考にした情報源リンク

  • GitHub上のGo言語リポジトリ: https://github.com/golang/go
  • コミットハッシュ: 360151d4e2b3990db67555a8c61b1e581294fc44
  • Go言語の初期のビルドシステムに関する情報は、主にGoのソースコードの歴史的なコミットログと、当時のGo開発者間のコミュニケーション(メーリングリストなど)から得られます。