step 3a — 标准库: tools/py_embed/make_stdlib_zip.py packs the vendored Lib/ into <build>/python27.zip (397 modules, 6876547 bytes, ZIP_STORED — the zlib module is trimmed out of our CPython, so zipimport cannot inflate). The mtpython_stdlib target builds it; py_embed_test and the launcher test both run against it. platform/ScriptLib/PythonHost sets the embedding flags before Py_Initialize and, after it, puts the zip on sys.path and re-registers encodings.search_function with codecs — 2.7 builds the codec registry once, at startup, so replacing sys.path afterwards needs it back. step 3b — pack 模块: UserInterface/PythonPackModule.cpp copied verbatim; packExist/packGet/ initpack read root/ and uiscript/ through CEterPackManager, i.e. through asset_io. The CHINA_CRYPT_KEY block in that unit is CAccountConnector's and is kept #if 0 until the login slice (2V1). UserInterface/StdAfx.h is a documented partial mirror — the original is the whole client's PCH. The launcher test now runs initpack() as RunMainScript does (its first line), reads system.py / uiscript / locale text back out of the packs, and confirms cp1252 decoding works off the zip. system.py still stops at "No module named app" — the C++ modules are the 2V0 slice. gates: port_map.py check 0 errors · key leak check 8/8 none · macOS ctest 27/27 · android + ios port_platform compile clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
208 lines
9.2 KiB
C++
208 lines
9.2 KiB
C++
// port/ScriptLib CPythonLauncher on the embedded interpreter (批次 2P step 2), driven exactly as
|
|
// 40250 UserInterface.cpp does it: Main() creates the launcher, calls Create(), and RunMainScript()
|
|
// then sets __DEBUG__ and __COMMAND_LINE__ with RunLine before RunFile("system.py") reads the script
|
|
// out of the registered packs (UserInterface.cpp:241-353, 419-434).
|
|
//
|
|
// The script layer's own modules (initapp, initpack, initwndMgr, ...) arrive with the 2V0 slice and the
|
|
// stdlib with step 3, so system.py is expected to stop at its first import ("No module named app") —
|
|
// what this test proves is the launcher: the interpreter starts, RunLine/RunMemoryTextFile/
|
|
// RunCompiledFile run code, and RunFile reaches the real script through CEterPackManager.
|
|
//
|
|
// port_python_launcher_test <40250 Client dir>
|
|
//
|
|
// Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which fails instead.
|
|
#include "ScriptLib/StdAfx.h"
|
|
#include "ScriptLib/PythonLauncher.h"
|
|
#include "EterPack/EterPackManager.h"
|
|
#include "UserInterface/StdAfx.h" // initpack
|
|
#include "../src/platform/ScriptLib/PythonHost.h"
|
|
#include "../src/platform/UserInterface/UserInterface.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#include <unistd.h>
|
|
|
|
static int g_failures = 0;
|
|
#define CHECK(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
|
|
++g_failures; \
|
|
} \
|
|
} while (0)
|
|
|
|
// The dictionary the launcher runs everything in, and the globals it leaves behind there.
|
|
static PyObject* main_dict()
|
|
{
|
|
return PyModule_GetDict(PyImport_AddModule((char*) "__main__"));
|
|
}
|
|
|
|
static long main_long(const char* name)
|
|
{
|
|
PyObject* main_module = PyImport_AddModule((char*) "__main__");
|
|
PyObject* value = PyDict_GetItemString(PyModule_GetDict(main_module), name);
|
|
return value && PyInt_Check(value) ? PyInt_AsLong(value) : -1;
|
|
}
|
|
|
|
static std::string main_string(const char* name)
|
|
{
|
|
PyObject* main_module = PyImport_AddModule((char*) "__main__");
|
|
PyObject* value = PyDict_GetItemString(PyModule_GetDict(main_module), name);
|
|
return value && PyString_Check(value) ? PyString_AS_STRING(value) : "";
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
const char* strict = std::getenv("MT_ASSETS_STRICT");
|
|
const bool is_strict = strict && std::string(strict) == "1";
|
|
const std::string client = argc > 1 ? argv[1] : "";
|
|
const std::string stdlib = argc > 2 ? argv[2] : PythonHost::DefaultStdLibPath();
|
|
if (client.empty() || chdir(client.c_str()) != 0 || access("pack/Index", 0) != 0)
|
|
{
|
|
std::fprintf(stderr, "port_python_launcher_test: no 40250 Client/pack at '%s'\n", client.c_str());
|
|
return is_strict ? 1 : 77;
|
|
}
|
|
|
|
PackSingletons();
|
|
CHECK(PackInitialize("pack"));
|
|
|
|
// What a real python27 installation did for 40250, the host does here: flags before Py_Initialize,
|
|
// stdlib on sys.path after it (platform/ScriptLib/PythonHost.cpp).
|
|
PythonHost::Configure();
|
|
|
|
{
|
|
CPythonLauncher launcher; // ctor runs Py_Initialize, as 40250 Main() does on the stack
|
|
CHECK(Py_IsInitialized());
|
|
CHECK(launcher.Create());
|
|
|
|
std::string stdlib_error;
|
|
CHECK(PythonHost::InstallStdLib(stdlib.c_str(), &stdlib_error));
|
|
if (!stdlib_error.empty())
|
|
std::fprintf(stderr, "InstallStdLib: %s\n", stdlib_error.c_str());
|
|
// The stdlib is what the script layer's imports and the CP949/cp1252 text in the packs need.
|
|
CHECK(launcher.RunLine("import os, string, types, codecs\nassert u'\\xe4'.encode('cp1252') == '\\xe4'"));
|
|
|
|
// RunMainScript's first line (UserInterface.cpp:243): the pack module, which is how system.py's
|
|
// import hook and the window system read root/ and uiscript/ out of the registered packs.
|
|
initpack();
|
|
CHECK(launcher.RunLine("import pack"));
|
|
CHECK(launcher.RunLine("assert pack.Exist('system.py') == 1"));
|
|
CHECK(launcher.RunLine("assert pack.Exist('no/such.py') == 0"));
|
|
CHECK(launcher.RunLine("__pack_len__ = len(pack.Get('system.py'))"));
|
|
CHECK(launcher.RunLine("assert len(pack.Get('uiscript/selectcharacterwindow.py')) > 0"));
|
|
CHECK(launcher.RunLine("assert len(pack.Get('locale/en/locale_game.txt')) > 0"));
|
|
// Only .py/.pyc/.txt come out of it, and a miss is None rather than an exception
|
|
// (packGet returns Py_BuildException(), which clears the error and builds None).
|
|
CHECK(launcher.RunLine("assert pack.Get('icon/item/00000.tga') is None"));
|
|
CHECK(launcher.RunLine("assert pack.Get('no/such.py') is None"));
|
|
|
|
// RegisterDebugFlag / RegisterCommandLine, verbatim from RunMainScript.
|
|
CHECK(launcher.RunLine("__DEBUG__ = 0"));
|
|
CHECK(launcher.RunLine("__COMMAND_LINE__ = \"\""));
|
|
CHECK(main_long("__DEBUG__") == 0);
|
|
CHECK(main_string("__COMMAND_LINE__") == "");
|
|
|
|
// Create() adds TRUE/FALSE to __builtin__, which the 40250 scripts use unqualified.
|
|
CHECK(launcher.RunLine("__ok__ = (TRUE, FALSE)"));
|
|
CHECK(launcher.RunLine("assert TRUE == 1 and FALSE == 0"));
|
|
|
|
// A failing line reports through Traceback(), which fetches (and so clears) the exception.
|
|
CHECK(!launcher.RunLine("raise RuntimeError('port test')"));
|
|
CHECK(PyErr_Occurred() == NULL);
|
|
|
|
// GetError() reads the pending exception's value with neither normalization nor a NULL check,
|
|
// so it is only defined while an error really is pending and was set with a string value — after
|
|
// RunLine it never is, because Traceback() got there first. Kept verbatim: it is what 40250 does.
|
|
PyErr_SetString(PyExc_RuntimeError, "port test error");
|
|
CHECK(std::string(launcher.GetError()) == "port test error");
|
|
PyErr_Clear();
|
|
|
|
// RunMemoryTextFile wraps the text in exec(compile(...)) after dropping CR, so a CRLF script
|
|
// from the pack compiles and the file name it is given reaches the code object.
|
|
const char crlf_script[] = "__mem__ = 1\r\ndef f():\r\n\treturn f.func_code.co_filename\r\n__mem__ = f()\r\n";
|
|
CHECK(launcher.RunMemoryTextFile("crlf.py", sizeof(crlf_script) - 1, crlf_script));
|
|
CHECK(main_string("__mem__") == "crlf.py");
|
|
|
|
// RunCompiledFile reads a .pyc through the ported PythonMarshal readers: magic, timestamp,
|
|
// then the code object.
|
|
const std::string pyc = std::string(std::getenv("TMPDIR") ? std::getenv("TMPDIR") : "/tmp") +
|
|
"/port_python_launcher_test.pyc";
|
|
PyObject* code = Py_CompileString("__pyc__ = 7\n", "compiled.py", Py_file_input);
|
|
CHECK(code != NULL);
|
|
if (code)
|
|
{
|
|
FILE* fp = fopen(pyc.c_str(), "wb");
|
|
CHECK(fp != NULL);
|
|
if (fp)
|
|
{
|
|
PyMarshal_WriteLongToFile(PyImport_GetMagicNumber(), fp, Py_MARSHAL_VERSION);
|
|
PyMarshal_WriteLongToFile(0, fp, Py_MARSHAL_VERSION); // mtime, ignored on read
|
|
PyMarshal_WriteObjectToFile(code, fp, Py_MARSHAL_VERSION);
|
|
fclose(fp);
|
|
CHECK(launcher.RunCompiledFile(pyc.c_str()));
|
|
CHECK(main_long("__pyc__") == 7);
|
|
}
|
|
Py_DECREF(code);
|
|
std::remove(pyc.c_str());
|
|
}
|
|
CHECK(!launcher.RunCompiledFile("no/such.pyc"));
|
|
|
|
// RunFile pulls the script out of the packs. system.py is the 40250 entry point; without the
|
|
// stdlib (step 3) and the C++ modules (2V0) it stops at its first import, which is still proof
|
|
// that the pack read path and the compile reached Python.
|
|
{
|
|
CMappedFile file;
|
|
const VOID* data = NULL;
|
|
CHECK(CEterPackManager::Instance().Get(file, "system.py", &data));
|
|
CHECK(file.Size() > 0);
|
|
CHECK(main_long("__pack_len__") == (long) file.Size()); // pack.Get returned the same entry
|
|
|
|
// Where it stops, spelled out: RunFile reports through Traceback(), which fetches the
|
|
// exception and so leaves nothing behind, and LogBoxf is a platform stub. Running the same
|
|
// source here once names it: today "No module named app", i.e. system.py compiles and runs
|
|
// until the first of the C++ modules RunMainScript registers (initapp, 2V0 slice) — the
|
|
// stdlib imports behind it come with step 3.
|
|
std::string source((const char*) data, file.Size());
|
|
source.erase(std::remove(source.begin(), source.end(), '\r'), source.end());
|
|
PyObject* result = PyRun_String(source.c_str(), Py_file_input, main_dict(), main_dict());
|
|
CHECK(result == NULL);
|
|
if (result)
|
|
Py_DECREF(result);
|
|
else
|
|
{
|
|
PyObject *exc = NULL, *value = NULL, *tb = NULL;
|
|
PyErr_Fetch(&exc, &value, &tb);
|
|
PyErr_NormalizeException(&exc, &value, &tb);
|
|
PyObject* text = value ? PyObject_Str(value) : NULL;
|
|
std::printf("system.py stops at %s: %s\n",
|
|
exc ? ((PyTypeObject*) exc)->tp_name : "?",
|
|
text ? PyString_AsString(text) : "");
|
|
CHECK(exc == PyExc_ImportError);
|
|
Py_XDECREF(text);
|
|
Py_XDECREF(exc);
|
|
Py_XDECREF(value);
|
|
Py_XDECREF(tb);
|
|
}
|
|
}
|
|
// Whatever it stops at, RunLine's Traceback() has already reported and cleared it, so there is
|
|
// nothing left for GetError() to read here either.
|
|
const bool ran = launcher.RunFile("system.py");
|
|
CHECK(!ran);
|
|
CHECK(PyErr_Occurred() == NULL);
|
|
CHECK(!launcher.RunFile("no/such/script.py")); // not in any pack: no file, no exception
|
|
|
|
launcher.Clear(); // Main() calls Clear() explicitly before the launcher leaves scope
|
|
}
|
|
CHECK(!Py_IsInitialized());
|
|
|
|
if (g_failures)
|
|
std::fprintf(stderr, "%d failure(s)\n", g_failures);
|
|
else
|
|
std::printf("port_python_launcher_test: PASS\n");
|
|
return g_failures ? 1 : 0;
|
|
}
|