[インデックス 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開発者間のコミュニケーション(メーリングリストなど)から得られます。