[インデックス 12563] ファイルの概要
このコミットは、Go言語のディストリビューションツール (misc/dist/bindist.go
) における変更です。具体的には、Goのバイナリ配布物(tarball)の作成方法を改善し、ファイルパーミッションの保持と所有者の予測可能性を向上させることを目的としています。
コミット
commit ac0789c63e23b2f10adb3c162c75558cba51fc38
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Sun Mar 11 23:07:38 2012 -0700
misc/dist: use archive/tar to generate tarballs
For people untarring with -p or as root, preserving file permissions.
This way we don't make tars owned by adg/eng or adg/staff or whatever
machine Andrew was on. Instead, we always build tarballs owned by predictable
users.
Except archive/tar doesn't seem to work.
Updates #3209.
R=golang-dev, adg
CC=dsymonds, golang-dev
https://golang.org/cl/5796064
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ac0789c63e23b2f10adb3c162c75558cba51fc38
元コミット内容
misc/dist: use archive/tar to generate tarballs
For people untarring with -p or as root, preserving file permissions.
This way we don't make tars owned by adg/eng or adg/staff or whatever
machine Andrew was on. Instead, we always build tarballs owned by predictable
users.
Except archive/tar doesn't seem to work.
Updates #3209.
R=golang-dev, adg
CC=dsymonds, golang-dev
https://golang.org/cl/5796064
変更の背景
このコミットの主な背景は、Go言語の公式バイナリ配布物(tar.gzファイル)の作成プロセスにおける、ファイルパーミッションと所有権の問題を解決することにあります。
以前のmisc/dist
ツールは、tar
コマンドラインユーティリティを外部プロセスとして呼び出してtarballを作成していました。この方法には以下の問題がありました。
- パーミッションの不整合:
tar -p
オプションを使用してパーミッションを保持したり、root
ユーザーとして展開したりする際に、元のファイルのパーミッションが正しく保持されない可能性がありました。 - 予測不可能な所有者: tarball内のファイルの所有者が、ビルドを実行したユーザー(例:
adg/eng
やadg/staff
など、Andrew Gerrand氏が使用していたマシン上のユーザー)に依存してしまい、一貫性がありませんでした。これは、異なる環境でビルドされた配布物間で所有者が異なったり、ユーザーがroot
として展開する際に不必要な権限の問題を引き起こす可能性がありました。
このコミットは、これらの問題を解決するために、Go標準ライブラリのarchive/tar
パッケージとcompress/gzip
パッケージを使用して、Goプログラム内で直接tarballを生成するように変更しました。これにより、ファイルパーミッションを明示的に設定し、所有者をroot
に固定することで、配布物の一貫性と信頼性を高めることを目指しました。
コミットメッセージには「Except archive/tar doesn't seem to work.」という記述がありますが、これはコミット時点での一時的な問題提起であり、最終的にはこのアプローチが採用され、機能していることを示唆しています。また、Updates #3209
とあるように、この変更はGoのIssue 3209に関連しています。ただし、GoのIssueトラッカーで直接「Issue 3209」を検索しても、このコミットに直接関連する詳細な情報は見つかりませんでした。これは、古いIssueがアーカイブされたか、別のプロジェクトのIssue番号である可能性も示唆しています。しかし、文脈から、ファイルパーミッションと所有権に関する問題であったことは明らかです。
前提知識の解説
このコミットを理解するためには、以下の技術的な概念とGo言語のパッケージに関する知識が必要です。
-
Tarアーカイブ (.tar):
tar
(Tape ARchiver) は、複数のファイルを一つのアーカイブファイルにまとめるためのユーティリティです。ディレクトリ構造やファイルパーミッション、タイムスタンプなどのメタデータを保持できます。-c
(create): 新しいアーカイブを作成します。-z
(gzip):gzip
圧縮を適用します。-f
(file): アーカイブファイルの名前を指定します。-C
(directory): 指定されたディレクトリに移動してから操作を実行します。-p
(preserve-permissions): ファイルのパーミッションを保持します。
-
Gzip圧縮 (.gz):
gzip
は、ファイルを圧縮するための一般的なユーティリティです。tar
と組み合わせて.tar.gz
(または.tgz
)という形式でよく使われます。
-
ファイルパーミッションと所有権:
- Unix/Linuxシステムでは、ファイルやディレクトリには所有者(User)、グループ(Group)、その他のユーザー(Others)に対する読み取り(r)、書き込み(w)、実行(x)のパーミッションが設定されます。
- 所有者とグループは、ファイルを作成したユーザーやそのユーザーが属するグループによって決まります。
root
ユーザーは、システム上のすべてのファイルに対して完全な権限を持つ特権ユーザーです。tar
アーカイブを展開する際に、元のファイルの所有者やパーミッションを保持するかどうかは重要な考慮事項です。特に、root
として展開する場合、アーカイブ内のファイルの所有者がroot
に設定されていると、展開後のファイルの所有者もroot
になります。
-
Go言語の標準ライブラリ:
archive/tar
パッケージ: Goプログラム内でtarアーカイブの読み書きを行うためのパッケージです。tar.NewWriter
を使用してtarアーカイブを作成し、tar.Header
構造体で各ファイルのエントリのメタデータ(ファイル名、サイズ、パーミッション、所有者など)を定義します。compress/gzip
パッケージ: Goプログラム内でgzip圧縮データの読み書きを行うためのパッケージです。gzip.NewWriter
を使用してgzip圧縮ストリームを作成します。os
パッケージ: オペレーティングシステムとのインタラクション(ファイル操作、ディレクトリ操作など)を提供します。os.Create
でファイルを作成し、os.FileInfo
インターフェースでファイルに関する情報(名前、サイズ、モード、変更時刻など)を取得できます。os.ModeType
はファイルの種類(ディレクトリ、シンボリックリンクなど)を識別するためのビットマスクです。path/filepath
パッケージ: ファイルパスの操作(結合、分割、ウォークなど)を提供します。filepath.Walk
関数は、指定されたディレクトリツリーを再帰的に走査し、各ファイルやディレクトリに対してコールバック関数を実行します。io
パッケージ: I/Oプリミティブ(Reader
、Writer
インターフェースなど)を提供します。io.Copy
関数は、Reader
からWriter
へデータをコピーするのに便利です。syscall
パッケージ: 低レベルのシステムコールへのアクセスを提供します。os.FileInfo
のSys()
メソッドは、基盤となるシステム固有のデータ構造を返します。Unix系システムでは、これは*syscall.Stat_t
型にキャストでき、ファイルのモード(パーミッション)などの詳細情報が含まれています。
技術的詳細
このコミットの主要な変更点は、misc/dist/bindist.go
ファイルにmakeTar
という新しい関数が導入され、既存のb.run
による外部tar
コマンドの呼び出しがこのmakeTar
関数に置き換えられたことです。
makeTar
関数の概要
makeTar
関数は、指定された作業ディレクトリ (workdir
) 内のファイルを読み込み、それらをtarg
という名前のgzip圧縮されたtarアーカイブとして書き出します。
-
アーカイブファイルの作成:
os.Create(targ)
: 出力先の.tar.gz
ファイルを作成します。gzip.NewWriter(f)
: 作成したファイルにgzip圧縮を適用するためのgzip.Writer
を作成します。tar.NewWriter(zout)
: gzip圧縮ストリームにtarアーカイブを書き込むためのtar.Writer
を作成します。
-
ディレクトリの走査とファイルの追加:
filepath.Walk(workdir, filepath.WalkFunc(...))
:workdir
以下のすべてのファイルとディレクトリを再帰的に走査します。filepath.WalkFunc
は、走査中に見つかった各エントリに対して呼び出されるコールバック関数です。- パスの整形:
path[len(workdir):]
とname = name[1:]
を使って、アーカイブ内のファイル名がworkdir
からの相対パスになるように整形します。 - フィルタリング:
!strings.HasPrefix(name, "go/")
の条件で、go/
ディレクトリ以下にないファイルはアーカイブに追加しないようにフィルタリングしています。これは、Goの配布物にはGoのソースコードやバイナリのみを含めるためです。 - ディレクトリのスキップ:
fi.IsDir()
がtrue
の場合、ディレクトリ自体はアーカイブに追加せず、その中身だけを処理します。 - ファイルタイプの処理:
fi.Mode()&os.ModeType == 0
で通常のファイルであることを確認し、tar.TypeReg
を設定します。それ以外の未知のファイルタイプの場合はlog.Fatalf
でエラーを発生させます。
-
Tarヘッダーの作成と書き込み:
hdr := &tar.Header{...}
: 各ファイルのエントリに対応するtar.Header
構造体を作成します。Name
: アーカイブ内のファイル名。Mode
: ファイルのパーミッション。ここで重要なのはint64(fi.Sys().(*syscall.Stat_t).Mode)
です。fi.Sys()
はファイルシステム固有の情報を返し、Unix系システムでは*syscall.Stat_t
にキャストすることで、元のファイルのパーミッションモードを取得できます。これにより、tar -p
で展開した際にパーミッションが保持されます。Size
: ファイルのサイズ。ModTime
: ファイルの最終変更時刻。Typeflag
: ファイルの種類(ここでは通常のファイル)。Uname: "root"
,Gname: "root"
: ここがこのコミットの重要なポイントで、アーカイブ内のファイルの所有者とグループを明示的にroot
に設定しています。これにより、ビルドを実行したユーザーに関わらず、常にroot
所有のファイルとして配布物が作成されます。
tw.WriteHeader(hdr)
: 作成したヘッダーをtarアーカイブに書き込みます。
-
ファイル内容のコピー:
os.Open(path)
: 元のファイルを開きます。io.Copy(tw, r)
: 開いたファイルの内容をtar.Writer
にコピーします。これにより、ファイルデータがアーカイブに追加されます。
-
アーカイブのクローズ:
tw.Close()
:tar.Writer
をクローズし、残りのデータをフラッシュします。zout.Close()
:gzip.Writer
をクローズし、gzip圧縮を完了します。f.Close()
: 出力ファイル自体をクローズします。
変更の意図
この変更により、Goの配布物作成プロセスは以下の利点を得ます。
- パーミッションの正確な保持:
syscall.Stat_t
から直接パーミッションを取得することで、より正確なパーミッション情報がアーカイブに埋め込まれます。 - 予測可能な所有者: すべてのファイルが
root:root
としてアーカイブされるため、配布物を展開する際に、ビルド環境に依存しない一貫した所有者になります。これは、特にroot
として展開する場合に、権限の問題を回避し、クリーンなインストールを保証します。 - Goネイティブな実装: 外部コマンドへの依存をなくし、Goの標準ライブラリのみで完結させることで、クロスプラットフォームでのビルドの信頼性が向上し、外部ツールの存在に依存しなくなります。
コアとなるコードの変更箇所
変更はmisc/dist/bindist.go
ファイルに集中しています。
--- a/misc/dist/bindist.go
+++ b/misc/dist/bindist.go
@@ -7,9 +7,11 @@
package main
import (
+ "archive/tar"
"archive/zip"
"bufio"
"bytes"
+ "compress/gzip"
"encoding/base64"
"errors"
"flag"
@@ -24,6 +26,7 @@ import (
"path/filepath"
"runtime"
"strings"
+ "syscall"
)
var (
@@ -181,7 +184,7 @@ func (b *Build) Do() error {
targ = fmt.Sprintf("go.%s.src", version)
}
targ += ".tar.gz"
- _, err = b.run("", "tar", "czf", targ, "-C", work, "go")
+ err = makeTar(targ, work)
targs = append(targs, targ)
case "darwin":
// arrange work so it's laid out as the dest filesystem
@@ -494,6 +497,73 @@ func cp(dst, src string) error {
return err
}
+func makeTar(targ, workdir string) error {
+ f, err := os.Create(targ)
+ if err != nil {
+ return err
+ }
+ zout := gzip.NewWriter(f)
+ tw := tar.NewWriter(zout)
+
+ filepath.Walk(workdir, filepath.WalkFunc(func(path string, fi os.FileInfo, err error) error {
+ if !strings.HasPrefix(path, workdir) {
+ log.Panicf("walked filename %q doesn't begin with workdir %q", path, workdir)
+ }
+ name := path[len(workdir):]
+
+ // Chop of any leading / from filename, leftover from removing workdir.
+ if strings.HasPrefix(name, "/") {
+ name = name[1:]
+ }
+ // Don't include things outside of the go subdirectory (for instance,
+ // the zip file that we're currently writing here.)
+ if !strings.HasPrefix(name, "go/") {
+ return nil
+ }
+ if *verbose {
+ log.Printf("adding to tar: %s", name)
+ }
+ 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",
+ }
+ err = tw.WriteHeader(hdr)
+ if err != nil {
+ return fmt.Errorf("Error writing file %q: %v", name, err)
+ }
+ r, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+ _, err = io.Copy(tw, r)
+ return err
+ }))
+
+ if err := tw.Close(); err != nil {
+ return err
+ }
+ if err := zout.Close(); err != nil {
+ return err
+ }
+ return f.Close()
+}
+
func makeZip(targ, workdir string) error {
f, err := os.Create(targ)
if err != nil {
コアとなるコードの解説
import
文の追加
archive/tar
, compress/gzip
, syscall
の3つのパッケージが新しくインポートされています。これらはそれぞれtarアーカイブの操作、gzip圧縮、そして低レベルのシステムコール(ファイルパーミッションの取得のため)に必要です。
b.run
の置き換え
// 変更前
_, err = b.run("", "tar", "czf", targ, "-C", work, "go")
// 変更後
err = makeTar(targ, work)
b.run
は外部コマンドを実行するためのヘルパー関数です。以前はここでtar
コマンドを呼び出していましたが、この行が新しく定義されたmakeTar
関数への呼び出しに置き換えられています。これにより、tarballの生成がGoプログラムの内部で完結するようになりました。
makeTar
関数の新規追加
このコミットの最も重要な部分です。
func makeTar(targ, workdir string) error {
f, err := os.Create(targ) // 出力ファイル(.tar.gz)を作成
if err != nil {
return err
}
zout := gzip.NewWriter(f) // gzip圧縮ライターを作成
tw := tar.NewWriter(zout) // tarアーカイブライターを作成
filepath.Walk(workdir, filepath.WalkFunc(func(path string, fi os.FileInfo, err error) 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", // 所有者をrootに設定
Gname: "root", // グループをrootに設定
}
err = tw.WriteHeader(hdr) // tarヘッダーを書き込み
if err != nil {
return fmt.Errorf("Error writing file %q: %v", name, err)
}
r, err := os.Open(path) // 元のファイルを開く
if err != nil {
return err
}
defer r.Close()
_, err = io.Copy(tw, r) // ファイル内容をtarアーカイブにコピー
return err
}))
if err := tw.Close(); err != nil { // tarライターをクローズ
return err
}
if err := zout.Close(); err != nil { // gzipライターをクローズ
return err
}
return f.Close() // 出力ファイルをクローズ
}
このmakeTar
関数は、filepath.Walk
を使って指定されたディレクトリツリーを走査し、見つかった各ファイルに対して以下の処理を行います。
- パスの正規化とフィルタリング:
workdir
からの相対パスに変換し、go/
プレフィックスを持つファイルのみを対象とします。 tar.Header
の作成: 各ファイルについてtar.Header
構造体を作成します。Mode: int64(fi.Sys().(*syscall.Stat_t).Mode)
: ここが重要な部分で、os.FileInfo
のSys()
メソッドが返すシステム固有の情報を*syscall.Stat_t
に型アサートすることで、Unix/Linuxシステムにおけるファイルのパーミッションモードを直接取得しています。これにより、元のファイルのパーミッションが正確にアーカイブに記録されます。Uname: "root"
,Gname: "root"
: ファイルの所有者とグループ名を明示的にroot
に設定しています。これにより、tarballを展開する際に、常にroot
ユーザーとroot
グループが所有者となることが保証され、ビルド環境に依存しない一貫性が実現されます。
- データの書き込み:
tw.WriteHeader(hdr)
でヘッダーを書き込んだ後、io.Copy(tw, r)
で元のファイルの内容をtarアーカイブにコピーします。
最後に、tw.Close()
, zout.Close()
, f.Close()
を呼び出して、すべてのライターとファイルを適切にクローズし、データのフラッシュとリソースの解放を行います。
この変更により、Goの配布物作成プロセスはより堅牢になり、ファイルパーミッションと所有権に関する問題が解決されました。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- このコミットのGo CL (Code Review): https://golang.org/cl/5796064
参考にした情報源リンク
- Go言語
archive/tar
パッケージドキュメント: https://pkg.go.dev/archive/tar - Go言語
compress/gzip
パッケージドキュメント: https://pkg.go.dev/compress/gzip - Go言語
os
パッケージドキュメント: https://pkg.go.dev/os - Go言語
path/filepath
パッケージドキュメント: https://pkg.go.dev/path/filepath - Go言語
io
パッケージドキュメント: https://pkg.go.dev/io - Go言語
syscall
パッケージドキュメント: https://pkg.go.dev/syscall tar
コマンドのmanページ (一般的な情報): https://man7.org/linux/man-pages/man1/tar.1.htmlgzip
コマンドのmanページ (一般的な情報): https://man7.org/linux/man-pages/man1/gzip.1.html- Google検索: "golang issue 3209" (関連するIssueの特定のため)