Oakmini エンジニアリングノート

大小文字を区別するAPFSボリュームでパス問題を検証する

CI/CD ·約 10 分

大小文字を区別するAPFSボリュームでパス問題を検証する

同じリポジトリでも、開発マシンでは問題なく動作する一方、リモートのビルドジョブでは Module not found が発生したり、パッケージ化した成果物から一部のリソースが欠落したりすることがあります。このような場合、キャッシュの削除を繰り返す前にファイルシステムを確認してください。macOSで一般的なデフォルトのファイルシステムはファイル名の大文字と小文字を区別しないため、Config.jsonconfig.json が同じパスとして扱われる可能性があります。スクリプト、依存関係、デプロイ先のいずれかが大小文字を厳密に照合した時点で、潜在していた問題が表面化します。最も安全な検証方法は、システムボリュームを変更することではなく、クラウドMacに隔離された大小文字を区別するAPFSボリュームをマウントすることです。

障害がパスの大小文字に起因するか確認する

失敗ログでは、最後の終了コードだけを見るのではなく、まずパスを探します。次のような現象は優先的に確認してください。

現象 考えられる原因 確認方法
ローカルではインポートできるが、ビルドジョブではモジュールが見つからない importの指定と実際のファイル名が一致していない git ls-files で完全なパスを照合する
2つのリソースをコミットしたのに1つしか残らない 名前の違いが大小文字だけである 大小文字を区別するボリュームへ再度クローンする
クリーンアップ後の初回ビルドが失敗する 古い成果物が誤った参照を覆い隠していた 出力ディレクトリを削除してフルビルドする
パッケージ化の段階でファイルが上書きされる スクリプトが名前を一律に変換している コピー処理と名前変更処理を確認する

あわせて diskutil info / を実行し、現在のシステムボリュームの File System Personality を記録します。この手順は環境の確認だけを目的としています。稼働中のシステムボリュームを直接変換しないでください。

パスの問題は、クリーンなワークスペースで再現する必要があります。古いキャッシュをテストボリュームへコピーすると、元の問題が再び隠れてしまいます。

隔離されたAPFSテストボリュームを作成する

スパースイメージは実際の書き込み量に応じて拡張されるため、一時的な検証に適しています。以下では、上限80GBのテストボリュームを作成します。プロジェクトに大容量の依存関係が含まれる場合は、リポジトリ、依存関係、ビルド成果物のピーク容量を考慮して空き領域を確保してください。

mkdir -p "$HOME/apfs-lab"
hdiutil create \
  -size 80g \
  -type SPARSEBUNDLE \
  -fs "Case-sensitive APFS" \
  -volname BuildCase \
  "$HOME/apfs-lab/BuildCase.sparsebundle"

hdiutil attach "$HOME/apfs-lab/BuildCase.sparsebundle"
diskutil info "/Volumes/BuildCase"

出力に大小文字を区別することが明示されているか確認し、マウントポイントが /Volumes/BuildCase であることも確認します。この一時ボリュームには、アクセスキー、署名用データ、長期保存するデータを書き込まないでください。

新しいボリュームへリポジトリを再取得する

既存のワークスペースをドラッグ&ドロップでコピーしないでください。競合したファイルがすでに失われている可能性があります。管理されたリポジトリから改めてクローンし、独立したビルドディレクトリを作成します。

mkdir -p /Volumes/BuildCase/work
cd /Volumes/BuildCase/work
git clone "$REPOSITORY_URL" project
cd project
git status --short

この時点では、git status の出力は空になるはずです。クローン中に対象パスがすでに存在すると報告された場合、通常はリポジトリツリー内に、大小文字を無視すると同名になるパスが存在します。

Gitのファイル名とスクリプト内の参照を監査する

次のスクリプトは、Gitで追跡されているパスだけを読み取り、Unicodeの正規形へ変換したうえで、大小文字を区別せずにグループ化します。ファイルを変更することはありません。

