Python3.11を最速インストールしようとしてSSLモジュールでハマった話

お疲れ様です。KBTです。
待ちに待ったPython3.11がリリースされてから早2ヶ月、皆さんそろそろPython3.6.Xからの乗り換えを検討されている頃でしょうか。
私も早速インストールしてみましたので、今回はインストール時にハマった話を書いてみたいと思います。

# 今回の環境
```
# cat /etc/centos-release
CentOS Stream release 8
```

# OpenSSLをインストールする
まずはOpenSSLをインストールします。
折角新しくインストールするなら今をときめくOpenSSL3系をインストールしたいですよね。
試しにdnfでインストールしようとしたところ、本日(2022/12/17)時点で3.0.1がインストールされるようです。
```
# dnf install openssl3
Last metadata expiration check: 3:49:02 ago on Sat 17 Dec 2022 08:33:35 AM JST.
Dependencies resolved.
==========================================================================================================================
 Package                         Architecture             Version                            Repository              Size
==========================================================================================================================
Installing:
 openssl3                        x86_64                   3.0.1-43.el8.1                     epel                   1.1 M
Installing dependencies:
  openssl3-libs                   x86_64                   3.0.1-43.el8.1                     epel                   2.4 M

Transaction Summary
==========================================================================================================================
Install  2 Packages
```

