Linux Mint に Docker をインストール

Docker 公式のおすすめに従って、Docker Desktop for Linux をインストールすることにした。

Linux Mint は Ubuntu がベースなので、Ubuntu のページを参考にする。

Docker は KVM を使うので、さきに KVM を有効にする。プロセッサは Intel だから、kvm_intelも有効にする。

takatoh@apostrophe:~$ modprobe kvm
takatoh@apostrophe:~$ modprobe kvm_intel

何も出力がないのはうまく行ったってことか。もし失敗したら kvm-ok コマンドで確認しろと書いてあるのでやってみる。

akatoh@apostrophe:~$ kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used

大丈夫らしい。kvm モジュールが有効になってるかどうかは、つぎのようにしてチェックできる。

takatoh@apostrophe:~$ lsmod | grep kvm
kvm_intel 487424 0
kvm 1404928 1 kvm_intel
irqbypass 12288 1 kvm

これも大丈夫そうだ。

KVM デバイス(/dev/kvm)に対するアクセス権をつける。

takatoh@apostrophe:~$ sudo usermod -aG kvm $USER

さて、ここまではどのディストリビューションでも同様だけど、ここからは Ubuntu 特有の部分。

Docker の apt リポジトリを登録するが、その前に下準備。

takatoh@apostrophe:~$ sudo apt-get update
takatoh@apostrophe:~$ sudo apt-get install ca-certificates curl
takatoh@apostrophe:~$ sudo install -m 0755 -d /etc/apt/keyrings
takatoh@apostrophe:~$ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
takatoh@apostrophe:~$ sudo chmod a+r /etc/apt/keyrings/docker.asc

で、リポジトリを登録。

takatoh@apostrophe:~$ echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

登録したら、apt-get update

takatoh@apostrophe:~$ sudo apt-get update
ヒット:1 http://ftp.tsukuba.wide.ad.jp/Linux/ubuntu noble InRelease
無視:2 http://ftp.jaist.ac.jp/pub/Linux/linuxmint/packages wilma InRelease
ヒット:3 http://ftp.tsukuba.wide.ad.jp/Linux/ubuntu noble-updates InRelease
ヒット:4 http://ftp.tsukuba.wide.ad.jp/Linux/ubuntu noble-backports InRelease
ヒット:5 http://ftp.jaist.ac.jp/pub/Linux/linuxmint/packages wilma Release
ヒット:6 https://packages.microsoft.com/repos/code stable InRelease
無視:8 https://download.docker.com/linux/ubuntu wilma InRelease
ヒット:9 http://security.ubuntu.com/ubuntu noble-security InRelease
エラー:10 https://download.docker.com/linux/ubuntu wilma Release
404 Not Found [IP: 2600:9000:26a6:a600:3:db06:4200:93a1 443]
パッケージリストを読み込んでいます... 完了
E: リポジトリ https://download.docker.com/linux/ubuntu wilma Release には Release ファイルがありません。
N: このようなリポジトリから更新を安全に行うことができないので、デフォルトでは更新が無効になっています。
N: リポジトリの作成とユーザ設定の詳細は、apt-secure(8) man ページを参照してください。

何やらエラーになった。リポジトリがおかしいようだけど……よく見ると、wilma とある。wilma は Linux Mint 22 のコードネームで、Ubuntu 24.04 は noble のはずだ。/etc/os-release ファイルがつぎのようになっている。

takatoh@apostrophe:~$ cat /etc/os-release
NAME="Linux Mint"
VERSION="22 (Wilma)"
ID=linuxmint
ID_LIKE="ubuntu debian"
PRETTY_NAME="Linux Mint 22"
VERSION_ID="22"
HOME_URL="https://www.linuxmint.com/"
SUPPORT_URL="https://forums.linuxmint.com/"
BUG_REPORT_URL="http://linuxmint-troubleshooting-guide.readthedocs.io/en/latest/"
PRIVACY_POLICY_URL="https://www.linuxmint.com/"
VERSION_CODENAME=wilma
UBUNTU_CODENAME=noble

つまり、リポジトリを登録するときに、$VERSION_CODENAME じゃなくて $UBUNTU_CODENAME を参照する必要があったってこと。今回はやり直す代わりに、直接ファイルを編集した。

takatoh@apostrophe:~$ sudo vim /etc/apt/sources.list.d/docker.list

今度は大丈夫のはず。

takatoh@apostrophe:~$ sudo apt-get update
ヒット:1 http://ftp.tsukuba.wide.ad.jp/Linux/ubuntu noble InRelease
ヒット:2 http://ftp.tsukuba.wide.ad.jp/Linux/ubuntu noble-updates InRelease
無視:3 http://ftp.jaist.ac.jp/pub/Linux/linuxmint/packages wilma InRelease
ヒット:4 http://ftp.tsukuba.wide.ad.jp/Linux/ubuntu noble-backports InRelease
ヒット:5 http://ftp.jaist.ac.jp/pub/Linux/linuxmint/packages wilma Release
取得:6 https://download.docker.com/linux/ubuntu noble InRelease [48.8 kB]
ヒット:7 https://packages.microsoft.com/repos/code stable InRelease
ヒット:8 http://security.ubuntu.com/ubuntu noble-security InRelease
取得:10 https://download.docker.com/linux/ubuntu noble/stable amd64 Packages [15.3 kB]
64.2 kB を 2秒 で取得しました (39.8 kB/s)
パッケージリストを読み込んでいます... 完了

あとは Docker (とその関連パッケージ)を apt-get install でインストール。

