Files
mtgodot-poc/tools/py_embed/make_stdlib_zip.py
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

65 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""Pack the vendored CPython 2.7.18 Lib/ into the python27.zip the host puts on sys.path.
make_stdlib_zip.py <Lib dir> <out.zip>
40250 shipped a real python27 installation, so `import encodings` found Lib/ through the
interpreter's prefix. The embedded interpreter has no prefix (Py_FrozenFlag, no site), so the host
supplies the stdlib as this one file — the arrangement validated on the device by
tools/py_embed_android/build-and-run.sh (PORT-PLAN 批次 2P step 3).
Entries are STORED, never deflated: the zlib module is not in the vendored Modules/ (see
docs/THIRD-PARTY.md), so zipimport could not inflate them. Only .py goes in; zipimport reads
source as happily as bytecode, and .pyc would pin the magic number to the build host's interpreter.
Written deterministically (sorted, fixed timestamp) so the file's sha256 identifies its contents.
The hash is written next to the zip as <out>.sha256 and ships with it: on Android/iOS the zip lives
in the read-only bundle and has to be copied into the sandbox before CPython can open it, and that
copy is accepted only when it hashes to this value (extension/src/python_stdlib.cpp, step 3c).
"""
from __future__ import annotations
import hashlib
import sys
import zipfile
from pathlib import Path
# Test suites, the Tk/2to3/bsddb/pip trees and the platform-specific stubs: none of the 40250 scripts
# or their imports reach them, and they are most of the size.
SKIP_DIRS = {"test", "tests", "lib-tk", "idlelib", "lib2to3", "bsddb", "ensurepip", "curses",
"distutils", "unittest", "wsgiref", "multiprocessing", "compiler"}
def main() -> int:
if len(sys.argv) != 3:
print(__doc__, file=sys.stderr)
return 2
lib, out = Path(sys.argv[1]), Path(sys.argv[2])
if not (lib / "os.py").is_file():
print(f"make_stdlib_zip: {lib} is not a CPython Lib/ directory", file=sys.stderr)
return 1
members = []
for path in sorted(lib.rglob("*.py")):
rel = path.relative_to(lib)
if rel.parts[0].startswith("plat-") or SKIP_DIRS.intersection(rel.parts[:-1]):
continue
members.append((rel, path))
out.parent.mkdir(parents=True, exist_ok=True)
tmp = out.with_suffix(".zip.tmp")
with zipfile.ZipFile(tmp, "w", zipfile.ZIP_STORED) as zf:
for rel, path in members:
info = zipfile.ZipInfo(rel.as_posix(), date_time=(1980, 1, 1, 0, 0, 0))
info.external_attr = 0o644 << 16
zf.writestr(info, path.read_bytes())
tmp.replace(out)
digest = hashlib.sha256(out.read_bytes()).hexdigest()
out.with_name(out.name + ".sha256").write_text(digest + "\n")
print(f"make_stdlib_zip: {out} ({len(members)} modules, {out.stat().st_size} bytes, sha256 {digest})")
return 0
if __name__ == "__main__":
raise SystemExit(main())