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

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

このコミットは、Go言語のパッケージ管理ツールである goinstall の機能改善に関するものです。具体的には、ビルド失敗時に gofix ツールを自動実行する -fix フラグの追加と、エラーハンドリングおよびエラー報告の改善が主な変更点です。

goinstall は、Goパッケージのダウンロード、ビルド、インストールを行うためのコマンドラインツールです。このコミットで変更された主要なファイルは以下の通りです。

  • src/cmd/goinstall/download.go: パッケージのダウンロードとバージョン管理システムとの連携に関するロジックが含まれています。このコミットでは、主にエラー処理の改善と、Repo インターフェースのメソッドシグネチャの調整が行われています。
  • src/cmd/goinstall/main.go: goinstall コマンドのメインロジック、フラグの定義、パッケージのインストールフロー、およびエラー報告のメカニズムが含まれています。このファイルで、-fix フラグの追加、新しいエラー型の導入、およびインストールプロセスのエラーハンドリングが大幅に改善されています。

コミット

commit 5a18aef67cbd707cd15e6412ddd089d0b6fb4738
Author: Andrew Gerrand <adg@golang.org>
Date:   Tue Nov 29 09:28:58 2011 +1100

    goinstall: add -fix flag to run gofix on packages on build failure
    goinstall: better error handling and reporting
    
    R=r, r, rsc, mattn.jp
    CC=golang-dev
    https://golang.org/cl/5421051

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

https://github.com/golang/go/commit/5a18aef67cbd707cd15e6412ddd089d0b6fb4738

元コミット内容

このコミットの目的は以下の2点です。

  1. goinstall-fix フラグを追加し、ビルドが失敗した場合にパッケージに対して gofix を実行するようにする。
  2. goinstall のエラーハンドリングとエラー報告を改善する。

変更の背景

Go言語は活発に開発されており、APIの変更や言語仕様の進化が頻繁に行われていました。これに伴い、古いGoのコードが新しいGoのバージョンでビルドできなくなるという問題が発生することがありました。gofix ツールは、このようなAPIの変更に対応するために、古いGoのコードを自動的に新しいAPIに書き換えることを目的としています。

goinstall はGoパッケージをビルド・インストールする際に、ビルドエラーが発生すると処理を中断していました。このコミット以前の goinstall は、ビルドエラーが発生した場合に、その原因が古いAPIの使用によるものかどうかを判断し、自動的に修正を試みる機能を持っていませんでした。ユーザーは手動で gofix を実行し、再度 goinstall を試す必要がありました。これは特に、多くの依存関係を持つプロジェクトや、Goのバージョンアップ後に既存のコードをビルドする際に手間となっていました。

また、エラー報告も改善の余地がありました。従来のエラー報告は、エラーの種類が不明瞭であったり、詳細な情報が不足していたりすることがあり、ユーザーが問題の原因を特定し、解決するのを困難にしていました。

このコミットは、これらの課題に対処し、goinstall の使いやすさと堅牢性を向上させることを目的としています。ビルド失敗時に gofix を自動実行することで、ユーザーの手間を省き、よりスムーズなパッケージのインストール体験を提供します。また、より詳細で構造化されたエラー報告により、問題解決の効率化を図っています。

前提知識の解説

このコミットを理解するためには、以下のGo言語に関する基本的な知識が必要です。

  • Go言語のパッケージ管理: Go言語では、コードはパッケージとして組織されます。goinstall は、これらのパッケージをインターネットから取得し、ローカル環境にビルドしてインストールするためのツールです。Goのパッケージは、通常、$GOPATH/src 以下に配置され、goinstall はこのパスを基準に動作します。
  • goinstall コマンド: goinstall は、指定されたインポートパスに基づいてGoパッケージをダウンロード、ビルド、インストールするコマンドです。依存関係も自動的に解決し、再帰的にインストールします。
  • gofix コマンド: gofix は、Go言語のツールチェーンに含まれるユーティリティで、Goのソースコードを自動的に修正し、新しいGoのバージョンやAPIの変更に対応させるために使用されます。例えば、Goのバージョンアップによって特定の関数のシグネチャが変更された場合、gofix は古いシグネチャを使用しているコードを新しいものに自動的に書き換えることができます。これは、Go言語の進化に伴うコードの互換性問題を緩和するために非常に重要なツールです。
  • Goのビルドプロセス: Goのソースコードは、go build コマンドによってコンパイルされ、実行可能なバイナリやライブラリが生成されます。このプロセス中に、構文エラー、型エラー、依存関係の欠如など、様々な理由でビルドが失敗することがあります。
  • エラーハンドリング (Error Handling): Go言語では、エラーは error インターフェースを実装する値として扱われます。関数は、通常、最後の戻り値として error 型の値を返します。nil はエラーがないことを意味し、非 nilerror 値はエラーが発生したことを示します。このコミットでは、より具体的なエラー情報を伝えるために、カスタムエラー型が導入されています。
  • flag パッケージ: Goの標準ライブラリに含まれる flag パッケージは、コマンドライン引数を解析するために使用されます。このコミットでは、新しいコマンドラインフラグ -fix を定義するために使用されています。
  • defer ステートメント: Goの defer ステートメントは、関数がリターンする直前に実行される関数呼び出しをスケジュールします。このコミットでは、ビルド失敗時の gofix 実行とリトライロジックを実装するために defer が活用されています。

