コンテンツにスキップ

検証とリリース

ツアーの締めくくりです。 型チェッカー、ヘッドレス実行とゲート、リリース、そして今できないことを見ます。

三つのコマンドと、それぞれが返すもの。yokan check はコンパイラなしで約1秒で拒否を返し、yokan show はウィンドウなしで約1秒で画面を出力し、yokan gate はコンパイルして両方の実行が同じ画面を描いたと報告する

三つのコマンドと、それぞれが返すもの。yokan check はコンパイラなしで約1秒で拒否を返し、yokan show はウィンドウなしで約1秒で画面を出力し、yokan gate はコンパイルして両方の実行が同じ画面を描いたと報告する

型チェッカーとの併用

yokan は型スタブを同梱しているので、pyright(VS Code の Pylance)によるチェックがそのまま働きます。 スタブは実行時の形をそのまま伝えます。 @store はクラス名がそのままインスタンスなので、Settings.set_dark(True) は正しくメソッド呼び出しとして扱われ、on_change=Settings.set_dark のようなハンドラ渡しも (bool) -> None として通ります。 @model@value はフィールドどおりのコンストラクタを持つと伝わり、Weak[Node] はチェッカーには Node | None として見えます(実際、読んだときの意味もその通りです)。

mypy には、クラスデコレータによる型の変換を適用しないという既知の制限があります。 このため mypy では @store のメソッド呼び出しが「self が渡されていない」と誤検出されます。 チェックには pyright を推奨します。

型チェッカーが見るのは Python の型です。 この形をコンパイラが受け取れるかどうかは、yokan check app.py が答えます。 アプリが import するモジュールをすべて調べ、見つかった拒否を全部 ファイル:行:列 の形で示します。 断るところがなければ何も言いません。 モジュール直下の文とビューの各行を、それぞれ独立に調べます。 一度の実行でファイル全体の答えが返るので、次の一つのために往復し直す必要はありません。 宣言そのものを断られたものを読んでいる文は、拒否ではなく「調べられなかった」として数えます。 そこで出るメッセージは、起きなかった宣言についてのものであって、それを読んでいる行についてのものではないからです。 コンパイラを起動しないので、編集しながら何度でも回せます。

check は警告も出します。 拒否ではありません。 翻訳は通っていて、それでも直したほうがよいところがある、という合図です。

$ yokan check app.py
app.py:37:9: warning — these assignments make a reference cycle: b.parent → a.kid → b. The compiled run counts references and never frees a cycle (the CPython you develop on collects it), so write the reference that points back as `Weak[...]`.
            b.parent = a
            ^

今ある警告はメモリの話です。 循環はコンパイル後に解放されず、開発に使う CPython は回収するので、二つの実行で振る舞いが分かれます。 その差はどのダンプにも現れないので、ゲートでは見つかりません。

見つかる形は二つです。 一つはフィールドの型で、二つ以上のモデルにまたがって輪が閉じている場合です(Kid.owner → Parent.kids → Kid)。 もう一つはハンドラの中の代入で、往復を書いた場合です(a.kid = b のあとに b.parent = a)。 後者があるので、自分自身のクラスを参照するモデルも見つかります。 型だけでは、リストや木と輪の区別がつかないからです。

--strict を付けると、警告が失敗になります。

エディタ

エディタが走らせるのは yokan check です。 Yokan のほかに入れるものはありません。 file:line:col: message と、その行とキャレットを出し、方言の内側なら何も言わず、所要はおよそ 0.1 秒です。 見つかった拒否は最初の一つではなく全部を報告するので、一度の実行でファイル全体の答えが返ります。

Vim と Neovim は設定二つで、拒否が quickfix に入ります。

setlocal makeprg=yokan\ check\ %
setlocal errorformat=%f:%l:%c:\ %m
autocmd BufWritePost *.py silent make! | redraw!

:copen で一覧、:cnext で順に移動し、どの項目もファイルのその桁を開きます。

