Pipenvを使っていたらハッシュエラーが出るようになった話


# これはなに

Pipenvを使っていたらある日謎のエラーが出るようになりました。

原因を突き止めるのに苦労したのでメモ的に書いていこうかと思った次第です。
最終的には解決したので、同じような症状で悩んでいて、対処法だけ知りたい方は[解決](#解決)までどうぞ。


# Pipenvを使っていたらある日謎のエラーが出ていることに気づく

最初にこのエラーに気づいたのはCIで自動テストを設定しようと思ったときでした。
docker環境内で自動テストを実行するコードを書いていたのですが、docker内で`pipenv sync`や`pipenv install`を実行する時に以下のエラーが出ていることに気づきました。

``` console
An error occurred while installing readme-renderer==24.0 --hash=sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f --hash=sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d! Will try again.
An error occurred while installing requests==2.22.0 --hash=sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4 --hash=sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31 --hash=sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4 --hash=sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31! Will try again.
An error occurred while installing requests-toolbelt==0.9.1 --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0! Will try again.
An error occurred while installing secretstorage==3.1.1 ; sys_platform == 'linux' --hash=sha256:20c797ae48a4419f66f8d28fc221623f11fc45b6828f96bdb1ad9990acb59f92 --hash=sha256:7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a! Will try again.
An error occurred while installing sphinx==2.2.1 --hash=sha256:31088dfb95359384b1005619827eaee3056243798c62724fd3fa4b84ee4d71bd --hash=sha256:52286a0b9d7caa31efee301ec4300dbdab23c3b05da1c9024b4e84896fb73d79! Will try again.
An error occurred while installing sphinx-git==11.0.0 --hash=sha256:6bf9d837de108c79fb7db585ebd590fd48f4d1f830b540420d0ca675f3b9f800! Will try again.
An error occurred while installing twine==3.1.0 --hash=sha256:1a87ae3f1e29a87a8ac174809bf0aa996085a0368fe500402196bda94b23aab3 --hash=sha256:ba8ba1b39987f1c22d9162f7dd3b4668388640e5f7158c15226624f88e464836! Will try again.
```

色々ためして、venv環境が無い状態で`pipenv install`や`pipenv sync`を実行するとエラーが発生することがわかりました。
hashが出ているのでひとまずPipfile.lockも消してみてから、pipenv install`を実行したがそれでもエラーが出る。
この時確認した状況は以下の通りです。

* venv環境、Pipfile.lockがなくても同様のエラーが出る。
* 上記でエラーが出ているモジュールを別のPipfileでインストールしてもこのエラーは発生しない。
* エラーは出ているがインストールは完了している。

さてさてこれはなんなんだ...

# Pipfileを眺めてみる

venv環境もPipfile.lockも消してなお、エラーが出るのでPipfileに原因があるのはほぼ確定です。
このときのPipfileが以下になります。

``` TOML
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]
flake8 = "*" 
autopep8 = "*" 
isort = "*" 
autoflake = "*" 
pytest = "*" 
pytest-cov = "*" 
qiitacli = {path = "."}
twine = "*" 
sphinx = "*" 
sphinx-git = "*" 
pypandoc = "*" 

[packages]
click = ">=7.0"
qiita_v2 = ">=0.2.1"

[requires]
python_version = "3" 

[scripts]
main = "python qiitacli/client.py"
```

僕の脳みそじゃどこがおかしいかわからないよ...

# ログを見てみる

`pipenv install`時に`-v`オプションを付けて出力を眺めてみました。
思ったより出力が多くてびっくりしましたw

その中で原因と思われるログが出ていたのがこちら。

```
ERROR: In --require-hashes mode, all requirements must have their versions pinned with ==. These do not:
    tqdm>=4.14 from https://files.pythonhosted.org/packages/bb/62/6f823501b3bf2bac242bd3c320b592ad1516b3081d82c77c1d813f076856/tqdm-4.39.0-py2.py3-none-any.whl#sha256=5a1f3d58f3eb53264387394387fe23df469d2a3fab98c9e7f99d5c146c119873 (from twine==3.1.0->-r /tmp/pipenv-pbslpsa_-requirements/pipenv-u629hk8s-requirement.txt (line 1))    readme-renderer>=21.0 from https://files.pythonhosted.org/packages/c3/7e/d1aae793900f36b097cbfcc5e70eef82b5b56423a6c52a36dce51fedd8f0/readme_renderer-24.0-py2.py3-none-any.whl#sha256=c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d (from twine==3.1.0->-r /tmp/pipenv-pbslpsa_-requirements/pipenv-u629hk8s-requirement.txt (line 1))
    requests-toolbelt!=0.9.0,>=0.8.0 from https://files.pythonhosted.org/packages/60/ef/7681134338fc097acef8d9b2f8abe0458e4d87559c689a8c306d0957ece5/requests_toolbelt-0.9.1-py2.py3-none-any.whl#sha256=380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f (from twine==3.1.0->-r /tmp/pipenv-pbslpsa_-requirements/pipenv-u629hk8s-requirement.txt (line 1))
    secretstorage; sys_platform == "linux" from https://files.pythonhosted.org/packages/82/59/cb226752e20d83598d7fdcabd7819570b0329a61db07cfbdd21b2ef546e3/SecretStorage-3.1.1-py3-none-any.whl#sha256=7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a (from keyring>=15.1->twine==3.1.0->-r /tmp/pipenv-pbslpsa_-requirements/pipenv-u629hk8s-requirement.txt (line 1))
