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

[インデックス 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.gzgo.zipなど)が、空のディレクトリや、ファイルを含まないディレクトリをアーカイブに含めないという問題点を指摘していました。

通常、tarやzipのようなアーカイブ形式では、ファイルだけでなくディレクトリ構造も保持することが期待されます。しかし、当時のGoのビルドスクリプト(misc/dist/bindist.go)は、ディレクトリをアーカイブに追加する際に、そのディレクトリが空であるか、あるいはファイルを含まない場合にスキップしてしまうロジックを含んでいました。これにより、ユーザーがGoのバイナリディストリビューションをダウンロードして展開した際に、期待されるディレクトリ構造の一部が欠落しているという問題が発生していました。

特に、pkg/tool/linux_amd64のような、特定のプラットフォーム向けのツールを格納するディレクトリが空の場合に、それがアーカイブに含まれないことが報告されていました。これは、Goのツールチェインの完全性や、ユーザーがソースからビルドする環境とバイナリディストリビューションを使用する環境との間で一貫性を欠く原因となっていました。

この修正は、Goのディストリビューションがより堅牢で、期待通りのディレクトリ構造を保持するようにするために不可欠でした。

前提知識の解説

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

  1. Go言語のビルドシステムとディストリビューション:

    • Go言語は、そのソースコードからコンパイラ、標準ライブラリ、各種ツール(goコマンド自体、gofmtなど)をビルドし、それらをまとめてバイナリディストリビューションとして提供しています。
    • misc/distディレクトリは、Goのソースツリー内で、このディストリビューションパッケージを作成するためのスクリプトやツールが置かれている場所です。bindist.goはその中心的な役割を担うGoプログラムです。
    • Goのディストリビューションは、通常、goコマンド、標準ライブラリのパッケージアーカイブ(.aファイル)、各種ツール、ドキュメントなどが含まれています。
  2. tarアーカイブとzipアーカイブ:

    • tar (Tape Archive): 主にUnix/Linux環境で広く使われるアーカイブ形式です。複数のファイルやディレクトリを一つのファイルにまとめることができます。ディレクトリ自体もエントリとしてアーカイブに含めることができ、そのパーミッションやタイムスタンプなどのメタデータも保存されます。
    • zip: 主にWindows環境で広く使われるアーカイブ形式ですが、クロスプラットフォームで利用されます。tarと同様に複数のファイルやディレクトリをまとめることができますが、通常は圧縮も同時に行われます。zip形式でもディレクトリをエントリとして含めることが可能です。
  3. filepath.Walk関数 (Go言語):

    • Goの標準ライブラリpath/filepathパッケージに含まれる関数で、指定されたディレクトリツリーを再帰的に走査するために使用されます。
    • filepath.Walk(root string, walkFn WalkFunc) errorというシグネチャを持ち、walkFnは走査中に見つかった各ファイルやディレクトリに対して呼び出されるコールバック関数です。
    • walkFnは、現在のパス、os.FileInfo(ファイルやディレクトリのメタデータ)、およびエラーを受け取ります。この関数がfilepath.SkipDirを返すと、そのディレクトリ以下の走査がスキップされます。
  4. os.FileInfoインターフェース (Go言語):

    • Goの標準ライブラリosパッケージに含まれるインターフェースで、ファイルやディレクトリに関する情報(名前、サイズ、パーミッション、最終更新時刻、ディレクトリかどうかなど)を提供します。
    • IsDir() boolメソッドは、そのFileInfoがディレクトリを表す場合にtrueを返します。
  5. archive/tarおよびarchive/zipパッケージ (Go言語):

    • Goの標準ライブラリで、それぞれtarアーカイブとzipアーカイブの読み書きをサポートします。
    • tar.Headerzip.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関数に追加されました。

  1. ディレクトリ名の正規化:

    if fi.IsDir() {
        fh.Name += "/" // append trailing slash
        fh.Method = zip.Store // no need to deflate 0 byte files
    }
    

    zip.FileHeaderNameフィールドに、ディレクトリであることを示すために末尾にスラッシュが追加されます。 fh.Method = zip.Storeは、ディレクトリは内容を持たないため、圧縮(zip.Deflate)する必要がなく、そのまま格納(zip.Store)すればよいことを示しています。これにより、アーカイブの作成効率も向上します。

  2. ディレクトリ内容のスキップ:

    if fi.IsDir() {
        return nil
    }
    

    zw.CreateHeader(fh)でヘッダが作成された後、再度fi.IsDir()のチェックが行われ、ディレクトリであればreturn nilしています。これは、ディレクトリのヘッダは作成するものの、その後に続くos.Open(path)io.Copy(w, r)といったファイル内容を読み書きする処理をスキップするためです。これにより、ディレクトリがファイルとして扱われることを防ぎます。

これらの変更により、makeTarmakeZipは、それぞれ適切な方法でディレクトリをアーカイブに含めることができるようになりました。

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

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のバイナリディストリビューションが、空のディレクトリを含む完全なディレクトリ構造を保持するようになり、ユーザーエクスペリエンスが向上しました。

関連リンク

参考にした情報源リンク