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

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

このコミットは、Go言語のコマンドラインツールであるgoコマンド自体(cmd/goディレクトリに存在する)が、自身のディレクトリ内で引数なしのgo installコマンドを実行した場合に正しく動作するように修正するものです。具体的には、ローカルファイルシステムのパスを適切に処理し、cmdディレクトリ内のパッケージを正しく解決できるように改善されました。

コミット

commit 4505ae3863d363a9ef76e6ee4ead162493c0b143
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jan 12 15:27:57 2012 -0800

    cmd/go: handle path to cmd directory

    Now it works to run 'go install' (no args) in cmd/go.

    Fixes #2679.

    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5543046

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

https://github.com/golang/go/commit/4505ae3863d363a9ef76e6ee4ead162493c0b143

元コミット内容

cmd/go: handle path to cmd directory

Now it works to run 'go install' (no args) in cmd/go.

Fixes #2679.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5543046

変更の背景

このコミットが行われる前は、Go言語のソースコードリポジトリ内のsrc/cmd/goディレクトリ(goコマンド自身のソースコードが置かれている場所)で、引数なしのgo installコマンドを実行すると、正しく動作しませんでした。これは、goコマンドが自身のパッケージを解決する際に、ローカルファイルシステムのパス(例: ./../で始まるパス、または絶対パス)の扱いが不十分であったためです。

この問題は、Goの旧来の課題追跡システムで「Issue 2679」として報告されていました。このコミットは、この特定のシナリオ(cmd/goディレクトリ内でのgo install)を修正し、goコマンドが自身のビルドとインストールを適切に行えるようにすることを目的としています。

前提知識の解説

go installコマンド

go installコマンドは、Goのパッケージをコンパイルし、その結果生成される実行可能ファイル(コマンドの場合)またはアーカイブファイル(ライブラリの場合)をGOPATH/binまたはGOROOT/bin(コマンドの場合)やGOPATH/pkgまたはGOROOT/pkg(ライブラリの場合)にインストールするために使用されます。引数なしで実行された場合、現在のディレクトリにあるパッケージをインストールしようとします。

Goのパッケージ管理とインポートパス

Goのパッケージは、ファイルシステム上のディレクトリ構造と対応しています。Goのソースコードは通常GOPATH/srcまたはGOROOT/src以下に配置され、インポートパス(例: "fmt", "net/http")はこれらのルートからの相対パスとして解決されます。

cmd/goディレクトリの特殊性

src/cmd/goディレクトリは、Go言語のツールチェインの中核であるgoコマンド自身のソースコードを格納しています。そのため、このディレクトリ内でgo installを実行するというシナリオは、goコマンドが自分自身をビルド・インストールするという、やや特殊なケースとなります。

Go標準ライブラリ関数

  • strings.HasPrefix(s, prefix string) bool: 文字列sが指定されたprefixで始まるかどうかを判定します。
  • strings.Contains(s, substr string) bool: 文字列sが指定されたsubstrを含むかどうかを判定します。
  • filepath.IsAbs(path string) bool: 指定されたパスが絶対パスであるかどうかを判定します。
  • filepath.Join(elem ...string) string: 複数のパス要素を結合して、プラットフォーム固有のパス区切り文字で区切られた単一のパスを生成します。
  • filepath.Separator: プラットフォーム固有のパス区切り文字(Windowsでは\\、Unix系では/)。
  • os.Stat(name string) (FileInfo, error): 指定されたファイルまたはディレクトリの情報を返します。
  • st.IsDir() bool: os.Statが返すFileInfoインターフェースのメソッドで、それがディレクトリであるかどうかを判定します。
  • exec.Command(name string, arg ...string) *Cmd: 指定されたコマンドと引数で新しいCmd構造体を作成します。
  • Cmd.CombinedOutput() ([]byte, error): コマンドを実行し、その標準出力と標準エラー出力を結合したバイトスライスとして返します。

... (glob pattern)

Goのコマンドラインツールでは、パッケージパスに...を使用することで、特定のディレクトリ以下のすべてのパッケージを意味するグロブパターンとして機能します。例えば、./...は現在のディレクトリ以下のすべてのパッケージを、cmd/...cmdディレクトリ以下のすべてのパッケージを指します。

技術的詳細

