Files
mtgodot-poc/project/python_stdlib_test.gd
T
shenleiandClaude Opus 5 ffbb5d48d6 port(2P step 3c): stage python27.zip into the mobile sandbox
CPython opens the stdlib zip with its own stdio, so it needs a real path.
On the desktop res:// is a directory and globalize_path() is enough; in an
exported build res:// is inside the PCK, which nothing outside Godot reads.

  make_stdlib_zip.py now also writes <out>.sha256
  mtpython_stdlib_project copies python27.zip + .sha256 into project/
    (gitignored), and both export presets' include_filter lists them, so
    they enter the PCK — checked with --export-pack
  extension/src/python_stdlib.{h,cpp} (Metin2Python.stdlib_path in
    GDScript): reads the bytes out of the PCK, writes
    user://python27.zip.part, hashes the file as written against the
    shipped digest, then renames. A sandbox copy is reused only when it
    matches, so a first start killed mid-copy and a zip replaced by a new
    build both re-stage instead of feeding zipimport a torn file.
  PythonHost::SetDefaultStdLibPath / DefaultStdLibPath() answer with that
    path ($MT_PYTHON_STDLIB still wins). python_stdlib.cpp is the only unit
    that knows res:// / user://; port_platform stays godot-free.

Also fixes the Windows gate, which step 3b broke: without mtpython the
MinGW build compiled UserInterface/StdAfx.h (it includes ScriptLib/StdAfx.h,
as the original PCH does). port/CMakeLists.txt excludes that header and
PythonPackModule.cpp with ScriptLib, and platform/CMakeLists.txt excludes
platform/ScriptLib/ the same way.

Not done: on-device staging. The macOS test drives the mobile path with
force_stage=true, and nothing in the app boot calls it yet — the
interpreter only starts in the process with 2V0.

gates: python_stdlib_test.gd PASS (in-place path, staging, sha256, bytes
equal res://, no .part, ZIPReader finds encodings/__init__.py, no re-copy
on a second call, corrupted copy re-staged) · macOS ctest 27/27 ·
mingw + android + ios port_platform compile clean · port_map.py check 0
errors · key leak check 8/8 none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 09:39:33 +09:00

78 lines
3.6 KiB
GDScript

# python_stdlib_test —— 内嵌解释器标准库的落盘(docs/PORT-PLAN.md 批次 2P step 3c)。
# 桌面上 res:// 就是磁盘目录,CPython 直接打开;Android/iOS 的 res:// 在 PCK 里,必须先复制进
# user:// 沙箱并按随包发的 sha256 校验。force_stage=true 就是在桌面上跑那条移动端路径。
# godot --headless --path project --script python_stdlib_test.gd
extends SceneTree
const ZIP_USER := "user://python27.zip"
const ZIP_PART := "user://python27.zip.part"
var _fail := 0
func _ck(ok: bool, message: String) -> void:
if not ok:
_fail += 1
printerr("FAIL: " + message)
func _init() -> void:
if not ClassDB.class_exists("Metin2Python"):
printerr("FAIL: Metin2Python extension is not registered")
quit(1)
return
if not FileAccess.file_exists("res://python27.zip"):
print("SKIP: python_stdlib_test(没有 res://python27.zip,需要 -DMTGODOT_EMBED_PYTHON=ON 的构建)")
quit(0)
return
var want := FileAccess.get_file_as_string("res://python27.zip.sha256").strip_edges()
_ck(want.length() == 64, "res://python27.zip.sha256 是一行 sha256")
# 桌面:res:// 在磁盘上,原地用,不复制。
if DirAccess.dir_exists_absolute(ProjectSettings.globalize_path("res://")):
var in_place := Metin2Python.stdlib_path(false)
_ck(in_place == ProjectSettings.globalize_path("res://python27.zip"),
"res:// 可直接读时返回原路径,不落盘:" + Metin2Python.last_error())
# 移动端路径:复制进沙箱,校验 sha256,返回真实文件系统路径。
if FileAccess.file_exists(ZIP_USER):
DirAccess.remove_absolute(ZIP_USER)
var staged := Metin2Python.stdlib_path(true)
_ck(staged != "", "落盘成功:" + Metin2Python.last_error())
_ck(staged == ProjectSettings.globalize_path(ZIP_USER), "返回的是 user:// 的真实路径")
_ck(FileAccess.file_exists(staged), "返回的路径在文件系统上存在")
_ck(FileAccess.get_sha256(ZIP_USER) == want, "落盘的 zip 与随包的 sha256 一致")
_ck(not FileAccess.file_exists(ZIP_PART), "不留下半成品 .part")
_ck(FileAccess.get_file_as_bytes(ZIP_USER) == FileAccess.get_file_as_bytes("res://python27.zip"),
"落盘的字节与 res:// 里的一致")
# zipimport 读不了 deflate(没有 zlib 模块),所以内容必须是 STORED。
var reader := ZIPReader.new()
var opened := reader.open(staged)
_ck(opened == OK, "落盘的 zip 能打开")
if opened == OK:
_ck(reader.file_exists("encodings/__init__.py"), "zip 里有 encodings 包")
_ck(reader.file_exists("os.py"), "zip 里有 os.py")
reader.close()
# 第二次启动:已经在沙箱里且 sha 对得上,直接复用。
var modified := FileAccess.get_modified_time(ProjectSettings.globalize_path(ZIP_USER))
var again := Metin2Python.stdlib_path(true)
_ck(again == staged, "第二次返回同一路径")
_ck(FileAccess.get_modified_time(ProjectSettings.globalize_path(ZIP_USER)) == modified,
"sha 相符时不重新复制")
# 上次启动被杀在半路 / 换了一版构建:沙箱里的副本对不上就重新落盘。
var f := FileAccess.open(ZIP_USER, FileAccess.WRITE)
f.store_string("torn")
f.close()
_ck(FileAccess.get_sha256(ZIP_USER) != want, "已经把沙箱里的副本弄坏")
var restaged := Metin2Python.stdlib_path(true)
_ck(restaged == staged, "坏副本之后仍返回同一路径:" + Metin2Python.last_error())
_ck(FileAccess.get_sha256(ZIP_USER) == want, "坏副本被重新落盘覆盖")
if _fail == 0:
print("PASS: python_stdlib_test (res:// -> user:// staging + sha256)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)