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

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

このコミットは、Go言語のツールチェインの一部である cmd/pack コマンドを、C言語で書かれた既存の実装からGo言語による新しい実装へと完全に書き換えるものです。これにより、pack コマンドはGoのビルドシステムから独立した「ツール」として位置づけられ、その機能セットも簡素化されています。

コミット

commit fdbf3d901b3c6c91ba0e5efe496f1518b53fd885
Author: Rob Pike <r@golang.org>
Date:   Wed Jan 15 09:13:52 2014 -0800

    cmd/pack: rewrite in Go
    Replace the pack command, a C program, with a clean reimplementation in Go.
    It does not need to reproduce the full feature set and it is no longer used by
    the build chain, but has a role in looking inside archives created by the build
    chain directly.
    
    Since it's not in C, it is no longer build by dist, so remove it from cmd/dist and
    make it a "tool" in cmd/go terminology.
    
    Fixes #2705
    
    R=rsc, dave, minux.ma, josharian
    CC=golang-codereviews
    https://golang.org/cl/52310044

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

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

元コミット内容

cmd/pack コマンドをGoで書き直す。 Cプログラムである pack コマンドを、Goでのクリーンな再実装に置き換える。 これは完全な機能セットを再現する必要はなく、ビルドチェーンではもはや使用されないが、ビルドチェーンによって直接作成されたアーカイブの中身を見る役割を持つ。

C言語ではないため、dist によってビルドされなくなり、cmd/dist から削除され、cmd/go の用語で「ツール」となる。

Fixes #2705

変更の背景

この変更の背景には、いくつかの重要な動機があります。

  1. C言語実装の複雑性と保守性: 元々 cmd/pack はPlan 9の ar ツールをベースにしたC言語で実装されていました。C言語での実装は、メモリ管理やポインタ操作の複雑さからバグが入り込みやすく、保守が困難になる傾向があります。Go言語への書き換えは、より安全で読みやすいコードベースを提供し、将来的なメンテナンスを容易にします。
  2. Goツールチェインの自己完結性: Goプロジェクトは、可能な限りGo言語自身でツールチェインを構築することを目指しています。C言語で書かれたツールをGoに移行することは、Goエコシステムの自己完結性を高め、クロスコンパイルや異なるプラットフォームでのビルドをよりスムーズにする上で重要です。
  3. ビルドチェーンからの分離と役割の明確化: コミットメッセージにあるように、cmd/pack はもはやGoのビルドチェーン(コンパイルやリンクのプロセス)で直接使用されなくなりました。これは、Goのビルドシステムが進化し、アーカイブ操作の内部的なニーズが変化したことを示唆しています。pack は、ビルドチェーンが生成したアーカイブを「検査する」ためのスタンドアロンツールとしての役割に特化されることになりました。これにより、pack はよりシンプルな機能セットで十分となり、不要な複雑さを取り除くことが可能になりました。
  4. Issue #2705の修正: このコミットは、GoのIssueトラッカーで報告されていた問題 #2705 を修正します。このIssueは「cmd/pack: remove from dist build」というタイトルで、cmd/packdist ビルドから削除されるべきであるという提案でした。これは、pack がもはやビルドプロセスに不可欠ではないという認識と一致しています。

これらの背景から、このコミットは単なる言語の書き換え以上の意味を持ち、Goツールチェインの設計思想と進化を反映した戦略的な変更であると言えます。

前提知識の解説

