blob: d449914d85bd847b30dab34815d8dab47c9439d3 [file]
#!/usr/bin/env python3
"""Generates overlay/config.in for an MPFR module from the MPFR source tree.
Parses configure.ac and acinclude.m4 to extract the superset of all symbols
that the autoconf build system may define, then writes a config.in template
with #undef directives for each.
"""
from __future__ import annotations
import argparse
import re
import shutil
import subprocess
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source_dir", type=Path, help="Path to MPFR source tree")
parser.add_argument("--version", help="MPFR version (derived from source if omitted)")
parser.add_argument(
"--bcr-root",
type=Path,
default=Path(__file__).resolve().parent.parent.parent,
help="Path to bazel-central-registry root",
)
return parser.parse_args()
def parse_version(source_dir: Path) -> str:
version_file = source_dir / "VERSION"
if version_file.exists():
text = version_file.read_text().strip()
return text.removesuffix("-dev")
mpfr_h = source_dir / "src" / "mpfr.h"
if not mpfr_h.exists():
raise FileNotFoundError(f"Neither VERSION nor src/mpfr.h found in {source_dir}")
text = mpfr_h.read_text()
major = re.search(r"MPFR_VERSION_MAJOR\s+(\d+)", text)
minor = re.search(r"MPFR_VERSION_MINOR\s+(\d+)", text)
patch = re.search(r"MPFR_VERSION_PATCHLEVEL\s+(\d+)", text)
if not all([major, minor, patch]):
raise ValueError("Could not extract version from mpfr.h")
return f"{major.group(1)}.{minor.group(1)}.{patch.group(1)}"
def _strip_m4_quotes(s: str) -> str:
s = s.strip()
if s.startswith("[") and s.endswith("]"):
s = s[1:-1]
return s.strip()
def _header_to_define(header: str) -> str:
return "HAVE_" + header.upper().replace("/", "_").replace(".", "_")
def _func_to_define(func: str) -> str:
return "HAVE_" + func.upper()
def _type_to_define(typename: str) -> str:
return "HAVE_" + typename.upper().replace(" ", "_")
def _member_to_define(member: str) -> str:
parts = member.strip().split(".")
if len(parts) == 2:
struct_part = parts[0].strip().upper().replace(" ", "_")
field_part = parts[1].strip().upper()
return f"HAVE_{struct_part}_{field_part}"
return "HAVE_" + member.upper().replace(" ", "_").replace(".", "_")
def extract_defines(text: str) -> set[str]:
"""Extract all symbol names from AC_DEFINE and AC_DEFINE_UNQUOTED calls."""
defines: set[str] = set()
for match in re.finditer(r"AC_DEFINE(?:_UNQUOTED)?\s*\(\s*\[?(\w+)\]?", text):
defines.add(match.group(1))
return defines
def extract_check_headers(text: str) -> set[str]:
"""Extract headers from AC_CHECK_HEADERS and AC_CHECK_HEADER calls."""
defines: set[str] = set()
for match in re.finditer(r"AC_CHECK_HEADERS?\s*\(\s*\[([^\]]+)\]", text):
for header in match.group(1).split():
header = header.strip().rstrip(",")
if header:
defines.add(_header_to_define(header))
return defines
def extract_check_funcs(text: str) -> set[str]:
"""Extract functions from AC_CHECK_FUNCS calls."""
defines: set[str] = set()
for match in re.finditer(r"AC_CHECK_FUNCS\s*\(\s*\[([^\]]+)\]", text):
for func in match.group(1).split():
func = func.strip().rstrip(",")
if func:
defines.add(_func_to_define(func))
return defines
def extract_check_types(text: str) -> set[str]:
"""Extract types from AC_CHECK_TYPES and AC_CHECK_TYPE calls."""
defines: set[str] = set()
for match in re.finditer(r"AC_CHECK_TYPES?\s*\(\s*\[([^\]]+)\]", text):
for typename in match.group(1).split(","):
typename = typename.strip()
if typename:
defines.add(_type_to_define(typename))
return defines
def extract_check_members(text: str) -> set[str]:
"""Extract members from AC_CHECK_MEMBERS calls."""
defines: set[str] = set()
for match in re.finditer(r"AC_CHECK_MEMBERS\s*\(\s*\[([^\]]+)\]", text):
for member in match.group(1).split(","):
member = member.strip()
if member:
defines.add(_member_to_define(member))
return defines
def extract_func_alloca(text: str) -> set[str]:
"""AC_FUNC_ALLOCA defines HAVE_ALLOCA and HAVE_ALLOCA_H."""
defines: set[str] = set()
if re.search(r"AC_FUNC_ALLOCA", text):
defines.add("HAVE_ALLOCA")
defines.add("HAVE_ALLOCA_H")
return defines
def extract_type_uintptr(text: str) -> set[str]:
"""AC_TYPE_UINTPTR_T may define uintptr_t."""
defines: set[str] = set()
if re.search(r"AC_TYPE_UINTPTR_T", text):
defines.add("HAVE_UINTPTR_T")
return defines
# Symbols that autoconf's standard macros always provide.
_STANDARD_AUTOCONF_DEFINES = {
"STDC_HEADERS",
"PACKAGE_NAME",
"PACKAGE_VERSION",
"PACKAGE_STRING",
"PACKAGE_BUGREPORT",
"PACKAGE_TARNAME",
"PACKAGE_URL",
"PACKAGE",
"VERSION",
}
def extract_source_references(source_dir: Path) -> set[str]:
"""Scan source and test files for config.h symbols used in preprocessor guards.
Catches defines like HAVE_STDINT_H that are used in #if / #ifdef but
not explicitly declared in configure.ac (e.g., when they come from
standard autoconf macros invoked implicitly).
"""
defines: set[str] = set()
pattern = re.compile(
r"(?:#\s*ifn?def\s+|#\s*if\s+(?:defined\s*\(?\s*)?|#\s*elif\s+(?:defined\s*\(?\s*)?)"
r"(HAVE_\w+|MPFR_\w+|WANT_\w+|NPRINTF_\w+|PRINTF_\w+|DECIMAL_\w+|WORDS_BIGENDIAN|STDC_HEADERS)"
)
for subdir in ["src", "tests"]:
d = source_dir / subdir
if not d.is_dir():
continue
for f in sorted(d.glob("*.[ch]")):
for match in pattern.finditer(f.read_text(errors="replace")):
defines.add(match.group(1))
return defines
def collect_all_defines(source_dir: Path) -> list[str]:
"""Parse all autoconf inputs and return a sorted list of all possible defines."""
texts: list[str] = []
for name in ["configure.ac", "acinclude.m4"]:
path = source_dir / name
if path.exists():
texts.append(path.read_text())
m4_dir = source_dir / "m4"
if m4_dir.is_dir():
for m4_file in sorted(m4_dir.glob("*.m4")):
texts.append(m4_file.read_text())
combined = "\n".join(texts)
all_defines: set[str] = set()
all_defines |= extract_defines(combined)
all_defines |= extract_check_headers(combined)
all_defines |= extract_check_funcs(combined)
all_defines |= extract_check_types(combined)
all_defines |= extract_check_members(combined)
all_defines |= extract_func_alloca(combined)
all_defines |= extract_type_uintptr(combined)
all_defines |= extract_source_references(source_dir)
all_defines |= _STANDARD_AUTOCONF_DEFINES
return sorted(all_defines)
def generate_config_in(defines: list[str]) -> str:
"""Generate the config.in template content."""
lines = ["/* config.h. Generated by generate_config_in.py. */", ""]
for define in defines:
lines.append(f"#undef {define}")
lines.append("")
return "\n".join(lines)
def main() -> None:
args = parse_args()
source_dir = args.source_dir.resolve()
if not (source_dir / "configure.ac").exists():
raise FileNotFoundError(f"No configure.ac found in {source_dir}")
version = args.version or parse_version(source_dir)
defines = collect_all_defines(source_dir)
out_dir = args.bcr_root / "modules" / "mpfr" / version / "overlay"
if not out_dir.exists():
raise FileNotFoundError(f"Overlay directory not found: {out_dir}")
out_path = out_dir / "config.in"
content = generate_config_in(defines)
out_path.write_text(content)
print(f"Wrote {out_path} ({len(defines)} defines)")
if __name__ == "__main__":
main()