Files
codex/third_party/voice/build_native.py
T
Benjamin Carlsson fc7d34ad67 Add native voice dependency build recipe (#41890)
## Why

The pinned voice source inputs can be prepared but do not yet provide compiled
native libraries for downstream integration.

## What changed

- Add `build_native.py` to build a shared-library prefix for the supported voice
  dependencies on native x64 and ARM64 GNU/Linux, macOS, and Windows MSVC hosts.
- Require explicit toolchain inputs, isolate dependency discovery to the output
  prefix, and record command logs and build state for provenance and failures.
- Expose the recipe and source inputs through `//third_party/voice:build_inputs`
  and document its prerequisites, outputs, and integration boundaries.

## Testing

- `PYTHONSAFEPATH=1 python3 -m unittest discover -s third_party/voice -p 'test_build_native.py'`

GitOrigin-RevId: 132d93561a8e1bb178518f1248cf87d031060aae
2026-08-31 18:05:19 +00:00

402 lines
14 KiB
Python

#!/usr/bin/env python3
"""Build the candidate native voice prefix with upstream build systems."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import platform
import re
import shlex
import subprocess
import sys
# Match the package builder: import this script's sibling under PYTHONSAFEPATH,
# without adding the caller's working directory to the module search path.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from prepare_sources import MANIFEST, load_sources, prepare_sources
TARGET_SYSTEMS = {
"apple-darwin": "Darwin",
"unknown-linux-gnu": "Linux",
"pc-windows-msvc": "Windows",
}
def validate_target(target, system, machine, libc, deployment_target):
architecture, separator, suffix = target.partition("-")
host_architecture = {"arm64": "aarch64", "amd64": "x86_64"}.get(
machine.lower(), machine.lower()
)
if (
not separator
or architecture not in ("aarch64", "x86_64")
or suffix not in TARGET_SYSTEMS
):
raise ValueError(f"Unsupported native voice target: {target}")
if TARGET_SYSTEMS[suffix] != system or architecture != host_architecture:
raise ValueError("Use a native build host matching the requested target")
if system == "Linux" and libc != "glibc":
raise ValueError("Native Linux voice builds require glibc")
if system == "Darwin" and not re.fullmatch(
r"[0-9]+\.[0-9]+(?:\.[0-9]+)?", deployment_target or ""
):
raise ValueError(
"Declare the macOS deployment target; do not inherit the host default"
)
class NativeBuild:
def __init__(self, args, inherited_environment):
validate_target(
args.target,
platform.system(),
platform.machine(),
platform.libc_ver()[0],
args.deployment_target,
)
self.args = args
self.output = args.output.absolute()
self.prefix = self.output / "prefix"
self.tools = self.output / "tools"
self.windows = args.target.endswith("windows-msvc")
# Compiler drivers such as clang++ select link behavior from argv[0].
self.toolchain = {
name: getattr(args, name).absolute()
for name in ("cc", "cxx", "cmake", "make", "pkg_config", "shell")
}
self.bootstrap_make = (args.bootstrap_make or args.make).absolute()
for tool in (*self.toolchain.values(), self.bootstrap_make):
if not tool.is_file():
raise ValueError(f"Missing build tool: {tool}")
if self.windows and not all(
inherited_environment.get(key) for key in ("INCLUDE", "LIB")
):
raise ValueError(
"Initialize the standard Visual Studio build environment first"
)
self.environment = {
key: value
for key, value in inherited_environment.items()
if key
in (
"HOME",
"TMPDIR",
"TMP",
"TEMP",
"SYSTEMROOT",
"SystemRoot",
"WINDIR",
"COMSPEC",
"INCLUDE",
"LIB",
"LIBPATH",
"HTTPS_PROXY",
"HTTP_PROXY",
"NO_PROXY",
)
}
paths = [
str(self.tools / "bin"),
*(str(p.parent) for p in self.toolchain.values()),
]
paths += (
inherited_environment.get("PATH", "").split(os.pathsep)
if self.windows
else ["/usr/bin", "/bin", "/usr/sbin", "/sbin"]
)
self.environment.update(
{
"PATH": os.pathsep.join(dict.fromkeys(paths)),
"LANG": "C",
"LC_ALL": "C",
"PYTHONDONTWRITEBYTECODE": "1",
"CC": str(self.toolchain["cc"]),
"CXX": str(self.toolchain["cxx"]),
"PKG_CONFIG": str(self.toolchain["pkg_config"]),
"PKG_CONFIG_PATH": "",
"PKG_CONFIG_LIBDIR": os.pathsep.join(
str(self.prefix / p) for p in ("lib/pkgconfig", "share/pkgconfig")
),
"CMAKE_PREFIX_PATH": str(self.prefix),
"NINJA": str(
self.tools / "bin" / ("ninja.exe" if self.windows else "ninja")
),
}
)
self.cmake_platform = []
if args.target.endswith("apple-darwin"):
self.environment["MACOSX_DEPLOYMENT_TARGET"] = args.deployment_target
self.cmake_platform = [
f"-DCMAKE_OSX_DEPLOYMENT_TARGET={args.deployment_target}"
]
self.record = {
"target": args.target,
"deployment_target": args.deployment_target,
"steps": [],
}
def run(self, name, command, cwd=None, environment=None):
command = [str(part) for part in command]
step = {"name": name, "command": command}
self.record["steps"].append(step)
print(f"Building {name}", flush=True)
with (self.output / f"{name}.log").open("w", encoding="utf-8") as log:
result = subprocess.run(
command,
cwd=cwd or self.output,
env=environment or self.environment,
stdout=log,
stderr=subprocess.STDOUT,
check=False,
)
step["exit_code"] = result.returncode
(self.output / "build-state.json").write_text(
json.dumps(self.record, indent=2) + "\n", encoding="utf-8"
)
result.check_returncode()
def cmake(self, name, options, *, bootstrap=False):
directory = self.output / "build" / name
prefix = self.tools if bootstrap else self.prefix
generator = (
("NMake Makefiles" if self.windows else "Unix Makefiles")
if bootstrap
else "Ninja"
)
make = self.bootstrap_make if bootstrap else self.environment["NINJA"]
self.run(
name + "-configure",
[
self.toolchain["cmake"],
"-S",
self.sources[name],
"-B",
directory,
"-G",
generator,
f"-DCMAKE_MAKE_PROGRAM={make}",
"-DCMAKE_BUILD_TYPE=Release",
f"-DCMAKE_INSTALL_PREFIX={prefix}",
"-DCMAKE_INSTALL_LIBDIR=lib",
f"-DCMAKE_C_COMPILER={self.toolchain['cc']}",
f"-DCMAKE_CXX_COMPILER={self.toolchain['cxx']}",
f"-DCMAKE_PREFIX_PATH={self.prefix}",
"-DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF",
"-DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=OFF",
"-DCMAKE_FIND_USE_CMAKE_ENVIRONMENT_PATH=OFF",
"-DFETCHCONTENT_FULLY_DISCONNECTED=ON",
*self.cmake_platform,
*options,
],
)
self.run(
name + "-build",
[
self.toolchain["cmake"],
"--build",
directory,
"--parallel",
self.args.jobs,
],
)
self.run(name + "-install", [self.toolchain["cmake"], "--install", directory])
def meson(self, name, options):
directory = self.output / "build" / name
meson = [sys.executable, self.sources["meson"] / "meson.py"]
quote = subprocess.list2cmdline if self.windows else shlex.join
include = quote([f"{'/I' if self.windows else '-I'}{self.prefix / 'include'}"])
link = (
[f"/LIBPATH:{self.prefix / 'lib'}"]
if self.windows
else [f"-L{self.prefix / 'lib'}", f"-Wl,-rpath,{self.prefix / 'lib'}"]
)
self.environment.update(
{"CFLAGS": include, "CXXFLAGS": include, "LDFLAGS": quote(link)}
)
self.run(
name + "-configure",
[
*meson,
"setup",
directory,
self.sources[name],
f"--prefix={self.prefix}",
"--libdir=lib",
"--buildtype=release",
"--wrap-mode=nofallback",
"-Dauto_features=disabled",
"-Ddefault_library=shared",
*options,
],
)
self.run(
name + "-build", [*meson, "compile", "-C", directory, "-j", self.args.jobs]
)
self.run(
name + "-install", [*meson, "install", "-C", directory, "--no-rebuild"]
)
def build(self):
self.output.mkdir()
manifest = MANIFEST.read_bytes()
prepare_sources(self.args.archives, self.output / "sources", manifest)
self.sources = {
s.name: self.output / "sources" / s.root for s in load_sources(manifest)
}
self.record["manifest_sha256"] = hashlib.sha256(manifest).hexdigest()
self.record["tools"] = {
name: str(path) for name, path in self.toolchain.items()
}
self.record["python"] = sys.version
self.run("cmake-version", [self.toolchain["cmake"], "--version"])
self.run("pkg-config-version", [self.toolchain["pkg_config"], "--version"])
self.cmake("ninja", ["-DBUILD_TESTING=OFF"], bootstrap=True)
self.cmake(
"zlib",
[
"-DZLIB_BUILD_TESTING=OFF",
"-DZLIB_BUILD_SHARED=ON",
"-DZLIB_BUILD_STATIC=OFF",
],
)
self.cmake(
"pcre2",
[
"-DBUILD_SHARED_LIBS=ON",
"-DBUILD_STATIC_LIBS=OFF",
"-DPCRE2_BUILD_TESTS=OFF",
"-DPCRE2_BUILD_PCRE2GREP=OFF",
"-DPCRE2_SUPPORT_LIBZ=OFF",
"-DPCRE2_SUPPORT_LIBBZ2=OFF",
"-DPCRE2_SUPPORT_LIBREADLINE=OFF",
"-DPCRE2_SUPPORT_LIBEDIT=OFF",
],
)
ffi_build = self.output / "build/libffi"
ffi_build.mkdir()
environment = self.environment.copy()
if self.windows:
wrapper = shlex.quote((self.sources["libffi"] / "msvcc.sh").as_posix())
architecture = (
"-m64" if self.args.target.startswith("x86_64-") else "-marm64"
)
environment.update(
{
"CC": f"{wrapper} {architecture}",
"CXX": f"{wrapper} {architecture}",
"LD": "link",
"CPP": "cl -nologo -EP",
"CXXCPP": "cl -nologo -EP",
"CPPFLAGS": "-DFFI_BUILDING_DLL",
}
)
self.run(
"libffi-configure",
[
self.toolchain["shell"],
(self.sources["libffi"] / "configure").as_posix(),
f"--prefix={self.prefix.as_posix()}",
"--enable-shared",
"--disable-static",
"--disable-docs",
],
cwd=ffi_build,
environment=environment,
)
self.run(
"libffi-build",
[self.toolchain["make"], f"-j{self.args.jobs}"],
cwd=ffi_build,
environment=environment,
)
self.run(
"libffi-install",
[self.toolchain["make"], "install"],
cwd=ffi_build,
environment=environment,
)
self.cmake(
"opus",
[
"-DOPUS_BUILD_SHARED_LIBRARY=ON",
"-DOPUS_BUILD_TESTING=OFF",
"-DOPUS_BUILD_PROGRAMS=OFF",
],
)
self.meson("proxy-libintl", [])
self.meson(
"glib",
[
"-Dtests=false",
"-Dinstalled_tests=false",
"-Dnls=disabled",
"-Ddocumentation=false",
"-Dintrospection=disabled",
"-Dlibmount=disabled",
"-Dselinux=disabled",
"-Dxattr=false",
],
)
self.meson(
"gstreamer",
[
"-Dregistry=false",
"-Doption-parsing=false",
"-Dtracer_hooks=false",
"-Dgst_parse=false",
"-Dtools=disabled",
"-Dptp-helper=disabled",
],
)
self.meson(
"gst-plugins-base",
[
"-Dapp=enabled",
"-Daudioconvert=enabled",
"-Daudioresample=enabled",
"-Dopus=enabled",
"-Dgl=disabled",
],
)
self.meson("gst-plugins-good", ["-Drtp=enabled", "-Drtpmanager=enabled"])
(self.output / "built.json").write_text(
json.dumps(self.record, indent=2) + "\n", encoding="utf-8"
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
for name in (
"archives",
"output",
"cc",
"cxx",
"cmake",
"make",
"pkg-config",
"shell",
):
parser.add_argument(f"--{name}", type=Path, required=True)
parser.add_argument(
"--bootstrap-make",
type=Path,
help="NMake on Windows; defaults to --make elsewhere",
)
parser.add_argument("--target", required=True)
parser.add_argument(
"--deployment-target",
help="Required on macOS; use the existing supported release minimum",
)
parser.add_argument("--jobs", type=int, default=8)
args = parser.parse_args()
if sys.version_info < (3, 12) or args.jobs < 1:
parser.error("Python 3.12+ and a positive --jobs value are required")
NativeBuild(args, os.environ).build()
if __name__ == "__main__":
main()