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

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

このコミットは、Go言語のコマンドラインツール cmd/go において、大文字・小文字を区別しないファイルシステム(例: OS XやWindows)上でのGoコードの互換性を確保するための重要な変更を導入しています。具体的には、パッケージ内のファイル名やインポートされる依存関係のパスにおいて、大文字・小文字のみが異なる衝突を検出して拒否する機能が追加されました。これにより、開発環境とデプロイ環境のファイルシステムの違いに起因する潜在的なバグや予期せぬ動作を防ぎます。

コミット

  • コミットハッシュ: 2d4164596f3bd798996732aaa01b95e70f91e8a8
  • 作者: Russ Cox rsc@golang.org
  • コミット日時: 2013年2月15日 金曜日 14:39:39 -0500

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

https://github.com/golang/go/commit/2d4164596f3bd798996732aaa01b95e70f91e8a8

元コミット内容

cmd/go: reject case-insensitive file name, import collisions

To make sure that Go code will work when moved to a
system with a case-insensitive file system, like OS X or Windows,
reject any package built from files with names differing
only in case, and also any package built from imported
dependencies with names differing only in case.

Fixes #4773.

R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/7314104

変更の背景

Go言語はクロスプラットフォーム開発を強く意識しており、異なるオペレーティングシステム(OS)上でのコードの互換性は非常に重要です。しかし、ファイルシステムの挙動はOSによって大きく異なります。特に、Linuxのような多くのUnix系OSではファイル名の大文字・小文字を厳密に区別する「ケースセンシティブ」なファイルシステムが一般的であるのに対し、macOS(OS X)やWindowsではファイル名の大文字・小文字を区別しない「ケースインセンシティブ」なファイルシステムが採用されています。

この違いが問題となるのは、例えば開発者がLinux環境で myFile.gomyfile.go という2つのファイルを同じディレクトリに作成できたとしても、そのコードをmacOSやWindows環境に移動すると、ファイルシステムが大文字・小文字を区別しないため、これらが同じファイルとして扱われてしまい、予期せぬエラーやビルドの失敗を引き起こす可能性があるためです。同様に、インポートパスにおいても math/randmath/Rand のような衝突が発生し得ます。

このコミットは、このようなファイルシステムの違いに起因する問題を未然に防ぐことを目的としています。Goのビルドツール cmd/go が、ビルド時にファイル名やインポートパスの大文字・小文字の衝突を検出し、エラーとして報告することで、開発者がクロスプラットフォーム互換性の問題を早期に発見し、修正できるようにします。これは、Goコードのポータビリティと信頼性を向上させるための重要な改善です。

前提知識の解説

ケースセンシティブとケースインセンシティブなファイルシステム

  • ケースセンシティブ (Case-sensitive): ファイル名やディレクトリ名の大文字・小文字を厳密に区別します。例えば、file.txtFile.txt は異なるファイルとして扱われます。Linuxや多くのUnix系OSのデフォルトの挙動です。
  • ケースインセンシティブ (Case-insensitive): ファイル名やディレクトリ名の大文字・小文字を区別しません。例えば、file.txtFile.txt は同じファイルとして扱われます。macOS(HFS+やAPFSのデフォルト)やWindows(NTFS)のデフォルトの挙動です。

Go言語のビルドプロセスでは、ソースファイルやインポートパスを解決する際にファイルシステムと対話します。そのため、開発環境とビルド環境のファイルシステムが大文字・小文字の扱いで異なる場合、上記のような問題が発生する可能性があります。

Unicodeと大文字・小文字の変換

文字列の大文字・小文字の変換は、単純なASCII文字だけでなく、Unicode文字も考慮する必要があります。Unicodeには、大文字・小文字の区別がない文字や、複数の文字が結合して大文字・小文字を形成するケースなど、複雑なルールが存在します。

  • strings.EqualFold: Go言語の標準ライブラリ strings パッケージに含まれる関数で、2つの文字列が大文字・小文字を区別せずに等しいかどうかを判定します。これはUnicodeのケースフォールディングルールに従います。
  • unicode.SimpleFold: Go言語の標準ライブラリ unicode パッケージに含まれる関数で、与えられたルーン(Unicodeコードポイント)の「単純な」ケースフォールディングを行います。これは、strings.EqualFold が内部で使用するより複雑なフォールディングルールの一部を構成します。SimpleFold(r) は、r とケースフォールディングで等価なルーンのうち、r より大きい最小のルーンを返します。そのようなルーンが存在しない場合は、r 以下の最小のルーンを返します。この関数を繰り返し適用することで、あるルーンのケースフォールディングにおける「最小」の表現を見つけることができます。

Goのパッケージビルドプロセス

go buildgo install などのコマンドは、Goのソースコードをコンパイルして実行可能ファイルやライブラリを生成します。このプロセスでは、ソースファイルの特定、依存関係の解決、コンパイル、リンクといったステップが含まれます。このコミットは、ソースファイルの特定と依存関係の解決の段階で、ファイル名やインポートパスの衝突を検出するロジックを追加しています。