takatoh@apostrophe:~$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています... 完了
状態情報を読み取っています... 完了
以下の追加パッケージがインストールされます:
docker-ce-rootless-extras libslirp0 pigz slirp4netns
提案パッケージ:
aufs-tools cgroupfs-mount | cgroup-lite
以下のパッケージが新たにインストールされます:
containerd.io docker-buildx-plugin docker-ce docker-ce-cli
docker-ce-rootless-extras docker-compose-plugin libslirp0 pigz slirp4netns
アップグレード: 0 個、新規インストール: 9 個、削除: 0 個、保留: 55 個。
123 MB のアーカイブを取得する必要があります。
この操作後に追加で 441 MB のディスク容量が消費されます。
続行しますか? [Y/n] y
取得:1 http://ftp.tsukuba.wide.ad.jp/Linux/ubuntu noble/universe amd64 pigz amd64 2.8-1 [65.6 kB]
取得:2 http://ftp.tsukuba.wide.ad.jp/Linux/ubuntu noble/main amd64 libslirp0 amd64 4.7.0-1ubuntu3 [63.8 kB]
取得:3 http://ftp.tsukuba.wide.ad.jp/Linux/ubuntu noble/universe amd64 slirp4netns amd64 1.2.1-1build2 [34.9 kB]
取得:4 https://download.docker.com/linux/ubuntu noble/stable amd64 containerd.io amd64 1.7.22-1 [29.5 MB]
取得:5 https://download.docker.com/linux/ubuntu noble/stable amd64 docker-buildx-plugin amd64 0.17.1-1~ubuntu.24.04~noble [30.3 MB]
取得:6 https://download.docker.com/linux/ubuntu noble/stable amd64 docker-ce-cli amd64 5:27.3.1-1~ubuntu.24.04~noble [15.0 MB]
取得:7 https://download.docker.com/linux/ubuntu noble/stable amd64 docker-ce amd64 5:27.3.1-1~ubuntu.24.04~noble [25.6 MB]
取得:8 https://download.docker.com/linux/ubuntu noble/stable amd64 docker-ce-rootless-extras amd64 5:27.3.1-1~ubuntu.24.04~noble [9,588 kB]
取得:9 https://download.docker.com/linux/ubuntu noble/stable amd64 docker-compose-plugin amd64 2.29.7-1~ubuntu.24.04~noble [12.7 MB]
123 MB を 2秒 で取得しました (70.1 MB/s)
以前に未選択のパッケージ pigz を選択しています。
(データベースを読み込んでいます ... 現在 545486 個のファイルとディレクトリがインストールされています。)
.../0-pigz_2.8-1_amd64.deb を展開する準備をしています ...
pigz (2.8-1) を展開しています...
以前に未選択のパッケージ containerd.io を選択しています。
.../1-containerd.io_1.7.22-1_amd64.deb を展開する準備をしています ...
containerd.io (1.7.22-1) を展開しています...
以前に未選択のパッケージ docker-buildx-plugin を選択しています。
.../2-docker-buildx-plugin_0.17.1-1~ubuntu.24.04~noble_amd64.deb を展開する準備をしています ...
docker-buildx-plugin (0.17.1-1~ubuntu.24.04~noble) を展開しています...
以前に未選択のパッケージ docker-ce-cli を選択しています。
.../3-docker-ce-cli_5%3a27.3.1-1~ubuntu.24.04~noble_amd64.deb を展開する準備をしています ...
docker-ce-cli (5:27.3.1-1~ubuntu.24.04~noble) を展開しています...
以前に未選択のパッケージ docker-ce を選択しています。
.../4-docker-ce_5%3a27.3.1-1~ubuntu.24.04~noble_amd64.deb を展開する準備をしています ...
docker-ce (5:27.3.1-1~ubuntu.24.04~noble) を展開しています...
以前に未選択のパッケージ docker-ce-rootless-extras を選択しています。
.../5-docker-ce-rootless-extras_5%3a27.3.1-1~ubuntu.24.04~noble_amd64.deb を展開する準備をしています ...
docker-ce-rootless-extras (5:27.3.1-1~ubuntu.24.04~noble) を展開しています...
以前に未選択のパッケージ docker-compose-plugin を選択しています。
.../6-docker-compose-plugin_2.29.7-1~ubuntu.24.04~noble_amd64.deb を展開する準備をしています ...
docker-compose-plugin (2.29.7-1~ubuntu.24.04~noble) を展開しています...
以前に未選択のパッケージ libslirp0:amd64 を選択しています。
.../7-libslirp0_4.7.0-1ubuntu3_amd64.deb を展開する準備をしています ...
libslirp0:amd64 (4.7.0-1ubuntu3) を展開しています...
以前に未選択のパッケージ slirp4netns を選択しています。
.../8-slirp4netns_1.2.1-1build2_amd64.deb を展開する準備をしています ...
slirp4netns (1.2.1-1build2) を展開しています...
docker-buildx-plugin (0.17.1-1~ubuntu.24.04~noble) を設定しています ...
containerd.io (1.7.22-1) を設定しています ...
docker-compose-plugin (2.29.7-1~ubuntu.24.04~noble) を設定しています ...
docker-ce-cli (5:27.3.1-1~ubuntu.24.04~noble) を設定しています ...
libslirp0:amd64 (4.7.0-1ubuntu3) を設定しています ...
pigz (2.8-1) を設定しています ...
docker-ce-rootless-extras (5:27.3.1-1~ubuntu.24.04~noble) を設定しています ...
slirp4netns (1.2.1-1build2) を設定しています ...
docker-ce (5:27.3.1-1~ubuntu.24.04~noble) を設定しています ...
man-db (2.12.0-4build2) のトリガを処理しています ...
libc-bin (2.39-0ubuntu8.3) のトリガを処理しています ...

これで完了。hello-world イメージを実行してみる。

takatoh@apostrophe:~$ sudo docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/

問題なさそうだ。

バージョンの確認をしておく。つぎの通り。

takatoh@apostrophe:~$ docker --version
Docker version 27.3.1, build ce12230
takatoh@apostrophe:~$ docker version
Client: Docker Engine - Community
Version: 27.3.1
API version: 1.47
Go version: go1.22.7
Git commit: ce12230
Built: Fri Sep 20 11:40:59 2024
OS/Arch: linux/amd64
Context: default

Server: Docker Engine - Community
Engine:
Version: 27.3.1
API version: 1.47 (minimum version 1.24)
Go version: go1.22.7
Git commit: 41ca978
Built: Fri Sep 20 11:40:59 2024
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.7.22
GitCommit: 7f7fdf5fed64eb6a7caf99b3e12efcf9d60e311c
runc:
Version: 1.1.14
GitCommit: v1.1.14-0-g2c9f560
docker-init:
Version: 0.19.0
GitCommit: de40ad0
takatoh@apostrophe:~$ docker compose version
Docker Compose version v2.29.7

mise で Ruby をインストール

Ruby をインストールするにはライブラリが足りなかったらしい。

ruby-build のページを参考に、つぎのようにライブラリをインストールしたら、Ruby のインストールもできた。

takatoh@apostrophe:~$ sudo apt install libz-dev libffi-dev libreadline-dev libssl-dev libyaml-dev