技術的詳細

このコミットの技術的な変更は、主に goinstall のエラー処理フローと、gofix の統合に焦点を当てています。

1. -fix フラグの追加と gofix の自動実行

  • src/cmd/goinstall/main.godoGofix = flag.Bool("fix", false, "gofix each package before building it") という新しいブーリアンフラグが追加されました。これにより、ユーザーは goinstall -fix のようにコマンドを実行することで、この機能を有効にできます。
  • installPackage 関数(後述)内に defer ステートメントが導入されました。この defer 関数は、installPackage がリターンする際に実行されます。
  • defer 関数内では、installErr (インストール中に発生したエラー) が nil でなく、かつ -fix フラグが有効な場合に gofix の実行が試みられます。
  • gofix の実行は、DependencyErrorBuildError 以外のエラーが原因でビルドが失敗した場合にのみ行われます。これは、依存関係のビルド失敗や、純粋なビルドエラーが原因の場合には gofix が問題を解決できない可能性が高いためです。
  • gofix 実行後、installPackageretry フラグを true にして再帰的に呼び出されます。これにより、gofix によってコードが修正された場合、自動的に再ビルドが試みられます。
  • 新しく追加された gofix 関数は、指定されたパッケージディレクトリ内のGoソースファイル (GoFiles および CgoFiles) に対して gofix コマンドを実行します。gofix の標準出力と標準エラーは、goinstall のそれらに直接リダイレクトされます。

2. エラーハンドリングとエラー報告の改善

  • カスタムエラー型の導入: 以前は一般的な errorf 関数でエラーを報告していましたが、このコミットでは以下の新しいカスタムエラー型が導入されました。
    • PackageError: 一般的なパッケージ関連のエラー。
    • DownloadError: パッケージのダウンロード中に発生したエラー。$GOPATH が設定されていない場合の追加情報も提供します。
    • DependencyError: 依存パッケージのビルド失敗に起因するエラー。
    • BuildError: パッケージのビルド中に発生したエラー。
    • RunError: 外部コマンド(例: git, hg, go build など)の実行中に発生したエラー。実行されたコマンド、ディレクトリ、出力、元のエラーを詳細に報告します。
  • install 関数の変更: 以前は void を返していた install 関数が error を返すように変更されました。これにより、エラーが呼び出し元に適切に伝播されるようになり、より堅牢なエラー処理が可能になりました。
  • main 関数の変更: main 関数内で、install から返されたエラーを捕捉し、os.Stderr に出力するように変更されました。これにより、エラーメッセージがユーザーに直接表示され、goinstall の終了コードもエラーの有無に応じて設定されるようになりました。
  • run 関数の改善: 外部コマンドを実行する run 関数が大幅に簡素化され、エラー発生時に RunError 型を返すようになりました。これにより、コマンドの実行失敗に関する詳細な情報(実行されたコマンド、作業ディレクトリ、標準出力/エラー出力、元のエラー)が提供され、デバッグが容易になります。
  • download.go の変更: RemoteRepo インターフェースの Repo メソッドのシグネチャが _ *http.Client から *http.Client に変更されました。これは、Goのリンターが未使用の引数に対して警告を出すのを避けるための慣習的な変更です。また、updateRepo 関数での外部コマンド実行エラーが RunError としてラップされるようになり、より詳細なエラー情報が提供されるようになりました。

これらの変更により、goinstall はエラー発生時に単に失敗するだけでなく、何が、なぜ失敗したのかをより明確にユーザーに伝えられるようになりました。また、gofix との連携により、一般的なビルドエラーの一部を自動的に解決し、開発者の負担を軽減します。

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

1. -fix フラグの定義 (src/cmd/goinstall/main.go)

+	doGofix           = flag.Bool("fix", false, "gofix each package before building it")

