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:
230
tools/web/build-webengine-usd.py
Normal file
230
tools/web/build-webengine-usd.py
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user