VS Code は problem matcher で同じ形を読みます。 .vscode/tasks.json に次を置きます。

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "yokan check",
      "type": "shell",
      "command": "yokan check ${file}",
      "presentation": { "reveal": "silent" },
      "problemMatcher": {
        "owner": "yokan",
        "fileLocation": ["autoDetect", "${workspaceFolder}"],
        "pattern": {
          "regexp": "^(.+?):(\\d+):(\\d+): (.+)$",
          "file": 1,
          "line": 2,
          "column": 3,
          "message": 4
        }
      }
    }
  ]
}

拒否は問題パネルに、それが指す行に付いて出ます。 VS Code にはタスクを保存時に走らせる仕組みが標準では無いので、これはキー割り当てかコマンドパレットから呼ぶことになります。 保存を見張る拡張を入れれば、保存のたびに走ります。

ほかのエディタも同じです。 check がこの形を出しているのは、これがコンパイラが四十年出してきた形だからです。 チェックアウトの中では、コマンドが uv run yokan_gate.py check <ファイル> に変わるだけです。

テスト

アプリは普通の Python のモジュールなので、テストも普通の Python のテストです。 使い慣れたテストランナーが、そのまま使えます。 yokan.headless(view, state, script) はウィンドウを出さずにスクリプトを流し、画面をテキストで返します(ステップ前のダンプと、ステップ後のダンプ)。 テストはその文字列を検査します。

編集の合間には、同じ実行をコマンドラインから呼べます。 画面を画像として残すこともできます。

