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>
231 lines
12 KiB
Python
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()
|