Files
workinf_Blender_Wasm/tools/web/build-webengine-sndfile.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

214 lines
11 KiB
Python

#!/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()