Mac で大文字小文字の区別が必要なファイル名を扱う

Mac 標準のファイルシステム [APFS (Apple File System)](https://ja.wikipedia.org/wiki/Apple_File_System) のデフォルトは、ファイル名の大文字・小文字を区別せず扱う [case-insensitive](https://ja.wikipedia.org/wiki/%E3%82%B1%E3%83%BC%E3%82%B9%E3%83%BB%E3%82%BB%E3%83%B3%E3%82%B7%E3%83%86%E3%82%A3%E3%83%96) となっています。一方、Linux のファイルシステムでは大文字・小文字を区別する case-sensitive が標準的[^linux-icase]であるため、Linux のファイルを Mac で扱う際に問題となる場合があります。この記事では[ディスクイメージ](https://ja.wikipedia.org/wiki/Apple%E3%83%87%E3%82%A3%E3%82%B9%E3%82%AF%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8)を使った解決方法を紹介します。

[^linux-icase]: ext4, XFS はオプションで case-insensitive に設定可能です。

## 大文字・小文字が問題になるケース (Linux カーネルの例)
大文字・小文字の区別が特に問題になるは、同じディレクトリ内に大文字・小文字だけが異なるファイルがある場合です。

具体的に自分が困ったのは Linux カーネルのソースコードです。例えば [include/uapi/linux/netfilter](https://github.com/torvalds/linux/blob/58720809f52779dc0f08e53e54b014209d13eebb/include/uapi/linux/netfilter/) というディレクトリには、[xt_CONNMARK.h](https://github.com/torvalds/linux/blob/58720809f52779dc0f08e53e54b014209d13eebb/include/uapi/linux/netfilter/xt_CONNMARK.h) と [xt_connmark.h](https://github.com/torvalds/linux/blob/58720809f52779dc0f08e53e54b014209d13eebb/include/uapi/linux/netfilter/xt_connmark.h) という大文字・小文字だけが異なる 2 つのファイルが存在しています。

大文字・小文字を区別しないデフォルトの APFS では、これらを同一のファイルとして扱うため、どちらか一方しか同じディレクトリに存在できません。そのため、Linux カーネルのコードを clone した直後からファイルの衝突によってダーティな状態となります。以下はコミット [58720809f527](https://github.com/torvalds/linux/tree/58720809f52779dc0f08e53e54b014209d13eebb) をデフォルトの APFS 上に clone した直後の結果です。ダーティな状態であるためブランチの切り替えなどの git 操作ができません。

```console
$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   include/uapi/linux/netfilter/xt_CONNMARK.h
	modified:   include/uapi/linux/netfilter/xt_DSCP.h
	modified:   include/uapi/linux/netfilter/xt_MARK.h
	modified:   include/uapi/linux/netfilter/xt_RATEEST.h
	modified:   include/uapi/linux/netfilter/xt_TCPMSS.h
	modified:   include/uapi/linux/netfilter_ipv4/ipt_ECN.h
	modified:   include/uapi/linux/netfilter_ipv4/ipt_TTL.h
	modified:   include/uapi/linux/netfilter_ipv6/ip6t_HL.h
	modified:   net/netfilter/xt_DSCP.c
	modified:   net/netfilter/xt_HL.c
	modified:   net/netfilter/xt_RATEEST.c
	modified:   net/netfilter/xt_TCPMSS.c
	modified:   tools/memory-model/litmus-tests/Z6.0+pooncelock+poonceLock+pombonce.litmus
```

## ディスクイメージを使った解決
APFS には大文字小文字を区別する `Case-sensitive APFS` というフォーマットが用意されているので、このファイルシステムを使えばこの問題は解決します。既存のディスクのファイルシステムを変更するのは影響が大きいため、ここではディスクイメージを使った方法を紹介します。

まず、大文字小文字を区別する `Case-sensitive APFS` のディスクイメージを `ディスクユーティリティ.app` [^disk-utility] または CLI の `hdiutil` コマンドで作成します。ここでは `hdiutil` を使った作成例を紹介します。

[^disk-utility]: 日本語表記の場合フォーマットの名前は `APFS (大文字/小文字を区別)` となります。

次のコマンドは 100 GB の `Case-sensitive APFS` のディスクイメージを [`SPARSEBUNDLE`](https://ja.wikipedia.org/wiki/Apple%E3%83%87%E3%82%A3%E3%82%B9%E3%82%AF%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8) 形式で `$HOME/src-case.sparsebundle` というファイル[^sparse]に作成する例です。`SPARSEBUNDLE` は [スパースファイル](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%91%E3%83%BC%E3%82%B9%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB)の一種であるため、実際に使用するまで容量を消費しません。

[^sparse]: `SPARSEBUNDLE` 形式の場合は実際はディレクトリです。`SPARSE` の場合は単一ファイルとなります。

```
hdiutil create -type SPARSEBUNDLE -fs 'Case-sensitive APFS' -size 100g \
  -volname src-case $HOME/src-case.sparsebundle
```

次にこのファイルをアタッチします。下記の例ではマウント先を `$HOME/src-case` に設定しています。自分は普段 `$HOME/src` 以下でソースコードを管理しているので、大文字小文字の区別が必要なリポジトリだけを `$HOME/src-case` に置き、`$HOME/src` にシンボリックリンクを貼る運用にしました。

```
hdiutil attach $HOME/src-case.sparsebundle -mountpoint $HOME/src-case
```

`$HOME/src-case` で大文字・小文字が区別されることを確認してみます。

```
$ touch $HOME/src-case/helloworld.txt
$ touch $HOME/src-case/HELLOWORLD.TXT
```

大文字・小文字だけが異なるファイルが共存できました。:tada:

```
$ ls $HOME/src-case/
HELLOWORLD.TXT helloworld.txt
```

このアタッチ状態は再起動すると失われます。Mac の起動時に自動的にアタッチしたい場合は、Launchd でアタッチコマンドを実行するようにします。以下は自分の設定例です。ユーザごとの launchd の設定を置く `~/Library/LaunchAgents` 以下に `attach-src-case.plist` という名前で保存しました。設定の詳細は `man launchd.plist` をご覧ください。

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>attach-src-case</string>
    <key>ProgramArguments</key>
    <array>
      <string>/bin/bash</string>
      <string>-c</string>
      <string>hdiutil attach $HOME/src-case.sparsebundle -mountpoint $HOME/src-case</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>ProcessType</key>
    <string>Interactive</string>
  </dict>
</plist>
```

:::note info
ProcessType を Interactive に設定している理由

Launchd の Job はデフォルトで CPU・I/O にスロットルがあり、ProcessType を指定しない場合(`Standard`)はこのスロットルによりアタッチに時間がかかります。Interactive を設定するとこの制限がなくなります。詳細は `man launchd.plist` をご覧ください。
:::


## :memo: 補足: case-insensitive と case-preserving
APFS のデフォルトではファイル名の大文字・小文字を区別せず扱う [case-insensitive](https://en.wikipedia.org/wiki/Case_sensitivity) です。一方、ファイル名としては大文字・小文字は区別されて保存されています。これは [case-preserving](https://en.wikipedia.org/wiki/Case_preservation) と呼ばれます。それぞれ別のコンセプトであるため注意が必要です。

例えば `heLLoWorLD.TXT` という大文字・小文字が混在したファイルを作成・表示すると、APFS のデフォルトでもファイル名としては大文字・小文字が区別され保存されていることがわかります。これが case-preserving です。

```console
$ echo 1 > heLLoWorLD.TXT

$ ls
heLLoWorLD.TXT
```

一方、大文字小文字だけが異なるファイル名を指定したときに同一のファイルとして扱われる動作が case-insensitive です。

```console
$ echo 1 > heLLoWorLD.TXT
$ echo 2 > HELLOWORLD.TXT

$ ls
heLLoWorLD.TXT

$ cat heLLoWorLD.TXT
2
```

case-sensitive のファイルシステムの場合は、下記のように別のファイルとして区別されます。

```console
$ echo 1 > heLLoWorLD.TXT
$ echo 2 > HELLOWORLD.TXT

$ ls
HELLOWORLD.TXT heLLoWorLD.TXT

$ cat heLLoWorLD.TXT
1

$ cat HELLOWORLD.TXT
2
```