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

[インデックス 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.gotmpdir/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言語のビルドシステムにおける相対インポートの扱いの複雑さと、それに伴う複数の問題がありました。

  1. Windows環境でのパスの問題: 以前のGoのビルドシステムでは、相対インポート(例: ./foo)を解決するために、コンパイラをソースファイルが存在するディレクトリで実行し、そのディレクトリからの相対パスでパッケージバイナリを解決しようとしていました。しかし、Windowsのファイルパスにはドライブレターを示すコロン(例: C:\Users\foo)が含まれます。このコロンは、Unix系のファイルシステムではディレクトリ名として使用できない文字であり、Goのビルドプロセスが一時ディレクトリ内にパッケージバイナリを保存する際に、このコロンを含むパスをそのままディレクトリ名として使用しようとすると問題が発生しました。具体的には、tmpdir/work/local/home/gopher のようなパスを生成する際に、Windowsのパス C:\Users\gopherC_Users_gopher のように変換する必要がありましたが、その変換が不完全であったり、一部の処理でコロンが残ってしまったりしたため、ビルドが失敗するケースがありました。

  2. Goパス外のパッケージのテストにおける問題: Goのテストフレームワークは、テスト対象のパッケージとテストコードを結合した testmain という特別な実行ファイルを生成します。この testmain は、テスト対象のパッケージがGoパス(GOPATH)の外部にある場合でも、そのパッケージ内の相対インポートを正しく解決できる必要がありました。以前の方式では、コンパイラのカレントワーキングディレクトリ(CWD)に依存して相対インポートを解決していましたが、testmain のようなシナリオでは、テストパッケージ自体の相対インポートと、テスト対象のパッケージ内の他の相対インポートとで、解決の基準となるディレクトリが異なる必要がありました。コンパイラのCWDを一つに固定する方式では、この二重の解決要件を満たすことができず、ビルドが破綻していました。

これらの問題は、Goのビルドシステムが相対インポートを「特別なケース」として扱い、ファイルシステム上の物理的な位置に強く依存して解決しようとしたことに起因していました。このコミットは、この「特別なケース」の扱いを廃止し、相対インポートを通常のインポートパスに変換することで、より堅牢でプラットフォームに依存しない解決メカニズムを導入することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラに関する前提知識が必要です。

  1. Goのパッケージとインポートパス:

    • Goのコードは「パッケージ」にまとめられます。各パッケージは一意の「インポートパス」を持ちます。例えば、"fmt""net/http" などです。
    • Goのソースファイルは、import "path/to/package" のように記述することで、他のパッケージの機能を利用できます。
    • Goのビルドツール (go build, go install など) は、このインポートパスに基づいて、GOPATHGOROOT 内の適切なディレクトリからパッケージのソースコードやコンパイル済みバイナリを探します。
  2. 相対インポート:

    • Goでは、./../ で始まるインポートパスを「相対インポート」と呼びます。これらは、現在のソースファイルが存在するディレクトリからの相対的な位置にあるパッケージを指します。
    • 例: import "./subpackage" は、現在のディレクトリの subpackage サブディレクトリにあるパッケージをインポートします。
    • 相対インポートは、主にローカルな開発や、単一のリポジトリ内で密接に関連する複数のパッケージを整理する際に使用されます。
  3. Goツールチェイン (cmd/go, gc):

    • cmd/go は、Go言語のビルド、テスト、実行、インストールなどを管理するコマンドラインツールです。ユーザーが go buildgo test などのコマンドを実行すると、cmd/go が内部的にGoコンパイラ (gc) やアセンブラ (go tool asm)、リンカ (go tool link) などのツールを呼び出します。
    • gc はGo言語の公式コンパイラです。Goのソースコードをコンパイルして、オブジェクトファイルやパッケージアーカイブを生成します。
    • コンパイラは、インポート文を解決する際に、インポートパスに基づいて必要なパッケージの定義やバイナリを探します。
  4. ビルドキャッシュと一時ディレクトリ:

    • Goのビルドシステムは、ビルド時間を短縮するために、コンパイル済みのパッケージバイナリをキャッシュします。
    • ビルドプロセス中には、一時的な作業ディレクトリ ($WORK または tmpdir) が作成され、中間ファイルや最終的な実行ファイルがそこに生成されます。
  5. Windowsのファイルパスとコロン:

    • Windowsのファイルパスは、ドライブレター(例: C:)とディレクトリパスで構成されます。コロンはドライブレターの区切り文字として特別な意味を持ちます。
    • Unix系システムでは、コロンはファイル名やディレクトリ名の一部として使用できますが、Windowsでは通常、ファイル名やディレクトリ名にコロンを含めることはできません(代替データストリームを除く)。この違いが、クロスプラットフォームでのパス処理において問題を引き起こすことがあります。

これらの知識が、コミットが解決しようとしている問題と、その解決策の技術的な詳細を理解する上で不可欠です。

技術的詳細

このコミットの技術的な核心は、Goコンパイラ (gc) に新しいオプション -D を導入し、相対インポートを「擬似的な絶対インポートパス」に変換するメカニズムを確立した点にあります。これにより、相対インポートがもはや特別なケースとして扱われる必要がなくなり、Goの通常のインポート解決ロジックで処理できるようになります。

具体的な変更点は以下の通りです。

  1. gc コンパイラへの -D オプションの追加:

    • src/cmd/gc/doc.gosrc/cmd/gc/lex.c に、新しいコマンドラインオプション -D path が追加されました。
    • このオプションは、コンパイラに対して、./../ で始まる相対インポートパスを、指定された path をプレフィックスとして解釈するように指示します。
    • 例えば、gc -D a/b/c を指定して import "./foo" が現れると、コンパイラはこれを import "a/b/c/foo" として扱います。
  2. 擬似インポートパス _/<full_path> の導入:

    • src/cmd/go/pkg.godirToImportPath という新しいヘルパー関数が追加されました。
    • この関数は、ファイルシステム上の絶対ディレクトリパス(例: /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" としてコンパイラに認識されます。
  3. cmd/go のビルドロジックの変更:

    • src/cmd/go/build.gosrc/cmd/go/pkg.go において、パッケージのビルド時に localPrefix というフィールドが導入されました。これは、相対インポートを解決するための基準となる擬似インポートパスを保持します。
    • goToolchain.gc 関数(Goコンパイラを呼び出す部分)が変更され、gc コマンドに -D p.localPrefix オプションが渡されるようになりました。これにより、コンパイラは相対インポートをこの localPrefix に基づいて解決します。
    • 以前の、コンパイラを特定のディレクトリで実行して相対インポートを解決しようとするロジック(gcdir の計算と使用)は削除されました。これにより、Windowsのパス問題や、Goパス外のテストパッケージの問題が根本的に解決されます。
    • Package 構造体にも localPrefix フィールドが追加され、相対インポートの解決基準がパッケージオブジェクト自体に保持されるようになりました。
  4. テストの修正と追加:

    • src/cmd/go/test.bash に、新しい相対インポートの挙動を検証するためのテストケースが追加されました。特に、testdata/local/easysub/main.gotestdata/testimport ディレクトリ内のファイルが追加され、相対インポートが正しく機能することを確認しています。

この新しいアプローチにより、相対インポートはもはやファイルシステム上の物理的な位置に依存する特殊なケースではなく、_/<full_path> という形式の通常のインポートパスに変換されることで、Goの既存のパッケージ解決メカニズムに統合されます。これにより、ビルドの堅牢性が向上し、クロスプラットフォームでの互換性も改善されました。

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

このコミットにおける主要なコード変更は、以下のファイルと関数に集中しています。

  1. src/cmd/gc/doc.go:

    • gc コンパイラのドキュメントに、新しい -D path フラグの説明が追加されました。
  2. src/cmd/gc/go.h:

    • localimport という新しいグローバル変数が宣言されました。これは -D オプションで指定されたパスを保持します。
  3. src/cmd/gc/lex.c:

    • main 関数内で、コマンドライン引数のパースロジックに -D オプションの処理が追加され、localimport 変数に値が設定されるようになりました。
    • islocalname 関数が修正され、... のような単一または二重ドットのインポートもローカルインポートとして認識するようになりました。
    • importfile 関数が大幅に修正されました。この関数はインポートパスを解決するGoコンパイラの中心的な部分です。
      • 以前は、相対インポートの場合にコンパイラのカレントワーキングディレクトリ (pathname) をプレフィックスとして使用していましたが、これが localimport 変数(-D オプションで指定されたパス)を優先的に使用するように変更されました。これにより、コンパイラが実行される物理的なディレクトリではなく、cmd/go が指定した論理的なプレフィックスに基づいて相対インポートが解決されるようになります。
  4. src/cmd/go/build.go:

    • action 構造体の objdirobjpkg の計算ロジックが簡素化されました。以前は local フラグに基づいて prefix (例: obj, local) を使用していましたが、これが削除され、b.worka.p.ImportPath を直接使用するようになりました。
    • build メソッド内の gcdir の計算ロジック(コンパイラを実行するディレクトリを決定する部分)が完全に削除されました。これは、-D オプションの導入により、コンパイラのCWDに依存する必要がなくなったためです。
    • goToolchain.gc 関数の呼び出しが変更され、dir 引数が削除され、代わりに -D p.localPrefixgcargs に追加されるようになりました。
    • run メソッドのシグネチャが変更され、shortenDir 引数が削除されました。これは、showOutputdir を直接使用するようになったためです。
    • includeArgs 関数が修正され、インクルードパスのリストに b.work + "/obj" ではなく b.work が追加されるようになりました。
  5. 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.PackageImportPath も新しい擬似インポートパスに設定されるようになりました。
    • Package.load 関数が修正されました。
      • p.localp.ImportPath の設定ロジックが削除され、loadImport で設定されるようになりました。
      • p.localPrefixp.ImportPath に設定されるようになりました。これは、合成された main パッケージで上書きされる可能性があります。
      • インポートパスを解決するループ内で、p1.local なパッケージの場合に path = p1.Dir ではなく path = p1.ImportPath を使用するように変更されました。
  6. src/cmd/go/run.go:

    • goFilesPackage で取得したパッケージが main パッケージでない場合にエラーを出すチェックが追加されました。
  7. src/cmd/go/test.bash:

    • 新しい相対インポートの挙動を検証するためのテストケースが追加されました。
  8. 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/mypkgC:\Users\user\project\mypkg)を、Goのインポートパスとして扱える「擬似インポートパス」に変換します。変換ルールは以下の通りです。

    1. パスの先頭に _ を付加します。
    2. 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 コマンドがパッケージのインポートパスを解決する中心的な関数)内で、ローカルインポート(./../)が検出された場合、そのパッケージの ImportPathdirToImportPath を使って生成された擬似インポートパスに設定されます。そして、この擬似インポートパスが localPrefix にも設定されます。 これにより、go コマンドは、各パッケージが自身の相対インポートを解決する際に使用すべき「基準パス」を内部的に保持できるようになります。

  • goToolchain.gc の呼び出し変更 (src/cmd/go/build.go): go コマンドが gc コンパイラを呼び出す際、以前はコンパイラを特定のディレクトリで実行することで相対インポートを解決しようとしていました。しかし、このコミットでは、そのロジックが削除され、代わりに gc コマンドに -D p.localPrefix オプションが明示的に渡されるようになりました。 つまり、go コマンドは、ビルド対象のパッケージ plocalPrefix(つまり、そのパッケージの擬似インポートパス)を -D オプションとして gc に渡します。gc はこの -D オプションの値を使って、ソースコード内の ./../ インポートを、_/<full_path>/<relative_path> のような完全なインポートパスに変換します。