Exception information:
Traceback (most recent call last):
  File "/hoge/.venv/lib/python3.6/site-packages/pip/_internal/cli/base_command.py", line 153, in _main
    status = self.run(options, args)
  File "/hoge/.venv/lib/python3.6/site-packages/pip/_internal/commands/install.py", line 382, in run
    resolver.resolve(requirement_set)
  File "/hoge/.venv/lib/python3.6/site-packages/pip/_internal/legacy_resolve.py", line 208, in resolve
    raise hash_errors
pip._internal.exceptions.HashErrors: In --require-hashes mode, all requirements must have their versions pinned with ==. These do not:
    tqdm>=4.14 from https://files.pythonhosted.org/packages/bb/62/6f823501b3bf2bac242bd3c320b592ad1516b3081d82c77c1d813f076856/tqdm-4.39.0-py2.py3-none-any.whl#sha256=5a1f3d58f3eb53264387394387fe23df469d2a3fab98c9e7f99d5c146c119873 (from twine==3.1.0->-r /tmp/pipenv-pbslpsa_-requirements/pipenv-u629hk8s-requirement.txt (line 1))    readme-renderer>=21.0 from https://files.pythonhosted.org/packages/c3/7e/d1aae793900f36b097cbfcc5e70eef82b5b56423a6c52a36dce51fedd8f0/readme_renderer-24.0-py2.py3-none-any.whl#sha256=c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d (from twine==3.1.0->-r /tmp/pipenv-pbslpsa_-requirements/pipenv-u629hk8s-requirement.txt (line 1))
    requests-toolbelt!=0.9.0,>=0.8.0 from https://files.pythonhosted.org/packages/60/ef/7681134338fc097acef8d9b2f8abe0458e4d87559c689a8c306d0957ece5/requests_toolbelt-0.9.1-py2.py3-none-any.whl#sha256=380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f (from twine==3.1.0->-r /tmp/pipenv-pbslpsa_-requirements/pipenv-u629hk8s-requirement.txt (line 1))
    secretstorage; sys_platform == "linux" from https://files.pythonhosted.org/packages/82/59/cb226752e20d83598d7fdcabd7819570b0329a61db07cfbdd21b2ef546e3/SecretStorage-3.1.1-py3-none-any.whl#sha256=7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a (from keyring>=15.1->twine==3.1.0->-r /tmp/pipenv-pbslpsa_-requirements/pipenv-u629hk8s-requirement.txt (line 1))
