enchanのメモ書き

計算機とフリルとラブライブ!

Ubuntu 20.04でPython 3.9.10をビルド

えー記事本文に入る前に申し上げておきます。技術記事ではありません。日記です。たまに備忘録と呼ばれるもの。それくらい軽いノリでいきます。

デフォルトのソフトウェアが苦手

Python、gcc、viやrubyなどの有名なプログラムは、OSにデフォルトで同梱されていることがあります。 これらはセットアップ直後から使用できるようになっており、インストールする手間が省けて非常に便利です。

しかしそれらはバージョンが古かったり、日本語に対応していなかったり、まあ色々と問題を抱えていることが多く、 デフォルトのソフトウェアをそのまま使うということはあまりないような気がします。気がしているだけです。

UbuntuデフォルトのPython

Ubuntu 20.04にはデフォルトとしてPython 3.8系がインストールされています。(時期により違いがあるかもしれませんが未検証)

$ which python3
/usr/bin/python3
$ /usr/bin/python3 --version
Python 3.8.10

/usr/binです。Linuxのことはよくわからないので適当ですが、どうやら/usrというのは

各種プログラムなど (引用元: @IT)

ユーザーアプリケーションがインストールされるディレクトリ。実行ファイルは bin、依存ライブラリは lib などにインストールされる。 (引用元: Qiita)

…という目的で使用されるディレクトリのようです。

実際に見てみると…

# 一部抜粋
/usr
├── bin
│   (省略)
│   ├── echo
│   (省略)
│   ├── python3 # !?
│   (省略)
│   └── whoami
├── games
(省略)
└── src

こんな構造になっていました。

なぜ/usrにPythonがいるのか???

…なんかこのPython、ちょっと場違いじゃないでしょうか。超汎用的なコマンドの中にポツンと一軒Python

このアウェー感はなんか見覚えがあります。

 
ここにいても、いいの?  
 
 
(無言)  

となってしまいそうです。Pythonのメンタルが心配です。このままでは周囲の重圧に押されてメジャーバージョンをひとつ落としてしまうかもしれません

もう少し詳しく調べてみると、LinuCさんのページがヒットしました。
これによれば、

/bin には、シングルユーザモードでも利用できるコマンド
/usr/bin には、シングルユーザモードで利用しない かつ パッケージ管理システムによって、システムに管理されるコマンドやプログラム
/usr/local/bin には、シングルユーザモードで利用しない かつ パッケージ管理システムによってシステムに管理されないコマンドやプログラム

が置かれるとのことなので、このpythonはaptで管理されているということになり…

$ apt info python3.8
Package: python3.8
Version: 3.8.10-0ubuntu1~20.04.2
Priority: important
...

パッケージの優先度がimportantであることから、UbuntuにおいてPython3は重要なプログラムであり、"それが欠けていることに気付いた経験豊富なUnixの人が、「一体何が起こっているのか、python3はどこにあるのか」と言うことが期待される" パッケージであるということがわかります。
(引用: debian.org)

つまり、ここにいてもよかったというわけです。なるほど。

それはそれとして

でも /usr/binなんかイヤです。バージョン管理くらい自分でさせてください。
random.randbytesが3.9以降ということに気づかずN時間潰したトラウマがあるので常に把握しておきたいのです。

こういった需要に応えるためかは不明ですが、Pythonそのものを管理するツールがいくつか存在します。pyenv, virtualenv, Anaconda etc....
これらに頼るのもアリといえばアリです。というか素人なんだからそうするべきです。

ですが、本当にそれでいいのでしょうか。

このまま、PythonをどうビルドしたらいいのかわからないままPythonistaを名乗って、本当に良いのでしょうか?

インストールしていくぞ

というわけでPython 3.9.10をソースインストールしていきます。 前置きが長すぎて半数程度の方がブラウザバックしていそうです。

ソースコードのダウンロード

Pythonのソースコードは公式のダウンロードページにあります。

インストーラやスポンサーの紹介を軽く聞き流し、Sources の Read more から…

インストールしたいバージョンを選んで…

Gzipped source tarball を落とします。 今回はブラウザのある環境≠インストールしたい環境だったので、リンクをコピーしてwgetします。curlでもいいです。

ダウンロード・展開すると、中にREADME.rstファイルが見つかるはずです。
内容はこのようになっています。Pythonの概要からインストール方法、前回バージョンからの変更点などが記述されています。

