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

[インデックス 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を作成していました。この方法には以下の問題がありました。

  1. パーミッションの不整合: tar -pオプションを使用してパーミッションを保持したり、rootユーザーとして展開したりする際に、元のファイルのパーミッションが正しく保持されない可能性がありました。
  2. 予測不可能な所有者: tarball内のファイルの所有者が、ビルドを実行したユーザー(例: adg/engadg/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言語のパッケージに関する知識が必要です。

  1. Tarアーカイブ (.tar):

    • tar (Tape ARchiver) は、複数のファイルを一つのアーカイブファイルにまとめるためのユーティリティです。ディレクトリ構造やファイルパーミッション、タイムスタンプなどのメタデータを保持できます。
    • -c (create): 新しいアーカイブを作成します。
    • -z (gzip): gzip圧縮を適用します。
    • -f (file): アーカイブファイルの名前を指定します。
    • -C (directory): 指定されたディレクトリに移動してから操作を実行します。
    • -p (preserve-permissions): ファイルのパーミッションを保持します。
  2. Gzip圧縮 (.gz):

    • gzipは、ファイルを圧縮するための一般的なユーティリティです。tarと組み合わせて.tar.gz(または.tgz)という形式でよく使われます。
  3. ファイルパーミッションと所有権:

    • Unix/Linuxシステムでは、ファイルやディレクトリには所有者(User)、グループ(Group)、その他のユーザー(Others)に対する読み取り(r)、書き込み(w)、実行(x)のパーミッションが設定されます。
    • 所有者とグループは、ファイルを作成したユーザーやそのユーザーが属するグループによって決まります。
    • rootユーザーは、システム上のすべてのファイルに対して完全な権限を持つ特権ユーザーです。
    • tarアーカイブを展開する際に、元のファイルの所有者やパーミッションを保持するかどうかは重要な考慮事項です。特に、rootとして展開する場合、アーカイブ内のファイルの所有者がrootに設定されていると、展開後のファイルの所有者もrootになります。
  4. 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プリミティブ(ReaderWriterインターフェースなど)を提供します。io.Copy関数は、ReaderからWriterへデータをコピーするのに便利です。
    • syscallパッケージ: 低レベルのシステムコールへのアクセスを提供します。os.FileInfoSys()メソッドは、基盤となるシステム固有のデータ構造を返します。Unix系システムでは、これは*syscall.Stat_t型にキャストでき、ファイルのモード(パーミッション)などの詳細情報が含まれています。

技術的詳細

このコミットの主要な変更点は、misc/dist/bindist.goファイルにmakeTarという新しい関数が導入され、既存のb.runによる外部tarコマンドの呼び出しがこのmakeTar関数に置き換えられたことです。

makeTar関数の概要

makeTar関数は、指定された作業ディレクトリ (workdir) 内のファイルを読み込み、それらをtargという名前のgzip圧縮されたtarアーカイブとして書き出します。

  1. アーカイブファイルの作成:

    • os.Create(targ): 出力先の.tar.gzファイルを作成します。
    • gzip.NewWriter(f): 作成したファイルにgzip圧縮を適用するためのgzip.Writerを作成します。
    • tar.NewWriter(zout): gzip圧縮ストリームにtarアーカイブを書き込むためのtar.Writerを作成します。
  2. ディレクトリの走査とファイルの追加:

    • 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でエラーを発生させます。
  3. 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アーカイブに書き込みます。
  4. ファイル内容のコピー:

    • os.Open(path): 元のファイルを開きます。
    • io.Copy(tw, r): 開いたファイルの内容をtar.Writerにコピーします。これにより、ファイルデータがアーカイブに追加されます。
  5. アーカイブのクローズ:

    • 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を使って指定されたディレクトリツリーを走査し、見つかった各ファイルに対して以下の処理を行います。

  1. パスの正規化とフィルタリング: workdirからの相対パスに変換し、go/プレフィックスを持つファイルのみを対象とします。
  2. tar.Headerの作成: 各ファイルについてtar.Header構造体を作成します。
    • Mode: int64(fi.Sys().(*syscall.Stat_t).Mode): ここが重要な部分で、os.FileInfoSys()メソッドが返すシステム固有の情報を*syscall.Stat_tに型アサートすることで、Unix/Linuxシステムにおけるファイルのパーミッションモードを直接取得しています。これにより、元のファイルのパーミッションが正確にアーカイブに記録されます。
    • Uname: "root", Gname: "root": ファイルの所有者とグループ名を明示的にrootに設定しています。これにより、tarballを展開する際に、常にrootユーザーとrootグループが所有者となることが保証され、ビルド環境に依存しない一貫性が実現されます。
  3. データの書き込み: tw.WriteHeader(hdr)でヘッダーを書き込んだ後、io.Copy(tw, r)で元のファイルの内容をtarアーカイブにコピーします。

最後に、tw.Close(), zout.Close(), f.Close()を呼び出して、すべてのライターとファイルを適切にクローズし、データのフラッシュとリソースの解放を行います。

この変更により、Goの配布物作成プロセスはより堅牢になり、ファイルパーミッションと所有権に関する問題が解決されました。

関連リンク

参考にした情報源リンク