feat(python): 在 app 进程里起 40250 脚本层(2P step 4,macOS)

移植 UserInterface/RunMainScript 到 platform/ScriptLib/PythonBoot:40250 把
CPythonLauncher 放在 Main() 的栈上,这里进程归 Godot,launcher 必须活过创建它的
那次调用,所以拆成 Start / RunMainScript / RunLine / Stop。37 个模块初始化函数里
只调已移植的 initpack(),system.py 因此仍停在第一个 import。

脚本报错能传回 GDScript,靠的是 platform/EterBase/Debug.cpp 实现 LogBox/LogBoxf:
ScriptLib 的 Traceback() 在 PyErr_Fetch 之后只剩这一条路,LastLogBoxMessage()
(platform/EterBase/LogBox.h)是 traceback 唯一幸存的地方。

GDScript 侧新增 Metin2PythonHost(python_host_node.{h,cpp})——唯一同时认识
godot-cpp 和 port 树的单元,PythonBoot 保持 godot-free。检查脚本
python_host_check.gd 分两段:解释器 + 标准库那段不需要 40250 的 pack,哪个平台都
能跑;system.py 那段需要 pack,没有就跳过。桌面入口 python_host_test.gd,真机入口
MT_TEST_MODE=python(导出模板不接受 --script)。

验证(macOS):标准库可 import、codec 注册表可用、sys.path 非空,system.py 停在和
ctest port.python_launcher 完全相同的 ImportError: No module named app;
ctest 27/27。Android/iOS 进程内未做,正式启动路径仍不起解释器(2V0)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-23 10:43:10 +09:00
co-authored by Claude Opus 5
parent e926759ff5
commit 0a1cdf3d3e
16 changed files with 638 additions and 76 deletions
+11
View File
@@ -24,6 +24,17 @@ func _ready() -> void:
t.name = "PackageRenderTest"
add_child(t)
return
# MT_TEST_MODE=python:在 app 进程里起内嵌解释器跑 system.pyPORT-PLAN 批次 2P step 4)。
# 导出模板不接受 --script,这是在真机 APK/IPA 里跑这条检查的唯一入口。
if OS.get_environment("MT_TEST_MODE") == "python":
AssetPack.ensure()
var fail: int = preload("res://python_host_check.gd").new().run()
if fail == 0:
print("PASS: python_host_check (app 进程内 system.py)")
else:
printerr("%d check(s) failed" % fail)
get_tree().quit(1 if fail else 0)
return
if OS.get_environment("MT_TEST_MODE") == "forest_render":
var forest := ForestMapRenderTest.new()
forest.name = "ForestMapRenderTest"
+60
View File
@@ -0,0 +1,60 @@
# python_host_check —— 在 app 进程里把内嵌解释器起来并跑 system.pydocs/PORT-PLAN.md 批次 2P step 4)。
#
# 和 ctest 的 `port.python_launcher` 跑的是同一条序列,区别只有一个:那边是独立可执行文件,这边在
# Godot 进程里,由 GDScript 发起 —— step 4 要验的就是这个差别。2V0 把 C++ 模块注册上之前,
# system.py 停在第一个 import`No module named app`),这就是当前的验收结果。
#
# 真机上通常没有 40250 的 Client/packAPK 里只有 python27.zip),所以分两段:解释器 + 标准库这一段
# 不需要 pack,任何平台都跑;system.py 这一段需要 pack,没有就跳过并说清楚。
#
# 桌面:python_host_test.gd--script);真机:MT_TEST_MODE=python 从 client_main.gd 进来。
extends RefCounted
# 2V0 之前 system.py 必然停在这里;2V0 之后这一行要跟着改。
const EXPECTED_STOP := "No module named app"
var fail := 0
func _ck(ok: bool, message: String) -> void:
if not ok:
fail += 1
printerr("FAIL: " + message)
else:
print("ok: " + message)
# 返回失败数;0 表示落在预期的停止点上(或按上面的规则跳过)。
func run() -> int:
if not ClassDB.class_exists("Metin2PythonHost"):
printerr("FAIL: Metin2PythonHost extension is not registered")
return 1
if not FileAccess.file_exists("res://python27.zip"):
print("SKIP: python_host_check(没有 res://python27.zip,需要 -DMTGODOT_EMBED_PYTHON=ON 的构建)")
return 0
# 40250 Main() 先注册 pack,再造 CPythonLauncher —— system.py 是从 pack 里读出来的。
var has_pack := AssetRoot.pack_ready()
var start_error: String = Metin2PythonHost.start("")
_ck(start_error == "", "解释器在 app 进程里起来了:" + start_error)
if start_error != "":
return fail
_ck(Metin2PythonHost.is_running(), "is_running() 为真")
_ck(Metin2PythonHost.start("") == "", "重复 start() 是空操作")
# 标准库真的在 sys.path 上(真机是沙盒里那份 python27.zip),codec 注册表也重建过。
_ck(Metin2PythonHost.run_line("import os, string, types, codecs") == "", "标准库可 import")
_ck(Metin2PythonHost.run_line("assert u'\\xe4'.encode('cp1252') == '\\xe4'") == "", "codec 注册表可用")
_ck(Metin2PythonHost.run_line("import sys; assert sys.path") == "", "sys.path 非空")
if has_pack:
# RunMainScriptinitpack() + __DEBUG__ + __COMMAND_LINE__ + RunFile("system.py")。
var err: String = Metin2PythonHost.run_main_script("")
print("system.py -> %s" % err)
_ck(err.contains(EXPECTED_STOP),
"system.py 停在预期的 '%s'2V0 之后这里会往前走)" % EXPECTED_STOP)
else:
print("SKIP: system.py 这一段(没有 40250 Client/pack%s" % AssetRoot.client_path())
Metin2PythonHost.stop()
_ck(not Metin2PythonHost.is_running(), "stop() 之后 is_running() 为假")
return fail
+15
View File
@@ -0,0 +1,15 @@
# python_host_test —— 桌面上跑 python_host_checkdocs/PORT-PLAN.md 批次 2P step 4)。
# godot --headless --path project --script python_host_test.gd
# 真机上同一份检查从 client_main.gd 的 MT_TEST_MODE=python 进来。
extends SceneTree
const PythonHostCheck = preload("res://python_host_check.gd")
func _init() -> void:
var fail: int = PythonHostCheck.new().run()
if fail == 0:
print("PASS: python_host_test (app 进程内 system.py)")
quit(0)
else:
printerr("%d check(s) failed" % fail)
quit(1)