2. 新しいエラー型の定義 (src/cmd/goinstall/main.go)

+type PackageError struct {
+	pkg string
+	err error
+}
+
+func (e *PackageError) Error() string {
+	return fmt.Sprintf("%s: %v", e.pkg, e.err)
+}
+
+type DownloadError struct {
+	pkg    string
+	goroot bool
+	err    error
+}
+
+func (e *DownloadError) Error() string {
+	s := fmt.Sprintf("%s: download failed: %v", e.pkg, e.err)
+	if e.goroot && os.Getenv("GOPATH") == "" {
+		s += " ($GOPATH is not set)"
+	}
+	return s
+}
+
+type DependencyError PackageError
+
+func (e *DependencyError) Error() string {
+	return fmt.Sprintf("%s: depends on failing packages:\\n\\t%v", e.pkg, e.err)
+}
+
+type BuildError PackageError
+
+func (e *BuildError) Error() string {
+	return fmt.Sprintf("%s: build failed: %v", e.pkg, e.err)
+}
+
+type RunError struct {
+	cmd, dir string
+	out      []byte
+	err      error
+}
+
+func (e *RunError) Error() string {
+	return fmt.Sprintf("%v\\ncd %q && %q\\n%s", e.err, e.dir, e.cmd, e.out)
+}

3. install 関数のシグネチャ変更と installPackage への委譲 (src/cmd/goinstall/main.go)