3.0.1～3.0.6には皆さんが昼夜を問わず対応に追われた悪名高い脆弱性が含まれていますので、これをそのままインストールするというのは大きな抵抗があることと思います。
参考:[CVE-2022-3602,CVE-2022-3786](https://www.openssl.org/blog/blog/2022/11/01/email-address-overflows/)

仕方がないので3.0.7のソースをダウンロードして手動でインストールすることにしましょう。

```
# dnf install perl
# dnf install perl-IPC-Cmd
# dnf install perl-Test-Simple
# curl https://www.openssl.org/source/openssl-3.0.7.tar.gz -o openssl-3.0.7.tar.gz
# tar xvzf openssl-3.0.7.tar.gz
# cd openssl-3.0.7
# ./Configure
# make
# make test
# make install_sw
```
はい、コンパイル～テストパス～インストールまで完了しましたね。
実際に全てのライブラリがインストール済みであることも`ldd /usr/local/bin/openssl`などでちゃんと確認しましょう。
※もしnot foundなライブラリがある場合はコンパイルしたものを/lib64にコピーしてください

正しくインストールができましたらopensslのバージョンを確認して満足感を補給しましょう。
```
# openssl version
OpenSSL 3.0.7 1 Nov 2022 (Library: OpenSSL 3.0.7 1 Nov 2022)
```
やったね!

# Python3.11をインストールする
そもそもPython3.11がdnfで入ってくれればわざわざソースからインストールする必要はないのですが、本日時点ではPython3.9までしか入れることができないようです。
仕方がないのでまずは必要な周辺機能をインストールします。
```
# dnf install zlib-devel
# dnf install bzip2-devel
# dnf install ncurses-devel
# dnf install libffi-devel
# dnf install libuuid libuuid-devel
# dnf install sqlite-devel
# dnf install readline-devel
# dnf install tk-devel
```

その後Python3.11をダウンロードしてコンパイルします。
```
# curl https://www.python.org/ftp/python/3.11.1/Python-3.11.1.tgz -o Python-3.11.1.tgz
# tar xzvf Python-3.11.1.tgz
# cd Python-3.11.1
# ./configure --enable-optimizations
# make
# make altinstall
```
はい、コンパイル～インストールまで終わりましたでしょうか。
ちゃんとインストールできたようでしたらバージョン情報を確認して満足感を補給しましょう。
エンジニアにとってコーヒーブレイクとバージョン情報の確認だけが明日を生き抜く原動力となることは広く知られています。
```
# python3.11 --version
Python 3.11.1
```
やったね!

# SSLでハマる
それではいよいよPython3.11を動かすべく、よく使うPythonモジュールをpipでインストールしていきましょう。
```
# python3.11 -m pip install "fastapi[all]"
 WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available.
 WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/fastapi/
 WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/fastapi/
 WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/fastapi/
 WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/fastapi/
 WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/fastapi/
 Could not fetch URL https://pypi.org/simple/fastapi/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/fastapi/ (Caused by SSLError("Can't connect to HTTPS URL because the SSL module is not available.")) - skipping
 ERROR: Could not find a version that satisfies the requirement fastapi[all] (from versions: none)
 ERROR: No matching distribution found for fastapi[all]
 WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available.
 Could not fetch URL https://pypi.org/simple/pip/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/pip/ (Caused by SSLError("Can't connect to HTTPS URL because the SSL module is not available.")) - skipping
 WARNING: There was an error checking the latest version of pip.
```

何やらWARNINGやらERRORやらの文字が躍っておりますね。
`the ssl module in Python is not available`とのことなので、PythonでSSLモジュールが使用できないようですね。
そんなことがあり得るのか確認してみましょう。
```
# python3.11 -m ssl
 Traceback (most recent call last):
   File "<frozen runpy>", line 198, in _run_module_as_main
   File "<frozen runpy>", line 88, in _run_code
   File "/usr/local/lib/python3.11/ssl.py", line 100, in <module>
     import _ssl             # if we can't import it, let the error propagate
     ^^^^^^^^^^^
 ModuleNotFoundError: No module named '_ssl'
```
アーリーエールー:sob:
どうしてこうなった、その謎を確かめるため我々はPythonコンパイル時のログを見直した。

# Pythonコンパイル時のエラー
Pythonコンパイル時のログは長文のため、その中からSSLに関連しそうなところを眺めてみると以下のような出力を見つけることができました。
俺でなきゃ見逃しちゃうね!
```
 building '_ssl' extension
 gcc -pthread -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fno-semantic-interposition -std=c11 -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -Werror=implicit-function-declaration -fvisibility=hidden -fprofile-use -fprofile-correction -I./Include/internal -fPIC -I/root/openssl-3.0.7/include -I./Include -I. -I/usr/local/include -I/root/Python-3.11.1/Include -I/root/Python-3.11.1 -c /root/Python-3.11.1/Modules/_ssl.c -o build/temp.linux-x86_64-3.11/root/Python-3.11.1/Modules/_ssl.o
 gcc -pthread -shared build/temp.linux-x86_64-3.11/root/Python-3.11.1/Modules/_ssl.o -L/root/openssl-3.0.7/lib -L/usr/local/lib -lssl -lcrypto -o build/lib.linux-x86_64-3.11/_ssl.cpython-311-x86_64-linux-gnu.so
 building '_hashlib' extension
 gcc -pthread -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fno-semantic-interposition -std=c11 -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -Werror=implicit-function-declaration -fvisibility=hidden -fprofile-use -fprofile-correction -I./Include/internal -fPIC -I/root/openssl-3.0.7/include -I./Include -I. -I/usr/local/include -I/root/Python-3.11.1/Include -I/root/Python-3.11.1 -c /root/Python-3.11.1/Modules/_hashopenssl.c -o build/temp.linux-x86_64-3.11/root/Python-3.11.1/Modules/_hashopenssl.o
 gcc -pthread -shared build/temp.linux-x86_64-3.11/root/Python-3.11.1/Modules/_hashopenssl.o -L/root/openssl-3.0.7/lib -L/usr/local/lib -lcrypto -o build/lib.linux-x86_64-3.11/_hashlib.cpython-311-x86_64-linux-gnu.so
 *** WARNING: renaming "_ssl" since importing it failed: /root/Python-3.11.1/build/lib.linux-x86_64-3.11/_ssl.cpython-311-x86_64-linux-gnu.so: undefined symbol: SSL_get1_peer_certificate
 *** WARNING: renaming "_hashlib" since importing it failed: /root/Python-3.11.1/build/lib.linux-x86_64-3.11/_hashlib.cpython-311-x86_64-linux-gnu.so: undefined symbol: EVP_MD_get_type
 
 The necessary bits to build these optional modules were not found:
 _dbm                  _gdbm                 nis                
 To find the necessary bits, look in setup.py in detect_modules() for the module's name.
 
 
 Following modules built successfully but were removed because they could not be imported:
 _hashlib              _ssl                                     
 
 
 Could not build the ssl module!
```
既に1回見逃して素通りしていることは棚に上げ、何とか原因と考えられる出力を見つけることができました。
どうやら共有ライブラリに一部のシンボルが存在しないと怒られているようです。
早速この事象について軽くググってみたのですが、今回の条件(Python3.11+opensslをソースからコンパイルしてインストールする)にマッチするページを見つけることはできませんでした。

異なるPythonのバージョンであれば幾つかの記事が見つかるのですが([ここ](https://stackoverflow.com/questions/41328451/ssl-module-in-python-is-not-available-when-installing-package-with-pip3)とか[ここ](https://yamadablog.web.fc2.com/articles/14/index.html)とか)、解決策として記載されているコードに該当する箇所が最新のPython側に存在しなかったりと一筋縄ではいかないようです。

# 解決策
Pythonのコンパイル前にModules/Setupを以下のように修正しました。
```diff
# To statically link OpenSSL:
- # _ssl _ssl.c $(OPENSSL_INCLUDES) $(OPENSSL_LDFLAGS) \
+ _ssl _ssl.c $(OPENSSL_INCLUDES) $(OPENSSL_LDFLAGS) \
- #    -l:libssl.a -Wl,--exclude-libs,libssl.a \
+     -l:libssl.a -Wl,--exclude-libs,libssl.a \
- #    -l:libcrypto.a -Wl,--exclude-libs,libcrypto.a
+     -l:libcrypto.a -Wl,--exclude-libs,libcrypto.a
- # _hashlib _hashopenssl.c $(OPENSSL_INCLUDES) $(OPENSSL_LDFLAGS) \
+ _hashlib _hashopenssl.c $(OPENSSL_INCLUDES) $(OPENSSL_LDFLAGS) \
- #    -l:libcrypto.a -Wl,--exclude-libs,libcrypto.a
+     -l:libcrypto.a -Wl,--exclude-libs,libcrypto.a
```
※219行目付近

また、`make clean`を実行してconfigureオプションを以下のように修正しました。
```
./configure --enable-optimizations --with-openssl-rpath=auto --with-openssl=$HOME/openssl-3.0.7 OPENSSL_LDFLAGS=-L$HOME/openssl-3.0.7 OPENSSL_LIBS=-l$HOME/openssl-3.0.7/ssl OPENSSL_INCLUDES=-I$HOME/openssl-3.0.7
```

その後再度コンパイルを実行しsslモジュールがインポートできるかを確認しましょう。
```
# python3.11
Python 3.11.1 (main, Dec 17 2022, 19:47:49) [GCC 8.5.0 20210514 (Red Hat 8.5.0-17)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import ssl
>>>
```
エラーなし!

続けてpipでモジュールのインストールも実行してみましょう。
```
# python3.11 -m pip install "fastapi[all]"
Collecting fastapi[all]
  Downloading fastapi-0.88.0-py3-none-any.whl (55 kB)
     qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq 55.5/55.5 kB 1.8 MB/s eta 0:00:00
(中略)
Successfully installed MarkupSafe-2.1.1 anyio-3.6.2 certifi-2022.12.7 click-8.1.3 dnspython-2.2.1 email-validator-1.3.0 fastapi-0.88.0 h11-0.14.0 httpcore-0.16.2 httptools-0.5.0 httpx-0.23.1 idna-3.4 itsdangerous-2.1.2 jinja2-3.1.2 orjson-3.8.3 pydantic-1.10.2 python-dotenv-0.21.0 python-multipart-0.0.5 pyyaml-6.0 rfc3986-1.5.0 six-1.16.0 sniffio-1.3.0 starlette-0.22.0 typing-extensions-4.4.0 ujson-5.6.0 uvicorn-0.20.0 uvloop-0.17.0 watchfiles-0.18.1 websockets-10.4
```
やったね!

# おわりに
大抵の問題は検索で凌いできた身としては検索で引っかからない問題にぶち当たるというのは新しい領域に足を踏み入れつつあるのかな、という気持ちになりました。
今後のことを考えると変なことはせず大人しくdnfで入るものを入れるようにしておけばこのような問題で時間を浪費することなくより創造的な時間を過ごすことができたことでしょう。
この記事が同じところでハマっている人の助けになれば幸いです。(そんな人いないかも・・・)