blob: d4b80be824d9925ae7add5b7d5dac395445fd84a [file]
#!/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())