from collections import defaultdict
import subprocess
import unicodedata

raw = subprocess.check_output(
    ["git", "ls-files", "-z"],
    text=True
)
groups = defaultdict(list)

for path in raw.split(""):
    if not path:
        continue
    key = unicodedata.normalize("NFC", path).casefold()
    groups[key].append(path)

found = False
for paths in groups.values():
    if len(paths) > 1:
        found = True
        print("COLLISION")
        for path in paths:
            print(f"  {path}")

raise SystemExit(1 if found else 0)

tools/check_path_case.py として保存し、python3 tools/check_path_case.py を実行します。終了コードが1の場合は、該当する名前を統一し、git mv で変更をコミットしてください。文字の大小だけを変更する場合は、いったん中間名へ変更してから目的の名前へ変更します。これにより、元のワークスペースで変更が無視されるのを防げます。

git mv Sources/config.json Sources/config.tmp
git mv Sources/config.tmp Sources/Config.json

ファイル名に衝突がなくても、参照が正しいとは限りません。ビルドスクリプト、リソースマニフェスト、プロジェクト設定、テストフィクスチャに古いパスが残っていないか、引き続き検索してください。特に、自動生成ファイルと手書きの設定との差異に注意が必要です。

検査を再現可能なビルドへ組み込む

テストジョブでは、最初にマウントポイントを検証し、次にパス監査を実行し、最後に空のディレクトリからビルドします。これにより、テストボリュームがマウントされていない場合に、ファイルがシステムボリュームへ書き戻されるのを防げます。

set -euo pipefail

test -d /Volumes/BuildCase
cd /Volumes/BuildCase/work/project
python3 tools/check_path_case.py

rm -rf .build-output
mkdir .build-output
./scripts/build.sh "$PWD/.build-output"

プロジェクトに統一されたビルドエントリーポイントがない場合は、既存のコマンドを scripts/build.sh にまとめ、出力ディレクトリを明示的に渡してください。依存関係のキャッシュは別途マウントできますが、初回検証では古いキャッシュを無効にします。検証に成功した後でキャッシュを1つずつ戻し、どのレイヤーが誤ったパスを持ち込んでいるかを特定します。

よくある落とし穴と最終確認

調査を完了する前に、以下を1項目ずつ確認してください。

  • リポジトリは古いディレクトリからコピーせず、テストボリューム内へ再度クローンしている。
  • ファイル名の修正には git mv を使用し、コミット上で明確な名前変更として確認できる。
  • スクリプトがパスを強制的にすべて小文字または大文字へ変換していない。
  • リソースのコピーコマンドは、同名のコピー先がある場合に暗黙的に上書きせず、エラーで終了する。
  • ビルド成果物、依存関係ディレクトリ、ログがすべて想定したマウントポイントに置かれている。
  • クリーンビルドと増分ビルドをそれぞれ実行し、結果が一致している。

検証が完了したら、必要なログを先にエクスポートしてからテストボリュームをアンマウントします。

hdiutil detach "/Volumes/BuildCase"

保持すべきデータがないことを確認してから、BuildCase.sparsebundle を削除してください。長期的な品質ゲートとして運用する場合は、テストデータではなく作成スクリプトを保持し、各ジョブの開始時にボリューム形式、空き容量、マウントパスを確認します。

よくある質問

システムディスクを大小文字区別形式へ変更する必要がありますか?

必要ありません。APFSスパースイメージを別ボリュームとしてマウントし、リポジトリとビルドディレクトリだけを配置する方法が安全です。

Gitだけではファイル名の大小文字衝突を検出できないのですか?

Gitは表記を保持しますが、大小文字を区別しない作業ツリーでは別名が同じファイルへ対応する場合があります。隔離ボリュームで再取得して確認してください。

専有のMac mini物理ノードが必要ですか?

利用可能な構成やノード、固定の課金期間を確認して、記事の手順を継続的に使えるクラウドMac環境で実践しましょう。

Mac miniを今すぐレンタル