blob: 0aafa2c7df6408d86e8fca472a34a713382321aa [file]
#!/usr/bin/env python3
"""Generate config header templates by parsing FFmpeg's configure script.
Extracts ARCH_LIST, HAVE_LIST, CONFIG_LIST, CONFIG_EXTRA, HAVE_LIST_PUB,
and COMPONENT_LIST from the configure script, then writes the header
templates into the version overlay directory:
<overlay>/config.h.in
<overlay>/libavutil/avconfig.h.in
<overlay>/libavutil/ffversion.h.in
Usage:
python3 generate_config_defs.py [--version 7.1.1.bcr.beta.5] /path/to/FFmpeg
python3 generate_config_defs.py /path/to/FFmpeg --ffmpeg-version 7.2
"""
import argparse
import os
import re
import sys
from collections import OrderedDict
from _overlay_utils import add_version_arg, resolve_overlay_dir
# ---------------------------------------------------------------------------
# Build-policy defaults: CONFIG_* flags set to 1 in the Bazel build.
# Everything not listed here defaults to 0.
# ---------------------------------------------------------------------------
CONFIG_ENABLED = {
# Libraries
"avcodec",
"avdevice",
"avfilter",
"avformat",
"avutil",
"swresample",
"swscale",
# Programs
"ffmpeg",
"ffprobe",
# Component categories
"filters",
# Subsystems
"faan",
"fast_unaligned",
"iamf",
# Features
"runtime_cpudetect",
"safe_bitstream_reader",
"static",
"swscale_alpha",
# Build flags
"large_tests",
"pic",
"ptx_compression",
# Documents
"manpages",
"podpages",
# External autodetect libs available in Bazel
"bzlib",
"zlib",
# Examples (enabled by default)
"avio_http_serve_files_example",
"avio_list_dir_example",
"avio_read_callback_example",
"decode_audio_example",
"decode_filter_audio_example",
"decode_filter_video_example",
"decode_video_example",
"demux_decode_example",
"encode_audio_example",
"encode_video_example",
"extract_mvs_example",
"filter_audio_example",
"hw_decode_example",
"mux_example",
"remux_example",
"resample_audio_example",
"scale_video_example",
"show_metadata_example",
"transcode_aac_example",
"transcode_example",
}
CONFIG_DERIVED = {
"gmp",
"mbedtls",
"network",
"openssl",
"protocols",
}
# Standard C99/POSIX math functions checked via AC_CHECK_LIB in the Bazel
# build (mirroring FFmpeg's check_mathfunc in configure). These are available
# on all supported platforms, so their #ifndef fallback defaults to 1 instead
# of 0. This ensures a working build even if the autoconf probe fails (e.g.
# due to ABI mismatches in the default AC_CHECK_LIB template on ARM64).
HAVE_MATH_FUNCS_DEFAULT_1 = frozenset(
{
"atanf",
"atan2f",
"cbrt",
"cbrtf",
"copysign",
"cosf",
"erf",
"exp2",
"exp2f",
"expf",
"hypot",
"isfinite",
"isinf",
"isnan",
"ldexpf",
"llrint",
"llrintf",
"log2",
"log2f",
"log10f",
"lrint",
"lrintf",
"powf",
"rint",
"round",
"roundf",
"sinf",
"trunc",
"truncf",
}
)
# CONFIG_* flags detected via autoconf at build time (emitted as #undef
# in the template, with #ifndef fallbacks). These are a subset of
# EXTERNAL_AUTODETECT_LIBRARY_LIST items that can be detected by
# compile-time checks rather than pkg-config.
AUTOCONF_DETECTED_CONFIGS = {
"appkit",
"audiotoolbox",
"avfoundation",
"coreimage",
"iconv",
"securetransport",
"videotoolbox",
}
# ---------------------------------------------------------------------------
# Configure script parser
# ---------------------------------------------------------------------------
def parse_configure(configure_path: str) -> dict[str, str]:
"""Extract all VARNAME="..." shell variable assignments."""
with open(configure_path) as f:
text = f.read()
variables: dict[str, str] = OrderedDict()
pattern = re.compile(
r'^([A-Z][A-Z0-9_]*)="(.*?)"',
re.MULTILINE | re.DOTALL,
)
for m in pattern.finditer(text):
name = m.group(1)
body = m.group(2)
variables[name] = body
return variables
def resolve_list(
var_name: str,
raw_vars: dict[str, str],
cache: dict[str, list[str]] | None = None,
) -> list[str]:
"""Resolve a shell variable to a flat list of identifiers.
Handles $VAR references and $(add_suffix SUFFIX $VAR) calls.
"""
if cache is None:
cache = {}
if var_name in cache:
return cache[var_name]
body = raw_vars.get(var_name, "")
items: list[str] = []
for token in body.split():
if token.startswith("$(add_suffix"):
continue
add_suffix_match = re.match(r"^\$\(add_suffix\s+(\S+)\s+\$(\S+)\)$", token)
if add_suffix_match:
suffix = add_suffix_match.group(1)
ref = add_suffix_match.group(2)
for item in resolve_list(ref, raw_vars, cache):
items.append(item + suffix)
continue
if token.startswith("$"):
ref = token.lstrip("$").strip("{}")
items.extend(resolve_list(ref, raw_vars, cache))
continue
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", token):
items.append(token)
cache[var_name] = items
return items
def resolve_have_list(raw_vars: dict[str, str], cache: dict[str, list[str]]) -> list[str]:
"""Resolve HAVE_LIST with special handling for $(add_suffix) in its body."""
body = raw_vars.get("HAVE_LIST", "")
items: list[str] = []
i = 0
tokens = body.split()
while i < len(tokens):
token = tokens[i]
if token == "$(add_suffix" and i + 2 < len(tokens):
suffix_tok = tokens[i + 1]
ref_tok = tokens[i + 2].rstrip(")")
ref = ref_tok.lstrip("$").strip("{}")
for item in resolve_list(ref, raw_vars, cache):
items.append(item + suffix_tok)
i += 3
continue
if token.startswith("$"):
ref = token.lstrip("$").strip("{}")
items.extend(resolve_list(ref, raw_vars, cache))
i += 1
continue
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", token):
items.append(token)
i += 1
return items
def dedup(items: list[str]) -> list[str]:
"""Remove duplicates while preserving order."""
seen: set[str] = set()
result: list[str] = []
for item in items:
if item not in seen:
seen.add(item)
result.append(item)
return result
def extract_all_lists(configure_path: str) -> dict[str, list[str]]:
"""Parse configure and return all resolved variable lists."""
raw_vars = parse_configure(configure_path)
cache: dict[str, list[str]] = {}
result = {}
result["ARCH_LIST"] = dedup(resolve_list("ARCH_LIST", raw_vars, cache))
result["HAVE_LIST"] = dedup(resolve_have_list(raw_vars, cache))
result["CONFIG_LIST"] = dedup(resolve_list("CONFIG_LIST", raw_vars, cache))
result["CONFIG_EXTRA"] = dedup(resolve_list("CONFIG_EXTRA", raw_vars, cache))
result["HAVE_LIST_PUB"] = dedup(resolve_list("HAVE_LIST_PUB", raw_vars, cache))
result["COMPONENT_LIST"] = dedup(resolve_list("COMPONENT_LIST", raw_vars, cache))
return result
# ---------------------------------------------------------------------------
# Version extraction
# ---------------------------------------------------------------------------
def extract_version(ffmpeg_root: str) -> str:
"""Read the FFmpeg version from the RELEASE file."""
release_path = os.path.join(ffmpeg_root, "RELEASE")
if os.path.isfile(release_path):
with open(release_path) as f:
return f.read().strip()
raise FileNotFoundError(f"Cannot find {release_path}")
# ---------------------------------------------------------------------------
# Starlark output generation
# ---------------------------------------------------------------------------
def generate_avconfig_h(have_list_pub: list[str]) -> list[str]:
"""Generate AVCONFIG_H lines from HAVE_LIST_PUB."""
defaults = {"bigendian": 0, "fast_unaligned": 1}
lines = [
"/* Generated by Bazel */",
"#ifndef AVUTIL_AVCONFIG_H",
"#define AVUTIL_AVCONFIG_H",
]
for item in have_list_pub:
val = defaults.get(item, 0)
lines.append(f"#define AV_HAVE_{item.upper()} {val}")
lines.append("#endif /* AVUTIL_AVCONFIG_H */")
return lines
def generate_ffversion_h(version: str) -> list[str]:
"""Generate FFVERSION_H lines."""
return [
"/* Generated by Bazel */",
"#ifndef AVUTIL_FFVERSION_H",
"#define AVUTIL_FFVERSION_H",
f'#define FFMPEG_VERSION "{version}"',
"#endif /* AVUTIL_FFVERSION_H */",
]
def generate_config_h_in(
arch_list: list[str],
have_list: list[str],
config_list: list[str],
) -> list[str]:
"""Generate CONFIG_H_IN lines."""
lines: list[str] = []
# --- Header guard and static metadata ---
lines.extend(
[
"/* Automatically generated by Bazel - do not modify! */",
"#ifndef FFMPEG_CONFIG_H",
"#define FFMPEG_CONFIG_H",
"",
"/* --- Static metadata --- */",
'#define FFMPEG_CONFIGURATION "bazel --disable-everything"',
'#define FFMPEG_LICENSE "LGPL version 2.1 or later"',
"#define CONFIG_THIS_YEAR 2025",
'#define FFMPEG_DATADIR "/usr/local/share/ffmpeg"',
'#define AVCONV_DATADIR "/usr/local/share/ffmpeg"',
'#define CC_IDENT "bazel"',
'#define BUILDSUF ""',
"#define SWS_MAX_FILTER_SIZE 256",
"",
"/* --- OS metadata (substituted by autoconf_hdr) --- */",
"#define OS_NAME @OS_NAME@",
'#define EXTERN_PREFIX "@EXTERN_PREFIX@"',
"#define EXTERN_ASM @EXTERN_ASM@",
'#define SLIBSUF "@SLIBSUF@"',
]
)
# --- Architecture flags ---
lines.append("")
lines.append("/* --- Architecture flags (detected via autoconf) --- */")
for arch in arch_list:
lines.append(f"#undef ARCH_{arch.upper()}")
# --- Conditional metadata ---
lines.append("")
lines.append("/* --- Conditional metadata (detected via autoconf) --- */")
lines.append("#undef AS_ARCH_LEVEL")
# --- HAVE_* feature detection ---
lines.append("")
lines.append("/* --- HAVE_* feature detection (detected via autoconf) --- */")
for item in sorted(have_list, key=lambda x: x.upper()):
lines.append(f"#undef HAVE_{item.upper()}")
# --- Platform-detected CONFIG_* ---
autoconf_configs = sorted(AUTOCONF_DETECTED_CONFIGS)
lines.append("")
lines.append("/* --- Platform-detected CONFIG_* (detected via autoconf) --- */")
for item in autoconf_configs:
lines.append(f"#undef CONFIG_{item.upper()}")
# --- Build policy (empty section header) ---
lines.append("")
lines.append("/* --- Build policy CONFIG_* (static) --- */")
# --- CONFIG_EXTRA subsystems ---
lines.append("")
lines.append("/* --- CONFIG_EXTRA subsystems (resolved by component selection) --- */")
lines.append('#include "config_extra.h"')
# --- Static CONFIG_* defines ---
lines.append("")
static_configs = sorted(
(c for c in config_list if c not in AUTOCONF_DETECTED_CONFIGS and c not in CONFIG_DERIVED),
key=lambda x: x.upper(),
)
for item in static_configs:
val = 1 if item in CONFIG_ENABLED else 0
lines.append(f"#define CONFIG_{item.upper()} {val}")
# --- Fallback defaults (all macros must be defined to 0 or 1) ---
lines.append("")
lines.append("// FFmpeg requires all detection macros defined to 0 or 1 (not undefined).")
lines.append("// Provide defaults for any that were not set by autoconf checks above.")
# ARCH_* fallbacks
for arch in arch_list:
name = f"ARCH_{arch.upper()}"
lines.extend([f"#ifndef {name}", f"#define {name} 0", "#endif"])
lines.extend(["#ifndef AS_ARCH_LEVEL", "#define AS_ARCH_LEVEL 0", "#endif"])
# HAVE_* fallbacks — standard math functions default to 1 (universally
# available on all supported platforms); everything else defaults to 0.
for item in sorted(have_list, key=lambda x: x.upper()):
name = f"HAVE_{item.upper()}"
default = 1 if item.lower() in HAVE_MATH_FUNCS_DEFAULT_1 else 0
lines.extend([f"#ifndef {name}", f"#define {name} {default}", "#endif"])
# Autoconf-detected CONFIG_* fallbacks
for item in autoconf_configs:
name = f"CONFIG_{item.upper()}"
lines.extend([f"#ifndef {name}", f"#define {name} 0", "#endif"])
lines.append("")
lines.append("#endif /* FFMPEG_CONFIG_H */")
return lines
def _write_file(path: str, lines: list[str]) -> None:
"""Write *lines* to *path* with a trailing newline."""
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
if lines:
f.write("\n")
print(f" wrote {path}", file=sys.stderr)
def main():
parser = argparse.ArgumentParser(
description="Generate config header templates from FFmpeg's configure script.",
)
parser.add_argument(
"ffmpeg_root",
help="Path to FFmpeg source tree root",
)
add_version_arg(parser)
parser.add_argument(
"--ffmpeg-version",
help="Override FFmpeg version string (default: read from RELEASE file)",
)
args = parser.parse_args()
overlay = resolve_overlay_dir(args.version)
configure_path = os.path.join(args.ffmpeg_root, "configure")
if not os.path.isfile(configure_path):
print(f"Error: {configure_path} not found", file=sys.stderr)
sys.exit(1)
lists = extract_all_lists(configure_path)
version = args.ffmpeg_version or extract_version(args.ffmpeg_root)
avconfig_lines = generate_avconfig_h(lists["HAVE_LIST_PUB"])
ffversion_lines = generate_ffversion_h(version)
config_h_in_lines = generate_config_h_in(
lists["ARCH_LIST"],
lists["HAVE_LIST"],
lists["CONFIG_LIST"],
)
_write_file(str(overlay / "config.h.in"), config_h_in_lines)
_write_file(str(overlay / "libavutil" / "avconfig.h.in"), avconfig_lines)
_write_file(str(overlay / "libavutil" / "ffversion.h.in"), ffversion_lines)
if __name__ == "__main__":
main()