このコミットは、主にsrc/cmd/go/main.gosrc/cmd/go/pkg.goの2つのファイルを変更しています。

src/cmd/go/main.goの変更点

  1. isLocalPath関数の導入: 新しいヘルパー関数isLocalPath(arg string) boolが追加されました。この関数は、引数argがローカルファイルシステム上のディレクトリを示すインポートパス(つまり、..../../のいずれかで始まるパス)である場合にtrueを返します。
    func isLocalPath(arg string) bool {
    	return arg == "." || arg == ".." || strings.HasPrefix(arg, "./") || strings.HasPrefix(arg, "../")
    }
    
  2. importPaths関数の修正: importPaths関数内で、strings.HasPrefix(a, "./") || strings.HasPrefix(a, "../")という条件が、新しく導入されたisLocalPath(a)に置き換えられました。これにより、ローカルパスの判定ロジックがより簡潔かつ正確になりました。特に、./...../...のようなパターンを処理する際に、ローカルパスとして認識されるようになります。
  3. runOut関数の導入: runOutという新しいヘルパー関数が追加されました。これは、外部コマンドを実行し、その標準出力と標準エラー出力を結合した結果をバイトスライスとして返すものです。コマンドの実行に失敗した場合は、エラーメッセージを標準エラー出力に書き込み、エラーを発生させます。これは、goコマンドが内部的に他のコマンド(例えば、go tool compileなど)を実行する際に利用されます。

src/cmd/go/pkg.goの変更点

  1. loadPackage関数の修正: loadPackage関数は、与えられた引数からGoパッケージをロードする役割を担っています。この関数に、cmdディレクトリ内のパッケージをローカルパスとして扱うための重要な変更が加えられました。

    • 標準コマンドのパス解決の改善: 以前は、cmd/で始まるパスが標準コマンドであると判断される条件に!filepath.IsAbs(arg)(引数が絶対パスではない)が含まれていました。このコミットでは、この!filepath.IsAbs(arg)の条件が削除されました。これにより、cmd/で始まるパスが絶対パスであっても、標準コマンドとして適切に処理される可能性が広がりました。

    • ローカルパスとしてのcmdディレクトリの検出: 新たに、引数が絶対パスまたはisLocalPathで判定されるローカルパスである場合に、それがGOROOT/src/cmdディレクトリ以下のパスであるかをチェックするロジックが追加されました。 具体的には、引数を絶対パスに変換し、それがGOROOT/src/cmdディレクトリのパスで始まるかどうか、そしてそれがディレクトリであるかどうかを確認します。この条件が満たされた場合、そのパスは標準コマンドのソースディレクトリとして認識され、適切なTreeimportPathが設定されます。これにより、cmd/goディレクトリ内でgo installを実行した際に、goコマンド自身が自身のパッケージを正しく見つけられるようになります。

これらの変更により、goコマンドは、ローカルファイルシステム上のパス、特にcmdディレクトリ内のパスをより柔軟かつ正確に解釈できるようになり、go installコマンドの動作が改善されました。

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

src/cmd/go/main.go

--- a/src/cmd/go/main.go
+++ b/src/cmd/go/main.go
@@ -192,7 +192,7 @@ func importPaths(args []string) []string {
 	}
 	var out []string
 	for _, a := range args {
-		if (strings.HasPrefix(a, "./") || strings.HasPrefix(a, "../")) && strings.Contains(a, "...") {
+		if isLocalPath(a) && strings.Contains(a, "...") {
 			out = append(out, allPackagesInFS(a)...)
 			continue
 		}
@@ -246,6 +246,17 @@ func run(cmdargs ...interface{}) {
 	}
 }

+func runOut(cmdargs ...interface{}) []byte {
+	cmdline := stringList(cmdargs...)
+	out, err := exec.Command(cmdline[0], cmdline[1:]...).CombinedOutput()
+	if err != nil {
+		os.Stderr.Write(out)
+		errorf("%v", err)
+		out = nil
+	}
+	return out
+}
+
 // matchPattern(pattern)(name) reports whether
 // name matches pattern.  Pattern is a limited glob
 // pattern in which '...' means 'any string' and there
@@ -422,3 +433,10 @@ func stringList(args ...interface{}) []string {
 	}
 	return x
 }
