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

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

このコミットは、Go言語のmisc/distパッケージにおけるarchive/tarヘッダーの生成方法を改善するものです。具体的には、tar.Headerのフィールドをより適切に、かつプラットフォーム固有の情報を考慮して設定するための変更が加えられています。これにより、生成されるtarアーカイブのメタデータがより正確になります。

コミット

commit c405b58f3fa7988d42b5e5e46910344f342c5b45
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Mar 12 21:49:43 2012 -0700

    misc/dist: better archive/tar Headers

    This should live in archive/tar later (CL 5796073) but we
    can always do that after Go 1 and stick it here for now.

    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/5754096

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

https://github.com/golang/go/commit/c405b58f3fa7988d42b5e5e46910344f342c5b45

元コミット内容

misc/dist: better archive/tar Headers

この変更は、archive/tarパッケージのヘッダーをより良くするためのものです。将来的にはarchive/tarパッケージ自体に組み込まれるべき変更(CL 5796073)ですが、Go 1リリース後でも可能であるため、当面はmisc/distに配置されます。

変更の背景

Go言語のディストリビューションツール(misc/dist)は、Goのバイナリ配布物を作成する際にtarアーカイブを使用します。このアーカイブに含まれるファイルのメタデータ(パーミッション、所有者、タイムスタンプなど)は、archive/tarパッケージのtar.Header構造体によって表現されます。

以前の実装では、tar.Headerのフィールド、特にファイルモードやタイプフラグの設定が限定的でした。例えば、シンボリックリンクやデバイスファイルなどの特殊なファイルタイプが適切に扱われていませんでした。また、ファイルのUID(ユーザーID)やGID(グループID)、アクセス時刻、変更時刻といったシステム固有の情報が、os.FileInfoから直接取得できる情報だけでは不十分でした。

このコミットの背景には、生成されるtarアーカイブの互換性と正確性を向上させるという目的があります。特に、異なるオペレーティングシステム(Darwin, Linuxなど)でアーカイブを作成する際に、それぞれのシステムコールから得られる詳細なファイル情報をtar.Headerに反映させる必要がありました。コミットメッセージにある「This should live in archive/tar later」という記述から、この機能が将来的にはGo標準ライブラリのarchive/tarパッケージに統合されるべき、より汎用的な改善であることが示唆されています。

前提知識の解説

archive/tarパッケージ

Go言語の標準ライブラリに含まれるarchive/tarパッケージは、TAR(Tape Archive)形式のファイルを読み書きするための機能を提供します。TARファイルは、複数のファイルを一つのアーカイブにまとめるための一般的な形式であり、Unix系システムで広く利用されています。

  • tar.Header: TARアーカイブ内の各ファイルまたはディレクトリのメタデータを表す構造体です。ファイル名、サイズ、パーミッション、変更時刻、ファイルタイプ(通常ファイル、ディレクトリ、シンボリックリンクなど)、所有者情報(UID, GID, Uname, Gname)などが含まれます。
  • tar.TypeReg: 通常のファイルを意味するtar.HeaderTypeflag定数です。
  • tar.TypeDir: ディレクトリを意味するtar.HeaderTypeflag定数です。
  • tar.TypeSymlink: シンボリックリンクを意味するtar.HeaderTypeflag定数です。

os.FileInfoインターフェース

os.FileInfoは、ファイルに関する情報(名前、サイズ、モード、変更時刻、ディレクトリかどうかなど)を提供するインターフェースです。os.Statos.Lstat関数を呼び出すことで、このインターフェースを実装した値を取得できます。

  • fi.Mode(): ファイルのモードとパーミッションを返します。これには、ファイルタイプ(通常ファイル、ディレクトリ、シンボリックリンクなど)と、読み書き実行のパーミッションが含まれます。
  • fi.Mode().Perm(): ファイルのパーミッションビット(例: 0755)のみを返します。
  • fi.Mode() & os.ModeType: ファイルタイプを示すビットマスクです。これを使って、ファイルが通常ファイル、ディレクトリ、シンボリックリンクなどのどのタイプであるかを判別できます。
  • fi.Sys(): 基盤となるシステムコールから得られるファイル情報(syscall.Stat_tなど)を返します。これはinterface{}型であり、具体的な型はOSによって異なります。

syscall.Stat_t構造体

syscallパッケージは、GoプログラムからOSのシステムコールに直接アクセスするための機能を提供します。syscall.Stat_tは、statシステムコールによって返されるファイルステータス情報を表す構造体です。この構造体には、ファイルのUID、GID、アクセス時刻、変更時刻、作成時刻、デバイス番号など、os.FileInfoでは直接提供されない詳細な情報が含まれています。

  • sys.Uid: ファイルの所有者ユーザーID。
  • sys.Gid: ファイルの所有者グループID。
  • sys.Atimespec (Darwin) / sys.Atim (Linux): ファイルの最終アクセス時刻。
  • sys.Ctimespec (Darwin) / sys.Ctim (Linux): ファイルの最終ステータス変更時刻(inode変更時刻)。

