| #!/usr/bin/env python3 | |
| # | |
| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| """Regenerate src/iceberg/cpp20_compatibility_internal.h. | |
| The generated header covers all possible public headers installed by | |
| iceberg_install_all_headers(): *.h and *.hpp files matching its PATTERN arguments, | |
| except those with "internal" in their name. The generate-public-headers pre-commit | |
| hook runs this script and fails when the checked-in header is out of date. | |
| Headers of optional components are grouped by directory or an explicit | |
| HEADER_GROUP in the installation call. cpp20_compatibility_test defines each | |
| group's macro only when the corresponding component is built. | |
| """ | |
| from __future__ import annotations | |
| import fnmatch | |
| import re | |
| import shlex | |
| import sys | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[2] | |
| SRC = ROOT / "src" | |
| OUTPUT = SRC / "iceberg" / "cpp20_compatibility_internal.h" | |
| # Install path prefix -> macro guarding it. The first match wins, so list more | |
| # specific prefixes first. Anything unmatched is always built. | |
| OPTIONAL_COMPONENTS = [ | |
| ("iceberg/catalog/hive", "ICEBERG_PUBLIC_HEADERS_HIVE"), | |
| ("iceberg/catalog/rest", "ICEBERG_PUBLIC_HEADERS_REST"), | |
| ("iceberg/catalog/sql", "ICEBERG_PUBLIC_HEADERS_SQL_CATALOG"), | |
| ("iceberg/arrow", "ICEBERG_PUBLIC_HEADERS_BUNDLE"), | |
| ("iceberg/avro", "ICEBERG_PUBLIC_HEADERS_BUNDLE"), | |
| ("iceberg/parquet", "ICEBERG_PUBLIC_HEADERS_BUNDLE"), | |
| ] | |
| INSTALL_CALL = re.compile(r"iceberg_install_all_headers\((.*?)\)", re.DOTALL) | |
| LICENSE = """\ | |
| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| """ | |
| def component_of(install_path: str, header_group: str | None = None) -> str | None: | |
| if header_group: | |
| return header_group | |
| for prefix, macro in OPTIONAL_COMPONENTS: | |
| if install_path == prefix or install_path.startswith(prefix + "/"): | |
| return macro | |
| return None | |
| def collect_public_headers() -> dict[str | None, list[str]]: | |
| headers: dict[str | None, list[str]] = {} | |
| for cmake_file in sorted(SRC.rglob("CMakeLists.txt")): | |
| for call in INSTALL_CALL.findall(cmake_file.read_text()): | |
| tokens = shlex.split(call, comments=True) | |
| if not tokens: | |
| continue | |
| install_path = tokens[0] | |
| try: | |
| pattern_start = tokens.index("PATTERN") + 1 | |
| except ValueError: | |
| pattern_start = len(tokens) | |
| try: | |
| group_start = tokens.index("HEADER_GROUP") | |
| header_group = tokens[group_start + 1] | |
| except ValueError: | |
| group_start = len(tokens) | |
| header_group = None | |
| patterns = tokens[pattern_start:group_start] or ["*.h", "*.hpp"] | |
| group = component_of(install_path, header_group) | |
| for header in cmake_file.parent.iterdir(): | |
| if not any(fnmatch.fnmatch(header.name, pattern) for pattern in patterns): | |
| continue | |
| if header.suffix not in {".h", ".hpp"} or "internal" in header.name: | |
| continue | |
| headers.setdefault(group, []).append(f"{install_path}/{header.name}") | |
| return headers | |
| def render(headers: dict[str | None, list[str]]) -> str: | |
| lines = [ | |
| LICENSE, | |
| "// Generated by ci/scripts/generate_public_headers.py; do not edit by hand.", | |
| "//", | |
| "// Includes every public (installed) header. cpp20_compatibility_test compiles", | |
| "// it as C++20 to keep the public headers usable from C++20.", | |
| "", | |
| "#pragma once", | |
| "", | |
| ] | |
| lines += [f'#include "{h}"' for h in sorted(headers.get(None, []))] | |
| optional_macros = {m for _, m in OPTIONAL_COMPONENTS} | { | |
| group for group in headers if group is not None | |
| } | |
| for macro in sorted(optional_macros): | |
| if macro not in headers: | |
| continue | |
| lines += ["", f"#ifdef {macro}"] | |
| lines += [f'# include "{h}"' for h in sorted(headers[macro])] | |
| lines += [f"#endif // {macro}"] | |
| return "\n".join(lines) + "\n" | |
| def main() -> int: | |
| content = render(collect_public_headers()) | |
| if OUTPUT.exists() and OUTPUT.read_text() == content: | |
| return 0 | |
| OUTPUT.write_text(content) | |
| print(f"Regenerated {OUTPUT.relative_to(ROOT)}; commit the updated file.") | |
| return 1 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |