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>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user