+
+// isLocalPath returns true if arg is an import path denoting
+// a local file system directory.  That is, it returns true if the
+// path begins with ./ or ../ .
+func isLocalPath(arg string) bool {
+	return arg == "." || arg == ".." || strings.HasPrefix(arg, "./") || strings.HasPrefix(arg, "../")
+}

src/cmd/go/pkg.go

--- a/src/cmd/go/pkg.go
+++ b/src/cmd/go/pkg.go
@@ -79,7 +79,7 @@ func loadPackage(arg string) (*Package, error) {
 	t, importPath, err := build.FindTree(arg)
 	dir := ""
 	// Maybe it is a standard command.
-	if err != nil && !filepath.IsAbs(arg) && strings.HasPrefix(arg, "cmd/") {
+	if err != nil && strings.HasPrefix(arg, "cmd/") {
 		goroot := build.Path[0]
 		p := filepath.Join(goroot.Path, "src", arg)
 		if st, err1 := os.Stat(p); err1 == nil && st.IsDir() {
@@ -89,6 +89,19 @@ func loadPackage(arg string) (*Package, error) {
 		}
 	}
+	// Maybe it is a path to a standard command.
+	if err != nil && (filepath.IsAbs(arg) || isLocalPath(arg)) {
+		arg, _ := filepath.Abs(arg)
+		goroot := build.Path[0]
+		cmd := filepath.Join(goroot.Path, "src", "cmd") + string(filepath.Separator)
+		if st, err1 := os.Stat(arg); err1 == nil && st.IsDir() && strings.HasPrefix(arg, cmd) {
+			t = goroot
+			importPath = filepath.FromSlash(arg[len(cmd):])
+			dir = arg
+			err = nil
+		}
+	}
+
 	if err != nil {
 		return nil, err
 	}

コアとなるコードの解説

src/cmd/go/main.go

  • isLocalPath関数: この関数は、Goのインポートパスがファイルシステム上のローカルディレクトリを指すかどうかを判定します。具体的には、パスが..../、または../のいずれかである場合にtrueを返します。これにより、goコマンドが./...のようなローカルのグロブパターンを正しく解釈できるようになります。

  • importPaths関数の変更: importPaths関数は、コマンドライン引数からインポートパスのリストを生成します。以前は、ローカルパスの判定にstrings.HasPrefixを直接使用していましたが、isLocalPath関数を導入することで、このロジックがより明確になり、保守性が向上しました。これにより、go install ./...のようなコマンドが、現在のディレクトリ以下のすべてのパッケージを正しく対象とできるようになります。

  • runOut関数: この関数は、外部コマンドを実行し、その標準出力と標準エラー出力をまとめて取得するためのユーティリティです。コマンドの実行中にエラーが発生した場合、そのエラーメッセージを標準エラー出力に表示し、エラーを返します。これは、goコマンドが内部でコンパイラやリンカなどの他のツールを呼び出す際に、その出力を効率的に処理するために使用されます。

src/cmd/go/pkg.go

  • loadPackage関数の変更: loadPackage関数は、指定された引数からGoパッケージの情報をロードします。
    1. 標準コマンドの絶対パス対応: 以前のコードでは、cmd/で始まるパスが標準コマンドであると判断される際に、そのパスが絶対パスではないという条件(!filepath.IsAbs(arg))がありました。この条件が削除されたことで、goコマンドはcmd/goのような標準コマンドのパスが絶対パスとして与えられた場合でも、それを正しく標準コマンドとして認識できるようになりました。
    2. ローカルなcmdディレクトリの検出: 新しく追加されたブロックは、引数が絶対パスであるか、またはisLocalPathで判定されるローカルパスである場合に、それがGOROOT/src/cmdディレクトリ以下のパスであるかを詳細にチェックします。このチェックでは、os.Statでパスが実際にディレクトリであること、そしてstrings.HasPrefixGOROOT/src/cmdのパスで始まることを確認します。これにより、goコマンドがsrc/cmd/goディレクトリ内で実行された際に、自身のパッケージをローカルパスとして正しく解決し、ビルド・インストールプロセスを進めることができるようになります。

これらの変更は、goコマンドが自身のソースコードを扱う際の堅牢性を高め、開発者がgoコマンドのソースディレクトリ内で直接作業する際の利便性を向上させました。

関連リンク

参考にした情報源リンク