このままだと見にくいので、以降はsphinxrst2html.pyによりhtmlに変換したものを貼っていきます。

例の儀式

まず見ていくのは Build Instructions セクションです。

なんとたった数個のコマンドでインストールできてしまうとのこと。早速やっていきましょう。

先程のルールに従い、インストール先を/usr/localとします。configure--prefix=/usr/localを渡すことでこれを設定できます。 ここでうっかり/usr/loclaとかtypoすると面白いことに大変なことになります。パス確認ヨシ!

諸々の最適化も有効にしたいので、--enable-optimizations --with-ltoも設定しておきましょう。
pipもあわせて新規インストールしたいので、こちらも有効にします。

./configure --enable-optimizations --with-lto --prefix=/usr/local --with-ensurepip=install

環境にもよりますが、2分以上かかったことは手元ではありませんでした。成功するとMakefileが生成されるので、makeします。
ここまでのメッセージは正直そこまで重要でもない(configureで何か起きていればエラーストップする)ので、いったんターミナルの履歴を消しておくのもアリかと思います。 というかここから先の出力の方が遥かに重要です。

make

makeが終了するまでには結構な時間を要します。ソシャゲの周回でもやっておきましょう。私は推しの誕生日が近いので必死に石集めをしています。
そういえば、最近遊戯王マスターデュエルを始めました。ウィッチクラフトが好きです。

ビルドしながら書いているのでどうでもいい話が挟まります。makeには20分もかかりました。

終了コード0だったので問題ないかと思いきや、すぐ近くになんかイヤな出力があります。

ここからが本番です。

エラーを潰す

Pythonのmakeは非常に優しく、どこかでコケてもなるべく./buildにその結果を保存しようとします。
そのため、エラーの原因を落ち着いて一つずつ潰してはmakeしてを繰り返すことができます。便利。

ffi.hが存在しない

ffiとは Foreign Function Interface (別の言語で定義された関数を使用するための仕組み?)のことです。
パスの並びから、PythonではC/C++の関数を呼び出すctypesモジュールで使用されるようです。

まずは、packages.ubuntu.comでパッケージとして公開されていないかを調べます。 検索フィールドにffi.hを入力、右のドロップダウンにパッケージの内容を選択して検索すると、このような結果になりました。

libffi-devというパッケージで提供されているようです。あとは通常通りaptでインストールします。
(そりゃもちろん公式からソースは落とせますが、それはもうただの泥沼なのでスルーします)

INFO: Can't locate Tcl/Tk libs and/or headers

Tcl/Tkがないというメッセージ。これはそのまま、tcltkという名前のパッケージをそれぞれインストール…するだけではダメで、 しっかりとtcl-devtk-devの方も併せてインストールする必要があります。罠です。

The necessary bits to build these optional modules were not found

オプションモジュールのビルドに必要な諸々が見つからないというエラー。今回は以下のように出力されていました:

The necessary bits to build these optional modules were not found:
_bz2                  _curses               _curses_panel      
_dbm                  _gdbm                 _lzma              
_sqlite3              _uuid                 readline
To find the necessary bits, look in setup.py in detect_modules() for the module's name.

多いな。多すぎます。よくみるモジュールからなんだこれみたいなモジュールまで多種多様です。

一つ一つパッケージ検索にかけ…てもいいのですが、この作業は正直苦行以外の何者でもないので、 pyenvのsuggested-build-environmentに頼ります。

それによれば、Ubuntuでは以下のコマンドを実行せよとのこと。

sudo apt-get update; sudo apt-get install make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev

…マジで多いな…

ncursesが5だったり(執筆現在の最新は6)しますが、とりあえず5を入れておきます。メジャーバージョンが違うと関数名が丸ごと変わっていたりして大変なことになるからです(1敗)。
よく見るとさっきインストールしたtkやらffiやらがチラッと顔を覗かせています。最初からこれに頼るべきだったのでした。

…しかし。

ほとんどのモジュールのビルドには成功しているようですが、最終的にこの子たちが残ってしまいました。

The necessary bits to build these optional modules were not found:
_dbm                  _gdbm                                    
To find the necessary bits, look in setup.py in detect_modules() for the module's name.


The following modules found by detect_modules() in setup.py, have been
built by the Makefile instead, as configured by the Setup files:
_abc                  atexit                pwd                
time  

