[インデックス 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点です。
goinstall
に-fix
フラグを追加し、ビルドが失敗した場合にパッケージに対してgofix
を実行するようにする。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
はエラーがないことを意味し、非nil
のerror
値はエラーが発生したことを示します。このコミットでは、より具体的なエラー情報を伝えるために、カスタムエラー型が導入されています。 flag
パッケージ: Goの標準ライブラリに含まれるflag
パッケージは、コマンドライン引数を解析するために使用されます。このコミットでは、新しいコマンドラインフラグ-fix
を定義するために使用されています。defer
ステートメント: Goのdefer
ステートメントは、関数がリターンする直前に実行される関数呼び出しをスケジュールします。このコミットでは、ビルド失敗時のgofix
実行とリトライロジックを実装するためにdefer
が活用されています。
技術的詳細
このコミットの技術的な変更は、主に goinstall
のエラー処理フローと、gofix
の統合に焦点を当てています。
1. -fix
フラグの追加と gofix
の自動実行
src/cmd/goinstall/main.go
にdoGofix = flag.Bool("fix", false, "gofix each package before building it")
という新しいブーリアンフラグが追加されました。これにより、ユーザーはgoinstall -fix
のようにコマンドを実行することで、この機能を有効にできます。installPackage
関数(後述)内にdefer
ステートメントが導入されました。このdefer
関数は、installPackage
がリターンする際に実行されます。defer
関数内では、installErr
(インストール中に発生したエラー) がnil
でなく、かつ-fix
フラグが有効な場合にgofix
の実行が試みられます。gofix
の実行は、DependencyError
やBuildError
以外のエラーが原因でビルドが失敗した場合にのみ行われます。これは、依存関係のビルド失敗や、純粋なビルドエラーが原因の場合にはgofix
が問題を解決できない可能性が高いためです。gofix
実行後、installPackage
はretry
フラグを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.go
の Repo
メソッドシグネチャ変更
- 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
フラグが有効でない)をチェックします。installErr
がDependencyError
またはBuildError
の場合、gofix
は実行されません。これは、これらのエラーがコードの構文的な問題ではなく、依存関係の欠如やビルド環境の問題に起因する可能性が高いためです。- 上記の条件を満たし、かつ
installErr
が存在する場合にのみ、gofix(pkg, dir, dirInfo)
が呼び出され、パッケージのソースコードが自動修正されます。 gofix
実行後、installErr = installPackage(pkg, parent, tree, true)
によって、installPackage
がretry=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.go
の Repo
メソッドシグネチャ変更
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
ディレクトリ)