[インデックス 12345] ファイルの概要
このコミットは、Go言語のコマンドラインツール cmd/go
における相対インポートの挙動を修正するものです。特に、Windows環境でのパス名の問題(コロン :
の使用)や、Goパス外のパッケージのテストにおける相対インポートの解決に関する根本的な問題を解決するために、Goコンパイラ (gc
) に新しいオプション -D
を導入し、擬似的なインポートパス階層 _/<full_path>
を用いることで、相対インポートを通常のインポートとして扱えるように変更しています。
コミット
commit 604f3751104e655f76e5368a3a4177d58fe1509c
Author: Russ Cox <rsc@golang.org>
Date: Fri Mar 2 22:16:02 2012 -0500
cmd/go: fix relative imports again
I tried before to make relative imports work by simply
invoking the compiler in the right directory, so that
an import of ./foo could be resolved by ./foo.a.
This required creating a separate tree of package binaries
that included the full path to the source directory, so that
/home/gopher/bar.go would be compiled in
tmpdir/work/local/home/gopher and perhaps find
a ./foo.a in that directory.
This model breaks on Windows because : appears in path
names but cannot be used in subdirectory names, and I
missed one or two places where it needed to be removed.
The model breaks more fundamentally when compiling
a test of a package that lives outside the Go path, because
we effectively use a ./ import in the generated testmain,
but there we want to be able to resolve the ./ import
of the test package to one directory and all the other ./
imports to a different directory. Piggybacking on the compiler's
current working directory is then no longer possible.
Instead, introduce a new compiler option -D prefix that
makes the compiler turn a ./ import into prefix+that,
so that import "./foo" with -D a/b/c turns into import
"a/b/c/foo". Then we can invent a package hierarchy
"_/\" with subdirectories named for file system paths:
import "./foo" in the directory /home/gopher becomes
import "_/home/gopher/foo", and since that final path
is just an ordinary import now, all the ordinary processing
works, without special cases.
We will have to change the name of the hierarchy if we
ever decide to introduce a standard package with import
path "_", but that seems unlikely, and the detail is known
only in temporary packages that get thrown away at the
end of a build.
Fixes #3169.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5732045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/604f3751104e655f76e5368a3a4177d58fe1509c
元コミット内容
cmd/go: fix relative imports again
以前の相対インポートの修正試みでは、コンパイラを適切なディレクトリで呼び出すことで ./foo
のようなインポートが ./foo.a
として解決されるようにしていました。これは、ソースディレクトリへの完全なパスを含むパッケージバイナリの独立したツリーを作成することを必要とし、例えば /home/gopher/bar.go
は tmpdir/work/local/home/gopher
でコンパイルされ、そのディレクトリ内で ./foo.a
を見つけることが期待されていました。
このモデルは、Windows上でパス名にコロン :
が含まれるためにサブディレクトリ名として使用できないという問題で破綻しました。また、コロンを削除する必要がある箇所がいくつか見落とされていました。
さらに根本的な問題として、Goパス外に存在するパッケージのテストをコンパイルする際にこのモデルは破綻します。生成される testmain
では実質的に ./
インポートを使用しますが、ここではテストパッケージの ./
インポートをあるディレクトリに解決し、他のすべての ./
インポートを別のディレクトリに解決できるようにする必要があります。コンパイラの現在の作業ディレクトリに依存する方式では、これが不可能でした。
代わりに、新しいコンパイラオプション -D prefix
を導入します。これにより、コンパイラは ./
インポートを prefix+that
に変換します。例えば、-D a/b/c
を指定して import "./foo"
とすると、import "a/b/c/foo"
になります。
この仕組みを利用して、ファイルシステムパスに名前が付けられたサブディレクトリを持つパッケージ階層 _/<full_path>
を考案します。例えば、/home/gopher
ディレクトリ内の import "./foo"
は import "_/home/gopher/foo"
となります。この最終的なパスは通常のインポートパスであるため、特別なケースを設けることなく、すべての通常の処理が機能します。
将来、インポートパスが _
である標準パッケージを導入することを決定した場合、この階層の名前を変更する必要があるかもしれませんが、それは起こりそうになく、この詳細はビルドの最後に破棄される一時的なパッケージ内でのみ知られています。
Issue #3169 を修正します。
変更の背景
このコミットの背景には、Go言語のビルドシステムにおける相対インポートの扱いの複雑さと、それに伴う複数の問題がありました。
-
Windows環境でのパスの問題: 以前のGoのビルドシステムでは、相対インポート(例:
./foo
)を解決するために、コンパイラをソースファイルが存在するディレクトリで実行し、そのディレクトリからの相対パスでパッケージバイナリを解決しようとしていました。しかし、Windowsのファイルパスにはドライブレターを示すコロン(例:C:\Users\foo
)が含まれます。このコロンは、Unix系のファイルシステムではディレクトリ名として使用できない文字であり、Goのビルドプロセスが一時ディレクトリ内にパッケージバイナリを保存する際に、このコロンを含むパスをそのままディレクトリ名として使用しようとすると問題が発生しました。具体的には、tmpdir/work/local/home/gopher
のようなパスを生成する際に、WindowsのパスC:\Users\gopher
をC_Users_gopher
のように変換する必要がありましたが、その変換が不完全であったり、一部の処理でコロンが残ってしまったりしたため、ビルドが失敗するケースがありました。 -
Goパス外のパッケージのテストにおける問題: Goのテストフレームワークは、テスト対象のパッケージとテストコードを結合した
testmain
という特別な実行ファイルを生成します。このtestmain
は、テスト対象のパッケージがGoパス(GOPATH
)の外部にある場合でも、そのパッケージ内の相対インポートを正しく解決できる必要がありました。以前の方式では、コンパイラのカレントワーキングディレクトリ(CWD)に依存して相対インポートを解決していましたが、testmain
のようなシナリオでは、テストパッケージ自体の相対インポートと、テスト対象のパッケージ内の他の相対インポートとで、解決の基準となるディレクトリが異なる必要がありました。コンパイラのCWDを一つに固定する方式では、この二重の解決要件を満たすことができず、ビルドが破綻していました。
これらの問題は、Goのビルドシステムが相対インポートを「特別なケース」として扱い、ファイルシステム上の物理的な位置に強く依存して解決しようとしたことに起因していました。このコミットは、この「特別なケース」の扱いを廃止し、相対インポートを通常のインポートパスに変換することで、より堅牢でプラットフォームに依存しない解決メカニズムを導入することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラに関する前提知識が必要です。
-
Goのパッケージとインポートパス:
- Goのコードは「パッケージ」にまとめられます。各パッケージは一意の「インポートパス」を持ちます。例えば、
"fmt"
や"net/http"
などです。 - Goのソースファイルは、
import "path/to/package"
のように記述することで、他のパッケージの機能を利用できます。 - Goのビルドツール (
go build
,go install
など) は、このインポートパスに基づいて、GOPATH
やGOROOT
内の適切なディレクトリからパッケージのソースコードやコンパイル済みバイナリを探します。
- Goのコードは「パッケージ」にまとめられます。各パッケージは一意の「インポートパス」を持ちます。例えば、
-
相対インポート:
- Goでは、
./
や../
で始まるインポートパスを「相対インポート」と呼びます。これらは、現在のソースファイルが存在するディレクトリからの相対的な位置にあるパッケージを指します。 - 例:
import "./subpackage"
は、現在のディレクトリのsubpackage
サブディレクトリにあるパッケージをインポートします。 - 相対インポートは、主にローカルな開発や、単一のリポジトリ内で密接に関連する複数のパッケージを整理する際に使用されます。
- Goでは、
-
Goツールチェイン (
cmd/go
,gc
):cmd/go
は、Go言語のビルド、テスト、実行、インストールなどを管理するコマンドラインツールです。ユーザーがgo build
やgo test
などのコマンドを実行すると、cmd/go
が内部的にGoコンパイラ (gc
) やアセンブラ (go tool asm
)、リンカ (go tool link
) などのツールを呼び出します。gc
はGo言語の公式コンパイラです。Goのソースコードをコンパイルして、オブジェクトファイルやパッケージアーカイブを生成します。- コンパイラは、インポート文を解決する際に、インポートパスに基づいて必要なパッケージの定義やバイナリを探します。
-
ビルドキャッシュと一時ディレクトリ:
- Goのビルドシステムは、ビルド時間を短縮するために、コンパイル済みのパッケージバイナリをキャッシュします。
- ビルドプロセス中には、一時的な作業ディレクトリ (
$WORK
またはtmpdir
) が作成され、中間ファイルや最終的な実行ファイルがそこに生成されます。
-
Windowsのファイルパスとコロン:
- Windowsのファイルパスは、ドライブレター(例:
C:
)とディレクトリパスで構成されます。コロンはドライブレターの区切り文字として特別な意味を持ちます。 - Unix系システムでは、コロンはファイル名やディレクトリ名の一部として使用できますが、Windowsでは通常、ファイル名やディレクトリ名にコロンを含めることはできません(代替データストリームを除く)。この違いが、クロスプラットフォームでのパス処理において問題を引き起こすことがあります。
- Windowsのファイルパスは、ドライブレター(例:
これらの知識が、コミットが解決しようとしている問題と、その解決策の技術的な詳細を理解する上で不可欠です。
技術的詳細
このコミットの技術的な核心は、Goコンパイラ (gc
) に新しいオプション -D
を導入し、相対インポートを「擬似的な絶対インポートパス」に変換するメカニズムを確立した点にあります。これにより、相対インポートがもはや特別なケースとして扱われる必要がなくなり、Goの通常のインポート解決ロジックで処理できるようになります。
具体的な変更点は以下の通りです。
-
gc
コンパイラへの-D
オプションの追加:src/cmd/gc/doc.go
とsrc/cmd/gc/lex.c
に、新しいコマンドラインオプション-D path
が追加されました。- このオプションは、コンパイラに対して、
./
や../
で始まる相対インポートパスを、指定されたpath
をプレフィックスとして解釈するように指示します。 - 例えば、
gc -D a/b/c
を指定してimport "./foo"
が現れると、コンパイラはこれをimport "a/b/c/foo"
として扱います。
-
擬似インポートパス
_/<full_path>
の導入:src/cmd/go/pkg.go
にdirToImportPath
という新しいヘルパー関数が追加されました。- この関数は、ファイルシステム上の絶対ディレクトリパス(例:
/home/gopher/my/pkg
)を受け取り、それをGoのインポートパス形式に変換します。変換されたパスは_/<full_path>
の形式になります。 - Windowsのパスに含まれるコロン(例:
C:\Users\foo
)は、_
に変換されます(例:_/C_/Users/foo
)。これにより、Windowsのパス名制約を回避しつつ、一意のインポートパスを生成します。 - 例:
/home/gopher
ディレクトリ内のimport "./foo"
は、go
コマンドによってgc
に渡される際に、-D _/home/gopher
のようなオプションが付けられ、結果的にimport "_/home/gopher/foo"
としてコンパイラに認識されます。
-
cmd/go
のビルドロジックの変更:src/cmd/go/build.go
とsrc/cmd/go/pkg.go
において、パッケージのビルド時にlocalPrefix
というフィールドが導入されました。これは、相対インポートを解決するための基準となる擬似インポートパスを保持します。goToolchain.gc
関数(Goコンパイラを呼び出す部分)が変更され、gc
コマンドに-D p.localPrefix
オプションが渡されるようになりました。これにより、コンパイラは相対インポートをこのlocalPrefix
に基づいて解決します。- 以前の、コンパイラを特定のディレクトリで実行して相対インポートを解決しようとするロジック(
gcdir
の計算と使用)は削除されました。これにより、Windowsのパス問題や、Goパス外のテストパッケージの問題が根本的に解決されます。 Package
構造体にもlocalPrefix
フィールドが追加され、相対インポートの解決基準がパッケージオブジェクト自体に保持されるようになりました。
-
テストの修正と追加:
src/cmd/go/test.bash
に、新しい相対インポートの挙動を検証するためのテストケースが追加されました。特に、testdata/local/easysub/main.go
やtestdata/testimport
ディレクトリ内のファイルが追加され、相対インポートが正しく機能することを確認しています。
この新しいアプローチにより、相対インポートはもはやファイルシステム上の物理的な位置に依存する特殊なケースではなく、_/<full_path>
という形式の通常のインポートパスに変換されることで、Goの既存のパッケージ解決メカニズムに統合されます。これにより、ビルドの堅牢性が向上し、クロスプラットフォームでの互換性も改善されました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下のファイルと関数に集中しています。
-
src/cmd/gc/doc.go
:gc
コンパイラのドキュメントに、新しい-D path
フラグの説明が追加されました。
-
src/cmd/gc/go.h
:localimport
という新しいグローバル変数が宣言されました。これは-D
オプションで指定されたパスを保持します。
-
src/cmd/gc/lex.c
:main
関数内で、コマンドライン引数のパースロジックに-D
オプションの処理が追加され、localimport
変数に値が設定されるようになりました。islocalname
関数が修正され、.
や..
のような単一または二重ドットのインポートもローカルインポートとして認識するようになりました。importfile
関数が大幅に修正されました。この関数はインポートパスを解決するGoコンパイラの中心的な部分です。- 以前は、相対インポートの場合にコンパイラのカレントワーキングディレクトリ (
pathname
) をプレフィックスとして使用していましたが、これがlocalimport
変数(-D
オプションで指定されたパス)を優先的に使用するように変更されました。これにより、コンパイラが実行される物理的なディレクトリではなく、cmd/go
が指定した論理的なプレフィックスに基づいて相対インポートが解決されるようになります。
- 以前は、相対インポートの場合にコンパイラのカレントワーキングディレクトリ (
-
src/cmd/go/build.go
:action
構造体のobjdir
とobjpkg
の計算ロジックが簡素化されました。以前はlocal
フラグに基づいてprefix
(例:obj
,local
) を使用していましたが、これが削除され、b.work
とa.p.ImportPath
を直接使用するようになりました。build
メソッド内のgcdir
の計算ロジック(コンパイラを実行するディレクトリを決定する部分)が完全に削除されました。これは、-D
オプションの導入により、コンパイラのCWDに依存する必要がなくなったためです。goToolchain.gc
関数の呼び出しが変更され、dir
引数が削除され、代わりに-D p.localPrefix
がgcargs
に追加されるようになりました。run
メソッドのシグネチャが変更され、shortenDir
引数が削除されました。これは、showOutput
がdir
を直接使用するようになったためです。includeArgs
関数が修正され、インクルードパスのリストにb.work + "/obj"
ではなくb.work
が追加されるようになりました。
-
src/cmd/go/pkg.go
:Package
構造体にlocalPrefix string
フィールドが追加されました。これは、パッケージの相対インポートを解決するための基準となる擬似インポートパスを保持します。dirToImportPath
という新しいヘルパー関数が追加されました。この関数は、ファイルシステム上のディレクトリパスを、Goの擬似インポートパス(例:_/<full_path>
)に変換します。Windowsのパスのコロンはアンダースコアに変換されます。loadImport
関数が大幅に修正されました。- ローカルインポートの場合の
pkgid
の決定ロジックが変更され、filepath.Join(srcDir, path)
ではなくdirToImportPath(filepath.Join(srcDir, path))
を使用して擬似インポートパスを生成するようになりました。 Package
オブジェクトのlocal
フラグとImportPath
が、この擬似インポートパスに基づいて設定されるようになりました。bp.ImportPath = importPath
の行が追加され、build.Package
のImportPath
も新しい擬似インポートパスに設定されるようになりました。
- ローカルインポートの場合の
Package.load
関数が修正されました。p.local
とp.ImportPath
の設定ロジックが削除され、loadImport
で設定されるようになりました。p.localPrefix
がp.ImportPath
に設定されるようになりました。これは、合成されたmain
パッケージで上書きされる可能性があります。- インポートパスを解決するループ内で、
p1.local
なパッケージの場合にpath = p1.Dir
ではなくpath = p1.ImportPath
を使用するように変更されました。
-
src/cmd/go/run.go
:goFilesPackage
で取得したパッケージがmain
パッケージでない場合にエラーを出すチェックが追加されました。
-
src/cmd/go/test.bash
:- 新しい相対インポートの挙動を検証するためのテストケースが追加されました。
-
src/cmd/go/test.go
:runTest
関数内の警告ロジックが修正され、a.p.local
なパッケージは警告の対象外となりました。builder.test
関数内で、外部テストパッケージ (pxtest
) を作成する際に、localPrefix
フィールドが親パッケージのlocalPrefix
からコピーされるようになりました。
これらの変更は、Goのビルドシステムにおけるインポート解決の根本的なアプローチを変更し、相対インポートをより堅牢で予測可能な方法で処理できるようにしています。
コアとなるコードの解説
このコミットの核となる変更は、Goコンパイラ (gc
) と go
コマンド (cmd/go
) の連携によって、相対インポートの解決方法を根本的に変えた点にあります。
gc
コンパイラ (src/cmd/gc/lex.c
) の変更
-D
オプションの導入:gc
コンパイラは、新しいコマンドラインオプション-D path
を受け入れるようになりました。このpath
は、相対インポート(./
や../
)を解決する際の「基準パス」として機能します。importfile
関数は、Goソースファイル内のimport
文を処理する部分です。以前は、相対インポートを見つけると、コンパイラが実行されているカレントワーキングディレクトリ (CWD) を基準としてそのパスを解決しようとしていました。しかし、このコミットでは、-D
オプションでlocalimport
変数に値が設定されている場合、CWDではなくこのlocalimport
の値を優先的にプレフィックスとして使用するように変更されました。 これにより、go
コマンドがgc
を呼び出す際に、コンパイラが実際にどのディレクトリで実行されているかに関わらず、go
コマンドが意図する論理的なインポートパスをコンパイラに伝えることができるようになります。
go
コマンド (src/cmd/go/pkg.go
, src/cmd/go/build.go
) の変更
-
dirToImportPath
関数の導入 (src/cmd/go/pkg.go
): この新しい関数は、ファイルシステム上の絶対パス(例:/home/user/project/mypkg
やC:\Users\user\project\mypkg
)を、Goのインポートパスとして扱える「擬似インポートパス」に変換します。変換ルールは以下の通りです。- パスの先頭に
_
を付加します。 - Windowsのパスに含まれるコロン
:
は、アンダースコア_
に変換されます。 例:/home/gopher/foo
は_/home/gopher/foo
に、C:\Users\gopher\foo
は_/C_/Users/gopher/foo
になります。 この擬似インポートパスは、Goの通常のインポート解決メカニズムで処理できる形式であり、ファイルシステム上の物理的なパスと一対一で対応します。
- パスの先頭に
-
Package
構造体へのlocalPrefix
フィールドの追加 (src/cmd/go/pkg.go
):Package
構造体は、Goのパッケージに関するメタデータ(インポートパス、ソースディレクトリ、ファイルリストなど)を保持します。このコミットでは、localPrefix
という新しいフィールドが追加されました。loadImport
関数(go
コマンドがパッケージのインポートパスを解決する中心的な関数)内で、ローカルインポート(./
や../
)が検出された場合、そのパッケージのImportPath
はdirToImportPath
を使って生成された擬似インポートパスに設定されます。そして、この擬似インポートパスがlocalPrefix
にも設定されます。 これにより、go
コマンドは、各パッケージが自身の相対インポートを解決する際に使用すべき「基準パス」を内部的に保持できるようになります。 -
goToolchain.gc
の呼び出し変更 (src/cmd/go/build.go
):go
コマンドがgc
コンパイラを呼び出す際、以前はコンパイラを特定のディレクトリで実行することで相対インポートを解決しようとしていました。しかし、このコミットでは、そのロジックが削除され、代わりにgc
コマンドに-D p.localPrefix
オプションが明示的に渡されるようになりました。 つまり、go
コマンドは、ビルド対象のパッケージp
のlocalPrefix
(つまり、そのパッケージの擬似インポートパス)を-D
オプションとしてgc
に渡します。gc
はこの-D
オプションの値を使って、ソースコード内の./
や../
インポートを、_/<full_path>/<relative_path>
のような完全なインポートパスに変換します。
全体としての動作
この変更により、相対インポートの解決は以下のように機能します。
- ユーザーが
go build ./my/package
のように相対パスでパッケージをビルドしようとします。 cmd/go
は、./my/package
をファイルシステム上の絶対パスに解決し、それをdirToImportPath
関数を使って_/<absolute_path_to_my_package>
のような擬似インポートパスに変換します。この擬似インポートパスが、ビルド対象のパッケージのImportPath
およびlocalPrefix
として設定されます。cmd/go
は、このパッケージをコンパイルするためにgc
コンパイラを呼び出します。この際、gc
に-D _/<absolute_path_to_my_package>
のようなオプションを渡します。gc
コンパイラは、ソースコード内でimport "./submodule"
のような相対インポートを見つけると、-D
オプションで指定されたプレフィックス(_/<absolute_path_to_my_package>
)と結合して、import "_/<absolute_path_to_my_package>/submodule"
のような通常のインポートパスに変換します。- 変換されたインポートパスは、Goの通常のパッケージ解決ルールに従って処理されます。これにより、相対インポートがファイルシステム上の物理的な位置に依存することなく、一貫した方法で解決されるようになります。
このアプローチは、Windowsのパス名問題や、Goパス外のテストパッケージの問題を、相対インポートを「特別なケース」として扱うのではなく、「通常のインポートパスの特殊な形式」として扱うことで解決しています。これにより、ビルドシステムの堅牢性と移植性が大幅に向上しました。
関連リンク
- Go Change-ID: https://golang.org/cl/5732045
- Go Issue: https://code.google.com/p/go/issues/detail?id=3169 (Fixes #3169)
参考にした情報源リンク
- https://github.com/golang/go/commit/604f3751104e655f76e5368a3a4177d58fe1509c (本コミットのGitHubページ)
- コミットメッセージの内容
- Go言語の公式ドキュメント (Goのパッケージ、インポートパス、ビルドシステムに関する一般的な知識)
- Go言語のソースコード (特に
cmd/go
とcmd/gc
の関連ファイル) - Windowsのファイルパスに関する一般的な知識
- Unix系ファイルシステムに関する一般的な知識
- コンパイラのインポート解決に関する一般的な知識