Python 製アプリの起動方法とパッケージング
概要
Python 製の Windows CE アプリを、Brain の辞書アプリから起動可能な形式にパッケージする仕様を記述する。 TODO
RasPython3.iconreadmeをちゃんと書く
RasPython3.iconPyAppLauncherに渡す引数の処理(--helpや--versionなどを独自に使えるようにする?Pythonに渡す?)
RasPython3.iconPyAppLauncherのバージョニング
RasPython3.iconデモアプリを完成させる
用語
「アプリ」: AppMain.exe や index.din を含むディレクトリ。SD カードの「アプリ」ディレクトリに配置される。
「PyAppLauncher」: Python の実行ファイルを起動する実行ファイル。引数と環境変数設定ファイルを Python に渡す。
「Python モジュール」: .py ファイルのこと。
「Python パッケージ」: __init__.py が含まれるディレクトリのこと。
背景
Brain の追加アプリは「アプリ」ディレクトリに配置される。Python で記述されたソフトをユーザーがインストールする方法は2つ考えられる。
グローバルな Python インタプリタをインストールした上で、アプリに PyAppLauncher と Python パッケージだけを配置する
PyAppLauncher、Python パッケージ、Python 本体がすべて同梱されたものを配置する
2024年1月現在でこの Python 本体を同梱する案は未実装
Windows CE には環境変数がないため、Python 内部で何らかの方法で環境変数をエミュレートする必要がある。また、モジュール検索に影響を与える諸要素(PYTHONPATH や _pth など)も考慮しなければならない。
実現したいこと
辞書アプリから直接 Python アプリを実行する
Explorer や CeOpener から Python モジュールや Python パッケージを実行する
アプリのポータビリティを確保する(アプリとその中のファイルには極力絶対パスを記述しない)
Python の起動パターン
起動パターンは3つある。
辞書アプリの「外部アプリ」画面 → PyAppLauncher → グローバルな python.exe → アプリパッケージ実行
辞書アプリの「外部アプリ」画面 → PyAppLauncher → アプリ同梱の python.exe → アプリパッケージ実行
Explorer や CeOpener など → グローバルな python.exe で Python モジュールを直接実行
1. 辞書アプリ → グローバルな python.exe → アプリパッケージ
辞書アプリからの起動には AppMain.exe (exeopener) が必要である。その横に、AppMain_.exe として PyAppLauncher を配置する。PyAppLauncher は、実行対象のモジュールやパッケージ名、環境変数が含まれる environ.ini を引数として Python に渡す。
code:グローバルな python.exe で実行されるアプリの構成
.
├── AppMain.exe (exeopener)
├── AppMain_.exe
├── app
│ ├── __init__.py
│ └── __main__.py
├── config.ini
├── environ.ini
└── index.din
table:ファイル内訳
AppMain.exe exeopener
AppMain_.exe app package の __main__ を起こす。具体的には python.exe -m app を実行する。
app app package
__init__.py app package の __init__.py。基本的には空。
__main__.py app package のエントリポイント。
config.ini AppMain_.exe が読む設定ファイル。
environ.ini Python に渡される環境変数が記述された設定ファイル。
index.din いつもの。
2. 辞書アプリ → 同梱された python.exe → アプリパッケージ
exe の連鎖はグローバルな Python のパターンと同じ。Python インタプリタは同梱のものを使う。2024年1月現在では未実装。
code:同梱する場合のディレクトリ案
.
├── AppMain.exe (exeopener)
├── AppMain_.exe
├── app
│ ├── __init__.py
│ └── __main__.py
├── config.ini
├── index.din
└── python
└── Pythonの環境全体
table:ファイル内訳
AppMain.exe exeopener
AppMain_.exe app package の __main__ を起こす。具体的には python.exe -m app を実行する。
app app package
__init__.py app package の __init__.py。基本的には空。
__main__.py app package のエントリポイント。
foo.py app package のモジュール。
config.ini AppMain_.exe が読む設定ファイル。内容は後述。
index.din いつもの。
python Python の環境がまるごと入っている。
3. Pythonモジュールを直接実行
レジストリの関連付けに .py を追加して、python3.exe に実行させる方法。
Python 3 単品のリリースに付属した associate.exe を実行すると、python3.exe の位置を自動もしくは config.ini で特定し関連付けを行う。関連付けが済んだ後、explorer などから直接スクリプトを実行する。
PyAppLauncher の挙動
PyAppLauncher (AppMain_.exe) は、同じディレクトリに置かれた設定ファイル config.ini に応じて Python を起動する。config.ini は =で結ばれた key-value 型の INI 形式で記述される。Python へのパスを変更可能にすることで、バンドルされた Python 3 とグローバルにインストールされた共通の Python 3 とを切替可能にする。
記述例は以下の通り。
code:config.ini
python=\Storage Card\python\python.exe
args=-m app
version=3.* # 3.xならなんでも
version=<=3.10 # 3.10以下
version=>=3.10, <3.12 # 3.10以上3.12未満
version=3.10.10 # 3.10.10のみ
environ_file=environ.ini
# または
environ_file_1=environ.ini
environ_file_2=optional.ini
省略 ... 既定値(= 読み込まない)
# 以下、テスト用フラグ
test_discover=true # keyが未指定もしくはvalueが空白文字列ならfalse、空白文字列でなければtrueとして扱う
python キーは、python.exe へのパス。相対パスであった場合は、PyAppLauncher の実行ファイルが存在するディレクトリから見たものと解釈する。省略された場合は、既知の検索パスからグローバルもしくは同梱された Python を検索し自動で発見する。検索パスは以下の通り。
\Storage Card\python\python.exe
puhitaku.icon ↑これは2024/05/01時点でスキップ(ファイル名にバージョンが入っていない exe の発見が複雑なため)
\Storage Card\python3.10\python3.10.exe
Python のバージョンが変わった時は新しい方のバージョンを検索順位の高い方へ追加する
puhitaku.icon もしかすると、見つけたバージョンを降順ソートして最初にあるものを実行、とする方がシンプルかも。バージョン指定にも従うことを忘れずに。
\Storage Card\python\python3.10.exe
\NAND3\python\python.exe
puhitaku.icon ↑これは2024/05/01時点でスキップ(ファイル名にバージョンが入っていない exe の発見が複雑なため)
\NAND3\python\python3.10.exe
\NAND3\python3.10\python3.10.exe
.\python\python.exe
puhitaku.icon ↑これは2024/05/01時点でスキップ(ファイル名にバージョンが入っていない exe の発見が複雑なため)
.\python\python3.10.exe
args キーは、python の exe に渡す引数。省略された場合は、対話モードが起動する。
version キーは、起動する Python のバージョンを指定する。python キーによって実行する Python が具体的に指定されていた場合、version キーは無視される。比較演算子とその等価性は PyPA のスペック をベースとしたサブセットとして以下のように定義する。 version=3.10.10 のように比較演算子がない場合は、PyPA が定義する "arbitrary equality"
version=~=3 のように ~= がある場合は、PyPA が定義する "compatible release"
version=>=3.10 のように >= <= がある場合は、PyPA が定義する "inclusive ordered comparison"
version=>3.10 のように > < がある場合は、PyPA が定義する "exclusive ordered comparison"
AND 条件は , で区切る
test_discover キーは、Python の検索処理をテストするフラグ。指定された Python を検索してその結果の成否と実行ファイルへのパスをダイアログで出力する 。
発見された Python を実行する時、PyAppLauncher は Python へ次のように引数を渡す。
code:cmdline
python.exe --env-set PYTHONPATH={Pythonパッケージの絶対パス} {python_argsの右辺}
RasPython3.icon suggestion: python_argsのダブルクオートを外しておきました。 puhitaku.icon 👍
環境変数の設定ファイルの名前を既定値(environ.ini)以外にもできるようにする。
そして、動作できるpythonのバージョンを制限できるようにする。制限に引っかかったら、ダイアログで続行するか確認など。
puhitaku.icon 👍: environ.ini 以外にもできるようにする
puhitaku.icon environ.ini の複数指定は、それが必要なユースケースが浮かんだら実装でOK
key-value 形式を取るが dict ではないのでキーは単に同じ environ_file を繰り返す、でも問題なく実装できると推測
puhitaku.icon 👍: 動作できるバージョンの制限
アプリを ZIP で配る時とかに python_version=>=3.10 みたいに書くと将来に渡り config.ini を変えずに動かせて良い
Python の自動検索の挙動に関わるので、バージョン指定に対して生成される検索パスを丁寧に定義したうえでコードをテストする必要がある
バージョン指定については車輪を再発明せずに PyPA のスペック に則るとユーザーフレンドリー。そのままだと複雑すぎるので、以下のようにサブセット実装を与えれば随分シンプルになるはず。 version=3.10.10 のように比較演算子がない場合は arbitrary equality
version=~=3 のように ~= がある場合は compatible release
version=>=3.10 のように >= <= がある場合は inclusive ordered comparison
version=>3.10 のように > < がある場合は exclusive ordered comparison
AND 条件は , で区切る
RasPython3.icon 了解です〜
思ったこと: key と value の間にスペースを許容してもいいかも、ただそれをやるなら environ.ini も仕様を揃えたいので TBD
RasPython3.icon とりあえずやってみましょう。で、なにか問題があるならやめる感じで。
puhitaku.icon 👍
RasPython3.icon suggestion これらも探索するのはどうでしょうか。(要議論?) puhitaku.icon 良さそう。Python のバージョン指定で書いているように、バージョン番号の入ったディレクトリについては検索パスを動的に生成することになりそう。
puhitaku.icon メモ: 提案されたパスは検索パスに統合しましたRasPython3.icon👍
puhitaku.icon python_ で始まるキーは冗長なので削っておきました
RasPython3.icon 削り忘れを削っておきました
RasPython3.icon ;でコメントアウトできるようにする
RasPython3.icon environ.iniの読み込みが任意になったのでそこの記述を削りました。
Python 本体の挙動
Windows CE では Python に環境変数が渡されない。これを克服するため、Brain Hackers のフォークには、環境変数を記述したファイル(以下、環境変数設定ファイル)の読み込み処理が追加されている。
グローバルな環境変数設定ファイルであるenviron.iniは、python の exe と同じ場所に存在する。存在しない場合は、pythonによって自動で生成される。デフォルトでは、内容は以下の通り。(UTF-8, BOM対応)
code:environ.ini
PYTHONCASEOK=1
PYTHONCASEOK は、
このファイルに環境変数を記述することで、そのpythonを使用するすべての場合において、記述された環境変数が適用される。
この設定は、pythonの起動直後に読み込まれ、--env-pathで指定されたどの設定ファイルよりも先に適用される。
環境変数を設定する際には、以下の書式で設定する。なお、VARNAMEの大文字小文字は無視される。(case-insensitive)
また、=の前後のスペース(\x20)は許容され、無視される。右辺が空白のみの場合は、右辺に何も書かなかった場合ど同様、環境変数が削除される。
code:environ.ini
VARNAME=VALUE
バックスラッシュによるエスケープ等はできない。バックスラッシュはただの文字として扱われる。
環境変数を削除するには、VALUEを記述せず空にしておく。ただしそのキーの環境変数が存在しない場合には何もしない。
ローカルな環境変数設定ファイルは、--env-pathオプションによりpythonに認識され、読み込まれる。ファイル名は何でも良い。--env-pathによる指定の書式は、以下の通りである。ただし、絶対パスはcase-sensitiveである。(RasPython3.icon TODO: 確認)
なお、指定されたファイルパスが存在しない場合はFatalErrorにより終了する。
code:cmdline
<pythonのexe> --env-path <設定ファイルの絶対パス> ...
--env-pathで指定された順に読み込まれ、後から読み込まれた設定が、グローバルなものを含めてそれ以前に読み込まれた値を上書きする。これによって、アプリケーションごとに異なる環境変数で起動できる。書式は、グローバルな設定ファイルと同じ。
ローカルな設定ファイルでは、基本的にPYTHONPATHを設定する。 古い情報、後述。
code:environ.ini
PYTHONPATH=(アプリの絶対パス)
--env-setオプションの追加により、environ.iniの読み込みが必須ではなくなった。そのため、environ.iniの中身は空またはファイル自体存在しなくてもよい。
args キーのテストケース
テスト環境の PyAppLauncher インストール箇所は以下の通り。
\Storage Card\アプリ\PyAppLauncher\AppMain.exe
テスト環境の Python インストール箇所は以下の通り。
\Storage Card\アプリ\PyAppLauncher\python\python3.10.exe
テスト環境に配置する Python パッケージは以下の通り。__main__.py には明示的に動作を確認するため tkinter の Hello World を入れておく。
\Storage Card\アプリ\PyAppLauncher\app\__init__.py
\Storage Card\アプリ\PyAppLauncher\app\__main__.py
正常系のテストケースは以下の通り。
存在するパッケージを指定
code:config.ini
args=-m app
成功条件: Hello World が表示される
異常系のテストケースは以下の通り。
存在しないパッケージを指定
code:config.ini
args=-m foo
成功条件: ImportError で終了する
environ_file* キーのテストケース
テスト環境の PyAppLauncher インストール箇所は以下の通り。
\Storage Card\アプリ\PyAppLauncher\AppMain.exe
テスト環境の Python インストール箇所は以下の通り。
\Storage Card\アプリ\PyAppLauncher\python\python3.10.exe
テスト環境に配置する Python パッケージは以下の通り。__main__.py には明示的に動作を確認するため tkinter のダイアログで repr(os.environ) を表示するプログラムを入れておく。
\Storage Card\アプリ\PyAppLauncher\app\__init__.py
\Storage Card\アプリ\PyAppLauncher\app\__main__.py
テスト環境に配置する environ_file とその中身は以下の通り。
\Storage Card\アプリ\PyAppLauncher\environ.ini
code: environ.ini
foo=1
\Storage Card\アプリ\PyAppLauncher\environ_1.ini
code: environ_1.ini
foo=2
bar=3
\Storage Card\アプリ\PyAppLauncher\environ_2.ini
foo キーの上書きおよび新規キー baz の追加動作を検証する内容になっている
code: environ_2.ini
foo=99
baz=4
正常系のテストケースは以下の通り。
environ_file 単体を指定
code: config.ini
args=-m app
environ_file=environ.ini
成功条件: {"foo": "1"} と表示される
environ_file_1 単体を指定
code: config.ini
args=-m app
environ_file_1=environ_1.ini
成功条件: {"foo": "2", "bar": "3"} と表示される(キーの順番は異なっていても良い)
environ_file_1 と environ_file_2 を指定
code: config.ini
args=-m app
environ_file_1=environ_1.ini
environ_file_2=environ_2.ini
成功条件: {"foo": "99", "bar": "3", "baz": 4} と表示される(キーの順番は異なっていても良い)
異常系のテストケースは以下の通り。
存在しない environ_file を指定
code: config.ini
args=-m app
environ_file=foo.ini
成功条件: FatalError で終了する
存在しない environ_file_1 を指定
code: config.ini
args=-m app
environ_file_1=foo.ini
成功条件: FatalError で終了する
Python 検索のテストケース
テストには test_discover キーを使い、成功条件に記された出力が得られるかどうかを確認する。
テスト環境の PyAppLauncher インストール箇所は以下の通り。
\Storage Card\アプリ\PyAppLauncher\AppMain.exe
テスト環境の Python インストール箇所は以下の通り。3.xx の xx がインクリメントしているのは検索の検証のためであり、実際にそのバージョンの Python を用意する必要はなく、ダミーファイルを置いて検証しても構わない。
\Storage Card\アプリ\PyAppLauncher\python\python3.10.exe
\Storage Card\アプリ\PyAppLauncher\python3.11\python3.11.exe
\Storage Card\python\python3.12.exe
\Storage Card\python3.13\python3.13.exe
\NAND3\python\python3.14.exe
\NAND3\python3.15\python3.15.exe
正常系のテストケースは以下の通り。
何も指定しない
code:config.ini
test_discover=true
成功条件: python3.15.exe を発見する(検索パスの上位にあるため最新のバージョンのため)
python.exe を指定: 相対パスで指定
code:config.ini
python=.\python\python3.10.exe
test_discover=true
成功条件: python3.10.exe を発見する
python.exe を指定: 絶対パスで指定
code:config.ini
python=\Storage Card\python\python3.12.exe
test_discover=true
成功条件: python3.12.exe を発見する
バージョンを指定: arbitrary equality 指定 =
code:config.ini
version=3.12
test_discover=true
成功条件: python3.12.exe を発見する
バージョンを指定: compatible release 指定 ~=
code:config.ini
version=~=3.0
test_discover=true
成功条件: python3.15.exe を発見する(互換性のあるリリースの中で最新のものを使うため)
RasPython3.icon compatible release はメジャーバージョンのみの指定がMUST NOTなので、3→3.0に変更
バージョンを指定: inclusive ordered 指定 <=
code:config.ini
version=<=3.13
test_discover=true
成功条件: python3.13.exe を発見する(指定された範囲内で最新のものを使うため)
バージョンを指定: inclusive ordered 指定 >=
code:config.ini
version=>=3.13
test_discover=true
成功条件: python3.15.exe を発見する(指定された範囲内で最新のものを使うため)
バージョンを指定: exclusive ordered 指定 <
code:config.ini
version=<3.13
test_discover=true
成功条件: python3.12.exe を発見する(指定された範囲内で最新のものを使うため)
バージョンを指定: exclusive ordered 指定 >
code:config.ini
version=>3.13
test_discover=true
成功条件: python3.15.exe を発見する(指定された範囲内で最新のものを使うため)
バージョンを指定: AND 指定
code:config.ini
version=<3.15,>3.10
test_discover=true
成功条件: python3.14.exe を発見する(指定された範囲内で最新のものを使うため)
バージョンを指定: AND 指定
code:config.ini
version=<=3.15,>=3.10
test_discover=true
成功条件: python3.15.exe を発見する(指定された範囲内で最新のものを使うため)
異常系のテストケースは以下の通り。
何も指定しない: 既知のパスに Python が存在しない
code:config.ini
test_discover=true
注意: このテストケースは Python をどこにも置いていない状態でテストする
成功条件: 発見に失敗する
python.exe を指定: 相対パスで存在しないパスを指定
code:config.ini
python=.\foo\python3.10.exe
test_discover=true
成功条件: 発見に失敗する
python.exe を指定: 絶対パスで存在しないパスを指定
code:config.ini
python=\Storage Card\foo\python3.12.exe
test_discover=true
成功条件: 発見に失敗する
バージョンを指定: arbitrary equality 指定 =
code:config.ini
version=3.99
test_discover=true
成功条件: 発見に失敗する
バージョンを指定: arbitrary equality 指定 =
code:config.ini
version=3.10.0
test_discover=true
成功条件: 発見に失敗する(python3.10.exe)のため
現在の判断基準だと、完全一致は精度含め判定しています。
バージョンを指定: compatible release 指定 ~=
code:config.ini
version=~=4.0
test_discover=true
成功条件: 発見に失敗する
RasPython3.icon compatible release はメジャーバージョンのみの指定がMUST NOTなので、4→4.0に変更して、テストケースを以下1つ追加します。
バージョンを指定: compatible release 指定 ~=
code:config.ini
version=~=3
test_discover=true
成功条件: 発見に失敗する(エラー)
バージョンを指定: inclusive ordered 指定 <=
code:config.ini
version=<=3.9
test_discover=true
成功条件: 発見に失敗する
バージョンを指定: inclusive ordered 指定 >=
code:config.ini
version=>=3.16
test_discover=true
成功条件: 発見に失敗する
バージョンを指定: exclusive ordered 指定 <
code:config.ini
version=<3.10
test_discover=true
成功条件: 発見に失敗する
バージョンを指定: exclusive ordered 指定 >
code:config.ini
version=>3.15
test_discover=true
成功条件: 発見に失敗する
バージョンを指定: AND 指定(矛盾)
code:config.ini
version=<3.12,>3.13
test_discover=true
成功条件: 発見に失敗する