ただし、ruby-build のページではつぎのように紹介されている。上の実行例に出てこないパッケージは、いろいろ試行錯誤してたらからいつの間にかインストールされていたのかもしれない。

apt-get install autoconf patch build-essential rustc libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev libdb-dev uuid-dev

ともあれ、これで準備は整った。

takatoh@apostrophe:~$ mise use [email protected]
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 19.6M 100 19.6M 0 0 45.2M 0 --:--:-- --:--:-- --:--:-- 45.2M
mise [email protected] ✓ installed mise ~/.mise.toml tools: [email protected]

OK。つぎのようにして Ruby がインストールされているのを確認できる。

takatoh@apostrophe:~$ mise ls
Tool Version Config Source Requested
node 20.17.0 ~/.mise.toml 20
python 3.11.10 ~/.mise.toml 3.11
ruby 3.2.5 ~/.mise.toml 3.2
takatoh@apostrophe:~$ ruby --version
ruby 3.2.5 (2024-07-26 revision 31d0f1a2e7) [x86_64-linux]

Linux Mint で mise を使う

OS のインストールが済んだら次はツール、というかプログラミング言語のインストールだ。よく使う Python や Ruby なんかをインストールする。

で、今までは pyenv とか rbenv を使ってたんだけど、最近見つけた mise (ミース)を使ってみようと思った。

mise は Rust 製のツールなので、まずは Rust をインストールする。公式サイトの手順に従えばいい。rustup という Rust のツールチェインを管理するツールをまずインストールする。

takatoh@apostrophe:~$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
info: downloading installer

Welcome to Rust!

This will download and install the official compiler for the Rust
programming language, and its package manager, Cargo.

Rustup metadata and toolchains will be installed into the Rustup
home directory, located at:

/home/takatoh/.rustup

This can be modified with the RUSTUP_HOME environment variable.

The Cargo home directory is located at:

/home/takatoh/.cargo

This can be modified with the CARGO_HOME environment variable.

The cargo, rustc, rustup and other commands will be added to
Cargo's bin directory, located at:

/home/takatoh/.cargo/bin

This path will then be added to your PATH environment variable by
modifying the profile files located at:

/home/takatoh/.profile
/home/takatoh/.bashrc

You can uninstall at any time with rustup self uninstall and
these changes will be reverted.

Current installation options:


default host triple: x86_64-unknown-linux-gnu
default toolchain: stable (default)
profile: default
modify PATH variable: yes

1) Proceed with standard installation (default - just press enter)
2) Customize installation
3) Cancel installation
>

info: profile set to 'default'
info: default host triple is x86_64-unknown-linux-gnu
info: syncing channel updates for 'stable-x86_64-unknown-linux-gnu'
info: latest update on 2024-09-05, rust version 1.81.0 (eeb90cda1 2024-09-04)
info: downloading component 'cargo'
info: downloading component 'clippy'
info: downloading component 'rust-docs'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: downloading component 'rustfmt'
info: installing component 'cargo'
info: installing component 'clippy'
info: installing component 'rust-docs'
15.9 MiB / 15.9 MiB (100 %) 10.1 MiB/s in 1s ETA: 0s
info: installing component 'rust-std'
26.8 MiB / 26.8 MiB (100 %) 13.5 MiB/s in 2s ETA: 0s
info: installing component 'rustc'
66.9 MiB / 66.9 MiB (100 %) 14.7 MiB/s in 4s ETA: 0s
info: installing component 'rustfmt'
info: default toolchain set to 'stable-x86_64-unknown-linux-gnu'

stable-x86_64-unknown-linux-gnu installed - rustc 1.81.0 (eeb90cda1 2024-09-04)


Rust is installed now. Great!

To get started you may need to restart your current shell.
This would reload your PATH environment variable to include
Cargo's bin directory ($HOME/.cargo/bin).

To configure your current shell, you need to source
the corresponding env file under $HOME/.cargo.

This is usually done by running one of the following (note the leading DOT):
. "$HOME/.cargo/env" # For sh/bash/zsh/ash/dash/pdksh
source "$HOME/.cargo/env.fish" # For fish

Rust に関するツールはすべて ~/.cargo/bin にインストールされる。一旦シェルを終了して起動し直すと(あるいは新しいシェルを起動すると)、cargo コマンドが使えるようになる。

takatoh@apostrophe:~$ cargo --version
cargo 1.81.0 (2dbb1af80 2024-08-20)

rustup show コマンドでツールチェインを確認できる。

takatoh@apostrophe:~$ rustup show
Default host: x86_64-unknown-linux-gnu
rustup home: /home/takatoh/.rustup

stable-x86_64-unknown-linux-gnu (default)
rustc 1.81.0 (eeb90cda1 2024-09-04)

mise のインストールには、mise の公式ドキュメントに従ってインストール用のシェルスクリプトを使う。

takatoh@apostrophe:~$ curl https://mise.run | sh
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 6300 100 6300 0 0 50786 0 --:--:-- --:--:-- --:--:-- 50806
mise: installing mise...
######################################################################## 100.0%
mise: installed successfully to /home/takatoh/.local/bin/mise
mise: run the following to activate mise in your shell:
echo "eval \"\$(/home/takatoh/.local/bin/mise activate bash)\"" >> ~/.bashrc

mise: this must be run in order to use mise in the terminal
mise: run `mise doctor` to verify this is setup correctly

これでインストールできた。

takatoh@apostrophe:~$ ~/.local/bin/mise --version
2024.9.5 linux-x64 (1f0f03e 2024-09-17)

インストールはできたが、アクティベートする必要がある。そのためのコードを .bashrc ファイルに書き込む。

takatoh@apostrophe:~$ echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc

それから環境変数 PATH の設定……なのだけど、アクティベートすると設定しくれるらしい。もう一度シェルを起動し直す。

takatoh@apostrophe:~$ mise --version
2024.9.5 linux-x64 (1f0f03e 2024-09-17)

上の通り、フルパスを指定しなくても mise コマンドが使える。

さて、ここまで来たらプログラミング言語のインストールができる。ドキュメントに従って mise use コマンドで Node.js をインストールしてみる。

takatoh@apostrophe:~$ mise use node@20
mise [email protected] ✓ installed mise ~/.mise.toml tools: [email protected]
takatoh@apostrophe:~$ node --version
v20.17.0

うまく行った!

つぎは Python。

