WindowsでPDMを使ってtorchvisionを入れようとしたらエラーで怒られてしまった人へ

# はじめに

Windowsで`pdm add torchvision`しようとしたら

```
assert record is not None, "In {}, {} is not mentioned in RECORD".format(
AssertionError: In C:\Users\hoge\AppData\Local\Temp\pip-unpack-XXXXXXXX\torchvision-0.11.1-cp39-cp39-win_amd64.whl, torchvision-0.11.1.dist-info/LICENSE is not mentioned in RECORD
```

とassertが出てインストールできなかった。本稿では何とかしてインストールするための方法を紹介する。

# 実行環境
OS: Windows 10 Pro (19044.1348)
pdm: version 1.10.3
pyenv-win: version 2.64.11
Python: version 3.9.6

# エラーの再現方法
1. `$mkdir tmp` で空のディレクトリを作成する。
1. powershellを開いて、上で作成したディレクトリをカレントディレクトリにする。
1. powershellのコンソールに`$pdm init`を入力しenter。この時聞かれるパッケージの初期設定については、Please enter the Python interpreter to use に対してはpyenv-winのpython 3.9.6を選択し、他の設定はデフォルトを選択する（何も入力せずにenter）。
1. `$pdm add torchvision==0.11.1`を入力する。

出力されるエラーメッセージ

```
ERRORS:
add torchvision failed:
Traceback (most recent call last):
  File "C:\Users\hoge\scoop\apps\pyenv\current\pyenv-win\versions\3.9.6\lib\concurrent\futures\thread.py", line 52, in run
    result = self.fn(*self.args, **self.kwargs)
  File "C:\Users\hoge\AppData\Roaming\pdm\venv\lib\site-packages\pdm\installers\synchronizers.py", line 190, in install_candidate
    self.manager.install(can)
  File "C:\Users\hoge\AppData\Roaming\pdm\venv\lib\site-packages\pdm\installers\manager.py", line 38, in install
    installer(candidate.build(), self.environment, candidate.direct_url())
  File "C:\Users\hoge\AppData\Roaming\pdm\venv\lib\site-packages\pdm\installers\installers.py", line 124, in install_wheel_with_cache
    dist_info_dir = _install_wheel(
  File "C:\Users\hoge\AppData\Roaming\pdm\venv\lib\site-packages\pdm\installers\installers.py", line 176, in _install_wheel
    for record_elements, stream in source.get_contents():
  File "C:\Users\hoge\AppData\Roaming\pdm\venv\lib\site-packages\installer\sources.py", line 170, in get_contents
    assert record is not None, "In {}, {} is not mentioned in RECORD".format(
AssertionError: In C:\Users\hoge\AppData\Local\Temp\pip-unpack-XXXXXXXX\torchvision-0.11.1-cp39-cp39-win_amd64.whl, torchvision-0.11.1.dist-info/LICENSE is not mentioned in RECORD

See C:\Users\hoge\AppData\Local\Temp\pdm-install-XXXXXXXXX.log for detailed debug log.
[InstallationError]: Some package operations are not complete yet
Add '-v' to see the detailed traceback
```


# エラーの原因

エラーメッセージの下の方を見てみると、

```
File "C:\Users\hoge\AppData\Roaming\pdm\venv\lib\site-packages\installer\sources.py", line 170, in get_contents
    assert record is not None, "In {}, {} is not mentioned in RECORD".format(
AssertionError: In C:\Users\hoge\AppData\Local\Temp\pip-unpack-XXXXXXXX\torchvision-0.11.1-cp39-cp39-win_amd64.whl, torchvision-0.11.1.dist-info/LICENSE is not mentioned in RECORD
```

とある。`C:\Users\hoge\AppData\Roaming\pdm\venv\lib\site-packages\installer\sources.py`を見てみると、エラーの発生個所は以下の関数らしい。