$ yokan show app.py --script "keydown:left,advance:33,advance:33" --frames shots/ --scale 3
Column[Canvas(160x120, scale=4, bg=#000000)[
  Sprite(assets/sheet.png, 0,0 8x8 at 54,100)
  PixelText(4, 4, "SCORE 0", #eeeeee)
]]

3 frames in shots/

コンパイルの前に拒否を調べるので、書き間違いは1秒で返ってきます。 ウィンドウは開かず画面を出力し、--frames を付けると各ステップのキャンバスを PNG で残します。 ffmpeg があれば --gif で1本にまとめます。 ディスプレイを使わないので、ssh 越しでも CI でも動きます。 描いているのは、ウィンドウと同じラスタライザです。 これが往復の速いほうで、遅いほうが yokan gate です。 リリースする実行と一致することを証明するのは、そちらの仕事です。

# tests/test_app.py
def test_clicking_counts(app, run):
    assert "count: 2" in run(app, "click:+1,click:+1")


def test_typing_greets(app, run):
    assert "Momo" in run(app, "input:Momo")
$ uv run --with pytest python -m pytest
2 passed

apprun は wheel に載っているプラグインが渡します。 Yokan を入れてあるテストなら、何も書かずに使えます。 appapp.py を実行せずに読み込み(run(...)__main__ の下にあります)、run はウィンドウを出さずにスクリプトを流します。 run のたびにアプリは最初の状態から始まるので、一つのテストで何度でも動かせますし、読める数は人が見る数と同じです。

このフィクスチャは必須ではありません。 from yokan_testing import run_script は同じ仕掛けを別の名前で呼ぶ書き方で、どちらも yokan.headless(view, state, script) の上に乗っています。

実行が返すのは Transcript です。 これまでどおりの文字列そのもので(in==str() の振る舞いは変わりません)、実行の内訳がそこに付いています。

def test_the_middle_of_the_run(app, run):
    t = run(app, "click:+1,dump,click:+10")
    assert "count: 0" in t.before      # 開いたときの画面
    assert "count: 1" in t.dumps[0]    # dump ステップが求めた画面
    assert "count: 11" in t.after      # 終わったときの画面
    assert t.steps[0].step == "click:+1"

a11y はアクセシビリティツリーを記録に残します。 スクリーンリーダーに渡されるのと同じものです。 mem はその実行が抱えているオブジェクトの数を数えます。

def test_a_screen_reader_hears_the_count(app, run):
    t = run(app, "click:+1,a11y")
    assert 'label "count: 1"' in t.a11y
    assert 'button "+1"' in t.a11y

画面を細かく検査するより、まるごと記録しておきたいことがあります。 それがスナップショットです。 snapshot(t) は記録を tests/__snapshots__/<テスト名>.dump と突き合わせ、--yokan-update がその内容を書き直します。 レビューの差分が、画面の変化としてそのまま読めるようになります。

def test_the_screen_is_what_it_was(app, run, snapshot):
    snapshot(run(app, "click:+1,click:+10"))

ゲートもテストの一つとして書けます。 gate("click:+1") はアプリをビルドし、そのスクリプトについて両方の実行を比べ、食い違えばその内容とともに失敗します。 --yokan-gate を付けると、テストが流すすべてのスクリプトについて同じことをします。 テスト一式が、そのまま「コンパイル済みのアプリも同じことをする」証明になります。

$ uv run --with pytest python -m pytest --yokan-gate
6 passed in 16.19s

これはコンパイルを伴うので、意図して遅いほうに置いてあります。 普段はテスト一式に gate(...) を一つ置いておき、リリース前に --yokan-gate を使うのがよい配分です。

スクリプトの語彙は、次の節にまとめてあります。 人がアプリにできることは、テストにもできます。 クリック、入力、キー、ファイルのドロップ、advance:1000 で1秒進めること、どれもです。 ハンドラもストアのメソッドも value クラスも普通の Python なので、計算だけの部分は直接呼んで確かめられます。

このテストが見ているのは開発実行、つまり CPython です。 リリースするバイナリがそれと一致するかどうかは別の話で、そちらは yokan gate が答えます(同じスクリプトを両方の実行に流し、バイト単位で比べます)。 二つは役割が違います。 テストは「アプリが正しいことをする」と言い、ゲートは「コンパイル済みのアプリが同じことをする」と言います。 checkshow とゲートがどう一つの往復になり、なぜエージェントが自分で回せるのかは、エージェントと作るにあります。

ヘッドレス実行とゲート

アプリはウィンドウなしでも動かせます。 検証はここから始まります。

$ PIXIE_SCRIPT="click:+1,input:Momo" uv run app.py

PIXIE_SCRIPT は環境変数です。 Yokan のアプリはどれもこれを読みます。 開発実行でも、リリースビルドのバイナリでも同じです。 指定するとアプリはウィンドウを開かず、画面をダンプし、書いたステップを再生し、もう一度画面をダンプして終了します。 yokan gateyokan show --script が内部で設定しているのもこれなので、同じスクリプトの文字列が三か所とも同じように使えます。 名前が Yokan ではないのは、これを読む仕組みが Yokan のものではないからです。 Yokan が土台にしている pixie のもので、あちらでも同じ綴りです。

ステップの語彙は click[@n]:<ラベル>(ボタン、リンク、表の列見出し)、input[@n]:<テキスト>submit[@n]slide[@n]:<値>select[@n]:<ラベル>(選択肢のある要素の項目、または表の行を先頭セルで)、key:<chord>keydown:<キー>keyup:<キー>menu:<項目>file:<path>drop:<path>hover[@n]:<i>(チャートの i 番目の点にポインタを載せる。hover: で外す)、advance:<ms>theme:light|darka11ymemdump です。 @n はツリー順で n 番目の一致を選ぶので、同じラベルのボタンが並ぶ行にも届きます(0 から数えるので、click@2:削除 は三つ目)。 dump はその時点の画面を出力するので、最初と最後だけでなく途中の状態も検査できます。 テキストに含めるカンマは \, と書きます(input:hello\, world)。 a11ymem は画面の中ではなく、画面の隣に出ます。 a11y はゲートも突き合わせます。 両方の実行が同じアクセシビリティツリーを組み立てるからです。 mem は読むためのもので、突き合わせません。 二つの実行が抱えるオブジェクトは、作りからして違うからです。 ステップの前後で、画面の内容がテキストになって標準出力に出ます。 テストからは、yokan.headless(view, state, script) が同じ文字列を返します。

ゲートは同じスクリプトを開発版とリリース版の両方で再生し、ダンプを突き合わせます。

$ yokan gate app.py --script "click:+1,input:Momo" --release
GATE OK — 2 dump lines identical in both runs

ファイルや DB に書くアプリは --fresh path/to/file.db を付けると、先に走った側の書き込みが、後に走る側の最初の読みに混ざりません。 PEP 723 の依存を持つアプリは、ゲート自体を uv run --with <dep> の下で回します。

リリース

$ yokan build app.py --release              # ネイティブバイナリ(検証なし)
$ yokan build app.py --release --app        # macOS の .app バンドル
$ yokan build app.py --release --bundle     # @py あり: ランタイム同梱フォルダ
$ yokan build app.py --release --bundle --app   # ランタイムごと .app に
$ yokan build app.py --release --onefile    # 1 ファイル配布
$ yokan build app.py --release --appimage   # Linux: 1 つの .AppImage

ネイティブビルドの前提は Rust ツールチェーンだけです。 コンパイル先の Rust クレート群はリポジトリに入っていますが、最初のネイティブビルドが、使っているバージョンに合うチェックアウトを ~/.cache/yokan/ に取ってきます。 チェックアウトの中で実行すればそちらを使い、PIXIE_REPO を指せば別の場所も使えます。 手順はインストールのページにまとまっています。

エスケープを使わないアプリのリリースバイナリは、それ自体が自己完結です(Python へのリンクはゼロ)。 --bundle は Python ランタイムと宣言済み依存を同梱したフォルダを、--onefile は 1 ファイル(stdlib のみ約 17 MB、numpy 込み約 21 MB。初回起動でキャッシュへ展開し、以後は約 40 ms で起動)を作ります。 ゲートは、その 1 ファイルに対してもスクリプトを再生できます。 ランタイムの隣には本物の python3 が 52 KB だけ同梱され、sys.executable はアプリではなくそれを指します。 補助プロセスを起動するライブラリ(multiprocessing は、何かがロックを取った時点でそうします)が起動するのは、アプリのもう一つのコピーではなく、このインタプリタです。

--appdist/<タイトル>.app を作ります。 Dock に名前が出て、ダブルクリックで起動でき、Finder から Applications へそのまま置けます。 --bundle --app なら CPython ランタイムまで .app の中に収まり、外に何も要りません。 アプリのファイルの隣に <名前>.png(または .icns)を置くと、アイコンとして取り込まれます。 --onefile は 1 ファイル形式なので .app とは排他です。

Linux では、形も Linux のものになります。 --appdist/<タイトル>.AppDir を作ります。 中身はバイナリ、AppRun.desktop エントリ、アイコン、そしてホストにあるとは限らないライブラリです。 --appimage はそれを dist/<タイトル>-<arch>.AppImage に詰めます。 このプラットフォームで人に渡す 1 ファイルがこれです。 同梱するライブラリは一つの表が決めていて、開発中に使う wheel も同じ表を読みます。 libfontconfiglibxkbcommon を同梱すると、ホストが持っているフォント設定やキーボードのデータとぶつかります。 それが Linux のパッケージの壊れ方なので、その手のものは外して、動かす機械のものを使います。 --carry-libs はこれを逆にするフラグです。 配る先にライブラリがないかもしれないときのために、C ランタイム以外をすべて運びます。 --bundle--onefile は CPython を Apple の流儀で運ぶものなので、ここでは名前を挙げて止まります。

本格アプリの例

demo/opsboard/ は三モジュール構成のダッシュボードです。 直和型のヘルスモデルを、ビューの match で分岐します。 ストア二つ、ラインチャート二枚、ラベル付きバーチャート、スロット付き KPI カード、grow で領域いっぱいに広がる仮想化アラートフィード、重大度フィルタ、fs によるレポート出力、テーマ切替、種付き乱数のモックテレメトリまで、この一つに入っています。 リリースビルドは 13.7 MB(strip 後 10.6 MB)で、Python へのリンクはありません。

$ uv run demo/opsboard/app.py
$ yokan build demo/opsboard/app.py --release

小さな例は demo/ にひとそろいあります(counter、todo、ledger、moods、geometry、cards、styled、tryfetch、pyops など)。 辞書で状態を持つ 2 本(run(state={...}))を除いて、どれもゲートを通っています。 その 2 本は設計上の開発専用で、ギャラリーにもそう書いてあります。

今できないこと

この範囲の外にあるものは断られます。 黙って挙動が変わることはありません。 断りの文はファイル名と行と列を挙げ、該当行を引用します。

$ yokan build app.py --release
widgets.py:5:40: not in the dialect — text() does not take `weight=`
        return text(label, size=12, weight=2)
                                           ^

今日の時点でできないことと、その理由です。

  • 捕まえない素の d[k] 読み。 読みの形は .get(key, default) で、無いキーをどう扱うかは呼び出し側が決めます。 try: v = d[k] except KeyError: と書けば、Python と同じようにキーの不在を捕まえられます。 try が捕まえるのは、名前に束ねた読みそのものです。
  • ストアやモデルのメソッドの中で、フィールドと同じ名前をローカル、引数、ループ変数に付けること。 Python では scoreself.score は別の名前ですが、コンパイル済みの側はフィールドをその名前のまま読むので、同じ score が二つの実行で別のものを指してしまいます。 score_ のように名前を変えれば、Python での意味は変わりません。
  • 片方の分岐でしか代入していないローカルを後で読むこと。 その分岐が実行されなければ、Python でも NameError になる形だからです。 if と else の両方で代入すれば読めます。
  • int ** int の負の指数。 結果の型が実行時に変わるためです。 どちらかを float にすれば書けます。
  • 辞書 state(run(state={...}))のコンパイル。 開発中は動きますが、コンパイルできる形は型付きの State です。
  • Protocol 束縛のヘルパをビューから呼ぶこと(ハンドラからは呼べます)。
  • Value クラスのメソッドをビューから呼ぶこと(ハンドラからは呼べます。ビューはフィールドを読みます)。
  • ストアとモデルのメソッドをビューから呼ぶこと。 画面を組み立てる側は状態を読むだけですが、メソッドは書き込むかもしれないからです。 読み取り専用の形は @property で、ビューはフィールドと同じように読めます。
  • モデルのリストをビューで直接繰り返すこと。 今日は、表示したい文字列をストア側で組み立ててから list_view に渡します。
  • キャンバスまわり:マウス、タイルマップ、カメラの移動はありません。 座標は整数のピクセルだけで、浮動小数点数は int(...) を書くように断ります。 倍率はアプリが宣言する数値で、ウィンドウに合わせて伸縮はしません。 伸縮させると、描かれた大きさがダンプの見えないところ(ウィンドウ)で決まってしまうからです。 スプライトの PNG はアプリの隣から探すので、見つからなければ何も描きません。 キャンバスが描いたものは、支援技術からも読めません。 1枚の画像として報告されるので、中身を伝える手段は a11y_label= です。
  • ストアのフィールドを Weak にすること。 ストアは所有する側なので、所有しない参照はモデル側(逆向きのポインタ)に置きます。
  • Vec などネイティブ側が既に使っている型名。 断られるので、別名(V2 など)を選びます。
  • モジュールのトップレベルに置いた文。 コンパイル済みのアプリはモジュールの宣言(import、State、クラス、def、style()、型 alias、リテラル定数、every(...) のタイマー、__main__ ガード)を読むだけで実行はしないため、関数の外に書いた count.set(5)fs.write_text(...) は断られます。 起動時の処理は def に書き、run(view, on_start=setup) で渡します。
  • ハンドラからタイマーを開始すること。 タイマーは宣言です(モジュールレベルの every(1.0, tick))。 ハンドラが変えるのは、ティックが読む状態のほうです。
  • taskon_error=。 失敗の経路は、エラーユニオンが入るのを待っています。 標準ライブラリ呼び出しの失敗は、その呼び出しを try / except で囲んで受けます。
  • 走り出した task を途中で止めることreport はタスクがどこまで進んだかを伝えますが、止まれと伝える手段はなく、task は最後まで走ります。
  • コンポーネントの local呼び出し位置で識別されます。 呼び出しの順を入れ替えると、状態も入れ替わります。
  • 同じ要素オブジェクトを二回置くこと。 一度置いた要素は使い切りで、二か所には置けません。
  • 注釈のないローカルのリストと辞書out: list[str] = []counts: dict[str, int] = {}。コンパイル側が要素の型や値の型を読めるのは、この注釈があるからです)。
  • 方言に形のないものを返す str のメソッド.format().translate()(実行時に組み立てるテンプレートや表)、.casefold()ßss に広げる写像で、他の大小変換とは別の Unicode 表が要ります)。 使えるのは .partition().rpartition().upper().lower().title().capitalize().swapcase().strip() / .lstrip() / .rstrip()(文字集合の有無どちらも)、.split().splitlines().join().startswith().endswith().replace().find().rfind().index().rindex().count().zfill().ljust().rjust().center().expandtabs().removeprefix().removesuffix().encode().is…() の族、len(s)s[i]s[a:b]in です。
  • fill、align、符号、幅、,、精度、d / f / e / % / s を超える書式指定#b / o / xng)。
  • Value クラスや Enum のコンポーネント引数、そして本体がコンテナ一つでない形(先頭の if、複数の要素。column でまとめます)。 コールバックと State の引数は使えます(受け取るコンポーネントは呼び出し箇所ごとのビューになります)。
  • set。 Python の set の反復順はコンパイル側で再現できないので、勝手な順に並べ替えるのではなく断ります。 list で足ります。 タプルは入りました(タプル)。 ただし形を書き下した場合だけで、Rust crate が返すタプルはまだ越えられません。 re.findall が群二つ以上を断るのは、そのためです。
  • スカラー、リスト、str キーの辞書、Value クラス、Optional 以外の @py の署名(モデル、入れ子のコンテナ)。
  • Optional をそのまま文字列にすることpickedT | None のときの f"{picked}")。 Python は None と書き、コンパイルされた実行は何も書きません。 先に絞り込んでから(if (v := picked) is not None:)、v を描いてください。
  • Yokan 自身のモジュールでは:ファイルのコピーや改名、ストリーミングやバイナリのダウンロード。
  • Python のモジュールではmath の六つ(それぞれ理由を挙げて断ります)、randomshuffle(リストをその場で並べ替えるものですが、ここではリストは State の中にあります。random.sample(xs(), len(xs())) で新しい順序を取って書き戻します)と gauss 以外の分布、int のリストに対する statistics(返り値が値によって int か float かに変わるためです)。 json.loads も断ります。 返るものの形が実行するまで決まらないからで、読むときは jsondoc のドットパスを使います。 json.dumps は、書き下したリテラルなら何段でも入れ子にできますが、アプリが保持している値は一段までです。 datetime からは、datetime.timereplacestrptime、リストや辞書に入れた date、ヘルパの引数としての date を断ります。 タイムゾーンは zoneinfo から来るもので、datetime.timezone の固定オフセットは入れません。 両方の実行がともに読めるのは、機械が名前で知っているゾーンだからです。 zoneinfo からは、実行中に決まるキー(コンパイル側は翻訳しながらキーを読みます)、State やフィールドやリストに入れたタイムゾーン付きの値、そして fold を断ります。 fold は時計が二度示す一時間のどちらを取るかを選ぶ引数で、この方言は Python の既定と同じく常に先のほうを取ります。 strftime が取るのは、CPython が自ら意味を定める指示子だけです。 %c%x%X%-d は、何を返すかが機械の都合で決まるので断ります。 re からは、Match(re.search を値として使うこと)と実行時に組み立てたパターンを断ります。 後者には、@py を使うように案内します。 小さなモジュールからは、リストをその場で並べ替えるもの(heapq.heappushbisect.insort。リストは State の中にあります)と、textwrap.wrap / fill / shorten(CPython 自身の正規表現で語を分けます)を断ります。 collections からは Counter 以外を断ります。 defaultdict(キーが無いときに何を返すかは、ここでは読む側が決めます)、deque(その場で書き換えるものですが、リストは State の中にあります)、namedtuple@value クラスが型付きで同じことをします)、OrderedDict(ここでの辞書はすでに順序を覚えています)、ChainMap です。 itertools からは、終わらないもの(countcyclerepeat)、それ自体がイテレータを返すもの(groupbytee)、関数を取るもの(starmaptakewhilefilterfalse)、最後のタプルだけ形の違う batched を断ります。 hashlib からは sha256sha1md5 以外を断り、hashlib.sha256(b).hexdigest() 以外の書き方も断ります。 理由を挙げて断るモジュールは、pathlibosdecimal です。
  • 新しい要素の周辺。 表の列幅はドラッグで変えられず、行のキーボード操作と複数選択もありません。 チャートには凡例がありません。 select はキーボードで操作できません。 ツールチップの表示は、スクリプトからホバーで確かめられません。 コンテキストメニューのパネルも、スクリプトからは開けません。 どちらも中身はダンプに出ます(ツールチップの文字列と、メニューの項目。項目は select: で選べます)。 キーボードに関わるものは、ヘッドレスの検証にまだ無いステップを待っています。 ドラッグの方はもう動かせます(split の仕切りは slide: で動きます)。 だから列幅の変更が待っているのは、検証の仕組みではなく要素の側です。
  • 二つめのウィンドウ。 いまは一アプリに一ウィンドウです。 engine のウィンドウルートがビューひとつを前提に書かれていて、ヘッドレス実行のダンプもその一本のツリーだからです。 ショートカット、クリップボード、メニューバー、ファイルダイアログ、落とされたファイル、ツールチップ、複数行フィールドは入っています。
  • 素のラッパを超えるデコレータの形:自分が引数を取るもの、ラッパが関数を二度呼ぶもの、関数の戻り値を使うもの。 関数をそのまま返すデコレータと、一度だけ呼ぶラッパはコンパイルされます。
  • Rust crate の境界での、ペイロード付き enum と、双子へのメソッド。 スカラ、String、List、Optional、str キーの辞書、構造体(入れ子も幅付きフィールドも)、enum、Result(複合型の返りも)までは越えます。 残る二つのうち、ペイロード enum は rpi-gen 自体の残件です。 メソッドは、rpi で宣言済みの struct に実装を接ぐところが残っています。 構造体のフィールドに enum やリストを置く形もまだで、どれも、呼べば理由を挙げて断られます。
  • @py を持つアプリは、Linux では Python を同梱できません--bundle--onefile が作るのは Apple のランタイムフォルダ(ロードコマンドの書き換えとアドホック署名)なので、Linux では名前を挙げて止まります。 エスケープを使わないアプリはどちらでも自己完結で、Linux では --app--appimage が package します。 エスケープを使うアプリは、いまのところホスト側の Python が要ります。
  • Windows。 プラットフォーム自身の答えはツリーに入っています。 キャッシュとアプリ自身のディレクトリの置き場所、ビルドが作る .exe--app がそこに書くフォルダ(--bundle--onefile--appimage は名前を挙げて止まります)、そして Ctrl を指す cmd です。 Windows のランナーでビルドとテストとゲートを回す CI のジョブは、緑になりました。 ワークスペースのテスト、tier gate、両方の実行を突き合わせるデモ、そして作ったのとは別の機械で動かすパッケージ済みのフォルダまで通っています。 足りないのはウィンドウです。 そのランナーでは gpui が Error creating DirectWriteTextSystem で止まるので、アプリは画面をテキストで出せても描けません。 実機の Windows で誰かがウィンドウを開くまで、入れられるのは macOS と Linux です。 いま入れられるのは macOS と Linux です。
  • 実測値はすべて macOS/arm64 のものです。 ほかのプラットフォームは、まだ測っていません。

このリストは、設計が決まるたびに更新されます。 背後にある設計原則は DESIGN.md にまとまっています。