#!/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 '), ('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);\n' ' static constexpr std::size_t Align = alignof(std::shared_ptr);'), ], "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;;-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()