blob: 8a09e6c03ebf2863827ae98635283393c4e712b2 [file]
#!/usr/bin/env python3
"""Regenerate FFmpeg component declarations and configure dependencies.
Usage:
python3 generate_component_defs.py --version 9.0.1.beta.bcr /path/to/ffmpeg
The source tree supplies component declarations, dependency assignments, and
CONFIG_EXTRA. The immutable --policy-version overlay supplies Bazel defaults,
supported platform components, and synthetic component settings. No configure
commands or compiler checks are executed.
"""
from __future__ import annotations
import argparse
import ast
import json
import re
from pathlib import Path
from _overlay_utils import SCRIPTS_DIR, add_version_arg, resolve_overlay_dir
from generate_config_defs import extract_all_lists
DEPENDENCY_FIELDS = ("deps", "deps_any", "select", "suggest", "conflict")
# These settings are part of the Bazel overlay, outside configure's CONFIG_EXTRA.
# Keep them separate from the upstream list so removed CONFIG_EXTRA entries do
# not survive an update merely because the previous overlay contained them.
SYNTHETIC_COMPONENTS = (
"dwt",
"error_resilience",
"h264_metadata",
"hevc_metadata",
"lsp",
"network",
"pixelutils",
"videotoolbox_encoder",
)
DECLARATIONS = {
"bsf": ("libavcodec/bitstream_filters.c", "FFBitStreamFilter", "bsf"),
"decoder": ("libavcodec/allcodecs.c", "FFCodec", "decoder"),
"demuxer": ("libavformat/allformats.c", "FFInputFormat", "demuxer"),
"encoder": ("libavcodec/allcodecs.c", "FFCodec", "encoder"),
"hwaccel": ("libavcodec/hwaccels.h", "FFHWAccel", "hwaccel"),
"indev": ("libavdevice/alldevices.c", "FFInputFormat", "demuxer"),
"muxer": ("libavformat/allformats.c", "FFOutputFormat", "muxer"),
"outdev": ("libavdevice/alldevices.c", "FFOutputFormat", "muxer"),
"parser": ("libavcodec/parsers.c", "(?:FFCodecParser|AVCodecParser)", "parser"),
"protocol": ("libavformat/protocols.c", "URLProtocol", "protocol"),
}
def literal_assignments(text: str) -> dict:
"""Read literal top-level Starlark assignments without executing Starlark."""
values = {}
for node in ast.parse(text).body:
if not isinstance(node, ast.Assign) or not isinstance(node.targets[0], ast.Name):
continue
try:
values[node.targets[0].id] = ast.literal_eval(node.value)
except (ValueError, TypeError):
pass
return values
def configure_dependencies(text: str) -> dict[str, dict[str, list[str]]]:
"""Extract configure's literal component dependency assignments."""
result: dict[str, dict[str, list[str]]] = {}
fields = "|".join(DEPENDENCY_FIELDS)
pattern = re.compile(
rf'^([a-zA-Z0-9_]+)_({fields})="([^"]*)"',
re.MULTILINE,
)
for match in pattern.finditer(text):
component, field, value = match.groups()
names = value.split()
if any(not re.fullmatch(r"[a-zA-Z0-9_]+", name) for name in names):
raise ValueError(f"unsupported configure assignment: {match.group(0)}")
result.setdefault(component, {})[field] = names
return result
def component_declarations(source: Path) -> tuple[dict[str, list[str]], dict[str, str]]:
"""Read the same registration declarations used by configure's find helpers."""
types = {}
for component_type, (filename, c_type, suffix) in DECLARATIONS.items():
text = (source / filename).read_text(encoding="utf-8")
names = re.findall(
rf"^extern const (?:struct )?{c_type}\s+ff_([a-zA-Z0-9_]+)_{suffix};",
text,
re.MULTILINE,
)
if not names:
raise ValueError(f"no {component_type} declarations in {filename}")
types[component_type] = sorted({f"{name}_{component_type}" for name in names})
filters = re.findall(
r"^extern const (?:FFFilter|AVFilter) ff_([avfsinkrc]{2,5}_([a-zA-Z0-9_]+));",
(source / "libavfilter/allfilters.c").read_text(encoding="utf-8"),
re.MULTILINE,
)
if not filters:
raise ValueError("no filter declarations in libavfilter/allfilters.c")
symbols = {f"{name}_filter": symbol for symbol, name in filters}
types["filter"] = sorted(symbols)
return dict(sorted(types.items())), dict(sorted(symbols.items()))
def format_assignment(name: str, value) -> str:
"""Format component names, lists, and registries as Starlark literals."""
if isinstance(value, list):
if name == "ALWAYS_AVAILABLE_LIBS":
return f"{name} = {json.dumps(value)}"
return name + " = [\n" + "".join(f" {json.dumps(v)},\n" for v in value) + "]"
if isinstance(value, dict):
lines = [name + " = {"]
for key, item in sorted(value.items()):
if isinstance(item, list):
lines.append(f" {json.dumps(key)}: [")
lines.extend(f" {json.dumps(v)}," for v in item)
lines.append(" ],")
continue
if isinstance(item, bool):
formatted = str(item)
else:
formatted = json.dumps(item, sort_keys=True)
lines.append(f" {json.dumps(key)}: {formatted},")
lines.append("}")
return "\n".join(lines)
raise TypeError(f"unsupported assignment {name}: {type(value).__name__}")
def policy_definitions(policy_text: str, components: set[str]) -> str:
"""Preserve the policy helpers while dropping names removed by FFmpeg."""
start = next(
node.end_lineno
for node in ast.parse(policy_text).body
if isinstance(node, ast.Assign)
and isinstance(node.targets[0], ast.Name)
and node.targets[0].id == "FILTER_SYMBOL_MAP"
)
text = "".join(policy_text.splitlines(keepends=True)[start:]).lstrip("\n")
lines = text.splitlines(keepends=True)
edits = []
filtered = {
"_PLATFORM_SPECIFIC_DEVICES",
"_EXTERNAL_COMPONENTS",
"MACOS_COMPONENTS",
"LINUX_COMPONENTS",
"WINDOWS_COMPONENTS",
}
for node in ast.parse(text).body:
if not isinstance(node, ast.Assign) or not isinstance(node.targets[0], ast.Name):
continue
name = node.targets[0].id
if name not in filtered:
continue
value = ast.literal_eval(node.value)
if isinstance(value, dict):
filtered_value = {key: item for key, item in value.items() if key in components}
else:
filtered_value = [item for item in value if item in components]
if filtered_value != value:
edits.append((node.lineno - 1, node.end_lineno, format_assignment(name, filtered_value) + "\n"))
for begin, end, replacement in reversed(edits):
lines[begin:end] = [replacement]
return "".join(lines).rstrip()
def generate_definitions(source: Path, policy_text: str, policy_version: str) -> str:
"""Generate registries from one FFmpeg release and one Bazel policy version."""
policy = literal_assignments(policy_text)
lists = extract_all_lists(str(source / "configure"))
dependencies = configure_dependencies((source / "configure").read_text(encoding="utf-8"))
types, symbols = component_declarations(source)
registry = {
name: dict(dependencies.get(name, {}), type=component_type)
for component_type, names in types.items()
for name in names
}
extras = set(lists["CONFIG_EXTRA"]) | set(SYNTHETIC_COMPONENTS)
extra_registry = {name: dependencies.get(name, {}) for name in sorted(extras)}
overlap = set(registry) & extras
if overlap:
raise ValueError(f"components also listed in CONFIG_EXTRA: {sorted(overlap)}")
# ffmpeg_config_checks.bzl enables HAVE_THREADS on every supported platform.
available = sorted(set(policy["ALWAYS_AVAILABLE_LIBS"]) | {"threads"})
components = set(registry)
sections = [
(
'"""Generated FFmpeg component declarations and configure dependencies.\n\n'
"Generated by generate_component_defs.py; do not edit.\n"
f"Bazel component policy comes from {policy_version}.\n"
'"""'
),
format_assignment("COMPONENT_TYPES", types),
format_assignment("COMPONENT_REGISTRY", registry),
format_assignment("CONFIG_EXTRA_REGISTRY", extra_registry),
format_assignment("CONFIG_LIST", sorted(set(lists["CONFIG_LIST"]))),
format_assignment("ALWAYS_AVAILABLE_LIBS", available),
format_assignment("PROFILE_MINIMAL", [name for name in policy["PROFILE_MINIMAL"] if name in components]),
format_assignment("PROFILE_EVERYTHING", sorted(components)),
format_assignment("FILTER_SYMBOL_MAP", symbols),
policy_definitions(policy_text, components),
]
return "\n\n".join(sections) + "\n"
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("ffmpeg_source_root", type=Path)
parser.add_argument("--policy-version", default="7.1.1.bcr.beta.7")
parser.add_argument("--check", action="store_true", help="Check the generated file without writing it.")
add_version_arg(parser)
args = parser.parse_args()
overlay = resolve_overlay_dir(args.version)
policy_file = SCRIPTS_DIR / args.policy_version / "overlay" / "component_defs.bzl"
output = generate_definitions(
args.ffmpeg_source_root,
policy_file.read_text(encoding="utf-8"),
args.policy_version,
)
output_file = overlay / "component_defs.bzl"
if args.check:
if output_file.read_text(encoding="utf-8") != output:
raise SystemExit(f"{output_file} needs regeneration")
else:
output_file.write_text(output, encoding="utf-8")
print(f" {'checked' if args.check else 'wrote'} {output_file}")
if __name__ == "__main__":
main()