spike(python): CPython 2.7.18 runs the 40250 root scripts on Android arm64
libpython2.7.a cross-builds with NDK 27.2 (API 24) after disabling HAVE_LANGINFO_H; 30 C modules built in. On a OnePlus 13 (API 36) the harness runs system.py -> prototype.RunApp() and imports the root modules with results identical to macOS (66/74; the 8 failures are stub values or dev-only scripts). py_embed_spike.py now isolates __main__ like CPythonLauncher and reports stdlib usage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,15 @@ py_embed_spike.txt); system.py redirects stdout, so nothing useful is printed.
|
||||
|
||||
python2.7 py_embed_spike.py [--out FILE] (reference root: MT_40250_SOURCE or audit manifest)
|
||||
"""
|
||||
import sys, types, os, glob, traceback, json
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO = os.path.abspath(os.path.join(HERE, "../../../.."))
|
||||
_ref = os.environ.get("MT_40250_SOURCE") or json.load(open(os.path.join(REPO, "audit/manifest.json")))["reference_root"]
|
||||
E = os.path.normpath(os.path.join(REPO, _ref, "../../Client/Eternexus"))
|
||||
import sys, types, os, glob, traceback
|
||||
if os.environ.get("MT_ETERNEXUS"): # on-device run: Eternexus tree pushed next to the binary
|
||||
E = os.environ["MT_ETERNEXUS"]
|
||||
else:
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO = os.path.abspath(os.path.join(HERE, "../../../.."))
|
||||
import json
|
||||
_ref = os.environ.get("MT_40250_SOURCE") or json.load(open(os.path.join(REPO, "audit/manifest.json")))["reference_root"]
|
||||
E = str(os.path.normpath(os.path.join(REPO, _ref, "../../Client/Eternexus"))) # json gives unicode
|
||||
OUT = sys.argv[sys.argv.index("--out") + 1] if "--out" in sys.argv else "py_embed_spike.txt"
|
||||
FILES = {}
|
||||
for p in glob.glob(E + "/root/*"): FILES[os.path.basename(p).lower()] = p
|
||||
@@ -48,7 +52,11 @@ for k, v in STR.items():
|
||||
setattr(sys.modules[m], f, (lambda v: lambda *a: v)(v))
|
||||
import __builtin__
|
||||
__builtin__.TRUE = 1; __builtin__.FALSE = 0 # CPythonLauncher::Create
|
||||
import __main__
|
||||
# A fresh __main__ like CPythonLauncher::Create (whose __builtins__ is the module, not a dict);
|
||||
# system.py and prototype.py exec into it, so they must not share this script's globals.
|
||||
__main__ = types.ModuleType("__main__")
|
||||
__main__.__builtins__ = __builtin__
|
||||
sys.modules["__main__"] = __main__
|
||||
__main__.__DEBUG__ = 0
|
||||
__main__.__COMMAND_LINE__ = ""
|
||||
src = pack.Get("system.py")
|
||||
@@ -56,6 +64,7 @@ try:
|
||||
exec compile(src, "system.py", "exec") in __main__.__dict__
|
||||
except BaseException:
|
||||
LOG.write("system.py raised:\n" + traceback.format_exc())
|
||||
sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__ # system.py redirects both to dbg
|
||||
ok, bad = [], []
|
||||
_pimp = __main__.__dict__["__pack_import"]
|
||||
for name in sorted(FILES):
|
||||
@@ -66,9 +75,22 @@ for name in sorted(FILES):
|
||||
try:
|
||||
_pimp(mod); ok.append(mod)
|
||||
except BaseException as e:
|
||||
bad.append((mod, "%s: %s" % (type(e).__name__, str(e)[:100])))
|
||||
LOG.write("modules loaded %d, failed %d\n" % (len(ok), len(bad)))
|
||||
for m, e in bad: LOG.write(" FAIL %s %s\n" % (m, e))
|
||||
LOG.write("native functions called during bootstrap: %d\n" % len(MISSING))
|
||||
LOG.write(" " + " ".join(sorted(MISSING)) + "\n")
|
||||
bad.append((mod, "%s: %s" % (type(e).__name__, repr(e.args)[:100])))
|
||||
try:
|
||||
LOG.write("modules loaded %d, failed %d\n" % (len(ok), len(bad)))
|
||||
for m, e in bad: LOG.write(" FAIL %s %s\n" % (m, e))
|
||||
LOG.write("native functions called during bootstrap: %d\n" % len(MISSING))
|
||||
LOG.write(" " + " ".join(sorted(MISSING)) + "\n")
|
||||
std = []
|
||||
for name, m in sorted(sys.modules.items()):
|
||||
if m is None or name in NATIVE or name in ("pack", "__main__"): continue
|
||||
try: f = m.__dict__.get("__file__")
|
||||
except Exception: continue
|
||||
if not isinstance(f, str) and f is not None: continue
|
||||
if f is None: std.append("builtin:" + name)
|
||||
elif f.endswith(".so"): std.append("ext:" + name)
|
||||
elif not f.startswith(E) and not f.startswith("<"): std.append("py:" + name)
|
||||
LOG.write("stdlib modules in use: " + " ".join(std) + "\n")
|
||||
except BaseException:
|
||||
LOG.write("report failed:\n" + traceback.format_exc())
|
||||
LOG.close()
|
||||
|
||||
+33
-13
@@ -8,7 +8,7 @@
|
||||
| 项 | 数据 |
|
||||
| --- | --- |
|
||||
| 解释器 | CPython 2.7.6 内嵌(`extern/include/Python-2.7`、`python27.lib`) |
|
||||
| 启动 | `ScriptLib/PythonLauncher.cpp`:`CPythonLauncher::Create` 注入 `TRUE`/`FALSE` 到 `__builtin__`,然后把 `system.py` 作为 `__main__` 运行;`system.py` 替换 `__import__`,从 pack 读源码并 `compile` |
|
||||
| 启动 | `ScriptLib/PythonLauncher.cpp`:`CPythonLauncher::Create` 注入 `TRUE`/`FALSE` 到 `__builtin__`,然后把 `system.py` 作为 `__main__` 运行(`__main__` 的 `__builtins__` 是模块,不是 dict);`system.py` 替换 `__import__`,从 pack 读源码并 `compile` |
|
||||
| 脚本 | `root/*.py` 90 个(其中 74 个 `.py` 模块),`uiscript/uiscript/*.py` 94 个 |
|
||||
| 语法 | 154 个文件在 Python 2.7.18 下全部能编译;Python 3 下有 56 个编译失败;约 292 行只在 Python 2 下成立(`print` 语句、`except X, e`、`has_key`、`iteritems` 等) |
|
||||
| 语义风险 | 约 185 行在整数之间用 `/`(Python 2 是整除,Python 3 是浮点);字符串是 CP949 字节串 |
|
||||
@@ -27,12 +27,16 @@
|
||||
- 按 `CPythonLauncher::Create` 注入 `TRUE`/`FALSE`,所有 C++ 模块用返回 0 的桩代替,`pack` 用目录代替;
|
||||
- 原样运行 40250 的 `system.py`,再通过它自己的 `__pack_import` 导入全部 74 个 root 模块。
|
||||
|
||||
结果:**不改任何脚本,67/74 个模块加载成功**,引导过程一路执行到 `prototype.py` 的
|
||||
`app.Create` 等调用。剩下 7 个失败都是桩的问题,不是解释器或脚本的问题:
|
||||
结果:**不改任何脚本**,`system.py` 引导后 `prototype.py` 完整执行了 `RunApp()`:创建 MainStream、进入 logo
|
||||
阶段、加载 `UIScript/PopupDialog.py`、`app.Loop`、Destroy。之后逐个导入全部 74 个 root 模块,66 个成功。
|
||||
剩下 8 个失败都不是解释器或脚本的问题:
|
||||
|
||||
- `chr.MOTION_SKILL + 121` 这类常量需要真实整数(真实模块用 `PyModule_AddIntConstant` 提供);
|
||||
- locale 数据路径(`app.GetLocaleServiceName` 等返回值决定走哪个 locale 目录);
|
||||
- `rootlibcythonizer`、`test_affect` 是开发用脚本,40250 运行时也不加载。
|
||||
- `prototype`:已经由 `system.py` 执行过,再导入时 `RunApp` 结束触发 `SystemExit`,属正常;
|
||||
- `game`、`interfacemodule`:`chr.MOTION_SKILL + 121` 这类常量需要真实整数(真实模块用 `PyModule_AddIntConstant` 提供);
|
||||
- `introloading`、`consolemodule`:locale 数据路径(`app.GetLocaleServiceName` 等返回值决定走哪个 locale 目录);
|
||||
- `system`:只能作为 `__main__` 运行;`rootlibcythonizer`、`test_affect` 是开发用脚本,40250 运行时也不加载。
|
||||
|
||||
引导过程中脚本调用了 33 个 C++ 模块函数(`app.Create`、`wndMgr.Register`、`net.SetHandler` 等)。
|
||||
|
||||
结论:在 Python 2.7 上,40250 的脚本和导入机制可以原封不动运行;工作量集中在 C++ 模块本身。
|
||||
|
||||
@@ -71,12 +75,28 @@ Godot 这边只剩一个宿主 Control:把输入转发给 `CPythonWindowManage
|
||||
`root/*.py` 和 `uiscript/` 直接从 pack 读取,和 40250 相同。port-map 中 `Client/root` 的 3350 个函数
|
||||
全部记为 `N_A`(原样运行,无需移植),python 层剩下约 1540 个 C++ 函数要移植。
|
||||
|
||||
## 下一步:原型 2(待确认后执行)
|
||||
## Android 验证(2026-09-22,已通过)
|
||||
|
||||
1. 把 CPython 2.7.18 静态编进 `libmtgodot`,先做 macOS arm64 和 Android arm64,再做 iOS 模拟器;
|
||||
2. 在 Godot 进程里运行 `py_embed_spike.py` 的同等流程(C++ 桩模块),确认 67/74 的结果在三个平台上一致;
|
||||
3. 验收门槛:Android 和 iOS 都能加载 `system.py` 并导入 `ui.py`。如果 Android/iOS 编译在合理时间内做不出来,
|
||||
就改用方案 B,并对每处 `/` 做单独核对。
|
||||
设备:一加 13(PJZ110),Android API 36,arm64-v8a。复现:`tools/py_embed_android/build-and-run.sh`。
|
||||
|
||||
原型 2 通过之前,python 层(`Client/root`、`EterPythonLib`、`ScriptLib`、`*Module.cpp`)不开工;
|
||||
逻辑层(阶段 2)不受影响,可以并行推进。
|
||||
- **编译**:CPython 2.7.18 官方源码,用 NDK 27.2 的 `aarch64-linux-android24-clang` 交叉 configure,
|
||||
`make libpython2.7.a` 一次通过。只需要一处改动:关掉 `HAVE_LANGINFO_H`,因为 bionic 从 API 26 才声明 `nl_langinfo`。
|
||||
另外 30 个 C 模块静态编进去,包括 `posix` `math` `time` `_struct` `operator` `itertools` `_collections`
|
||||
`cStringIO` `cPickle` `_locale` `binascii` `datetime` `select` `fcntl` 等。全部编译通过,库大小 13 MB(含调试信息)。
|
||||
- **链接**:测试程序 `tools/py_embed_android/main.c` 链接 `libpython2.7.a -lm -ldl`,没有任何未定义符号。
|
||||
- **运行**:`Py_NoSiteFlag`/`Py_IgnoreEnvironmentFlag`/`Py_FrozenFlag` 启动,标准库以不压缩的 zip(8 MB)
|
||||
加入 `sys.path`,然后运行 `py_embed_spike.py`:**结果与 macOS 逐项相同**,66/74 个模块加载,失败的 8 个完全相同,
|
||||
引导期调用的 33 个 C++ 函数也完全相同。
|
||||
- **尚未验证**:
|
||||
- 这次是用 `adb shell` 运行的独立可执行文件,还没有在 APK 的 app 进程里跑过;
|
||||
- 标准库是从文件系统读的,还没有走 PCK 或 `asset_io`。
|
||||
|
||||
这两点属于集成工作,不涉及解释器本身能不能跑。
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 把 CPython 2.7.18 放进 `extension/third_party/`,用我们自己的 CMake 编译(macOS / Android / iOS 共用一份源码清单和
|
||||
`pyconfig.h`),静态链接进 `libmtgodot`;
|
||||
2. `pack` 模块走 `asset_io`,标准库的纯 Python 文件放进资源包,用 `system.py` 自带的导入钩子加载;
|
||||
3. 在 APK 里跑同样的引导流程(C++ 桩模块),然后按同样方法验证 iOS;
|
||||
4. 之后 python 层才开始移植:`ScriptLib/PythonLauncher.cpp` → `EterPythonLib` 窗口系统 → `*Module.cpp`。
|
||||
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# Embedded CPython 2.7.18 on Android: build libpython2.7.a with the NDK, link a harness that runs
|
||||
# py_embed_spike.py on a connected device. See docs/PYTHON-EMBED-EVAL.md ("Android 验证").
|
||||
# tools/py_embed_android/build-and-run.sh [WORKDIR] (default: build/py_embed_android)
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO="$(cd "$HERE/../.." && pwd)"
|
||||
W="${1:-$REPO/build/py_embed_android}"
|
||||
NDK="${ANDROID_NDK_HOME:-$(ls -d "$HOME/Library/Android/sdk/ndk/"* | sort -V | head -1)}"
|
||||
TC="$NDK/toolchains/llvm/prebuilt/$(ls "$NDK/toolchains/llvm/prebuilt" | head -1)/bin"
|
||||
API=24
|
||||
ADB="${ADB:-adb}"
|
||||
REF="${MT_40250_SOURCE:-$REPO/$(python3 -c 'import json;print(json.load(open("'"$REPO"'/audit/manifest.json"))["reference_root"])')}"
|
||||
E="$REF/../../Client/Eternexus"
|
||||
DEV=/data/local/tmp/pyspike
|
||||
|
||||
mkdir -p "$W" && cd "$W"
|
||||
[ -f Python-2.7.18.tar.xz ] || curl -sSLO https://www.python.org/ftp/python/2.7.18/Python-2.7.18.tar.xz
|
||||
echo "b62c0e7937551d0cc02b8fd5cb0f544f9405bafc9a54d3808ed4594812edef43 Python-2.7.18.tar.xz" | shasum -a 256 -c -
|
||||
[ -d Python-2.7.18 ] || tar xf Python-2.7.18.tar.xz
|
||||
touch Python-2.7.18/Include/graminit.h Python-2.7.18/Python/graminit.c \
|
||||
Python-2.7.18/Include/Python-ast.h Python-2.7.18/Python/Python-ast.c # no host pgen needed
|
||||
|
||||
mkdir -p cfg && cd cfg
|
||||
if [ ! -f pyconfig.h ]; then
|
||||
env -u CPPFLAGS -u LDFLAGS -u CFLAGS -u LIBS CONFIG_SITE=/dev/null \
|
||||
CC="$TC/aarch64-linux-android$API-clang" AR="$TC/llvm-ar" RANLIB="$TC/llvm-ranlib" READELF="$TC/llvm-readelf" \
|
||||
../Python-2.7.18/configure --host=aarch64-linux-android --build=aarch64-apple-darwin \
|
||||
--disable-ipv6 --without-ensurepip --disable-shared ac_cv_file__dev_ptmx=yes ac_cv_file__dev_ptc=no > configure.log
|
||||
# bionic declares nl_langinfo only from API 26
|
||||
sed -i '' 's|^#define HAVE_LANGINFO_H 1|/* #undef HAVE_LANGINFO_H (nl_langinfo needs API 26) */|' pyconfig.h
|
||||
# compile the C modules the 40250 scripts and their stdlib imports need into libpython
|
||||
for m in array math _struct time operator _random _collections _heapq itertools strop _functools datetime \
|
||||
_bisect _locale binascii cStringIO cPickle select fcntl; do
|
||||
sed -i '' -E "s/^#($m )/\1/" Modules/Setup
|
||||
done
|
||||
sed -i '' -E 's/^(math mathmodule.c _math.c) # -lm/\1/; s/^(time timemodule.c) # -lm/\1/; s/^(_locale _localemodule.c) # -lintl/\1/' Modules/Setup
|
||||
rm -f Modules/config.c
|
||||
fi
|
||||
env -u CPPFLAGS -u LDFLAGS -u CFLAGS make -j8 libpython2.7.a > make.log
|
||||
cd ..
|
||||
|
||||
"$TC/aarch64-linux-android$API-clang" -O2 -fPIE -pie -Icfg -IPython-2.7.18/Include "$HERE/main.c" \
|
||||
cfg/libpython2.7.a -lm -ldl -o pyspike
|
||||
(cd Python-2.7.18/Lib && rm -f "$W/python27.zip" && zip -q -0 -r "$W/python27.zip" . -i '*.py' \
|
||||
-x 'test/*' 'lib-tk/*' 'idlelib/*' 'lib2to3/*' 'bsddb/*' 'ensurepip/*' '*/test/*' '*/tests/*' 'plat-*')
|
||||
|
||||
rm -rf stage && mkdir -p stage/Eternexus/locale_en
|
||||
cp -R "$E/root" "$E/uiscript" stage/Eternexus/ && cp -R "$E/locale_en/locale" stage/Eternexus/locale_en/
|
||||
cp pyspike python27.zip "$REPO/.agents/skills/metin2-40250-parity-audit/scripts/py_embed_spike.py" stage/
|
||||
"$ADB" shell rm -rf "$DEV" && "$ADB" push stage "$DEV" >/dev/null && "$ADB" shell chmod 755 "$DEV/pyspike"
|
||||
"$ADB" shell "cd $DEV && MT_ETERNEXUS=$DEV/Eternexus ./pyspike $DEV 2>&1 | grep -v 'ignored\$' | head -3; sed -n '/modules loaded/,/native functions/p' result.txt"
|
||||
@@ -0,0 +1,33 @@
|
||||
/* On-device check: embed CPython 2.7 the way CPythonLauncher does, then run py_embed_spike.py. */
|
||||
#include <Python.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *home = argc > 1 ? argv[1] : "/data/local/tmp/pyspike";
|
||||
char buf[2048], spike[1024];
|
||||
|
||||
Py_NoSiteFlag = 1;
|
||||
Py_IgnoreEnvironmentFlag = 1;
|
||||
Py_DontWriteBytecodeFlag = 1;
|
||||
Py_FrozenFlag = 1; /* no "Could not find platform libraries" noise */
|
||||
Py_SetProgramName(argv[0]);
|
||||
Py_SetPythonHome((char *)home);
|
||||
Py_Initialize();
|
||||
printf("Py_Initialize ok: %s\n", Py_GetVersion());
|
||||
|
||||
snprintf(buf, sizeof buf,
|
||||
"import sys\n"
|
||||
"sys.path[:] = ['%s/python27.zip']\n"
|
||||
"sys.argv = ['py_embed_spike.py', '--out', '%s/result.txt']\n", home, home);
|
||||
if (PyRun_SimpleString(buf) != 0)
|
||||
return 2;
|
||||
|
||||
snprintf(spike, sizeof spike, "%s/py_embed_spike.py", home);
|
||||
FILE *fp = fopen(spike, "r");
|
||||
if (!fp) { perror(spike); return 3; }
|
||||
int rc = PyRun_SimpleFileExFlags(fp, spike, 1, NULL);
|
||||
printf("spike rc=%d\n", rc);
|
||||
Py_Finalize();
|
||||
return rc;
|
||||
}
|
||||
Reference in New Issue
Block a user