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>
214 lines
11 KiB
Python
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()
|