chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
95
tools/web/build-webengine-python.py
Normal file
95
tools/web/build-webengine-python.py
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the local desktop-version CPython archive for the WebEngine memory."""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--source', type=Path, default=Path('/home/mes123456/working/build_oiio_deps/build/python/src/external_python'))
|
||||
args = parser.parse_args()
|
||||
base = ROOT / 'build_web_python313'
|
||||
source, build = base / 'source', base / 'build'
|
||||
report_dir = ROOT / 'tests/golden/WBV2-P1424-001/embedded-python-build' / datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')
|
||||
report_dir.mkdir(parents=True)
|
||||
report = dict(status='BUILDING', validation='NOT_RUN', source=str(args.source), steps=[])
|
||||
try:
|
||||
patchlevel = (args.source / 'Include/patchlevel.h').read_text()
|
||||
if '#define PY_VERSION "3.13.13"' not in patchlevel:
|
||||
raise ValueError('The embedded runtime requires the supplied Blender CPython 3.13.13 source')
|
||||
report['version'] = '3.13.13'
|
||||
report['sourceIdentity'] = {name: hashlib.sha256((args.source / name).read_bytes()).hexdigest()
|
||||
for name in ['Include/patchlevel.h', 'configure', 'LICENSE']}
|
||||
if not source.exists():
|
||||
shutil.copytree(args.source, source, ignore=shutil.ignore_patterns(
|
||||
'*.o', '*.a', '*.so', '*.so.*', '*.pyc', '__pycache__', '.git', 'build',
|
||||
'python', 'Makefile', 'pyconfig.h', 'config.log', 'config.status', 'config.cache',
|
||||
'pybuilddir.txt', '_bootstrap_python', '_freeze_module', 'Setup.local', 'Setup.stdlib', 'Setup.bootstrap', 'config.c'))
|
||||
# Keep the copied upstream source reproducible without rebuilding or
|
||||
# replacing the archive on every implementation cycle.
|
||||
if 'web_engine_python_unraisable' not in (source / 'Python/errors.c').read_text():
|
||||
subprocess.run(['patch', '-p1', '-i',
|
||||
str(ROOT / 'tools/web/patches/cpython-3.13-webengine-unraisable.patch')],
|
||||
cwd=source, check=True)
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
build_python = ROOT / '.deps/native/python/bin/python3.13'
|
||||
env = dict(os.environ)
|
||||
env['CFLAGS'] = (env.get('CFLAGS', '') + ' -DPY_CALL_TRAMPOLINE=1').strip()
|
||||
env['LD_LIBRARY_PATH'] = str(ROOT / '.deps/native/python/lib') + ':' + env.get('LD_LIBRARY_PATH', '')
|
||||
env['PYTHONHOME'] = str(ROOT / '.deps/native/python')
|
||||
def run(name, command):
|
||||
with (report_dir / (name + '.log')).open('w') as log:
|
||||
result = subprocess.run(command, cwd=ROOT, env=env, stdout=log, stderr=subprocess.STDOUT)
|
||||
report['steps'].append(dict(name=name, exitCode=result.returncode))
|
||||
if result.returncode:
|
||||
raise RuntimeError(name + ' failed; see ' + str(report_dir))
|
||||
if not (build / 'Makefile').exists() or 'PY_CALL_TRAMPOLINE' not in (build / 'Makefile').read_text():
|
||||
# Shell arguments stay positional; source paths are never interpolated as shell code.
|
||||
run('configure', ['bash', '-c',
|
||||
'source tools/web/emscripten-env.sh && cd "$1" && CONFIG_SITE="$2/Tools/wasm/config.site-wasm32-emscripten" emconfigure "$2/configure" --host=wasm32-unknown-emscripten --build=x86_64-pc-linux-gnu --with-emscripten-target=browser --disable-wasm-pthreads --disable-wasm-dynamic-linking --disable-shared --without-ensurepip --disable-test-modules --prefix=/python --with-build-python="$3"',
|
||||
'web-python-build', str(build), str(source), str(build_python)])
|
||||
configuration = build / 'pyconfig.h'
|
||||
if '#define PY_CALL_TRAMPOLINE 1' not in configuration.read_text():
|
||||
configuration.write_text(configuration.read_text() + '\n#define PY_CALL_TRAMPOLINE 1\n')
|
||||
frozen = build / 'Python/frozen_modules/getpath.h'
|
||||
frozen.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not frozen.exists():
|
||||
run('freeze-getpath', [str(build_python), str(source / 'Programs/_freeze_module.py'),
|
||||
'getpath', str(source / 'Modules/getpath.py'), str(frozen)])
|
||||
run('archive', ['bash', '-c', 'source tools/web/emscripten-env.sh && emmake make -C "$1" -j4 libpython3.13.a', 'web-python-build', str(build)])
|
||||
stdlib = base / 'python313.zip'
|
||||
with zipfile.ZipFile(stdlib, 'w', compression=zipfile.ZIP_STORED) as archive:
|
||||
for file in sorted((source / 'Lib').rglob('*.py')):
|
||||
relative = file.relative_to(source / 'Lib')
|
||||
if any(part in {'test', 'tests', '__pycache__', 'idlelib', 'tkinter', 'turtledemo', 'ensurepip'} for part in relative.parts):
|
||||
continue
|
||||
archive.write(file, relative.as_posix())
|
||||
report['artifacts'] = {}
|
||||
for file in [build / 'libpython3.13.a', build / 'pyconfig.h', stdlib,
|
||||
build / 'Modules/expat/libexpat.a', build / 'Modules/_hacl/libHacl_Hash_SHA2.a',
|
||||
build / 'Modules/_decimal/libmpdec/libmpdec.a']:
|
||||
data = file.read_bytes()
|
||||
report['artifacts'][str(file.relative_to(ROOT))] = dict(bytes=len(data), sha256=hashlib.sha256(data).hexdigest())
|
||||
report['status'] = 'BUILT_UNVERIFIED'
|
||||
license_dir = ROOT / 'web/app/public/vendor/blender'
|
||||
license_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source / 'LICENSE', license_dir / 'CPYTHON-LICENSE.txt')
|
||||
except BaseException as error:
|
||||
report.update(status='BUILD_INCOMPLETE', error=str(error))
|
||||
raise
|
||||
finally:
|
||||
(report_dir / 'build.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
print(json.dumps(dict(directory=str(report_dir), **report)), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user