[インデックス 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 のようなツールが個々のパッケージのビルドを管理していました。
このコミットの背景には、以下のような課題があったと考えられます。
- ビルドの柔軟性の欠如: 以前の
gobuildは、ビルド対象のソースファイルを明示的に指定する必要がありました。これは、特に多数のファイルや複数のパッケージを含むディレクトリを扱う際に、手動での管理が煩雑になる原因となっていました。 - パッケージ管理の非効率性: 1つのディレクトリに複数のGoパッケージが存在する場合、
gobuildがそれらを適切に処理できない、または手動での介入が必要となる制約がありました。 - ビルドプロセスの自動化不足: パッケージ名の推論やテストの自動実行といった機能が不足しており、開発者が手動で多くのビルドステップを実行する必要がありました。
- Makefileの複雑性: 各パッケージの
Makefileがgobuildの呼び出し方やアーカイブの管理について多くの詳細を記述する必要があり、冗長性や保守性の問題が生じていました。
これらの課題を解決し、Go言語のビルドシステムをより堅牢で使いやすく、自動化されたものにするために、gobuild の機能拡張が不可欠でした。このコミットは、その初期の重要なステップの一つと言えます。
前提知識の解説
このコミットを理解するためには、以下の技術的背景知識が役立ちます。
-
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を呼び出してビルドを行っていました。
-
Unix/Linuxの基本的なコマンドと概念:
fork()/exec()/waitfor(): プロセス管理のためのシステムコール。gobuildが外部コマンド(コンパイラ、アーカイバなど)を実行するために使用します。dup(): ファイルディスクリプタを複製するシステムコール。標準入出力のリダイレクトなどに使用されます。sysfatal(): エラーが発生した場合にプログラムを終了させるための関数。Biobuf: バッファリングされたI/Oを扱うための構造体。smprint(): 文字列をフォーマットして新しい文字列を返す関数。getenv(): 環境変数の値を取得する関数。GOROOT,GOOS,GOARCHなどのGo関連の環境変数が使用されます。unlink(): ファイルを削除するシステムコール。ar(archiver): オブジェクトファイルやライブラリファイルをアーカイブ(.aファイル)にまとめるためのユーティリティ。
-
Go言語のパッケージ構造:
- Goのソースファイルは
package宣言によってどのパッケージに属するかを定義します。 - Goのパッケージは通常、ディレクトリに対応しますが、このコミット以前は1つのディレクトリに複数のパッケージを置くことが
gobuildで直接サポートされていなかった可能性があります。
- Goのソースファイルは
これらの知識は、コミットのコード変更がGoのビルドプロセスにどのように影響するか、そしてなぜこれらの変更が必要とされたのかを深く理解する上で重要です。
技術的詳細
このコミットの技術的詳細は、主に src/cmd/gobuild/gobuild.c の変更に集約されています。
-
gobuildの引数処理の変更:- 以前の
gobuildは、gobuild [-m] packagename *.go *.c *.sのように、ビルド対象のパッケージ名とソースファイルを明示的に指定する必要がありました。 - 変更後、
usageメッセージがgobuild [-m] [packagename...]となり、引数なしで実行された場合(argc == 0の場合)にsourcefilenames関数を呼び出してカレントディレクトリ内の.go,.c,.sファイルを自動的にスキャンするようになりました。これにより、ビルドの自動化が促進されます。
- 以前の
-
メモリ管理ヘルパーの追加:
emallocとereallocという関数が追加されました。これらはmallocとreallocのラッパーで、メモリ確保に失敗した場合にsysfatalを呼び出してプログラムを終了させます。これにより、メモリ管理がより堅牢になります。
-
ar(アーカイバ) 呼び出しの抽象化:arという新しい関数が追加されました。これは、指定されたパッケージ名とファイルリストを受け取り、6arコマンドを呼び出してオブジェクトファイルをアーカイブ(.aファイル)に追加する処理をカプセル化します。これにより、アーカイブ操作のコードが整理され、再利用性が向上します。
-
パッケージ名の自動推論 (
getpkg関数):getpkgという重要な関数が追加されました。この関数は.goファイルを読み込み、その内容からpackage宣言を解析してパッケージ名を抽出します。- 抽出されたパッケージ名は内部の
pkgリストに保存され、重複がなければ追加されます。これにより、gobuildはソースファイルから自動的にパッケージ名を推論し、1つのディレクトリに複数のGoパッケージが存在する場合でも適切に処理できるようになりました。
-
ビルドプロセスのリファクタリング:
main関数内のビルドロジックが大幅に書き換えられました。以前は単純な「繰り返しパス」方式でしたが、新しい実装ではpending,fail,successという3つのジョブリストを使用して、依存関係が解決されるまでコンパイルを試行し続ける、より洗練されたビルドスケジューリングが行われるようになりました。- 各パスでコンパイルが成功したファイルは、対応するパッケージのアーカイブに追加され、その後オブジェクトファイルは削除されます。
- コンパイルが失敗したファイルは次のパスで再試行されます。これにより、循環依存などの問題がある場合でも、可能な限り多くのファイルをビルドしようとします。
-
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など) も、複数パッケージとパスベースのビルドに対応するように更新されました。
-
gotestスクリプトの改善:src/cmd/gotest/gotestシェルスクリプトは、コマンドライン引数の解析をより堅牢に行うようになりました。test*.goファイルを自動的に検出するロジックが改善されました。- テスト実行後の一時ファイルのクリーンアップのために
trapコマンドが追加され、スクリプトの堅牢性が向上しました。
-
標準ライブラリの
Makefileの更新:src/lib/*/Makefileファイル群は、gobuildの変更に合わせて簡素化されました。- 以前は
gobuild -m <pkgname> <files>のようにgobuildの呼び出し方がコメントに記載されていましたが、これがgobuild -m >Makefileに変更されました。これは、gobuildがMakefile全体を生成するようになったことを示しています。 - 各
MakefileからPKG,PKGDIR,install,nukeの定義が削除され、gobuildが生成する共通のルールに依存するようになりました。 default: packagesとtest: packagesルールが追加され、ビルドとテストの実行方法が統一されました。- アーカイブ作成のルール (
a1:,a2:など) も、gobuildが生成する形式に合わせて、$(AR) grc <pkgname>.a <object_files>のように変更されました。
これらの変更により、gobuild は単なるコンパイラのラッパーから、Goプロジェクトのビルドとテストを自動化する、よりインテリジェントなツールへと進化しました。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に src/cmd/gobuild/gobuild.c に集中しています。
-
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を呼び出す。
-
src/cmd/gotest/gotest:- 引数解析ロジックの変更 (行 10-25):
gofilesの設定方法が改善され、引数がない場合にtest*.goを自動検出。 trapコマンドの追加 (行 38): 一時ファイルのクリーンアップ。
- 引数解析ロジックの変更 (行 10-25):
-
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 拡張子を持つファイル) を読み込み、そのファイルが属するパッケージ名を特定します。
- ファイルが
.goファイルでない場合、または開けない場合はnilを返します。 - ファイルを1行ずつ読み込みます。
- 各行の先頭の空白をスキップします。
- 行が "package" で始まり、その後に空白が続く場合、それがパッケージ宣言であると判断します。
- "package" キーワードとそれに続く空白をスキップし、実際のパッケージ名を取得します。
- 取得したパッケージ名が既に
pkgリスト(gobuildがこれまでに発見したパッケージ名のリスト)に存在するかを確認します。 - 存在すれば、既存のポインタを返します。
- 存在しなければ、新しいパッケージとして
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: packagesとtest: 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$gofilesがxの場合)、$(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開発者間のコミュニケーション(メーリングリストなど)から得られます。