#!/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()