ビルドタグ(Build Tags)

Go言語では、ソースファイルの先頭に// +build tagのようなコメントを追加することで、特定のビルド条件を満たす場合にのみそのファイルをコンパイルするように指定できます。これをビルドタグと呼びます。

このコミットでは、stat_darwin.goには// +build darwinが、stat_linux.goには// +build linuxがそれぞれ追加されています。これは、Darwin(macOS)システムでビルドされる場合はstat_darwin.goが、Linuxシステムでビルドされる場合はstat_linux.goがそれぞれコンパイルされ、他のOSでは無視されることを意味します。これにより、OS固有のシステムコールや構造体を利用したコードを、プラットフォームごとに分離して記述することが可能になります。

技術的詳細

このコミットの主要な変更点は、misc/dist/bindist.go内のmakeTar関数におけるtar.Headerの生成ロジックの改善と、プラットフォーム固有のファイル情報取得のための新しいファイルの導入です。

  1. tarFileInfoHeader関数の導入:

    • 以前はmakeTar関数内で直接tar.Headerを初期化し、os.FileInfoから情報を手動でコピーしていました。
    • 新しいtarFileInfoHeader関数は、os.FileInfoとファイルパス(シンボリックリンクの場合にos.Readlinkで使用)を受け取り、tar.Headerの基本的なフィールド(Name, ModTime, Mode, Typeflag, Size, Linkname)を初期化します。
    • ファイルタイプ(通常ファイル、ディレクトリ、シンボリックリンク、デバイスファイルなど)に応じて、tar.HeaderModeフィールドにTAR仕様で定義されている対応する定数(c_ISREG, c_ISDIR, c_ISLNKなど)をOR演算で追加します。これにより、TARヘッダーのモード情報がより正確になります。
    • シンボリックリンクの場合、os.Readlinkを使用してリンク先のパスを取得し、h.Linknameに設定します。
  2. sysStat変数の導入とプラットフォーム固有の実装:

    • tarFileInfoHeader関数内で、グローバル変数sysStatnilでない場合に呼び出されるロジックが追加されました。sysStatfunc(fi os.FileInfo, h *tar.Header) error型の関数ポインタです。
    • このsysStat変数は、misc/dist/stat_darwin.gomisc/dist/stat_linux.goという新しいファイルで、それぞれのOS固有のinit関数内で初期化されます。
    • stat_darwin.go: Darwin(macOS)システムの場合、sysStatos.FileInfoSys()メソッドから*syscall.Stat_tを取得し、そこからUid, Gid, AccessTime, ChangeTimetar.Headerに設定します。
    • stat_linux.go: Linuxシステムの場合も同様に、sysStat*syscall.Stat_tからUid, Gid, AccessTime, ChangeTimetar.Headerに設定します。Linuxのsyscall.Stat_tではタイムスタンプフィールドの名前がAtim, Ctimとなっています。
    • このメカニズムにより、tarFileInfoHeaderは汎用的なヘッダー生成ロジックを提供しつつ、sysStatを通じてOS固有の詳細なファイルメタデータ(UID, GID, アクセス/変更時刻など)をtar.Headerに注入できるようになります。
  3. bindist.goの変更点:

    • syscallパッケージのインポートが削除されました。これは、syscall.Stat_tへの直接アクセスがsysStat関数にカプセル化されたためです。
    • makeTar関数内で、ファイルの処理時にtarFileInfoHeaderを呼び出してtar.Headerを取得するように変更されました。
    • tar.HeaderUnameGnameは引き続き"root"に設定され、UidGidは明示的に0に設定されます。これは、Goの配布物を作成する際に、アーカイブ内のファイルの所有者を一貫して"root"として扱うためのものです。ただし、sysStatが設定されている場合は、OS固有のUID/GIDが優先されます。

これらの変更により、Goのディストリビューションが生成するtarアーカイブは、より正確で互換性の高いファイルメタデータを含むようになります。特に、UID/GIDやタイムスタンプといったシステム固有の情報が、各OSのシステムコールを通じて適切に反映されるようになりました。

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

misc/dist/bindist.go