このコミットを理解するためには、以下の概念について知っておく必要があります。

  1. ar (Archiver) ツール:

    • ar はUnix系システムで広く使われているアーカイブユーティリティです。複数のファイルを一つのアーカイブファイル(通常は .a 拡張子を持つ)にまとめるために使用されます。
    • 主に静的ライブラリ(.lib.a)を作成する際に利用されます。コンパイラが生成したオブジェクトファイル(.o)をまとめて、リンカが利用しやすい形式にします。
    • ar の基本的な操作には、ファイルの追加 (r)、削除 (d)、リスト表示 (t)、展開 (x) などがあります。
    • Go言語の cmd/pack は、この ar ツールのGo言語版として機能します。
  2. Goのビルドチェーン:

    • Goのビルドチェーンとは、Goのソースコードが実行可能なバイナリになるまでの一連のプロセスを指します。これには、コンパイル、アセンブル、リンクなどが含まれます。
    • 初期のGoでは、cmd/pack がこのビルドチェーンの一部として、オブジェクトファイルをアーカイブにまとめる役割を担っていました。
    • しかし、Goのビルドシステムは進化し、内部的なアーカイブ処理が cmd/pack に依存しなくなったため、その役割が変更されました。
  3. cmd/dist:

    • cmd/dist は、GoのソースコードからGoツールチェイン自体をビルドするための内部ツールです。Goのコンパイラ、リンカ、その他の標準ツールなどをビルドする際に使用されます。
    • このコミット以前は、cmd/packcmd/dist によってビルドされていました。しかし、Goへの書き換えとビルドチェーンからの分離に伴い、cmd/dist の管理下から外されました。
  4. cmd/go の「ツール」:

    • Goのツールチェインには、go buildgo run といった主要なコマンドの他に、go tool サブコマンドを通じてアクセスできる補助的なツール群があります。例えば、go tool vetgo tool pprof などです。
    • これらのツールは、Goのビルドプロセスに直接組み込まれているわけではありませんが、開発者がGoプログラムを開発・デバッグ・分析する上で役立つユーティリティです。
    • cmd/pack が「ツール」として位置づけられるということは、go tool pack として実行されるようになり、Goのビルドシステムとは独立したユーティリティとしての役割が強調されることを意味します。
  5. Goの os パッケージと io パッケージ:

    • os パッケージは、オペレーティングシステム機能へのプラットフォームに依存しないインターフェースを提供します。ファイル操作(オープン、作成、読み書き)、ディレクトリ操作、プロセス管理などが含まれます。
    • io パッケージは、I/Oプリミティブの基本的なインターフェースを提供します。ReaderWriter インターフェースは、様々なデータソースやシンクに対して統一的な読み書き操作を可能にします。
    • C言語のファイルI/O関数(open, read, write など)や、Plan 9の Biobuf のようなカスタムバッファリングI/Oは、Go言語ではこれらの標準パッケージの機能に置き換えられます。

技術的詳細

このコミットの技術的な詳細を掘り下げると、C言語からGo言語への移行に伴う設計思想と実装パターンの変化が明確になります。

C言語版 cmd/pack (src/cmd/pack/ar.c) の特徴

  • Plan 9 ar の移植: ar.c は、Plan 9オペレーティングシステムの ar ツールをベースにしていました。Plan 9は、Unixとは異なる独自の設計思想を持つOSであり、そのツールも独特のI/Oモデルやシステムコールを使用しています。
  • Biobuf の使用: ar.c では、Plan 9の Biobuf というカスタムバッファリングI/O構造体が多用されていました。これは、標準Cライブラリの FILE ポインタとは異なる独自のAPIを提供します。
  • 手動メモリ管理: C言語であるため、mallocfree を用いた手動でのメモリ管理が必須でした。これにより、メモリリークやセグメンテーション違反といった問題が発生するリスクがありました。
  • グローバル変数の多用: aflag, bflag, vflag といったコマンドラインオプションや、astart, amiddle, aend といった一時ファイル管理用のポインタがグローバル変数として宣言されており、コードの可読性や並行処理の妨げになる可能性がありました。
  • #define を用いたマクロ: HEADER_IO のようなマクロが定義されており、コードの抽象化に利用されていましたが、デバッグが困難になることがあります。
  • Go固有の拡張: __.GOSYMDEF__.PKGDEF といったGo固有のセクションをアーカイブ内に管理するロジックが含まれていました。これは、Goのコンパイラがパッケージの型情報をインポートするために使用していました。