```

内部で`pip install`する時に`--require-hashes`オプション付けてるんですね。
[hash checking mode](https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode)というらしい、このログ見るまで存在を知りませんでした。

`In --require-hashes mode, all requirements must have their versions pinned with ==. These do not` 要約するとhash checking modeの時はすべてのモジュールのバージョンを`==`で指定しなきゃだめよ！！ってことっぽいですね。(英語ワカラナイ)

そもそもエラーの出ていないモジュールもPipfileでバージョン指定してないし、謎は深まるばかり。

# requirements.txtを眺めてみる

ログを見るにPipenvコマンドはrequirements.txt相当のファイルを/tmp配下に生成しては、pipコマンドに`-r`オプションを付けて実行しているよう。
それらのファイルの中身を実際に見てました。

方法としてはPipenv実行時にひたすら`sudo cp -rf /tmp/pipenv* .`を連打。絶対にもっといい方法があるはずだけれどひとまずこれで。

コピーしたファイルのなかの一つを見てみるとこんな感じ

```
twine==3.1.0 --hash=sha256:1a87ae3f1e29a87a8ac174809bf0aa996085a0368fe500402196bda94b23aab3 --hash=sha256:ba8ba1b39987f1c22d9162f7dd3b4668388640e5f7158c15226624f88e464836
```

ほー、モジュールひとつずつにハッシュ値を付けたrequirements.txtを生成して、`-r`オプションに渡しているんだ。

ハッシュ値を確認してみましたが、実際に公開されているファイルと同値でした。

試しにこのrequirements.txtファイルを使って手で作ったvenv環境にインストールしてみます。

``` console
$ echo -n ' twine==3.1.0 --hash=sha256:1a87ae3f1e29a87a8ac174809bf0aa996085a0368fe500402196bda94b23aab3 --hash=sha256:ba8ba1b39987f1c22d9162f7dd3b4668388640e5f7158c15226624f88e464836' > requirements.txt
$ python3 -m venv venv
$ ./venv/bin/pip install -r requirements.txt --require-hashes
Collecting twine==3.1.0
  Using cached https://files.pythonhosted.org/packages/74/45/1016cad7eb7cbda959a701d1dfa88b9118306677018ac3de224a6a6e7751/twine-3.1.0-py3-none-any.whl
Collecting readme-renderer>=21.0
Collecting requests-toolbelt!=0.9.0,>=0.8.0
Collecting tqdm>=4.14
Collecting requests>=2.20
Collecting keyring>=15.1
Collecting importlib-metadata; python_version < "3.8"
Requirement already satisfied: setuptools>=0.7.0 in ./venv/lib/python3.6/site-packages (from twine==3.1.0->-r requirements.txt (line 1)) (42.0.1)
Collecting pkginfo>=1.4.2
ERROR: In --require-hashes mode, all requirements must have their versions pinned with ==. These do not:
    readme-renderer>=21.0 from https://files.pythonhosted.org/packages/c3/7e/d1aae793900f36b097cbfcc5e70eef82b5b56423a6c52a36dce51fedd8f0/readme_renderer-24.0-py2.py3-none-any.whl#sha256=c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d (from twine==3.1.0->-r requirements.txt (line 1))
    requests-toolbelt!=0.9.0,>=0.8.0 from https://files.pythonhosted.org/packages/60/ef/7681134338fc097acef8d9b2f8abe0458e4d87559c689a8c306d0957ece5/requests_toolbelt-0.9.1-py2.py3-none-any.whl#sha256=380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f (from twine==3.1.0->-r requirements.txt (line 1))
    tqdm>=4.14 from https://files.pythonhosted.org/packages/bb/62/6f823501b3bf2bac242bd3c320b592ad1516b3081d82c77c1d813f076856/tqdm-4.39.0-py2.py3-none-any.whl#sha256=5a1f3d58f3eb53264387394387fe23df469d2a3fab98c9e7f99d5c146c119873 (from twine==3.1.0->-r requirements.txt (line 1))
    requests>=2.20 from https://files.pythonhosted.org/packages/51/bd/23c926cd341ea6b7dd0b2a00aba99ae0f828be89d72b2190f27c11d4b7fb/requests-2.22.0-py2.py3-none-any.whl#sha256=9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31 (from twine==3.1.0->-r requirements.txt (line 1))
    keyring>=15.1 from https://files.pythonhosted.org/packages/b1/08/ad1ae7262c8146bee3be360cc766d0261037a90b44872b080a53aaed4e84/keyring-19.2.0-py2.py3-none-any.whl#sha256=f5bb20ea6c57c2360daf0c591931c9ea0d7660a8d9e32ca84d63273f131ea605 (from twine==3.1.0->-r requirements.txt (line 1))
    importlib-metadata; python_version < "3.8" from https://files.pythonhosted.org/packages/f6/d2/40b3fa882147719744e6aa50ac39cf7a22a913cbcba86a0371176c425a3b/importlib_metadata-0.23-py2.py3-none-any.whl#sha256=d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af (from twine==3.1.0->-r requirements.txt (line 1))
    pkginfo>=1.4.2 from https://files.pythonhosted.org/packages/e6/d5/451b913307b478c49eb29084916639dc53a88489b993530fed0a66bab8b9/pkginfo-1.5.0.1-py2.py3-none-any.whl#sha256=a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32 (from twine==3.1.0->-r requirements.txt (line 1))