--- a/misc/dist/bindist.go
+++ b/misc/dist/bindist.go
@@ -26,7 +26,6 @@ import (
 	"path/filepath"
 	"runtime"
 	"strings"
-	"syscall"
 )
 
 var (
@@ -527,22 +526,16 @@ func makeTar(targ, workdir string) error {
 		if fi.IsDir() {
 			return nil
 		}
-		var typeFlag byte
-		switch {
-		case fi.Mode()&os.ModeType == 0:
-			typeFlag = tar.TypeReg
-		default:
-			log.Fatalf("makeTar: unknown file for file %q", name)
-		}
-		hdr := &tar.Header{
-			Name:     name,
-			Mode:     int64(fi.Sys().(*syscall.Stat_t).Mode),
-			Size:     fi.Size(),
-			ModTime:  fi.ModTime(),
-			Typeflag: typeFlag,
-			Uname:    "root",
-			Gname:    "root",
+		hdr, err := tarFileInfoHeader(fi, path)
+		if err != nil {
+			return err
 		}
+		hdr.Name = name
+		hdr.Uname = "root"
+		hdr.Gname = "root"
+		hdr.Uid = 0
+		hdr.Gid = 0
+
 		err = tw.WriteHeader(hdr)
 		if err != nil {
 			return fmt.Errorf("Error writing file %q: %v", name, err)
@@ -686,3 +679,64 @@ func lookPath(prog string) (absPath string, err error) {
 	}
 	return
 }
+
+// sysStat, if non-nil, populates h from system-dependent fields of fi.
+var sysStat func(fi os.FileInfo, h *tar.Header) error
+
+// Mode constants from the tar spec.
+const (
+	c_ISDIR  = 040000
+	c_ISFIFO = 010000
+	c_ISREG  = 0100000
+	c_ISLNK  = 0120000
+	c_ISBLK  = 060000
+	c_ISCHR  = 020000
+	c_ISSOCK = 0140000
+)
+
+// tarFileInfoHeader creates a partially-populated Header from an os.FileInfo.
+// The filename parameter is used only in the case of symlinks, to call os.Readlink.
+// If fi is a symlink but filename is empty, an error is returned.
+func tarFileInfoHeader(fi os.FileInfo, filename string) (*tar.Header, error) {
+	h := &tar.Header{
+		Name:    fi.Name(),
+		ModTime: fi.ModTime(),
+		Mode:    int64(fi.Mode().Perm()), // or'd with c_IS* constants later
+	}
+	switch {
+	case fi.Mode()&os.ModeType == 0:
+		h.Mode |= c_ISREG
+		h.Typeflag = tar.TypeReg
+		h.Size = fi.Size()
+	case fi.IsDir():
+		h.Typeflag = tar.TypeDir
+		h.Mode |= c_ISDIR
+	case fi.Mode()&os.ModeSymlink != 0:
+		h.Typeflag = tar.TypeSymlink
+		h.Mode |= c_ISLNK
+		if filename == "" {
+			return h, fmt.Errorf("archive/tar: unable to populate Header.Linkname of symlinks")
+		}
+		targ, err := os.Readlink(filename)
+		if err != nil {
+			return h, err
+		}
+		h.Linkname = targ
+	case fi.Mode()&os.ModeDevice != 0:
+		if fi.Mode()&os.ModeCharDevice != 0 {
+			h.Mode |= c_ISCHR
+			h.Typeflag = tar.TypeChar
+		} else {
+			h.Mode |= c_ISBLK
+			h.Typeflag = tar.TypeBlock
+		}
+	case fi.Mode()&os.ModeSocket != 0:
+		h.Mode |= c_ISSOCK
+	default:
+		return nil, fmt.Errorf("archive/tar: unknown file mode %v", fi.Mode())
+	}
+	if sysStat != nil {
+		return h, sysStat(fi, h)
+	}
+	return h, nil
+}

misc/dist/stat_darwin.go (新規ファイル)

--- /dev/null
+++ b/misc/dist/stat_darwin.go
@@ -0,0 +1,32 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+package main
+
+import (
+	"archive/tar"
+	"os"
+	"syscall"
+	"time"
+)
+
+func init() {
+	sysStat = func(fi os.FileInfo, h *tar.Header) error {
+		sys, ok := fi.Sys().(*syscall.Stat_t)
+		if !ok {
+			return nil
+		}
+		h.Uid = int(sys.Uid)
+		h.Gid = int(sys.Gid)
+		// TODO(bradfitz): populate username & group.  os/user
+		// doesn't cache LookupId lookups, and lacks group
+		// lookup functions.
+		h.AccessTime = time.Unix(sys.Atimespec.Unix())
+		h.ChangeTime = time.Unix(sys.Ctimespec.Unix())
+		// TODO(bradfitz): major/minor device numbers?
+		return nil
+	}
+}

misc/dist/stat_linux.go (新規ファイル)

--- /dev/null
+++ b/misc/dist/stat_linux.go
@@ -0,0 +1,32 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build linux
+
+package main
+
+import (
+	"archive/tar"
+	"os"
+	"syscall"
+	"time"
+)
+
+func init() {
+	sysStat = func(fi os.FileInfo, h *tar.Header) error {
+		sys, ok := fi.Sys().(*syscall.Stat_t)
+		if !ok {
+			return nil
+		}
+		h.Uid = int(sys.Uid)
+		h.Gid = int(sys.Gid)
+		// TODO(bradfitz): populate username & group.  os/user
+		// doesn't cache LookupId lookups, and lacks group
+		// lookup functions.
+		h.AccessTime = time.Unix(sys.Atim.Unix())
+		h.ChangeTime = time.Unix(sys.Ctim.Unix())
+		// TODO(bradfitz): major/minor device numbers?
+		return nil
+	}
+}

コアとなるコードの解説

misc/dist/bindist.go

  • import "syscall"の削除: 以前はos.FileInfoSys()メソッドから直接syscall.Stat_tに型アサーションして利用していましたが、このロジックがsysStat関数にカプセル化されたため、bindist.goからはsyscallパッケージの直接的なインポートが不要になりました。
  • makeTar関数の変更:
    • ファイルのtar.Headerを生成する部分が、新しく導入されたtarFileInfoHeader関数を呼び出す形に変更されました。これにより、ヘッダー生成ロジックがモジュール化され、可読性と保守性が向上しました。
    • hdr.Uid = 0hdr.Gid = 0が明示的に設定されています。これは、Goの配布物に含まれるファイルの所有者情報を一貫させるための措置です。ただし、後述のsysStatが設定されている場合は、OS固有のUID/GIDがこれらの値を上書きする可能性があります。
  • sysStat変数の宣言: var sysStat func(fi os.FileInfo, h *tar.Header) errorとして宣言されています。この関数ポインタは、OS固有のファイル情報をtar.Headerに設定するためのフックとして機能します。デフォルトではnilですが、プラットフォーム固有のinit関数によって設定されます。
  • TARモード定数: c_ISDIR, c_ISFIFO, c_ISREGなどの定数が定義されています。これらはTAR仕様で定義されているファイルモードのビットマスクであり、tarFileInfoHeader関数内でtar.HeaderModeフィールドにファイルタイプに応じた適切な値を設定するために使用されます。
  • tarFileInfoHeader関数の実装:
    • この関数は、os.FileInfoからtar.Headerの基本的なフィールド(Name, ModTime, Mode, Typeflag, Size)を初期化します。
    • fi.Mode()os.ModeTypeのビット演算によってファイルタイプを判別し、それに応じてh.Typeflagh.Modeを設定します。
    • シンボリックリンク(os.ModeSymlink)の場合、os.Readlinkを呼び出してリンク先のパスを取得し、h.Linknameに設定します。この際、ファイルパスが提供されていない場合はエラーを返します。
    • デバイスファイル(os.ModeDevice)やソケットファイル(os.ModeSocket)も適切に処理し、対応するTARモード定数とタイプフラグを設定します。
    • 最後に、sysStatnilでない場合、つまりプラットフォーム固有のsysStat関数が定義されている場合にそれを呼び出し、tar.HeaderにOS固有の詳細情報を追加します。

misc/dist/stat_darwin.go および misc/dist/stat_linux.go

  • ビルドタグ: それぞれのファイルの先頭に// +build darwin// +build linuxというビルドタグが記述されています。これにより、GoコンパイラはターゲットOSに応じて適切なファイルのみをコンパイルします。
  • init関数の実装:
    • これらのファイルにはinit関数が定義されています。Goプログラムでは、パッケージの初期化時にinit関数が自動的に実行されます。
    • init関数内で、グローバル変数sysStatに、そのOS固有のファイル情報取得ロジックを実装した匿名関数が代入されます。
    • この匿名関数は、os.FileInfoSys()メソッドから*syscall.Stat_tを取得し、その中のUid, Gid, Atimespec/Atim(アクセス時刻)、Ctimespec/Ctim(変更時刻)といったフィールドをtar.Headerにコピーします。
    • これにより、tarFileInfoHeader関数が呼び出された際に、sysStatを通じてプラットフォーム固有の正確なファイルメタデータがtar.Headerに反映されるようになります。コメントにあるTODOは、ユーザー名やグループ名の解決、およびデバイスのメジャー/マイナー番号の取得が将来的な改善点であることを示しています。

これらの変更は、Goのarchive/tarパッケージが生成するアーカイブの品質と互換性を向上させるための重要なステップであり、特にクロスプラットフォームでのアーカイブ作成において、より正確なファイルメタデータを提供することを可能にしています。

関連リンク

参考にした情報源リンク