```python:source.pyの152行目～
    def get_contents(self):
        # type: () -> Iterator[WheelContentElement]
        """Sequential access to all contents of the wheel (including dist-info files).

        This implementation requires that every file that is a part of the wheel
        archive has a corresponding entry in RECORD. If they are not, an
        :any:`AssertionError` will be raised.
        """
        # Convert the record file into a useful mapping
        record_lines = self.read_dist_info("RECORD").splitlines()
        records = installer.records.parse_record_file(record_lines)
        record_mapping = {record[0]: record for record in records}

        for item in self._zipfile.infolist():
            if item.filename[-1:] == "/":  # looks like a directory
                continue

            record = record_mapping.pop(item.filename, None)
            assert record is not None, "In {}, {} is not mentioned in RECORD".format(
                self._zipfile.filename,
                item.filename,
            )  # should not happen for valid wheels

            with self._zipfile.open(item) as stream:
                stream_casted = cast("BinaryIO", stream)
                yield record, stream_casted

```
 
この関数はPDMが内部で使っているinstallerというパッケージのものらしい。この関数の説明文によると、この関数はインストールしようとしたwheelに含まれている各ファイルが、whlのdist-infoのRECORDに書いてあることを確認し、書いてなかったら先のエラーを出すらしい。

先のエラーを見直すと、`AssertionError: In C:\Users\hoge\AppData\Local\Temp\pip-unpack-XXXXXXXX\torchvision-0.11.1-cp39-cp39-win_amd64.whl, torchvision-0.11.1.dist-info/LICENSE is not mentioned in RECORD`とあり、torchvision-0.11.1.dist-info/LICENSEがRECORDに書いてないらしい。[^1]

本当に書いていないことを確かめるため、[torchvisionのpypi](https://pypi.org/project/torchvision/#files)からtorchvision-0.11.1-cp39-cp39-win_amd64.whlをダウンロードして、unzipしてみる。unzipしたファイルのtorchvision-0.11.1.dist-info/RECORDを見てみると、121行目に`torchvision-0.11.1.dist-info\LICENSE`とある。一方で、上のエラーメッセージでは`torchvision-0.11.1.dist-info/LICENSE is not mentioned in RECORD`と書いてあった。確かに`torchvision-0.11.1.dist-info/LICENSE`はRECORDに記載されていないものの、ディレクトリ区切り文字が異なる`torchvision-0.11.1.dist-info\LICENSE`が記載されている。ということで、RECORDに記載されているパス(`torchvision-0.11.1.dist-info\LICENSE`)と、wheelに含まれているファイルのパス(`torchvision-0.11.1.dist-info/LICENSE`)の区切り文字が異なってしまうことがエラーの原因らしい。

# エラーの回避方法

回避方法の方針としては、RECORDとinstallerのディレクトリ区切り文字を一致させてやれば良さそうである。そのための雑な方法として、

```python:sources.pyの161行目
record_lines = self.read_dist_info("RECORD").splitlines()
```

を

```python:sources.pyの161行目
record_lines = [ line.replace("\\", "/") for line in self.read_dist_info("RECORD").splitlines()]
```

に置換する。`self.read_dist_info("RECORD").splitlines()`の返り値はRECORDファイルの各行が入っているリストで、`[ line.replace("\\", "/") for line in self.read_dist_info("RECORD").splitlines()]`はRECORDの各行の`\`を`/`に置換したものである。

この方法だと、Pathだけでなく、RECORDに含まれる各ファイルのハッシュも置換してしまう。セキュリティ的にまずいはずなので、パスのみを置換の範囲にするべきです。


[^1]: RECORDが何なのかというと、"RECORD is a list of (almost) all the files in the wheel and their secure hashes. Unlike PEP 376, every file except RECORD, which cannot contain a hash of itself, must include its hash. The hash algorithm must be sha256 or better; specifically, md5 and sha1 are not permitted, as signed wheel files rely on the strong hashes in RECORD to validate the integrity of the archive." らしい。[PEP 427](https://www.python.org/dev/peps/pep-0427/)より。