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:
213
tools/web/build-webengine-sndfile.py
Normal file
213
tools/web/build-webengine-sndfile.py
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build pinned libsndfile/Xiph/MPEG codecs for native WebEngine audio."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import shutil
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
VERSION = "1.2.2"
|
||||
# Blender build_files/build_environment/cmake/versions.cmake.
|
||||
ARCHIVE_MD5 = "04e2e6f726da7c5dc87f8cf72f250d04"
|
||||
# Debian's unmodified upstream tag archive of the same version (different
|
||||
# packaging from the upstream release tarball), retained for offline reuse.
|
||||
TAG_ARCHIVE_SHA256 = "ffe12ef8add3eaca876f04087734e6e8e029350082f3251f565fa9da55b52121"
|
||||
|
||||
# Same releases and digests as Blender's versions.cmake. All algorithms remain
|
||||
# in the upstream native libraries; no browser-side encoding implementation.
|
||||
CODECS = (
|
||||
("ogg", "libogg-1.3.5", ".tar.gz",
|
||||
"0eb4b4b9420a0f51db142ba3f9c64b333f826532dc0f48c6410ae51f4799b664", []),
|
||||
("vorbis", "libvorbis-1.3.7", ".tar.gz",
|
||||
"0e982409a9c3fc82ee06e08205b1355e5c6aa4c36bca58146ef399621b0ce5ab", []),
|
||||
("flac", "flac-1.4.2", ".tar.xz",
|
||||
"e322d58a1f48d23d9dd38f432672865f6f79e73a6f9cc5a5f57fcaa83eb5a8e4",
|
||||
["-DBUILD_CXXLIBS=OFF", "-DBUILD_PROGRAMS=OFF", "-DBUILD_EXAMPLES=OFF",
|
||||
"-DBUILD_DOCS=OFF", "-DBUILD_MANPAGES=OFF", "-DWITH_OGG=ON"]),
|
||||
("opus", "opus-1.3.1", ".tar.gz",
|
||||
"65b58e1e25b2a114157014736a3d9dfeaad8d41be1c8179866f144a2fb44ff9d",
|
||||
["-DOPUS_BUILD_PROGRAMS=OFF", "-DOPUS_BUILD_TESTING=OFF",
|
||||
"-DOPUS_INSTALL_PKG_CONFIG_MODULE=ON", "-DOPUS_STACK_PROTECTOR=OFF"]),
|
||||
)
|
||||
|
||||
# LAME matches Blender's versions.cmake. Blender uses FFmpeg for decoding and
|
||||
# does not pin mpg123: pin the original upstream archive required by libsndfile
|
||||
# separately. Digest from Debian's mpg123_1.32.10-1+deb13u1.dsc source metadata.
|
||||
MPEG_CODECS = (
|
||||
("lame-3.100", ".tar.gz", "md5", "83e260acbe4389b54fe08e0bdbf7cddb",
|
||||
"https://deb.debian.org/debian/pool/main/l/lame/lame_3.100.orig.tar.gz",
|
||||
["--disable-frontend", "--disable-decoder", "--disable-analyzer-hooks", "--disable-nasm"]),
|
||||
("mpg123-1.32.10", ".tar.xz", "sha256",
|
||||
"98867a796fa1eeb161847d211734d585ff61c56712fbc333de416fea7b59de29",
|
||||
"https://deb.debian.org/debian/pool/main/m/mpg123/mpg123_1.32.10.orig.tar.xz",
|
||||
["--with-cpu=generic", "--disable-components", "--enable-libmpg123", "--disable-network"]),
|
||||
)
|
||||
|
||||
|
||||
def build_mpeg(base, prefix, jobs, env):
|
||||
emscripten = ROOT / ".emsdk/upstream/emscripten"
|
||||
for release, suffix, algorithm, digest, url, options in MPEG_CODECS:
|
||||
source = base / release
|
||||
if not (source / "configure").is_file():
|
||||
filename = release + suffix
|
||||
cached = [base / filename, Path.home() / "working/packages" / filename,
|
||||
Path.home() / "working/build_oiio_deps/downloads" / filename]
|
||||
archive = next((path for path in cached if path.is_file()), None)
|
||||
if archive is None:
|
||||
archive = cached[0]
|
||||
partial = archive.with_suffix(".partial")
|
||||
with urllib.request.urlopen(url, timeout=60) as response, partial.open("wb") as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
partial.replace(archive)
|
||||
if hashlib.new(algorithm, archive.read_bytes()).hexdigest() != digest:
|
||||
raise RuntimeError(f"Pinned {release} archive digest mismatch")
|
||||
with tarfile.open(archive) as package:
|
||||
package.extractall(base, filter="data")
|
||||
build = base / (release + "-build")
|
||||
build.mkdir(exist_ok=True)
|
||||
if release.startswith("lame-"):
|
||||
# LAME 3.100's 2017 GNU config helper predates Emscripten. Reuse
|
||||
# the helper shipped with our embedded CPython, not a fake host
|
||||
# triplet which could enable native CPU assembly or ELF tooling.
|
||||
shutil.copy2(ROOT / "build_web_python313/source/config.sub", source / "config.sub")
|
||||
configure = [str(emscripten / "emconfigure"), str(source / "configure"),
|
||||
"--host=wasm32-unknown-emscripten", f"--prefix={prefix}",
|
||||
"--disable-shared", "--enable-static", *options]
|
||||
codec_env = dict(env, CFLAGS="-O2 -fPIC", CXXFLAGS="-O2 -fPIC")
|
||||
# Autoconf source lists remain upstream-owned; only rerun configuration
|
||||
# after build inputs change, leaving existing codec objects reusable.
|
||||
settings = json.dumps([configure, codec_env["CFLAGS"], codec_env["CXXFLAGS"]])
|
||||
stamp = build / "web-configure.json"
|
||||
if not stamp.is_file() or stamp.read_text() != settings:
|
||||
subprocess.run(configure, cwd=build, env=codec_env, check=True)
|
||||
stamp.write_text(settings)
|
||||
subprocess.run([str(emscripten / "emmake"), "make", f"-j{jobs}", "install"],
|
||||
cwd=build, env=codec_env, check=True)
|
||||
|
||||
|
||||
def build_codecs(base, prefix, jobs, common, env):
|
||||
for family, release, suffix, digest, options in CODECS:
|
||||
source = base / release
|
||||
if not (source / "CMakeLists.txt").is_file():
|
||||
filename = release + suffix
|
||||
cached = [base / filename, Path.home() / "working/packages" / filename]
|
||||
archive = next((path for path in cached if path.is_file()), None)
|
||||
if archive is None:
|
||||
archive = cached[0]
|
||||
partial = archive.with_suffix(".partial")
|
||||
url = f"https://downloads.xiph.org/releases/{family}/{filename}"
|
||||
with urllib.request.urlopen(url, timeout=60) as response, partial.open("wb") as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
partial.replace(archive)
|
||||
if hashlib.sha256(archive.read_bytes()).hexdigest() != digest:
|
||||
raise RuntimeError(f"Blender-pinned {release} archive digest mismatch")
|
||||
with tarfile.open(archive) as package:
|
||||
package.extractall(base, filter="data")
|
||||
if family == "opus" and not (source / "opus_buildtype.cmake").is_file():
|
||||
# The pinned 1.3.1 release omits this build-type defaults helper.
|
||||
# We set CMAKE_BUILD_TYPE explicitly. Keep the original source list
|
||||
# and configuration; only make the absent defaults include optional.
|
||||
cmake = source / "CMakeLists.txt"
|
||||
cmake.write_text(cmake.read_text().replace(
|
||||
"include(opus_buildtype.cmake)",
|
||||
"include(opus_buildtype.cmake OPTIONAL)"))
|
||||
build = base / (family + "-build")
|
||||
subprocess.run(["cmake", "-S", str(source), "-B", str(build), *common,
|
||||
"-DBUILD_TESTING=OFF", *options], cwd=ROOT, env=env, check=True)
|
||||
subprocess.run(["cmake", "--build", str(build), "--target", "install",
|
||||
"--parallel", str(jobs)], cwd=ROOT, env=env, check=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", type=Path)
|
||||
parser.add_argument("--archive", type=Path)
|
||||
parser.add_argument("--jobs", type=int, default=3)
|
||||
args = parser.parse_args()
|
||||
base = ROOT / "build_web_sndfile"
|
||||
base.mkdir(exist_ok=True)
|
||||
source = args.source
|
||||
if source is not None and not (source / "CMakeLists.txt").is_file():
|
||||
raise FileNotFoundError(f"No libsndfile source at {source}")
|
||||
if source is None:
|
||||
local = (Path.home() / "working/build_oiio_deps/build/sndfile/src/"
|
||||
"external_sndfile")
|
||||
source = local if (local / "CMakeLists.txt").is_file() else base / f"libsndfile-{VERSION}"
|
||||
if not (source / "CMakeLists.txt").is_file():
|
||||
if args.archive:
|
||||
archive = args.archive
|
||||
else:
|
||||
cached = [Path.home() / f"working/packages/libsndfile-{VERSION}.tar.xz",
|
||||
base / f"libsndfile-{VERSION}.tar.xz",
|
||||
base / f"libsndfile_{VERSION}.orig.tar.gz"]
|
||||
archive = next((path for path in cached if path.is_file()), None)
|
||||
if archive is None:
|
||||
archive = cached[-1]
|
||||
url = ("https://deb.debian.org/debian/pool/main/libs/libsndfile/"
|
||||
f"libsndfile_{VERSION}.orig.tar.gz")
|
||||
partial = archive.with_suffix(".partial")
|
||||
with urllib.request.urlopen(url, timeout=45) as response, partial.open("wb") as out:
|
||||
shutil.copyfileobj(response, out)
|
||||
partial.replace(archive)
|
||||
contents = archive.read_bytes()
|
||||
if (hashlib.md5(contents).hexdigest() != ARCHIVE_MD5 and
|
||||
hashlib.sha256(contents).hexdigest() != TAG_ARCHIVE_SHA256):
|
||||
raise RuntimeError("Blender-pinned libsndfile archive digest mismatch")
|
||||
with tarfile.open(archive) as package:
|
||||
package.extractall(base, filter="data")
|
||||
source = base / f"libsndfile-{VERSION}"
|
||||
prefix = base / "install"
|
||||
common = [
|
||||
"-G", "Ninja",
|
||||
"-DCMAKE_TOOLCHAIN_FILE=" + str(ROOT / ".emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"),
|
||||
"-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_PREFIX=" + str(prefix),
|
||||
"-DEMSCRIPTEN_SYSTEM_PROCESSOR=wasm32",
|
||||
"-DCMAKE_INSTALL_LIBDIR=lib", "-DBUILD_SHARED_LIBS=OFF",
|
||||
"-DCMAKE_PREFIX_PATH=" + str(prefix),
|
||||
"-DCMAKE_FIND_ROOT_PATH=" + str(prefix),
|
||||
]
|
||||
# Never let host pkg-config select ELF libraries for a WASM link.
|
||||
env = dict(os.environ, PKG_CONFIG_LIBDIR=str(prefix / "lib/pkgconfig"),
|
||||
PKG_CONFIG_PATH=str(prefix / "lib/pkgconfig"))
|
||||
build_codecs(base, prefix, args.jobs, common, env)
|
||||
build_mpeg(base, prefix, args.jobs, env)
|
||||
# libsndfile 1.2.2 only exposes Vorbis quality, although libvorbis already
|
||||
# implements managed bitrate encoding. Keep this native extension in a
|
||||
# reproducible source patch and install its command declaration with the
|
||||
# archive, so Audaspace cannot silently use an unpatched system header.
|
||||
marker = "SFC_WEB_SET_VORBIS_BITRATE"
|
||||
patched_header = marker in (source / "include/sndfile.h").read_text()
|
||||
patched_encoder = marker in (source / "src/ogg_vorbis.c").read_text()
|
||||
if patched_header != patched_encoder:
|
||||
raise RuntimeError("Incomplete WebEngine Vorbis patch in libsndfile source")
|
||||
if not patched_header:
|
||||
subprocess.run(["patch", "--batch", "--forward", "-p1", "-i",
|
||||
str(ROOT / "tools/web/patches/libsndfile-vorbis-bitrate.patch")],
|
||||
cwd=source, check=True)
|
||||
configure = [
|
||||
"cmake", "-S", str(source), "-B", str(base / "build"), *common,
|
||||
"-DBUILD_PROGRAMS=OFF", "-DBUILD_EXAMPLES=OFF", "-DBUILD_TESTING=OFF",
|
||||
"-DENABLE_EXTERNAL_LIBS=ON", "-DENABLE_MPEG=ON",
|
||||
"-DINSTALL_MANPAGES=OFF", "-DINSTALL_PKGCONFIG_MODULE=ON",
|
||||
]
|
||||
# Pin every find-module input, including entries cached by the old PCM build.
|
||||
for library, variable in (("ogg", "OGG"), ("vorbis", "Vorbis_Vorbis"),
|
||||
("vorbisenc", "Vorbis_Enc"), ("vorbisfile", "Vorbis_File"),
|
||||
("FLAC", "FLAC"), ("opus", "OPUS"),
|
||||
("mp3lame", "MP3LAME"), ("mpg123", "mpg123")):
|
||||
configure += [f"-D{variable}_LIBRARY={prefix}/lib/lib{library}.a",
|
||||
f"-D{variable}_INCLUDE_DIR={prefix}/include"]
|
||||
subprocess.run(configure, cwd=ROOT, env=env, check=True)
|
||||
subprocess.run(["cmake", "--build", str(base / "build"), "--target", "install",
|
||||
"--parallel", str(args.jobs)], cwd=ROOT, env=env, check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user