-func install(pkg, parent string) {
+func install(pkg, parent string) error {
...
-	// Install prerequisites.
-	dir := filepath.Join(tree.SrcDir(), filepath.FromSlash(pkg))
-	dirInfo, err := build.ScanDir(dir)
-	if err != nil {
-		terrorf(tree, "%s: %v\\n", pkg, err)
-		return
-	}
-	// We reserve package main to identify commands.
-	if parent != "" && dirInfo.Package == "main" {
-		terrorf(tree, "%s: found only package main in %s; cannot import", pkg, dir)
-		return
-	}
-	for _, p := range dirInfo.Imports {
-		if p != "C" {
-			install(p, pkg)
-		}
-	}
-	if errors_ {
-		return
-	}
+	// Install the package and its dependencies.
+	if err := installPackage(pkg, parent, tree, false); err != nil {
+		return err
+	}
+
+	if remote {
+		// mark package as installed in goinstall.log
+		logged := logPackage(pkg, tree)
+
+		// report installation to the dashboard if this is the first
+		// install from a public repository.
+		if logged && public {
+			maybeReportToDashboard(pkg)
+		}
+	}
+
+	return nil
+}

4. installPackage 関数の導入と gofix 実行ロジック (src/cmd/goinstall/main.go)

+func installPackage(pkg, parent string, tree *build.Tree, retry bool) (installErr error) {
+	printf("%s: install\\n", pkg)
+
+	// Read package information.
+	dir := filepath.Join(tree.SrcDir(), filepath.FromSlash(pkg))
+	dirInfo, err := build.ScanDir(dir)
+	if err != nil {
+		return &PackageError{pkg, err}
+	}
+
+	// We reserve package main to identify commands.
+	if parent != "" && dirInfo.Package == "main" {
+		return &PackageError{pkg, fmt.Errorf("found only package main in %s; cannot import", dir)}
+	}
+
+	// Run gofix if we fail to build and -fix is set.
+	defer func() {
+		if retry || installErr == nil || !*doGofix {
+			return
+		}
+		if e, ok := (installErr).(*DependencyError); ok {
+			// If this package failed to build due to a
+			// DependencyError, only attempt to gofix it if its
+			// dependency failed for some reason other than a
+			// DependencyError or BuildError.
+			// (If a dep or one of its deps doesn't build there's
+			// no way that gofixing this package can help.)
+			switch e.err.(type) {
+			case *DependencyError:
+				return
+			case *BuildError:
+				return
+			}
+		}
+		gofix(pkg, dir, dirInfo)
+		installErr = installPackage(pkg, parent, tree, true) // retry
+	}()
+
+	// Install prerequisites.
+	for _, p := range dirInfo.Imports {
+		if p == "C" {
+			continue
+		}
+		if err := install(p, pkg); err != nil {
+			return &DependencyError{pkg, err}
+		}
+	}
+
+	// Install this package.
+	if *useMake {
+		err := domake(dir, pkg, tree, dirInfo.IsCommand())
+		if err != nil {
+			return &BuildError{pkg, err}
+		}
+		return nil
+	}
+	script, err := build.Build(tree, pkg, dirInfo)
+	if err != nil {
+		return &BuildError{pkg, err}
+	}
+	if *nuke {
+		printf("%s: nuke\\n", pkg)
+		script.Nuke()
+	} else if *clean {
+		printf("%s: clean\\n", pkg)
+		script.Clean()
+	}
+	if *doInstall {
+		if script.Stale() {
+			printf("%s: install\\n", pkg)
+			if err := script.Run(); err != nil {
+				return &BuildError{pkg, err}
+			}
+		} else {
+			printf("%s: up-to-date\\n", pkg)
+		}
+	}
+
+	return nil
+}

5. gofix 関数の定義 (src/cmd/goinstall/main.go)

+// gofix runs gofix against the GoFiles and CgoFiles of dirInfo in dir.
+func gofix(pkg, dir string, dirInfo *build.DirInfo) {
+	printf("%s: gofix\\n", pkg)
+	files := append([]string{}, dirInfo.GoFiles...)
+	files = append(files, dirInfo.CgoFiles...)
+	for i, file := range files {
+		files[i] = filepath.Join(dir, file)
+	}
+	cmd := exec.Command("gofix", files...)
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err != nil {
+		logf("%s: gofix: %v", pkg, err)
+	}
+}

6. run 関数のエラー報告改善 (src/cmd/goinstall/main.go)

-func run(dir string, stdin []byte, cmd ...string) error {
-	return genRun(dir, stdin, cmd, false)
-}
-
-// quietRun is like run but prints nothing on failure unless -v is used.
-func quietRun(dir string, stdin []byte, cmd ...string) error {
-	return genRun(dir, stdin, cmd, true)
-}
-
-// genRun implements run and quietRun.
-func genRun(dir string, stdin []byte, arg []string, quiet bool) error {
+func run(dir string, stdin []byte, arg ...string) error {
 	cmd := exec.Command(arg[0], arg[1:]...)
 	cmd.Stdin = bytes.NewBuffer(stdin)
 	cmd.Dir = dir
 	printf("cd %s && %s %s\\n", dir, cmd.Path, strings.Join(arg[1:], " "))
-	out, err := cmd.CombinedOutput()
-	if err != nil {
-		if !quiet || *verbose {
-			if dir != "" {
-				dir = "cd " + dir + "; "
-			}
-			fmt.Fprintf(os.Stderr, "%s: === %s%s\\n", cmd.Path, dir, strings.Join(cmd.Args, " "))
-			os.Stderr.Write(out)
-			fmt.Fprintf(os.Stderr, "--- %s\\n", err)
-		}
-		return errors.New("running " + arg[0] + ": " + err.Error())
+	if out, err := cmd.CombinedOutput(); err != nil {
+		if *verbose {
+			fmt.Fprintf(os.Stderr, "%v\\n%s\\n", err, out)
+		}
+		return &RunError{strings.Join(arg, " "), dir, out, err}
 	}
 	return nil
 }

7. download.goRepo メソッドシグネチャ変更

-	Repo(_ *http.Client) (url, root string, vcs *vcs, err error)
+	Repo(*http.Client) (url, root string, vcs *vcs, err error)

コアとなるコードの解説

1. -fix フラグの定義

flag.Bool("fix", false, ...) は、goinstall コマンドに -fix という新しいブーリアン型のコマンドライン引数を追加します。デフォルト値は false で、このフラグが指定された場合にのみ gofix の自動実行が有効になります。

2. 新しいエラー型の定義

Go言語の error インターフェースを実装する複数のカスタムエラー型が定義されています。これらの型は、エラーが発生したコンテキスト(パッケージ、ダウンロード、依存関係、ビルド、外部コマンド実行)に応じて、より具体的な情報を提供します。

  • PackageError, DownloadError, DependencyError, BuildError: これらは、それぞれ pkg (パッケージ名) と err (元のエラー) を持ち、Error() メソッドで整形されたエラーメッセージを返します。DownloadError$GOPATH が設定されていない場合にその旨を追記するロジックも持ちます。
  • RunError: 外部コマンドの実行失敗を詳細に報告するためのエラー型です。cmd (実行されたコマンド文字列)、dir (実行ディレクトリ)、out (コマンドの標準出力/エラー出力)、err (元のエラー) を含み、これらを組み合わせて非常に詳細なエラーメッセージを生成します。これにより、ユーザーはどのコマンドが、どのディレクトリで、どのような出力とともに失敗したのかを正確に把握できます。

これらのカスタムエラー型を使用することで、goinstall はエラーの種類を区別し、より適切なエラーメッセージをユーザーに提示できるようになります。

3. install 関数の変更

install 関数は、Goパッケージのインストールプロセス全体を管理する主要な関数です。このコミットでは、そのシグネチャが func install(pkg, parent string) から func install(pkg, parent string) error に変更されました。これにより、install 関数内で発生したエラーが呼び出し元に明示的に返されるようになり、エラー処理の連鎖が改善されました。

また、以前 install 関数内に直接記述されていたパッケージのスキャン、依存関係の解決、ビルド、インストールといったロジックの大部分が、新しく導入された installPackage 関数に委譲されました。これにより、関数の責務が明確になり、コードの可読性と保守性が向上しています。

4. installPackage 関数の導入と gofix 実行ロジック

installPackage 関数は、個々のGoパッケージのインストールとビルドのロジックをカプセル化するために導入されました。この関数は、以下の重要な機能を含んでいます。

  • エラーハンドリング: パッケージのスキャン、依存関係のインストール、ビルドの各段階で発生するエラーを捕捉し、適切なカスタムエラー型(PackageError, DependencyError, BuildError など)でラップして返します。
  • defer を用いた gofix の自動実行とリトライ:
    defer func() {
        if retry || installErr == nil || !*doGofix {
            return
        }
        // ... エラーの種類に応じたgofix実行条件のチェック ...
        gofix(pkg, dir, dirInfo)
        installErr = installPackage(pkg, parent, tree, true) // retry
    }()
    
    この defer ブロックは、installPackage 関数が終了する直前に実行されます。
    • retry || installErr == nil || !*doGofix: この条件は、gofix を実行すべきでないケース(既にリトライ済み、エラーがない、-fix フラグが有効でない)をチェックします。
    • installErrDependencyError または BuildError の場合、gofix は実行されません。これは、これらのエラーがコードの構文的な問題ではなく、依存関係の欠如やビルド環境の問題に起因する可能性が高いためです。
    • 上記の条件を満たし、かつ installErr が存在する場合にのみ、gofix(pkg, dir, dirInfo) が呼び出され、パッケージのソースコードが自動修正されます。
    • gofix 実行後、installErr = installPackage(pkg, parent, tree, true) によって、installPackageretry=true の状態で再帰的に呼び出されます。これにより、gofix による修正が成功した場合、パッケージのビルドが自動的に再試行されます。

このロジックは、Goのビルドプロセスにおける一般的な課題(APIの変更によるビルドエラー)を自動的に解決しようとする、非常に実用的な改善です。

5. gofix 関数の定義

gofix 関数は、指定されたディレクトリ内のGoソースファイルに対して gofix コマンドを実行するためのヘルパー関数です。exec.Command("gofix", files...) を使用して gofix バイナリを呼び出し、その標準出力と標準エラーを goinstall の出力に直接接続します。これにより、gofix の実行状況がユーザーにリアルタイムで表示されます。

6. run 関数のエラー報告改善

run 関数は、goinstall が内部で外部コマンド(例: git, hg, go build など)を実行するために使用される汎用的なヘルパー関数です。このコミットでは、run 関数がコマンドの実行に失敗した場合に、より詳細なエラー情報を含む RunError 型を返すように変更されました。

以前は、エラー発生時に単にエラーメッセージを標準エラーに出力し、汎用的な errors.New を返していました。変更後、cmd.CombinedOutput() でコマンドの出力とエラー出力をまとめて取得し、エラーが発生した場合は RunError を構築して返します。RunError は、実行されたコマンド、作業ディレクトリ、コマンドの出力、および元のエラーをカプセル化するため、デバッグ時に非常に役立つ情報を提供します。

7. download.goRepo メソッドシグネチャ変更

RemoteRepo インターフェースの Repo メソッドの引数 _ *http.Client*http.Client に変更されました。Goでは、引数名が _ で始まる場合、その引数は使用されないことを示します。この変更は、引数が実際に使用されるかどうかにかかわらず、より一般的なシグネチャを使用するようにするための慣習的な調整であり、コードの意図を明確にするものです。

関連リンク

  • Go言語公式サイト: https://golang.org/
  • goinstall のドキュメント (Go 1.0時点): goinstall はGo 1.11で go get に統合され、現在は独立したコマンドとしては存在しません。しかし、当時の機能についてはGoの古いドキュメントやブログ記事で参照できます。
  • gofix のドキュメント: gofix はGoのツールチェーンの一部として提供されています。詳細はGoの公式ドキュメントを参照してください。

参考にした情報源リンク

  • Go Gerrit Change 5421051: https://golang.org/cl/5421051
  • Go言語の公式ドキュメント (当時のバージョンに準ずる)
  • Go言語のソースコード (特に src/cmd/goinstall ディレクトリ)