_dbm_gdbmは先程のコマンドの中には含まれていませんでした。 3.9系で追加されたのかな?と思いきやdbmモジュールは2.7時代からあったそうなので、どうやらそういうわけでもなさそう。

…というわけで再びインストールです。_gdbmlibgdbm-devパッケージ、_dbmlibgdbm-compat-devパッケージです。 dbmの方に至ってはわかるわけがありません。なんだこのパッケージ名は。

とりあえずビルドエラーの方はひととおり解決です。

テスト

インストールする前にmake testを実行します。テストケースは425件もあるので気長に待ちましょう。私は今ドラゴンメイドでデッキを組んでいます。

test_embed test_tabnanny test_venvのテストケースでfailしました。おい。

こういう時はテストケースを単体で実行します。./python -m test (モジュール名) -v を実行することでモジュール単位でのテストが可能です。
ログを見ると…

AssertionError: {...
-  'base_executable': '/home/enchantcode/\udce3\udc83\udc87\udce3\udc82\udcb9\udce3\udc82\udcaf\udce3\udc83\udc88\udce3\udc83\udc83\udce3\udc83\udc97/Python-3.9.10/_testembed',
+  'base_executable': '/home/enchantcode/デスクトップ/Python-3.9.10/_testembed',
....}

はい。そりゃそうです。間違ってもパスに英数字以外が含まれるディレクトリでPythonをビルドしてはいけません
適当な場所に移してconfigureからやり直しです。もう面倒なので&&でくっつけてまとめてやります。

test_pyexpatでfailしました。なんで???? Pythonのソースインストールはこれが初めてではないのですが、今回はやたらコケます。
該当コードを確認すると…

def test_exception(self):
    parser = expat.ParserCreate()
    parser.StartElementHandler = self.StartElementHandler
    try:
        parser.Parse(b"<a><b><c/></b></a>", True)
        self.fail()
    except RuntimeError as e:
        self.assertEqual(e.args[0], 'a',
                            "Expected RuntimeError for element 'a', but" + \
                            " found %r" % e.args[0])
        ......
        if sysconfig.is_python_build() and not (sys.platform == 'win32' and platform.machine() == 'ARM'):
            self.assertIn('call_with_frame("StartElement"', entries[1][3])

一旦意図的にfailさせ、その後アサーションしているようです。こういう組み方があるんですね…(間違っていたら教えてください)
その後、WindowsかARMでなく、pythonのビルド中であれば self.assertIn 以降が実行される、ということになっています。

該当行では 'call_with_frame("StartElement"' in entries[1][3] が評価されるのですが、これがうまくいっていないようです。
これ公式でもテスト通ってないんじゃないの(大変失礼)と思いましたが、どうやらそうでもなさそう。

これが技術記事であれば真面目に検証するべきですが、あくまで日記なので今回は華麗にスルーします。

インストール

一抹の不安は残りますが、インストールしてしまいましょう。

sudo make install を実行します。同じprefixで複数のPythonが共存する可能性のある場合はaltinstallを使用しましょう。
インストールが完了したら、忘れずにPATHを通します。久々にbash_profileを開いたらめちゃくちゃ汚くてびっくりしました。部屋とプロファイルの掃除は定期的に行いましょう。

試してみる

$ python3
Python 3.9.10 (main, Feb 19 2022, 17:55:49) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

良好。

$ pip --version
pip 21.2.4 from /usr/local/lib/python3.9/site-packages/pip (python 3.9)

いい感じです。試しにnumpyでもインストールしてみましょう。

$ pip install numpy
省略

$ pip show numpy
Name: numpy
Version: 1.22.2
Summary: NumPy is the fundamental package for array computing with Python.
Home-page: https://www.numpy.org
Author: Travis E. Oliphant et al.
Author-email: 
License: BSD
Location: /home/enchantcode/.local/lib/python3.9/site-packages
Requires: 
Required-by: 

大丈夫そうですね!
若干インストール先のパスがキモいですが、これはまあPythonをインストールした場所のせいであり云々…
まあ~/local/とかにインストールすればちゃんとそっちに入ってくれます。それはまた次の記事で。

疲れた

…はい。疲れました。備忘録とはいえ約9000文字は書きすぎです。

ソースインストールをやるたびにパッケージマネージャの恩恵の凄まじさがわかります。 今回もapt頼りの部分がありましたが、この記事がPV300を超えたら追える限りソースインストールする縛りでPythonを入れてみようと思います。

fflush(stdout)