技術的詳細

このコミットは、大文字・小文字を区別しない衝突を検出するために、主に以下の2つの新しい関数を src/cmd/go/main.go に追加し、それらを src/cmd/go/pkg.go のパッケージロードロジックに組み込んでいます。

  1. toFold(s string) string: この関数は、与えられた文字列 s を、strings.EqualFold(s, t)toFold(s) == toFold(t) となるような「フォールドされた」形式に変換します。これにより、strings.EqualFold を多数の文字列ペアに対してO(N^2)で呼び出す代わりに、各文字列を一度 toFold で変換し、その結果をマップのキーとして使用することで、O(N)に近い効率で衝突を検出できるようになります。

    • 高速パス: 文字列がすべてASCII文字で、大文字を含まない場合、そのままの文字列を返します。これは、ほとんどのパスがこの条件を満たすため、パフォーマンスを向上させます。
    • 低速パス: ASCII以外の文字や大文字が含まれる場合、bytes.Buffer を使用して新しい文字列を構築します。各ルーンに対して unicode.SimpleFold を繰り返し適用し、そのルーンのケースフォールディングにおける最小の表現を見つけます。さらに、A-ZのASCII大文字はa-zの小文字に変換されます。
  2. foldDup(list []string) (string, string): この関数は、文字列のリスト list の中に、strings.EqualFold で等しい2つの文字列が存在するかどうかを報告します。衝突が見つかった場合、その2つの文字列を返します。衝突がない場合は "", "" を返します。 内部では toFold 関数を利用し、map[string]stringclash という名前で使用して、フォールドされた文字列をキーとして元の文字列を保存します。これにより、効率的に衝突を検出します。

これらの関数は、src/cmd/go/pkg.go のパッケージロードロジックに統合されます。

  • ファイル名の衝突検出: Package.load メソッド内で、パッケージを構成するすべてのファイル(Goソースファイル、Cgoファイル、無視されたGoファイル、C/H/S/Syso/Swigファイル、テストファイルなど)のリストに対して foldDup が呼び出されます。もし foldDup が衝突を報告した場合、PackageError が生成され、ビルドが中断されます。エラーメッセージには、衝突した2つのファイル名が含まれます。

  • インポートパスの衝突検出: Package.load メソッドの後半で、パッケージがインポートする依存関係のリスト (p.Deps) に対して foldDup が呼び出されます。このチェックは、下位の依存関係ツリーでエラーが発生していない場合にのみ実行されます。もし foldDup が衝突を報告した場合、同様に PackageError が生成され、ビルドが中断されます。エラーメッセージには、衝突した2つのインポートパスが含まれます。

これらの変更により、Goのビルドツールは、大文字・小文字を区別しないファイルシステム上での潜在的な問題を事前に検出し、開発者に警告することで、より堅牢なクロスプラットフォーム開発を支援します。

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

このコミットでは、以下の3つのファイルが変更されています。

  1. src/cmd/go/main.go:

    • toFold 関数が追加されました。
    • foldDup 関数が追加されました。
    • stringList ヘルパー関数が追加されました(既存の関数を拡張)。
  2. src/cmd/go/pkg.go:

    • PackageErrorError() メソッドに、ImportStack が空の場合の処理が追加されました。
    • Package.load メソッド内で、パッケージの入力ファイル名に対するケースインセンシティブな衝突チェックが追加されました。foldDup を使用して、GoFiles, CgoFiles, IgnoredGoFiles などのリストをチェックします。
    • Package.load メソッド内で、インポートされる依存関係のパスに対するケースインセンシティブな衝突チェックが追加されました。foldDup を使用して、p.Deps をチェックします。
  3. src/cmd/go/test.bash:

    • issue 4773 に関連する新しいテストケースが追加されました。
    • math/randmath/Rand のようなインポートパスの衝突を検出するテスト。
    • file.goFILE.go のようなファイル名の衝突を検出するテスト。
    • これらのテストは、ケースセンシティブなファイルシステムとケースインセンシティブなファイルシステムの両方で動作するように考慮されています。

コアとなるコードの解説

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

toFold(s string) string

func toFold(s string) string {
	// Fast path: all ASCII, no upper case.
	// Most paths look like this already.
	for i := 0; i < len(s); i++ {
		c := s[i]
		if c >= utf8.RuneSelf || 'A' <= c && c <= 'Z' {
			goto Slow
		}
	}
	return s

Slow:
	var buf bytes.Buffer
	for _, r := range s {
		// SimpleFold(x) cycles to the next equivalent rune > x
		// or wraps around to smaller values. Iterate until it wraps,
		// and we've found the minimum value.
		for {
			r0 := r
			r = unicode.SimpleFold(r0)
			if r <= r0 {
				break
			}
		}
		// Exception to allow fast path above: A-Z => a-z
		if 'A' <= r && r <= 'Z' {
			r += 'a' - 'A'
		}
		buf.WriteRune(r)
	}
	return buf.String()
}