全体としての動作

この変更により、相対インポートの解決は以下のように機能します。

  1. ユーザーが go build ./my/package のように相対パスでパッケージをビルドしようとします。
  2. cmd/go は、./my/package をファイルシステム上の絶対パスに解決し、それを dirToImportPath 関数を使って _/<absolute_path_to_my_package> のような擬似インポートパスに変換します。この擬似インポートパスが、ビルド対象のパッケージの ImportPath および localPrefix として設定されます。
  3. cmd/go は、このパッケージをコンパイルするために gc コンパイラを呼び出します。この際、gc-D _/<absolute_path_to_my_package> のようなオプションを渡します。
  4. gc コンパイラは、ソースコード内で import "./submodule" のような相対インポートを見つけると、-D オプションで指定されたプレフィックス(_/<absolute_path_to_my_package>)と結合して、import "_/<absolute_path_to_my_package>/submodule" のような通常のインポートパスに変換します。
  5. 変換されたインポートパスは、Goの通常のパッケージ解決ルールに従って処理されます。これにより、相対インポートがファイルシステム上の物理的な位置に依存することなく、一貫した方法で解決されるようになります。

このアプローチは、Windowsのパス名問題や、Goパス外のテストパッケージの問題を、相対インポートを「特別なケース」として扱うのではなく、「通常のインポートパスの特殊な形式」として扱うことで解決しています。これにより、ビルドシステムの堅牢性と移植性が大幅に向上しました。

関連リンク

参考にした情報源リンク

  • https://github.com/golang/go/commit/604f3751104e655f76e5368a3a4177d58fe1509c (本コミットのGitHubページ)
  • コミットメッセージの内容
  • Go言語の公式ドキュメント (Goのパッケージ、インポートパス、ビルドシステムに関する一般的な知識)
  • Go言語のソースコード (特に cmd/gocmd/gc の関連ファイル)
  • Windowsのファイルパスに関する一般的な知識
  • Unix系ファイルシステムに関する一般的な知識
  • コンパイラのインポート解決に関する一般的な知識