takatoh@apostrophe:~$ mise use [email protected]
mise hint use multiple versions simultaneously with mise use [email protected] [email protected]
mise hint disable this hint with mise settings set disable_hints python_multi or all with mise settings set disable_hints "*"
mise hint installing precompiled python from indygreg/python-build-standalone
if you experience issues with this python (e.g.: running poetry), switch to python-build by running mise settings set python_compile 1
mise hint disable this hint with mise settings set disable_hints python_precompiled or all with mise settings set disable_hints "*"
mise [email protected] ✓ installed mise ~/.mise.toml tools: [email protected]
takatoh@apostrophe:~$ python --version
Python 3.11.10

Ruby はどうか。

takatoh@apostrophe:~$ mise use [email protected]
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 14.6M 100 14.6M 0 0 75.4M 0 --:--:-- --:--:-- --:--:-- 75.6M
BUILD FAILED (Linuxmint 22 on x86_64 using ruby-build 20240917)
You can inspect the build directory at /tmp/ruby-build.20240918205818.20859.tDse9P
See the full build log at /tmp/ruby-build.20240918205818.20859.log
mise ~/.cache/mise/ruby/ruby-build/bin/ruby-build failed
==> Downloading openssl-3.0.15.tar.gz...
-> curl -q -fL -o openssl-3.0.15.tar.gz https://dqw8nmjcqpjn7.cloudfront.net/23c666d0edf20f14249b3d8f0368acaee9ab585b09e1de82107c66e1f3ec9533
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 14.6M 100 14.6M 0 0 75.4M 0 --:--:-- --:--:-- --:--:-- 75.6M
==> Installing openssl-3.0.15...
-> ./config "--prefix=$HOME/.local/share/mise/installs/ruby/3.2.5/openssl" "--openssldir=$HOME/.local/share/mise/installs/ruby/3.2.5/openssl/ssl" zlib-dynamic no-ssl3 shared
-> make -j 6

BUILD FAILED (Linuxmint 22 on x86_64 using ruby-build 20240917)

You can inspect the build directory at /tmp/ruby-build.20240918205818.20859.tDse9P
See the full build log at /tmp/ruby-build.20240918205818.20859.log
mise ~/.cache/mise/ruby/ruby-build/bin/ruby-build exited with non-zero status: exit code 1
mise Run with --verbose or MISE_VERBOSE=1 for more information

あれ、なんか失敗したぞ?

でも ruby コマンドは使える。どういうこと?

takatoh@apostrophe:~$ ruby --version
ruby 3.2.3 (2024-01-18 revision 52bb2ac0a6) [x86_64-linux-gnu]

というわけで、今日はここまで。

[追記]

rubymise でインストールされたものじゃなく、もとから入ってるものだった。

takatoh@apostrophe:~$ which ruby
/usr/bin/ruby

これに対して、mise でインストールされた pythonnode は ~/.local/share/mise/installs/ 以下にある。

takatoh@apostrophe:~$ which python
/home/takatoh/.local/share/mise/installs/python/3.11/bin/python
takatoh@apostrophe:~$ which node
/home/takatoh/.local/share/mise/installs/node/20/bin/node

Linux Mint を試す

メインに使っている Linux マシンは Ubuntu 20.04 LTS で、その Ubuntu は今年の春に長期サポート版である Ubuntu 24.04 LTS がリリースされたわけだけど、これまで通り日本語REMIX版がリリースされるのを待っていた。ところが、今回からは日本語REMIX版はリリースされないことになったという。

そんなわけで、時期を逸してしまっていたんだけど、Ubuntu をベースにした Linux Mint を試してみることにした。

公式サイトから Linux Mint 22 Cinnamon Edition の ISOイメージをダウンロードして USB メモリのインストールメディアを用意した。

USB メモリからブートすると、Linux Mint が起動する。GUI 画面左上に「Install Linux Mint」アイコンがあるので、これをダブルクリックするとインストーラが起動する。あとはインストーラの案内の通りに進めるだけなので特に難しいことはない。

これでしばらく使ってみよう。

※時間ができたらもう少し書き足す。

Apacheのmod_rewriteとBフラグ

PHP のプログラムを Docker コンテナで動かそうとして、Apache の mod_rewrite の設定でちょっと苦労したのでメモ。

主題は PHP じゃないので、プログラムは簡単な例にしておく。クエリーパラメータ name を受け取って、挨拶を返すだけのプログラムだ。

<?php
$name = $_GET["name"];
echo "Hello, " . $name . "!" . PHP_EOL;
?>

Docker イメージは php:8.2-apache を使う。

takatoh@apostrophe:myphpapp$ docker run -d --rm -p 80:80 -v .:/var/www/html php:8.2-apache

これで localhost:80 で待ち受けているはず。curl コマンドで試してみる。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello.php?name=Andy
Hello, Andy!

期待通りだ。

つぎに、PHP プログラムに渡すパラメータ name を、クエリーパラメータではなく、パスの一部にしたい。つまり、http://localhost/hello/Andy という URL でアクセスしたい。

そこで、つぎのような .htaccess ファイルを書いた。

RewriteEngine on
RewriteRule ^hello/(.*)\$ hello.php?name=\$1

それから、Apache で上の .htaccess が有効になるように Dockerfile を書いた。デフォルトでは mod_rewrite 自体が有効になってないんだそうだ。

FROM php:8.2-apache
RUN a2enmod rewrite
RUN sed -ri -e 's!AllowOverride None!AllowOverride FileInfo!g' /etc/apache2/apache2.conf

mod_rewrite を有効にして、なおかつ、.htaccess ファイルでルールを設定するのを許可している。これを元に Docker イメージをビルド。

takatoh@apostrophe:myphpapp$ docker build -t myphpapp:1 .

コンテナを起動する。

takatoh@apostrophe:myphpapp$ docker run -d --rm -p 80:80 -v .:/var/www/html myphpapp:1

さっきと同じように curl で試してみよう。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello/Andy
Hello, Andy!

うまくいった!もちろんクエリーパラーメータで渡すのでも期待通りに動く。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello.php?name=Andy
Hello, Andy!

ところが、パス形式のパラーメータ部分(Andy の部分)に空白文字を使うとうまく動いてくれない(curl で URLエンコードする方法がわからなかったので、空白文字の代わりにエンコードした %20 を使っている)。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello/Andy%20Weir
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
<hr>
<address>Apache/2.4.57 (Debian) Server at localhost Port 80</address>
</body></html>