Go言語版 cmd/pack (src/cmd/pack/pack.go) の特徴

  • Go標準ライブラリの活用: os, io, fmt, log, strconv, strings, time, unicode/utf8 といったGoの豊富な標準ライブラリが活用されています。これにより、プラットフォーム間の互換性が向上し、コードの信頼性が高まります。
  • 構造体とメソッドによるオブジェクト指向的な設計: Archive 構造体や Entry 構造体が定義され、それぞれに関連するメソッド(readMetadata, scan, output, skip など)が実装されています。これにより、コードのモジュール性が高まり、各機能の責任が明確になります。
  • エラーハンドリングの改善: Goのエラーハンドリングパターン(多値戻り値と if err != nil)が採用されており、C言語の errno やグローバルなエラーフラグに依存するよりも、エラーの発生源と伝播が明確になります。log.Fatal を使用して、致命的なエラーが発生した際にプログラムを終了させています。
  • 簡素化された機能セット: コミットメッセージにあるように、Go版 pack は「完全な機能セットを再現する必要がない」ため、C版に存在した一部の複雑なロジック(例: シンボルテーブルの重複チェック、P フラグによるパスのプレフィックス削除など)が削除または簡素化されています。特に、ar.c にあった scanobjscanpkg のようなGo固有のメタデータ処理は、pack.go には直接見当たりません。これは、Goのビルドチェーンが pack に依存しなくなったため、これらの機能が不要になったことを示しています。
  • アーカイブフォーマットの明確化: arHeaderentryHeader といった定数でアーカイブのヘッダーフォーマットが明確に定義されており、読み書きのロジックがより理解しやすくなっています。
  • テストの追加: pack_test.go が追加され、Go版 pack の基本的な機能がテストされるようになりました。これにより、コードの品質と信頼性が向上します。

変更の具体的な影響

  • ビルドプロセスの変更: src/cmd/dist/build.c から cmd/pack のビルドが削除され、src/cmd/go/pkg.gocmd/packtoTool として追加されました。これは、packgo tool pack として実行される独立したユーティリティになったことを意味します。
  • Cソースコードの削除: src/cmd/pack/ar.csrc/cmd/pack/Makefile が削除され、C言語の依存関係が解消されました。
  • ドキュメントの更新: src/cmd/pack/doc.go が更新され、Go版 pack の新しい使い方と簡素化された機能が反映されました。特に、__.PKGDEF__.GOSYMDEF といったGo固有のセクションに関する記述が削除されています。

全体として、この変更はGoツールチェインの成熟と、Go言語の設計原則(シンプルさ、安全性、効率性)を反映したものです。

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

このコミットにおけるコアとなるコードの変更は、主に以下の3つのファイルに集約されます。

  1. src/cmd/pack/ar.c の削除:

    • C言語で書かれた cmd/pack の旧実装全体が削除されました。これは約1600行に及ぶ大規模な削除です。
    • このファイルには、ar アーカイブの読み書き、メンバーの追加・削除・抽出、シンボルテーブルの管理、Go固有の __.PKGDEF セクションの処理など、pack コマンドの全てのロジックが含まれていました。
  2. src/cmd/pack/pack.go の新規追加:

    • Go言語で書かれた cmd/pack の新実装が追加されました。これは約400行の新しいコードです。
    • このファイルには、main 関数、コマンドライン引数のパース (setOp)、アーカイブのオープンと作成 (archive, create), アーカイブヘッダーの検証 (mustBeArchive), エントリのメタデータ読み込み (readMetadata), アーカイブのスキャンとアクションの実行 (scan), エントリの出力 (output), スキップ (skip), ファイル名の一致判定 (match) など、Go版 pack の主要なロジックが含まれています。
    • C言語版にあったGo固有のメタデータ処理(scanobj, scanpkg など)は、この新しい実装には含まれていません。
  3. src/cmd/pack/pack_test.go の新規追加:

    • Go言語で書かれた cmd/pack のテストコードが追加されました。これは約250行の新しいコードです。
    • TestPack 関数を中心に、pack コマンドの r (追加), t (リスト), x (展開), p (表示) といった基本的な操作がテストされています。
    • テストは、一時ディレクトリにアーカイブファイルを作成し、pack コマンドを実行してその出力を検証する形式で書かれています。