```

お！再現したぞ！

でもエラーが出ているのはrequirements.txtに記述されてるモジュールとは別のモジュール...

このhash checking modeってのは依存パッケージもすべてバージョン指定で記述する必要があるんですね。
でもじゃあなぜ普通に`pipenv install`する時はエラーが出ないのか。

# --no-depsとは

もうしばらくログを確認していると正常に動作している時はpipでインストールする時に`--no-deps`というオプションが付いていることを見つけました。

* エラー時のログ

```
$ ['/hoge/.venv/bin/pip', 'install', '--verbose', '--upgrade', '-r', '/tmp/pipenv-9owwlzhi-requirements/pipenv-65yqxf03-requirement.txt', '-i', 'https://pypi.org/simple', '--require-hashes']
```

* 正常時のログ

```
$ ['/hoge/.venv/bin/pip', 'install', '--verbose', '--upgrade', '--no-deps', '-r', '/tmp/pipenv-5dvxlnyt-requirements/pipenv-pvatvj5c-requirement.txt', '-i', 'https://pypi.org/simple', '--require-hashes']
```

おおおこれっぽい！

`--no-deps`は`Don't install package dependencies.`つまり依存パッケージをインストールしないオプションですね。
なるほでこれならさっきのrequirements.txtでも正常に動作しそうです。

``` console
$ ./venv/bin/pip install -r requirements.txt --require-hashes --no-deps 
Collecting twine==3.1.0
  Using cached https://files.pythonhosted.org/packages/74/45/1016cad7eb7cbda959a701d1dfa88b9118306677018ac3de224a6a6e7751/twine-3.1.0-py3-none-any.whl
Installing collected packages: twine
Successfully installed twine-3.1.0
```

できた。

少しずつ原因がつかめてきました。

あと残るは謎は **なぜ`--no-deps`オプションを付けてくれなかったのか** だけですね。

# 最後の鍵はeditable

最後はPipenvのソースを読むしかありません。

https://github.com/pypa/pipenv

`pipenv install`を実行した時に実際に`pip install ...`コマンドを実行している場所を見てみましょう。

実際にpipコマンドを実行している箇所は`pipenv/core.py`内の`pip_install`という関数でした。
呼び出される順番として`do_install -> do_init -> do_install_dependencies -> batch_install -> pip_install`とまあ色々有りましたが、、、
結論だけいうと`batch_install`関数内の以下の箇所で`--no-deps`のオプションを付けるか否かのフラグが設定されていました。

``` Python
        if not PIPENV_RESOLVE_VCS and is_artifact and not dep.editable:
            install_deps = True 
            no_deps = False
```

depってのはインストールするモジュールの情報が入ったクラスです。
`PIPENV_RESOLVE_VCS`じゃなくて、`is_artifact`で、モジュールが`editable`じゃなくて、、、

さっぱりわからん

なんとなく「ソースからインストールするモジュールでeditableのフラグが付いていないもの」が`--no-deps`の付与に関係してそうですね。
`pipenv install -h`したらこんなものがありました。

```
$ pipenv install -h
略
  -e, --editable TEXT      An editable python package URL or path, often to a
                           VCS repo.
```

あ、、、

# 解決

エラーの原因となったPipfileをもう一度見てみましょう。

``` TOML
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]
flake8 = "*"
autopep8 = "*"
isort = "*"
autoflake = "*"
pytest = "*"
pytest-cov = "*"
qiitacli = {path = "."}
twine = "*"
sphinx = "*"
sphinx-git = "*"
pypandoc = "*"

[packages]
click = ">=7.0"
qiita_v2 = ">=0.2.1"

[requires]
python_version = "3"

[scripts]
main = "python qiitacli/client.py"
```

はい原因は

``` TOML
qiitacli = {path = "."}
```

こいつでしたね。

開発中のモジュールをローカルにインストールするときによく`pipenv install .`とかしてたんですがこれが問題でした。
正しくは`pipenv install -e .`こう。その結果できたPipfileがこう。

```
qiitacli = {editable = true,path = "."}
```

editableフラグが付いてますね。

この状態なら正常にインストールが完了しました。

# まとめ

これ昔はちゃんと`-e`オプション付けて入れてたんですけど、オプションなくてもインストール出来ることに気がついてからはそのままインストールするようにしちゃってました。(多分)

ちなみに公式のドキュメントにもしっかり書いてありました。 <https://pipenv-ja.readthedocs.io/ja/translate-ja/basics.html#editable-dependencies-e-g-e>

せめて使ってるオプションの意味くらいは理解しようね。自分。