クエリーパラメータ形式では大丈夫。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello.php?name=Andy%20Weir
Hello, Andy Weir!

ブラウザでも試したけど同じ結果。さて、困った。

結論を先に書くと、RewiterRule の最後に B フラグをつければ解決する。Apache の公式ドキュメントにはちゃんと書いてある。が、参考にした日本語の解説ページの中には書いてあるページはなかった。探し方が悪いのかもしれないけど。

ともかく、.htaccess ファイルをつぎのように書き換えた。

RewriteEngine on
RewriteRule ^hello/(.*)\$ hello.php?name=\$1 [B]

再度試してみる。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello/Andy%20Weir
Hello, Andy Weir!

OK!!!

一件落着。

後々のために、Apache のドキュメントから解説を引用しておこう(DeepL 翻訳)。

[B] フラグは、変換を適用する前に英数字以外の文字をエスケープするよう RewriteRule に指示します。

mod_rewrite は URL をマッピングする前にエスケープを解除しなければならないので、 後方参照は適用される時点でエスケープされません。B フラグを使うと、後方参照中の英数字以外の文字がエスケープされます

‘x & y/z’という検索語が与えられた場合、ブラウザはそれを’x%20%26%20y%2Fz’としてエンコードし、リクエストを’search/x%20%26%20y%2Fz’とします。Bフラグがない場合、このリライトルールは’search.php?term=x & y/z’にマップされますが、これは有効なURLではないため、search.php?term=x%20&y%2Fz=としてエンコードされ、意図されたものではありません。

この同じルールにBフラグを設定すると、パラメータは出力URLに渡される前に再エンコードされ、結果として/search.php?term=x%20%26%20%y%2Fzに正しくマッピングされます

Flask-Migrateでハマったけどなんとかなった話

なんとかはなったけど、イマイチ理解はできてない話でもある。

自作の書籍管理WEBアプリに、保管場所を表す BookShelf モデルを追加して、データベース(SQLite3)のアップグレードを実行したところでハマった。

アップグレード前のデータベースには、4つのテーブルがあった。( )内はモデルのクラス名。

  • books (Book)
  • categories (Category)
  • formats (Format)
  • coverarts (CoverArt)

ここに保管場所を記録するための bookshelves テーブルを追加したい。モデルクラスは BookShelf。models.py ファイルにこんなふうに定義した。

class BookShelf(db.Model):
    __tablename__ = 'bookshelves'
    id           = db.Column(db.Integer, primary_key=True)
    name         = db.Column(db.String)
    description  = db.Column(db.String)
    books        = db.relationship('Book', backref='bookshelf', lazy='dynamic')

    def __repr__(self):
        return f'<BookShelf id={self.id} name={self.name}>'

    def to_dictionary(self):
        return {
            'id':          self.id,
            'name':        self.name,
            'description': self.description
        }

app.py ファイルはこうなってる(変更なし)。最後の import views は、ルーティングと対応する処理を定義したモジュールをインポートしている。

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate


app = Flask(__name__)
app.config.from_pyfile('./bruschetta.conf')

db = SQLAlchemy(app)

import models

migrate = Migrate(app, db)

import views

この状態で flask db migrate を実行すれば、データベースをマイグレートするスクリプトを生成してくれる。実行するとこうなった(poetry をパッケージ管理に使ってるので poetry 経由だ)。

takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run flask db migrate -m "create bookshelves table"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.

migrations/versions 以下にマイグレート用のスクリプトができるはずだけどできていない。

takatoh@apostrophe:bruschetta-feature-bookshelf$ ls migrations/versions
0bff8cd02ef3_create_tables.py           __pycache__
981c2baab82e_create_coverarts_table.py

もとからあった2つしかない。おかしいと思いつつも flask db upgrade を実行してみたけど、当然のようにエラーになった。

takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade 0bff8cd02ef3 -> 981c2baab82e, Create coverarts table
Traceback (most recent call last):
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
(以下略)

いや、待て。マイグレート用のスクリプトが作られてないなら、データベースもアップグレードしないんだから、エラーもなく、何もせずに終了するはずでは?

そこで、データベースの中を覗いてみることにした。

takatoh@apostrophe:bruschetta-feature-bookshelf$ sqlite3 instance/bruschetta.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> .tables
alembic_version  categories       formats        
books            coverarts

テーブルが5つある。bookscategoriesformats は一番始めに作ったテーブル、coverarts はあとから追加したテーブル、そして alembic_version は Flask-Migrate が内部で使っている alembic というデータベースマイグレーション用のライブラリが利用するテーブルだ。

alembic_version に何が格納されているか見てみる(その前にヘッダを表示するようにしておく)。

sqlite> .headers on
sqlite> SELECT * FROM alembic_version;
version_num
0bff8cd02ef3

version_num というカラムが1つだけあって、0bff8cd02ef3 という値(文字列)が格納されている。この値はマイグレーション用スクリプトの識別子で、ファイル名の頭に付いているほか、ファイルの中にも Revision ID の値として書き込まれている。

この 0bff8cd02ef3 という値は、過去2回のマイグレーションのうちの最初の方の値だ。2回目のマイグレーション(coverarts テーブルを追加したとき)にこの値も書き換えられるはず、だと思うんだけどどういうわけかそうならなかったんじゃないか。だとすれば、これを書き換えてやればいいのかも。

sqlite> UPDATE alembic_version SET version_num="981c2baab82e" WHERE version_num="0bff8cd02ef3";
sqlite> SELECT * FROM alembic_version;
version_num
981c2baab82e

これで書き換わった。データベースを抜けて、flask db migrate を改めて実行する。

takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run flask db migrate -m "create bookshelves table"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'bookshelves'
INFO  [alembic.autogenerate.compare] Detected added column 'books.bookshelf_id'
INFO  [alembic.autogenerate.compare] Detected added foreign key (bookshelf_id)(id) on table books
INFO  [alembic.autogenerate.compare] Detected added foreign key (coverart_id)(id) on table books
  Generating /home/takatoh/w/Bruschetta/bruschetta-feature-
  bookshelf/migrations/versions/3ccfddd40b69_create_bookshelves_table.py ...  done

おお、今度はうまくいったっぽい。実際、migrations/versions 以下に新しいファイルが生成されている。3ccfddd40b69_create_bookshelves_table.py がそれだ。