その他、以下のファイルも変更されています。

  • src/cmd/dist/build.c:
    • buildordercleantab 配列から "cmd/pack" のエントリが削除されました。これは、cmd/packcmd/dist によるビルド対象から外されたことを意味します。
  • src/cmd/go/pkg.go:
    • goTools マップに "cmd/pack": toTool が追加されました。これは、cmd/packgo tool サブコマンドを通じて利用可能な「ツール」として登録されたことを意味します。
  • src/cmd/pack/Makefile の削除:
    • C言語版 pack のビルドに使用されていたMakefileが削除されました。
  • src/cmd/pack/doc.go の変更:
    • pack コマンドのドキュメントが更新され、Go版の新しい使用法と、g (PKGDEFの維持), S (アーカイブを安全とマーク), P (プレフィックス削除) といったGo固有のオプションが削除されたことが反映されました。

コアとなるコードの解説

Go言語版 cmd/pack のコアとなるコードは、src/cmd/pack/pack.go に実装されています。

main 関数とコマンドライン引数の処理

func main() {
	log.SetFlags(0)
	// need "pack op archive" at least.
	if len(os.Args) < 3 {
		usage()
	}
	setOp(os.Args[1]) // コマンドライン引数の最初の要素から操作を決定
	var ar *Archive
	switch op {
	case 'p':
		ar = archive(os.Args[2], os.O_RDONLY, os.Args[3:])
		ar.scan(ar.printContents)
	case 'r':
		ar = archive(os.Args[2], os.O_RDWR, os.Args[3:])
		ar.scan(ar.skipContents) // 既存のコンテンツをスキップ
		ar.addFiles()            // 新しいファイルを追加
	case 't':
		ar = archive(os.Args[2], os.O_RDONLY, os.Args[3:])
		ar.scan(ar.tableOfContents)
	case 'x':
		ar = archive(os.Args[2], os.O_RDONLY, os.Args[3:])
		ar.scan(ar.extractContents)
	default:
		usage()
	}
	if len(ar.files) > 0 {
		log.Fatalf("pack: file %q not in archive", ar.files[0])
	}
}

// setOp はコマンドライン引数から操作 (p, r, t, x) と verbose フラグを解析します。
func setOp(arg string) {
	for _, r := range arg {
		switch r {
		case 'p', 'r', 't', 'x':
			if op != 0 {
				usage() // 複数の操作は指定できない
			}
			op = r
		case 'v':
			if verbose {
				usage() // v フラグは一度だけ指定可能
			}
			verbose = true
		default:
			usage() // 不正なオプション
		}
	}
}

main 関数は、コマンドライン引数を解析し、指定された操作 (p, r, t, x) に応じて Archive オブジェクトを初期化し、対応する処理 (scan, addFiles など) を呼び出します。setOp 関数は、p, r, t, x のいずれかの操作と、v (verbose) フラグを解析します。

Archive 構造体とアーカイブ操作

type Archive struct {
	fd    *os.File // Open file descriptor.
	files []string // Explicit list of files to be processed.
}

// archive は指定されたアーカイブファイルを開くか、必要に応じて作成します。
func archive(name string, mode int, files []string) *Archive {
	fd, err := os.OpenFile(name, mode, 0)
	if err != nil && mode == os.O_RDWR && os.IsNotExist(err) {
		fd, err = create(name) // ファイルが存在しない場合は作成
	}
	if err != nil {
		log.Fatal("pack: ", err)
	}
	mustBeArchive(fd) // アーカイブヘッダーを検証
	return &Archive{
		fd:    fd,
		files: files,
	}
}

// create は新しいアーカイブファイルを作成し、ヘッダーを書き込みます。
func create(name string) (*os.File, error) {
	fd, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
	if err != nil {
		return nil, err
	}
	fmt.Fprint(fd, arHeader) // アーカイブヘッダーを書き込む
	fd.Seek(0, 0)            // ファイルポインタを先頭に戻す
	return fd, nil
}