この関数は、文字列を「フォールドされた」形式に変換します。これは、strings.EqualFold が等しいと判断する文字列が、この関数によって同じ結果を返すように設計されています。

  • 高速パス: 文字列がすべてASCII文字で、かつ大文字を含まない場合(例: foo/bar)、そのままの文字列を返します。これは、Goのパスの多くがこの形式であるため、不要な処理をスキップしてパフォーマンスを向上させます。
  • 低速パス: 高速パスの条件を満たさない場合(Unicode文字が含まれる、またはASCII大文字が含まれる場合)、bytes.Buffer を使用して新しい文字列を構築します。
    • 各ルーン r に対して、unicode.SimpleFold(r) を繰り返し呼び出します。SimpleFold は、ルーンのケースフォールディングにおける次の等価なルーンを返します。これを繰り返すことで、そのルーンのケースフォールディングにおける「最小」の表現(例えば、AaÄ などがすべて同じ最小のルーンに変換される)を見つけます。
    • ASCIIの大文字(AからZ)は、対応する小文字に変換されます。これは、高速パスの条件(大文字を含まない)と整合性を保つための例外処理です。

foldDup(list []string) (string, string)

func foldDup(list []string) (string, string) {
	clash := map[string]string{}
	for _, s := range list {
		fold := toFold(s)
		if t := clash[fold]; t != "" {
			if s > t {
				s, t = t, s
			}
			return s, t
		}
		clash[fold] = s
	}
	return "", ""
}

この関数は、与えられた文字列リスト list の中に、大文字・小文字を区別しない場合に衝突する2つの文字列が存在するかどうかをチェックします。

  • clash というマップを作成し、toFold で変換した文字列をキー、元の文字列を値として格納します。
  • リストの各文字列 s について、まず toFold(s) を計算します。
  • もし clash マップに既に同じ fold 値を持つエントリが存在する場合、それは大文字・小文字を区別しない衝突が発生したことを意味します。この場合、衝突した2つの文字列(現在の s とマップに格納されていた t)を返します。返される順序は、辞書順で小さい方が先になるように調整されます。
  • 衝突が見つからなかった場合、"", "" を返します。

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

Package.load メソッド内のファイル名衝突検出

	// Check for case-insensitive collision of input files.
	// To avoid problems on case-insensitive files, we reject any package
	// where two different input files have equal names under a case-insensitive
	// comparison.
	f1, f2 := foldDup(stringList(
		p.GoFiles,
		p.CgoFiles,
		p.IgnoredGoFiles,
		p.CFiles,
		p.HFiles,
		p.SFiles,
		p.SysoFiles,
		p.SwigFiles,
		p.SwigCXXFiles,
		p.TestGoFiles,
		p.XTestGoFiles,
	))
	if f1 != "" {
		p.Error = &PackageError{
			ImportStack: stk.copy(),
			Err:         fmt.Sprintf("case-insensitive file name collision: %q and %q", f1, f2),
		}
		return p
	}

このコードブロックは、Goパッケージを構成する様々な種類のソースファイル(.go, .c, .h など)のリストを結合し、foldDup 関数に渡してファイル名の衝突をチェックします。衝突が検出された場合(f1 が空でない場合)、PackageError を生成し、ビルドプロセスを停止します。エラーメッセージには、衝突したファイル名が明示されます。

Package.load メソッド内のインポートパス衝突検出

	// In the absence of errors lower in the dependency tree,
	// check for case-insensitive collisions of import paths.
	if len(p.DepsErrors) == 0 {
		dep1, dep2 := foldDup(p.Deps)
		if dep1 != "" {
			p.Error = &PackageError{
				ImportStack: stk.copy(),
				Err:         fmt.Sprintf("case-insensitive import collision: %q and %q", dep1, dep2),
			}
			return p
		}
	}

このコードブロックは、パッケージがインポートする依存関係のパスリスト (p.Deps) に対して foldDup を呼び出し、インポートパスの衝突をチェックします。このチェックは、依存関係ツリーの下位で既にエラーが発生していない場合にのみ実行されます。衝突が検出された場合、同様に PackageError を生成し、ビルドプロセスを停止します。エラーメッセージには、衝突したインポートパスが明示されます。

これらの変更により、Goのビルドツールは、ファイルシステムが大文字・小文字を区別しない環境での潜在的な問題を、ビルド時に積極的に検出して報告するようになります。

関連リンク

  • Go Issue #4773: https://golang.org/issue/4773 (コミットメッセージに記載されているが、Goの公式Issueトラッカーでは見つからず、JetBrains GoLandのIssueとして検索結果が出たため、このリンクは一般的なGoのIssueトラッカーの形式として記載)
  • Go CL 7314104: https://golang.org/cl/7314104 (Goのコードレビューシステム Gerrit のチェンジリストリンク)

参考にした情報源リンク