takatoh@apostrophe:bruschetta-feature-bookshelf$ ls migrations/versions
0bff8cd02ef3_create_tables.py
3ccfddd40b69_create_bookshelves_table.py
981c2baab82e_create_coverarts_table.py
__pycache__

では、flask db upgrade を実行してみよう。

takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade 981c2baab82e -> 3ccfddd40b69, create bookshelves table
Traceback (most recent call last):
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/bin/flask", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/flask/cli.py", line 1064, in main
    cli.main()
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/decorators.py", line 33, in new_func
    return f(get_current_context(), *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/flask/cli.py", line 358, in decorator
    return __ctx.invoke(f, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/flask_migrate/cli.py", line 150, in upgrade
    _upgrade(directory, revision, sql, tag, x_arg)
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/flask_migrate/__init__.py", line 111, in wrapped
    f(*args, **kwargs)
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/flask_migrate/__init__.py", line 200, in upgrade
    command.upgrade(config, revision, sql=sql, tag=tag)
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/command.py", line 403, in upgrade
    script.run_env()
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/script/base.py", line 583, in run_env
    util.load_python_file(self.dir, "env.py")
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/util/pyfiles.py", line 95, in load_python_file
    module = load_module_py(module_id, path)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/util/pyfiles.py", line 113, in load_module_py
    spec.loader.exec_module(module)  # type: ignore
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap_external>", line 940, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/migrations/env.py", line 96, in <module>
    run_migrations_online()
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/migrations/env.py", line 90, in run_migrations_online
    context.run_migrations()
  File "<string>", line 8, in run_migrations
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/runtime/environment.py", line 948, in run_migrations
    self.get_context().run_migrations(**kw)
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/runtime/migration.py", line 627, in run_migrations
    step.migration_fn(**kw)
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/migrations/versions/3ccfddd40b69_create_bookshelves_table.py", line 27, in upgrade
    with op.batch_alter_table('books', schema=None) as batch_op:
  File "/home/takatoh/.pyenv/versions/3.11.7/lib/python3.11/contextlib.py", line 144, in __exit__
    next(self.gen)
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/operations/base.py", line 398, in batch_alter_table
    impl.flush()
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/operations/batch.py", line 162, in flush
    fn(*arg, **kw)
  File "/home/takatoh/w/Bruschetta/bruschetta-feature-bookshelf/.venv/lib/python3.11/site-packages/alembic/operations/batch.py", line 669, in add_constraint
    raise ValueError("Constraint must have a name")
ValueError: Constraint must have a name

おおう、またエラーだ。”Constrain must have a name” (制約には名前が必要)だと。ググってみると StackOverflow にズバリの回答を見つけた。

曰く、これは正常な動作で、なぜなら SQLite3 は ALTER table をサポートしてないから。解決法としては、SQLAlchemy のメタデータにすべてのタイプの制約の命名テンプレートを作成するのがいい(今後のためにも)、という。

そういうわけで、app.py をつぎのように書き換えた。

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from sqlalchemy import MetaData


app = Flask(__name__)
app.config.from_pyfile('./bruschetta.conf')

convention = {
    'ix': 'ix_%(column_0_label)s',
    'uq': 'uq_%(table_name)s_%(column_0_name)s',
    'ck': 'ck_%(table_name)s_%(constraint_name)s',
    'fk': 'fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s',
    'pk': 'pk_%(table_name)s'
}
metadata = MetaData(naming_convention=convention)
db = SQLAlchemy(app, metadata=metadata)

import models

migrate = Migrate(app, db)

import views

そしてさっきのマイグレーションスクリプトを削除して、やり直す。

takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run flask db migrate -m "create bookshelves table"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added column 'books.bookshelf_id'
INFO  [alembic.autogenerate.compare] Detected added foreign key (bookshelf_id)(id) on table books
INFO  [alembic.autogenerate.compare] Detected added foreign key (coverart_id)(id) on table books
  Generating /home/takatoh/w/Bruschetta/bruschetta-feature-
  bookshelf/migrations/versions/1821fe006433_create_bookshelves_table.py ...  done
takatoh@apostrophe:bruschetta-feature-bookshelf$ poetry run flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade 981c2baab82e -> 1821fe006433, create bookshelves table

今度はちゃんとデータベースをアップグレードできたようだ。データベースの中も見てみる。

takatoh@apostrophe:bruschetta-feature-bookshelf$ sqlite3 instance/bruschetta.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> .tables
alembic_version  bookshelves      coverarts      
books            categories       formats        
sqlite> .headers on
sqlite> SELECT * FROM alembic_version;
version_num
1821fe006433

bookshelves テーブルもできているし、alembic_version テーブルも更新されている。

これでやっと、WEBアプリの機能追加にかかれる。

Ruby:HTTPクライアントライブラリを http gem にのりかえた

すごーく久しぶりに、Ruby で HTTP アクセスするってことをやった。使い慣れた httpclient を使ったんだけど、こんなエラーが出た。

C:/Ruby32-x64/lib/ruby/gems/3.2.0/gems/httpclient-2.8.3/lib/httpclient/ssl_socket.rb:103:in `connect': SSL_connect returned=1 errno=0 peeraddr=[2606:4700:3032::ac43:8791]:443 state=error: certificate verify failed (certificate has expired) (OpenSSL::SSL::SSLError)
(以下略)

SSLのエラーだ。

ググって調べてみると、httpclient gem は信頼できる証明書を独自に持っていてそれがもうメンテされていない、ということが解った。

上の記事では、システムのデフォルトの証明書を利用することで回避する方法も書かれている。けど、RubyGems.org をみると httpclient gem の最後のリリースは 2016/9 で、これはもう他のライブラリにのりかえるべきだろう。

で、さらにググって調べたところ、http という gem の評判がよさげに見えたのでこれを使ってみることにした。こんな名前よく空いてたな。

使い方は GitHub の Wiki にまとまっている。

httpclient みたいにインスタンスを作る必要がなくて 、require "http" したら HTTP モジュールの関数が使える。例えば GET の場合はこう。

irb(main):001:0> require "http"
=> true
irb(main):002:0> res = HTTP.get("https://blog.panicblanket.com")
=> #<HTTP::Response/1.1 200 OK {"Date"=>"Sat, 06 Jan 2024 20:51:15 GMT", "Content-Type"=>"text/...

レスポンスのコードは #code 、ボディは #to_s で取得できる。

irb(main):003:0> res.code
=> 200
irb(main):004:0> res.to_s
=> "<!DOCTYPE html>\n\n<html lang=\"ja\">\n\n\t<head>\n\n\t\t<meta http-equiv=\"content-type\" content=\"text/html\" charset=\"UTF-8\" />\n\t\t<meta 
(以下略)

こんな感じ。

GETPOSTPUTDELETE などの HTTP メソッドの他にも WEBDAV なんかもサポートしてる。使いやすそうだ。

Dockerコンテナ上のMediaWikiをアップグレードする

つい先日(4日前)に最新版のDocker公式のコンテナイメージ 1.41.0 がリリースされているのを見つけたので、ローカルネットワークで運用している MediaWiki をアップグレードした。これまでのバージョンは 1.35.0。

まずは、データベースのバックアップをとる。データベースもMariaDBのDockerコンテナだ。

takatoh@unclemeat:~$ docker exec -it mywiki-db mysqldump -hlocalhost -P3306 -uroot -prootpass mywiki > mywiki.sql

mywiki-db がデータベースのコンテナ。このコンテナの中で mysqldump コマンドを実行して、mywiki.sql ファイルに書き込んでいる。mysqldump-h オプションはホスト、-P はポート、-u はユーザ、-p はパスワード。-p と引数のあいだに空白を入れるとどういうわけかうまく行かない。

バックアップをとったら、いったんコンテナを止める。

takatoh@unclemeat:~$ cd docker-configuration
takatoh@unclemeat:~/docker-configuration$ docker compose down

docker-compose.yml ファイルを書き換える。MediaWiki イメージのバージョンを1.35.0から1.41.0に書き換えるだけだ。

version: "3"

services:

  mywiki:
    image: mediawiki:1.41.0
    container_name: mywiki
    restart: always
    depends_on:
      - mywiki-db
    volumes:
      - "${CONTAINER_ENV_DIR}/mywiki/images:/var/www/html/images"
      - "${CONTAINER_ENV_DIR}/mywiki/LocalSettings.php:/var/www/html/LocalSettings.php"
      - "${CONTAINER_ENV_DIR}/mywiki/php.ini-production:/usr/local/etc/php/php.ini"
      - "${CONTAINER_ENV_DIR}/mywiki/extensions/YouTube:/var/www/html/extensions/YouTube"
    ports:
      - 9090:80

  mywiki-db:
    image: mariadb:10.5.6-focal
    container_name: mywiki-db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: mywiki
      MYSQL_USER: wikiuser
      MYSQL_PASSWORD: wikipass
    volumes:
      - "${CONTAINER_ENV_DIR}/mywiki/db:/var/lib/mysql"
    ports:
      - 8836:3306

書き換えが終わったら、コンテナを起動する。

takatoh@unclemeat:~/docker-configuration$ docker compose up -d

問題なく起動したけど、ブラウザでアクセスするとエラーが出ていた。データベースのテーブルにカラムが見つからないらしい。MediaWiki のアップブレードでデータベースのスキーマも更新する必要があるようだ。↓このページが参考になった。

MediaWiki のディレクトリで更新スクリプトを実行すればいいようだ。MediaWiki は Dockerコンテナで動いているので、コンテナの中で実行する。

takatoh@unclemeat:~/docker-configuration$ docker exec -it mywiki bash
root@a423a4fe2c67:/var/www/html# php maintenance/run.php update.php
MediaWiki 1.41.0 Updater

Your composer.lock file is up to date with current dependencies!
Going to run database updates for bsl_wiki
Depending on the size of your database this may take a while!
Abort with control-c in the next five seconds (skip this countdown with --quick) ...0
...collations up-to-date.
(以下略)

無事、更新スクリプトが終了したら、念のためコンテナを再起動する。で、改めてブラウザでアクセスすると、今度はエラーなくページが表示された。

これで完了。

Python:文字列から同じ文字が連続する位置と長さを得る

たとえば、aabbcccdeeee という文字列から、a が0文字目から2文字、b が2文字目から2文字……という情報が欲しい(最初の文字を0文字目とする)。

最初に書いた関数がこれ。

def charpos(s):
    current = None
    result = []
    for i, c in enumerate(s):
        if current is None:
            current = c
            result.append([c, i, 1])
        elif c == current:
            result[-1][2] += 1
        else:
            current = c
            result.append([c, i, 1])
    return result

「文字、位置、長さが入ったリスト」のリストが返ってくる。実行してみるとこうなる。

>>> charpos('aabbcccdeeee')
[['a', 0, 2], ['b', 2, 2], ['c', 4, 3], ['d', 7, 1], ['e', 8, 4]]

期待するものはできた。けど、もっとスマートにいかないものか。分岐の current is Noneelse のコードが重複してるのを整理すればすっきりはするけど、素朴な実装には変わりがない(素朴なのが悪いわけではないとも思うけども)。

あと余談だけど、こういう場合には、外側のリストの要素はリストじゃなくてタプルのほうがいいと思うんだよね。

さて、itertools にある groupby を使ってみた。この関数は iterable なオブジェクトから連続する要素をグループにまとめてくれる(タプルになる)。

>>> import itertools
>>> for g in itertools.groupby('aabbcccdeeee'):
...     print(g)
...
('a', <itertools._grouper object at 0x00000207D778F760>)
('b', <itertools._grouper object at 0x00000207D778F730>)
('c', <itertools._grouper object at 0x00000207D778F760>)
('d', <itertools._grouper object at 0x00000207D778F730>)
('e', <itertools._grouper object at 0x00000207D778F760>)

各タプルの2つ目の要素は itertools._grouper のオブジェクトだけど、リストにしてやると次のようになる。

>>> for g in itertools.groupby('aabbcccdeeee'):
...     print(g[0], list(g[1]))
...
a ['a', 'a']
b ['b', 'b']
c ['c', 'c', 'c']
d ['d']
e ['e', 'e', 'e', 'e']

あとはこれを数えてやればいい。

def charpos2(s):
    idx = 0
    result = []
    for g in itertools.groupby(s):
        l = len(list(g[1]))
        result.append((g[0], idx, l))
        idx += l
    return result
>>> charpos2('aabbcccdeeee')
[('a', 0, 2), ('b', 2, 2), ('c', 4, 3), ('d', 7, 1), ('e', 8, 4)]

まぁ満足。だけどもう少し……というわけで、畳み込み関数を使ってみたらどうだろう。Haskell には mapAccumL っていう関数があるけど、Python だと itertools.accumulate が似た感じだ。こうなった。

def charpos3(s):
    g = itertools.groupby(s)
    acc = itertools.accumulate(g, lambda a, b: (b[0], a[1]+a[2], len(list(b[1]))), initial=('', 0, 0))
    return list(acc)[1:]
>>> charpos3('aabbcccdeeee')
[('a', 0, 2), ('b', 2, 2), ('c', 4, 3), ('d', 7, 1), ('e', 8, 4)]

期待通りの結果は得られたけど、これはかえって解りにくいか。

Python:FletとPyInstallerでGUIアプリを作る

Poetryでプロジェクトを作る。

takatoh@sofa: w > poetry new fletsample
Created package fletsample in fletsample
takatoh@sofa: w > cd fletsample

Fletを追加。

takatoh@sofa: fletsample > poetry add flet

flet create コマンドでプロジェクトを初期化。--templateオプションはひな形の指定で、minimumcounter がある(デフォルトは minimum)。

takatoh@sofa: fletsample > poetry run flet create --template counter .

Copying from template version 0.0.0.post9.dev0+cdc6738
 identical  .
    create  .gitattributes
    create  .gitignore
    create  assets
    create  assets\favicon.png
    create  assets\icon.png
    create  assets\manifest.json
    create  main.py
  conflict  README.md
 Overwrite README.md? [Y/n] y


Done. Now run:

flet run

見ての通りいくつかのファイルが作られる。main.py がメインのファイル。テンプレートに counter を指定したので、ボタンをクリックするとカウンターの数値が変わるアプリの実装が書かれている。

import flet as ft


def main(page: ft.Page):
    page.title = "Flet counter example"
    page.vertical_alignment = ft.MainAxisAlignment.CENTER

    txt_number = ft.TextField(value="0", text_align=ft.TextAlign.RIGHT, width=100)

    def minus_click(e):
        txt_number.value = str(int(txt_number.value) - 1)
        page.update()

    def plus_click(e):
        txt_number.value = str(int(txt_number.value) + 1)
        page.update()

    page.add(
        ft.Row(
            [
                ft.IconButton(ft.icons.REMOVE, on_click=minus_click),
                txt_number,
                ft.IconButton(ft.icons.ADD, on_click=plus_click),
            ],
            alignment=ft.MainAxisAlignment.CENTER,
        )
    )


ft.app(main)

アプリを実行するには、flet run コマンド。新しいウィンドウでアプリが起動する。+ ボタンと - ボタンをクリックするたびに表示されてる数値が1ずつ変わる。そっけないアプリだけどちゃんと動く。

takatoh@sofa: fletsample > poetry run flet run

-w オプションを指定すると、webアプリとして起動し、ブラウザが開く。

takatoh@sofa: fletsample > poetry run flet run -w
http://127.0.0.1:50767

PyInstaller を追加。

takatoh@sofa: fletsample > poetry add --group dev pyinstaller
Using version ^6.1.0 for pyinstaller

Updating dependencies
Resolving dependencies...

The current project's Python requirement (>=3.10,<4.0) is not compatible with some of the required packages Python requirement:
  - pyinstaller requires Python <3.13,>=3.8, so it will not be satisfied for Python >=3.13,<4.0

Because no versions of pyinstaller match >6.1.0,<7.0.0
 and pyinstaller (6.1.0) requires Python <3.13,>=3.8, pyinstaller is forbidden.
So, because fletsample depends on pyinstaller (^6.1.0), version solving failed.

  • Check your dependencies Python requirement: The Python requirement can be specified via the `python` or `markers` properties

    For pyinstaller, a possible solution would be to set the `python` property to ">=3.10,<3.13"

    https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies,
    https://python-poetry.org/docs/dependency-specification/#using-environment-markers

……なんかエラーが出た。

調べてみると、pyproject.toml ファイルで Python のバージョンを ^3.10 と指定されてるのが原因らしい。PyPI.org の pyinstaller のページには、Python のバージョンについていこんなふうに書いてある。

3.8-3.12. Note that Python 3.10.0 contains a bug making it unsupportable by PyInstaller. PyInstaller will also not work with beta releases of Python 3.13.

pyinstaller 6.1.0 | PyPI.org

これを読むと ^3.10 でよさそうなものだけど……。ともかく、エラーメッセージにあるように、pyproject.toml の中の Python のバージョン指定を書き換えた(3.10.0も避けた)。

python = ">=3.10.1,<3.13"

もう一度、インストール実行。

takatoh@sofa: fletsample > poetry add --group dev pyinstaller

今度は無事インストールできた。ところで、--group dev はパッケージを開発用にインストールする。従来の --dev オプションは非推奨で代わりにこっちを使え、とマニュアルに書いてある。 ともあれ、この時点で pyproject.toml ファイルは次のようになった。

[tool.poetry]
name = "fletsample"
version = "0.1.0"
description = ""
authors = ["takatoh <[email protected]>"]
readme = "README.md"

[tool.poetry.dependencies]
python = ">=3.10.1,<3.13"
flet = "^0.10.3"


[tool.poetry.group.dev.dependencies]
pyinstaller = "^6.1.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

GUI アプリをビルドするにはメインのソースファイルを指定して flet pack コマンドを実行する。--name オプションは出来上がるファイルの名前を指定している。省略すると mein.exe になる。

takatoh@sofa: fletsample > poetry run flet pack --name fletsample main.py

ビルドされたファイルは dist フォルダの中にできる。

takatoh@sofa: fletsample > ls dist

    Directory: C:\Users\takatoh\Documents\w\fletsample\dist

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2023/10/21    20:40       31291901 fletsample.exe

エクスプローラで探してファイルをダブルクリックすると、GUIアプリが起動した。うまくいってるようだ。試しに、ファイルをほかの場所、例えばデスクトップに移動しても、ダブルクリックすればちゃんと起動する。

Flet はまだバージョン1.0に届いてなくて開発途中のようだけど、簡単なGUIアプリを作るのにはいいんじゃないかと思えた。見た目もモダンでいい。

PyInstaller も初めて使ったけど、Python をインストールしなくてもいいので、他の人に使ってもらうのにはちょうどいいんじゃないかな。ファイルサイズが30MB以上もあるけど、Pythonの実行環境を含んでいるからこれは仕方がないかな。

[追記]

pyproject.toml での Python のバージョン指定、^3.10 だと 3.13 もOKってことになる。だからエラーが出るんだ。