[インデックス 12836] ファイルの概要
このコミットは、Go言語の公式バイナリディストリビューション(tarおよびzipアーカイブ)にディレクトリが含まれないというバグを修正するものです。具体的には、misc/dist/bindist.go
ファイル内のmakeTar
関数とmakeZip
関数が修正され、ディレクトリが適切にアーカイブに追加されるようになりました。
コミット
commit 4e9f7047dcf4ab0c059baaac4f5c5a06dc6b38bb
Author: Dave Cheney <dave@cheney.net>
Date: Thu Apr 5 11:39:12 2012 +1000
misc/dist: include directories in distribution tar and zip files.
Fixes #3458.
R=adg, dsymonds
CC=golang-dev
https://golang.org/cl/5969071
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4e9f7047dcf4ab0c059baaac4f5c5a06dc6b38bb
元コミット内容
misc/dist: include directories in distribution tar and zip files.
Fixes #3458.
R=adg, dsymonds
CC=golang-dev
https://golang.org/cl/5969071
変更の背景
このコミットは、Go言語のIssue #3458を修正するために行われました。このIssueは、Goの公式バイナリディストリビューション(go.tar.gz
やgo.zip
など)が、空のディレクトリや、ファイルを含まないディレクトリをアーカイブに含めないという問題点を指摘していました。
通常、tarやzipのようなアーカイブ形式では、ファイルだけでなくディレクトリ構造も保持することが期待されます。しかし、当時のGoのビルドスクリプト(misc/dist/bindist.go
)は、ディレクトリをアーカイブに追加する際に、そのディレクトリが空であるか、あるいはファイルを含まない場合にスキップしてしまうロジックを含んでいました。これにより、ユーザーがGoのバイナリディストリビューションをダウンロードして展開した際に、期待されるディレクトリ構造の一部が欠落しているという問題が発生していました。
特に、pkg/tool/linux_amd64
のような、特定のプラットフォーム向けのツールを格納するディレクトリが空の場合に、それがアーカイブに含まれないことが報告されていました。これは、Goのツールチェインの完全性や、ユーザーがソースからビルドする環境とバイナリディストリビューションを使用する環境との間で一貫性を欠く原因となっていました。
この修正は、Goのディストリビューションがより堅牢で、期待通りのディレクトリ構造を保持するようにするために不可欠でした。
前提知識の解説
このコミットを理解するためには、以下の技術的背景知識が役立ちます。
-
Go言語のビルドシステムとディストリビューション:
- Go言語は、そのソースコードからコンパイラ、標準ライブラリ、各種ツール(
go
コマンド自体、gofmt
など)をビルドし、それらをまとめてバイナリディストリビューションとして提供しています。 misc/dist
ディレクトリは、Goのソースツリー内で、このディストリビューションパッケージを作成するためのスクリプトやツールが置かれている場所です。bindist.go
はその中心的な役割を担うGoプログラムです。- Goのディストリビューションは、通常、
go
コマンド、標準ライブラリのパッケージアーカイブ(.a
ファイル)、各種ツール、ドキュメントなどが含まれています。
- Go言語は、そのソースコードからコンパイラ、標準ライブラリ、各種ツール(
-
tarアーカイブとzipアーカイブ:
- tar (Tape Archive): 主にUnix/Linux環境で広く使われるアーカイブ形式です。複数のファイルやディレクトリを一つのファイルにまとめることができます。ディレクトリ自体もエントリとしてアーカイブに含めることができ、そのパーミッションやタイムスタンプなどのメタデータも保存されます。
- zip: 主にWindows環境で広く使われるアーカイブ形式ですが、クロスプラットフォームで利用されます。tarと同様に複数のファイルやディレクトリをまとめることができますが、通常は圧縮も同時に行われます。zip形式でもディレクトリをエントリとして含めることが可能です。
-
filepath.Walk
関数 (Go言語):- Goの標準ライブラリ
path/filepath
パッケージに含まれる関数で、指定されたディレクトリツリーを再帰的に走査するために使用されます。 filepath.Walk(root string, walkFn WalkFunc) error
というシグネチャを持ち、walkFn
は走査中に見つかった各ファイルやディレクトリに対して呼び出されるコールバック関数です。walkFn
は、現在のパス、os.FileInfo
(ファイルやディレクトリのメタデータ)、およびエラーを受け取ります。この関数がfilepath.SkipDir
を返すと、そのディレクトリ以下の走査がスキップされます。
- Goの標準ライブラリ
-
os.FileInfo
インターフェース (Go言語):- Goの標準ライブラリ
os
パッケージに含まれるインターフェースで、ファイルやディレクトリに関する情報(名前、サイズ、パーミッション、最終更新時刻、ディレクトリかどうかなど)を提供します。 IsDir() bool
メソッドは、そのFileInfo
がディレクトリを表す場合にtrue
を返します。
- Goの標準ライブラリ
-
archive/tar
およびarchive/zip
パッケージ (Go言語):- Goの標準ライブラリで、それぞれtarアーカイブとzipアーカイブの読み書きをサポートします。
tar.Header
やzip.FileHeader
は、アーカイブ内の各エントリ(ファイルやディレクトリ)のメタデータ(名前、サイズ、モードなど)を定義するために使用されます。tar.Writer.WriteHeader()
やzip.Writer.CreateHeader()
は、これらのヘッダ情報に基づいてアーカイブに新しいエントリを作成します。
技術的詳細
このコミットの核心は、misc/dist/bindist.go
ファイル内のmakeTar
関数とmakeZip
関数における、ディレクトリの扱い方の変更です。
makeTar
関数における変更
makeTar
関数は、Goのディストリビューションをtarアーカイブとして作成する役割を担っています。変更前は、filepath.Walk
でディレクトリを走査する際に、fi.IsDir()
がtrue
(つまり、現在のエントリがディレクトリである)の場合にreturn nil
していました。これは、ディレクトリ自体をtarアーカイブに追加する処理をスキップすることを意味します。
変更後、このif fi.IsDir() { return nil }
という行が削除されました。これにより、filepath.Walk
がディレクトリを検出した際にも、そのディレクトリのtar.Header
が作成され、tw.WriteHeader(hdr)
によってアーカイブに書き込まれるようになりました。
ただし、tarアーカイブではディレクトリ自体は内容を持たないため、その後に続くio.Copy(tw, r)
のようなファイル内容を書き込む処理は、ディレクトリの場合には実行されません。これは、tar.Header
がディレクトリであることを示していれば十分であり、内容を書き込む必要がないためです。
makeZip
関数における変更
makeZip
関数は、Goのディストリビューションをzipアーカイブとして作成する役割を担っています。makeZip
関数もmakeTar
と同様に、変更前はfilepath.Walk
でディレクトリを検出した場合にreturn nil
していました。
変更後、if fi.IsDir() { return nil }
という行が削除されました。しかし、zipアーカイブにおけるディレクトリの扱いはtarとは少し異なります。zipでは、ディレクトリは通常、名前の末尾にスラッシュ(/
)が付いたエントリとして表現されます。また、ディレクトリは内容を持たないため、圧縮する必要がありません。
このコミットでは、以下の重要な変更がmakeZip
関数に追加されました。
-
ディレクトリ名の正規化:
if fi.IsDir() { fh.Name += "/" // append trailing slash fh.Method = zip.Store // no need to deflate 0 byte files }
zip.FileHeader
のName
フィールドに、ディレクトリであることを示すために末尾にスラッシュが追加されます。fh.Method = zip.Store
は、ディレクトリは内容を持たないため、圧縮(zip.Deflate
)する必要がなく、そのまま格納(zip.Store
)すればよいことを示しています。これにより、アーカイブの作成効率も向上します。 -
ディレクトリ内容のスキップ:
if fi.IsDir() { return nil }
zw.CreateHeader(fh)
でヘッダが作成された後、再度fi.IsDir()
のチェックが行われ、ディレクトリであればreturn nil
しています。これは、ディレクトリのヘッダは作成するものの、その後に続くos.Open(path)
やio.Copy(w, r)
といったファイル内容を読み書きする処理をスキップするためです。これにより、ディレクトリがファイルとして扱われることを防ぎます。
これらの変更により、makeTar
とmakeZip
は、それぞれ適切な方法でディレクトリをアーカイブに含めることができるようになりました。
コアとなるコードの変更箇所
misc/dist/bindist.go
ファイルにおいて、以下の変更が行われました。
makeTar
関数 (L574-L598)
--- a/misc/dist/bindist.go
+++ b/misc/dist/bindist.go
@@ -574,9 +574,6 @@ func makeTar(targ, workdir string) error {
if *verbose {
log.Printf("adding to tar: %s", name)
}
- if fi.IsDir() {
- return nil
- }
hdr, err := tarFileInfoHeader(fi, path)
if err != nil {
return err
@@ -598,6 +595,9 @@ func makeTar(targ, workdir string) error {
if err != nil {
return fmt.Errorf("Error writing file %q: %v", name, err)
}
+ if fi.IsDir() {
+ return nil
+ }
r, err := os.Open(path)
if err != nil {
return err
makeZip
関数 (L626-L660)
--- a/misc/dist/bindist.go
+++ b/misc/dist/bindist.go
@@ -626,9 +626,6 @@ func makeZip(targ, workdir string) error {
zw := zip.NewWriter(f)
err = filepath.Walk(workdir, func(path string, fi os.FileInfo, err error) error {
- if fi.IsDir() {
- return nil
- }
if !strings.HasPrefix(path, workdir) {
log.Panicf("walked filename %q doesn't begin with workdir %q", path, workdir)
}
@@ -655,10 +652,17 @@ func makeZip(targ, workdir string) error {
}
fh.Name = name
fh.Method = zip.Deflate
+ if fi.IsDir() {
+ fh.Name += "/" // append trailing slash
+ fh.Method = zip.Store // no need to deflate 0 byte files
+ }
w, err := zw.CreateHeader(fh)
if err != nil {
return err
}
+ if fi.IsDir() {
+ return nil
+ }
r, err := os.Open(path)
if err != nil {
return err
コアとなるコードの解説
makeTar
関数
元のコードでは、filepath.Walk
のコールバック関数内で、fi.IsDir()
がtrue
の場合に即座にreturn nil
していました。これは、ディレクトリが見つかった場合に、そのディレクトリをtarアーカイブに追加する処理を完全にスキップすることを意味していました。
修正では、この最初のif fi.IsDir() { return nil }
が削除されました。これにより、ディレクトリであってもtarFileInfoHeader
が呼び出され、tar.Header
が作成されるようになりました。このヘッダには、エントリがディレクトリであるという情報が含まれます。
その後、tw.WriteHeader(hdr)
によってこのディレクトリのヘッダがアーカイブに書き込まれます。そして、その直後にif fi.IsDir() { return nil }
というチェックが追加されました。この位置でのreturn nil
は、ディレクトリのヘッダは書き込んだものの、その後に続くos.Open(path)
やio.Copy
といったファイル内容を読み書きする処理をスキップするためのものです。これにより、ディレクトリがファイルとして扱われることなく、正しくアーカイブにディレクトリエントリとして追加されるようになりました。
makeZip
関数
makeZip
関数もmakeTar
と同様に、最初のif fi.IsDir() { return nil }
が削除されました。これにより、ディレクトリであってもzip.FileHeader
が作成されるようになりました。
重要な追加変更は、zip.FileHeader
の設定部分です。
if fi.IsDir() { fh.Name += "/" }
: Zipアーカイブの慣例に従い、ディレクトリ名に末尾のスラッシュを追加しています。これにより、展開時にディレクトリとして正しく認識されます。if fi.IsDir() { fh.Method = zip.Store }
: ディレクトリは内容を持たないため、圧縮する必要がありません。zip.Store
は無圧縮で格納することを意味し、これによりアーカイブ作成のパフォーマンスが向上します。
これらの設定の後、zw.CreateHeader(fh)
でディレクトリのヘッダがZipアーカイブに書き込まれます。そして、makeTar
と同様に、その直後にif fi.IsDir() { return nil }
というチェックが追加されました。これは、ディレクトリのヘッダは書き込んだものの、その後に続くファイル内容の読み書き処理をスキップするためのものです。
これらの変更により、Goのバイナリディストリビューションが、空のディレクトリを含む完全なディレクトリ構造を保持するようになり、ユーザーエクスペリエンスが向上しました。
関連リンク
- Go Issue #3458: https://github.com/golang/go/issues/3458
- Go CL 5969071: https://golang.org/cl/5969071 (Gerrit Code Review)
参考にした情報源リンク
- Go言語の公式ドキュメント (特に
os
、path/filepath
、archive/tar
、archive/zip
パッケージ): https://pkg.go.dev/ - tarファイル形式の仕様 (例: POSIX.1-1988): https://pubs.opengroup.org/onlinepubs/009695399/utilities/pax.html#tag_04_13_07
- Zipファイル形式の仕様 (例: PKWARE APPNOTE.TXT): https://pkware.com/webdocs/casestudies/APPNOTE.TXT
- Go言語のソースコードリポジトリ: https://github.com/golang/go
- Go言語のGerrit Code Reviewシステム: https://go.googlesource.com/go/+/refs/heads/master
- Dave Cheney氏のブログや関連情報 (Goコミュニティにおける著名な貢献者): https://dave.cheney.net/
- Go言語のメーリングリスト (golang-dev): https://groups.google.com/g/golang-dev