// mustBeArchive はファイルのヘッダーが正しいアーカイブヘッダーであることを検証します。
func mustBeArchive(fd *os.File) {
	buf := make([]byte, len(arHeader))
	_, err := io.ReadFull(fd, buf)
	if err != nil || string(buf) != arHeader {
		log.Fatal("pack: file is not an archive: bad header")
	}
}

Archive 構造体は、開かれたアーカイブファイル (fd) と、処理対象のファイルリスト (files) を保持します。archive 関数は、アーカイブファイルを開くか、存在しない場合は create 関数を呼び出して新規作成します。mustBeArchive 関数は、アーカイブの先頭にある !<arch>\n というマジック文字列を読み込み、それが正しいアーカイブであることを確認します。

Entry 構造体とエントリのメタデータ処理

type Entry struct {
	name  string
	mtime int64
	uid   int
	gid   int
	mode  os.FileMode
	size  int64
}

// readMetadata はアーカイブ内の次のエントリのメタデータを読み込み、解析します。
func (ar *Archive) readMetadata() *Entry {
	buf := make([]byte, entryLen)
	_, err := io.ReadFull(ar.fd, buf)
	if err == io.EOF {
		return nil // エントリが残っていない
	}
	if err != nil || buf[entryLen-2] != '`' || buf[entryLen-1] != '\n' {
		log.Fatal("pack: file is not an archive: bad entry") // 不正なエントリヘッダー
	}
	entry := new(Entry)
	entry.name = strings.TrimRight(string(buf[:16]), " ") // ファイル名をパース
	// ... (mtime, uid, gid, mode, size のパースロジック) ...
	return entry
}

// scan はアーカイブを順次スキャンし、各エントリに対して指定されたアクションを実行します。
func (ar *Archive) scan(action func(*Entry)) {
	for {
		entry := ar.readMetadata()
		if entry == nil {
			break // 全てのエントリを処理した
		}
		action(entry) // 各エントリに対してアクションを実行
	}
}

Entry 構造体は、アーカイブ内の各ファイルエントリのメタデータ(名前、更新時刻、UID、GID、パーミッション、サイズ)を保持します。readMetadata メソッドは、アーカイブから固定長のヘッダーを読み込み、その内容をパースして Entry 構造体に格納します。scan メソッドは、アーカイブ内の全てのエントリを順次読み込み、それぞれに対して引数で渡された action 関数を実行します。

ファイル内容の操作

// output はエントリの内容を指定された io.Writer にコピーします。
func (ar *Archive) output(entry *Entry, w io.Writer) {
	n, err := io.Copy(w, io.LimitReader(ar.fd, entry.size)) // ファイル内容をコピー
	if err != nil {
		log.Fatal("pack: ", err)
	}
	if n != entry.size {
		log.Fatal("pack: short file")
	}
	if entry.size&1 == 1 {
		_, err := ar.fd.Seek(1, 1) // パディングバイトをスキップ
		if err != nil {
			log.Fatal("pack: ", err)
		}
	}
}

// skip はエントリの内容を読み飛ばします。
func (ar *Archive) skip(entry *Entry) {
	size := entry.size
	if size&1 == 1 {
		size++ // パディングバイトを考慮
	}
	_, err := ar.fd.Seek(size, 1) // ファイルポインタを移動
	if err != nil {
		log.Fatal("pack: ", err)
	}
}

output メソッドは、アーカイブ内のファイルの内容を io.Writer にコピーします。io.LimitReader を使用して、エントリのサイズ分だけを読み込むように制限しています。アーカイブフォーマットの仕様により、ファイルサイズが奇数の場合は1バイトのパディングが追加されるため、それも考慮してスキップします。skip メソッドは、ファイルの内容を実際に読み込むことなく、ファイルポインタを次のエントリの開始位置まで移動させます。

これらのGo言語のコードは、C言語版と比較して、Goの標準ライブラリと慣用的なエラーハンドリング、構造体とメソッドによるモジュール化された設計が特徴です。これにより、コードの可読性、保守性、信頼性が大幅に向上しています。

関連リンク

参考にした情報源リンク