Files
workinf_Blender_Wasm/tools/web/build-webengine-usd.py
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
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>
2026-11-10 11:18:01 +08:00

231 lines
12 KiB
Python

#!/usr/bin/env python3
"""Build pinned native dependencies for CacheFile USD_create_handle in WebEngine.
The TBB stage is independently resumable. This builds the OpenUSD core and
original static pxr Python extensions, not the Blender reader.
No tests or application candidates are built; existing objects are retained.
"""
import argparse
import hashlib
import os
from pathlib import Path
import re
import subprocess
import tarfile
import urllib.request
import zipfile
ROOT = Path(__file__).resolve().parents[2]
BASE = ROOT / "build_web_usd"
INSTALL = BASE / "install"
TOOLCHAIN = ROOT / ".emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"
VERSIONS = ROOT / "blender-5.2.0/build_files/build_environment/cmake/versions.cmake"
def source_for(name, repository, directory):
versions = VERSIONS.read_text()
def value(suffix):
return re.search(r"set\(" + name + "_" + suffix + r" ([^)]+)\)", versions)[1]
version = value("VERSION")
tag = version if version.startswith("v") else "v" + version
filename = ("oneTBB-" if name == "TBB" else "usd-") + tag + ".tar.gz"
archive = BASE / filename
expected = value("HASH")
algorithm = value("HASH_TYPE").lower()
if not archive.exists():
with urllib.request.urlopen(
"https://codeload.github.com/" + repository + "/tar.gz/refs/tags/" + tag,
timeout=60,
) as response:
data = response.read()
if hashlib.new(algorithm, data).hexdigest() != expected:
raise SystemExit(name + " download differs from Blender's pinned hash")
archive.write_bytes(data)
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != expected:
raise SystemExit(name + " archive differs from Blender's pinned hash")
source = BASE / (directory + "-" + version.removeprefix("v"))
if not (source / "CMakeLists.txt").exists():
with tarfile.open(archive) as package:
package.extractall(BASE, filter="data")
return source, version
def configure(source, build, options):
env = os.environ.copy()
host_python = ROOT / ".deps/native/python"
env["LD_LIBRARY_PATH"] = str(host_python / "lib") + ":" + env.get("LD_LIBRARY_PATH", "")
subprocess.run([
"cmake", "-S", str(source), "-B", str(build), "-G", "Ninja",
"-DCMAKE_TOOLCHAIN_FILE=" + str(TOOLCHAIN),
"-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_LIBDIR=lib",
"-DCMAKE_INSTALL_PREFIX=" + str(INSTALL),
"-DCMAKE_CXX_FLAGS=-fexceptions -fsigned-char",
"-DBUILD_SHARED_LIBS=OFF", *options,
], cwd=ROOT, env=env, check=True)
def build_target(build, target):
subprocess.run(["cmake", "--build", str(build), "--target", target,
"--parallel", "8"], cwd=ROOT, check=True)
def adapt_web_source(source):
# These adaptations preserve upstream behavior and ABI assertions. They
# must also be present in installed headers consumed by Blender.
edits = {
"pxr/base/tf/pyObjWrapper.h": [
('#include "pxr/base/arch/pragmas.h"',
'#include "pxr/base/arch/pragmas.h"\n\n#include <memory>'),
('static constexpr std::size_t Size = 16;\n'
' static constexpr std::size_t Align = 8;',
'// Match the native shared_ptr member on wasm32 as well as 64-bit hosts.\n'
' static constexpr std::size_t Size = sizeof(std::shared_ptr<void>);\n'
' static constexpr std::size_t Align = alignof(std::shared_ptr<void>);'),
],
"cmake/defaults/gccclangshareddefaults.cmake": [
('if(CMAKE_USE_PTHREADS_INIT)\n',
'if(CMAKE_USE_PTHREADS_INIT AND NOT (EMSCRIPTEN AND EMSCRIPTEN_WITHOUT_PTHREAD))\n'),
],
"cmake/macros/Private.cmake": [
('if(NOT TARGET shared_libs)\n message(STATUS "Skipping Python module',
'if(NOT TARGET shared_libs AND NOT PXR_WEB_STATIC_PYTHON)\n message(STATUS "Skipping Python module'),
(' # Add the module target.\n add_library(${LIBRARY_NAME}\n SHARED',
' # Embedded CPython loads original wrappers through its builtin table.\n'
' set(pythonLibraryType SHARED)\n'
' if(PXR_WEB_STATIC_PYTHON)\n'
' set(pythonLibraryType STATIC)\n'
' endif()\n'
' # Add the module target.\n add_library(${LIBRARY_NAME}\n ${pythonLibraryType}'),
(' # Accumulate Python module names.\n',
' if(PXR_WEB_STATIC_PYTHON)\n'
' set_property(GLOBAL APPEND PROPERTY PXR_WEB_PYTHON_MODULES\n'
' "${pyModuleName}|${LIBRARY_NAME}")\n'
' endif()\n\n # Accumulate Python module names.\n'),
(' TARGETS ${LIBRARY_NAME}\n LIBRARY DESTINATION ${libInstallPrefix}',
' TARGETS ${LIBRARY_NAME}\n ARCHIVE DESTINATION ${libInstallPrefix}\n'
' LIBRARY DESTINATION ${libInstallPrefix}'),
],
"pxr/CMakeLists.txt": [
('pxr_core_epilogue()\n',
'pxr_core_epilogue()\n\n'
'include("${PROJECT_SOURCE_DIR}/cmake/macros/WebStaticPython.cmake")\n'),
],
"cmake/macros/Public.cmake": [
('if(TARGET shared_libs AND PXR_ENABLE_PYTHON_SUPPORT)',
'if((TARGET shared_libs OR PXR_WEB_STATIC_PYTHON) AND PXR_ENABLE_PYTHON_SUPPORT)'),
],
}
for relative, replacements in edits.items():
path = source / relative
text = path.read_text()
original = text
for before, after in replacements:
if after in text:
continue
if text.count(before) != 1:
raise SystemExit("OpenUSD source differs at " + relative)
text = text.replace(before, after, 1)
if text != original:
path.write_text(text)
helper = source / "cmake/macros/WebStaticPython.cmake"
contents = (ROOT / "tools/web/openusd-static-python.cmake").read_text()
if not helper.exists() or helper.read_text() != contents:
helper.write_text(contents)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--stage", choices=("tbb", "usd", "all"), default="all")
parser.add_argument("--usd-target", default="install",
help="Original OpenUSD target; install includes schema/plugin resources")
args = parser.parse_args()
BASE.mkdir(exist_ok=True)
if args.stage in ("tbb", "all"):
source, _ = source_for("TBB", "uxlfoundation/oneTBB", "oneTBB")
build = BASE / "tbb-build"
configure(source, build, [
"-DEMSCRIPTEN_WITHOUT_PTHREAD=ON", "-DTBB_TEST=OFF",
"-DTBB_STRICT=OFF", "-DTBBMALLOC_BUILD=OFF",
"-DTBBMALLOC_PROXY_BUILD=OFF", "-DTBB_ENABLE_IPO=OFF",
])
build_target(build, "install")
if args.stage in ("usd", "all"):
if not (INSTALL / "lib/cmake/TBB/TBBConfig.cmake").exists():
raise SystemExit("Build the pinned Web TBB stage first")
source, version = source_for("USD", "PixarAnimationStudios/OpenUSD", "OpenUSD")
adapt_web_source(source)
python = ROOT / "build_web_python313"
if not (python / "build/libpython3.13.a").exists():
raise SystemExit("Reuse/build the embedded Web CPython 3.13 archive first")
# FindPython expects pyconfig.h beside Python.h. Preserve CPython's
# separate source/build trees with a local header overlay, using the
# WASM configuration rather than the host interpreter's ABI.
python_include = BASE / "python-include"
python_include.mkdir(exist_ok=True)
for header in (python / "source/Include").iterdir():
destination = python_include / header.name
if not destination.exists():
destination.symlink_to(header, target_is_directory=header.is_dir())
config = python_include / "pyconfig.h"
if not config.exists():
config.symlink_to(python / "build/pyconfig.h")
build = BASE / "usd-build"
configure(source, build, [
"-DCMAKE_CXX_FLAGS=-fexceptions -fsigned-char -I" + str(python / "build"),
"-DCMAKE_PREFIX_PATH=" + str(INSTALL),
"-DCMAKE_FIND_ROOT_PATH=" + str(INSTALL),
"-DTBB_DIR=" + str(INSTALL / "lib/cmake/TBB"),
"-DOPENSUBDIV_ROOT_DIR=" + str(ROOT / "blender-5.2.0/extern/opensubdiv-source"),
"-DOPENSUBDIV_OSDCPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdCPU.a"),
"-DOPENSUBDIV_OSDGPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdGPU.a"),
"-DEMSCRIPTEN_WITHOUT_PTHREAD=ON",
# OpenUSD retains static registration through LINK_LIBRARY.
# Emscripten/wasm-ld supports these flags, but CMake 3.28's
# Emscripten platform does not declare the feature itself.
"-DCMAKE_LINK_LIBRARY_USING_WHOLE_ARCHIVE_SUPPORTED=TRUE",
"-DCMAKE_LINK_LIBRARY_USING_WHOLE_ARCHIVE=-Wl,--whole-archive;<LINK_ITEM>;-Wl,--no-whole-archive",
"-DPXR_FIND_TBB_IN_CONFIG=ON", "-DPXR_BUILD_MONOLITHIC=ON",
"-DPXR_SET_INTERNAL_NAMESPACE=pxrBlender_v" + version.replace(".", "_"),
"-DPXR_ENABLE_PYTHON_SUPPORT=ON",
"-DPXR_WEB_STATIC_PYTHON=ON",
"-DPython3_EXECUTABLE=" + str(ROOT / ".deps/native/python/bin/python3.13"),
"-DPython3_INCLUDE_DIR=" + str(python_include),
# CMake 3.28 validates its previous internal include path before
# applying an updated public path during incremental configure.
"-D_Python3_INCLUDE_DIR=" + str(python_include),
"-DPython3_LIBRARY=" + str(python / "build/libpython3.13.a"),
"-DPXR_PY_UNDEFINED_DYNAMIC_LOOKUP=ON",
"-DPXR_BUILD_TESTS=OFF", "-DPXR_BUILD_EXAMPLES=OFF",
"-DPXR_BUILD_TUTORIALS=OFF", "-DPXR_BUILD_USD_TOOLS=OFF",
"-DPXR_BUILD_USDVIEW=OFF",
# Blender's Web USD reader consumes the native shape adapters.
# Reuse the existing Emscripten OpenSubdiv package from the main
# build tree; do not silently disable imaging and leave headers out
# of the install set.
"-DPXR_BUILD_IMAGING=ON",
"-DPXR_BUILD_USD_IMAGING=ON",
"-DOPENSUBDIV_INCLUDE_DIR=" + str(ROOT / "blender-5.2.0/extern/opensubdiv-source"),
"-DOPENSUBDIV_CPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdCPU.a"),
"-DOPENSUBDIV_GPU_LIBRARY=" + str(ROOT / "build_web_blender6/lib/libosdGPU.a"),
"-DPXR_BUILD_USD_VALIDATION=OFF",
"-DPXR_BUILD_EXEC=OFF", "-DPXR_ENABLE_GL_SUPPORT=OFF",
"-DPXR_ENABLE_VULKAN_SUPPORT=OFF", "-DPXR_ENABLE_OPENVDB_SUPPORT=OFF",
"-DPXR_ENABLE_MATERIALX_SUPPORT=OFF", "-DPXR_ENABLE_OSL_SUPPORT=OFF",
"-DPXR_BUILD_DOCUMENTATION=OFF", "-DPXR_VALIDATE_GENERATED_CODE=OFF",
])
build_target(build, args.usd_target)
if args.usd_target == "install":
# Only Python resources belong in the embedded package. Static
# wrapper archives in lib/python are linked, never shipped as data.
package = INSTALL / "lib/python"
output = INSTALL / "lib/webengine-pxr.zip"
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive:
for path in sorted(package.rglob("*.py")):
archive.write(path, path.relative_to(package))
if __name__ == "__main__":
main()