mirror of https://github.com/mongodb/mongo
224 lines
7.5 KiB
Python
224 lines
7.5 KiB
Python
"""Cleanup Bazel target headers
|
|
1. Evaluate expression into a list of cc_library targets.
|
|
2. Identify headers defined outside of the package directory.
|
|
3. Lookup target that should claim a given header.
|
|
4. If said target exists, check for cycles by modifying BUILD.bazel and building.
|
|
5. Print report with targets and buildozer commands to fix each one.
|
|
"""
|
|
|
|
# TODO(SERVER-94780) Add buildozer dep to poetry
|
|
import json
|
|
import os
|
|
import pprint
|
|
import subprocess
|
|
import sys
|
|
from typing import Annotated, Dict, List, Optional, Tuple
|
|
|
|
import typer
|
|
|
|
# Get relative imports to work when the package is not installed on the PYTHONPATH.
|
|
if __name__ == "__main__" and __package__ is None:
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
import buildscripts.util.buildozer_utils as bd_utils
|
|
|
|
CC_LIB_SUFFIX = "_with_debug"
|
|
|
|
|
|
def move_header(
|
|
fix_target: str, header: str, new_dep: Optional[str] = None, add_header: bool = False
|
|
) -> None:
|
|
bd_utils.bd_remove([fix_target], "hdrs", [header])
|
|
if new_dep:
|
|
bd_utils.bd_add([fix_target], "deps", [new_dep])
|
|
if add_header:
|
|
bd_utils.bd_add([new_dep], "hdrs", [header])
|
|
|
|
|
|
def undo_header_move(
|
|
fix_target: str, header: str, new_dep: Optional[str] = None, remove_header: bool = False
|
|
) -> None:
|
|
if new_dep:
|
|
bd_utils.bd_remove([fix_target], "deps", [new_dep])
|
|
if remove_header:
|
|
bd_utils.bd_remove([new_dep], "hdrs", [header])
|
|
bd_utils.bd_add([fix_target], "hdrs", [header])
|
|
|
|
|
|
def useful_print(fixes: Dict) -> None:
|
|
for target, target_fixes in fixes.items():
|
|
print("-", target)
|
|
print(" Fixes:\n")
|
|
for header, commands in target_fixes["fixes"].items():
|
|
print(f" -{header}:")
|
|
for cmd in commands:
|
|
print(" ", cmd)
|
|
|
|
|
|
class HeaderFixer:
|
|
def __init__(self):
|
|
# TODO(SERVER-94781) Remove SCons dep
|
|
subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"buildscripts/scons.py",
|
|
"--build-profile=opt",
|
|
"--bazel-includes-info=dummy", # TODO Allow no library to be passed.
|
|
"--libdeps-linting=off",
|
|
"--ninja=disabled",
|
|
"$BUILD_ROOT/scons/$VARIANT_DIR/sconf_temp",
|
|
]
|
|
)
|
|
with open(".bazel_include_info.json") as f:
|
|
bazel_include_info = json.load(f)
|
|
self.bazel_exec = bazel_include_info["bazel_exec"]
|
|
self.bazel_config = bazel_include_info["config"]
|
|
|
|
def _query(
|
|
self, query: str, config: bool = False, args: List[str] = []
|
|
) -> subprocess.CompletedProcess:
|
|
query_cmd = "cquery" if config else "query"
|
|
p = subprocess.run(
|
|
[self.bazel_exec, query_cmd] + self.bazel_config + args + [query],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
return p
|
|
|
|
def _build(self, target: str) -> subprocess.CompletedProcess:
|
|
p = subprocess.run(
|
|
[self.bazel_exec, "build"] + self.bazel_config + [target],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
return p
|
|
|
|
def _fix_package(self, package: str):
|
|
pass
|
|
|
|
def _create_header_target(self):
|
|
pass
|
|
|
|
def _find_misplaced_headers(self, target: str) -> List[str]:
|
|
p = self._query(f"labels(hdrs,{target}{CC_LIB_SUFFIX})")
|
|
misplaced_headers = []
|
|
target_package = target.split(":")[0] + ":"
|
|
for line in p.stdout.splitlines():
|
|
if not line.startswith("//"):
|
|
continue
|
|
if "." not in line:
|
|
continue
|
|
# skip if local
|
|
if line.startswith(target_package):
|
|
continue
|
|
misplaced_headers.append(line.split(" ")[0])
|
|
|
|
return misplaced_headers
|
|
|
|
def _find_header_target(self, header: str) -> Tuple[Optional[str], bool]:
|
|
potential_target = header.split(".")[0]
|
|
p = self._query(f"attr(srcs,{potential_target}.cpp,//...)")
|
|
target = None
|
|
for line in p.stdout.splitlines():
|
|
line = line.split()[0]
|
|
if not line.startswith("//"):
|
|
continue
|
|
if line.endswith(CC_LIB_SUFFIX):
|
|
target = line[: -len(CC_LIB_SUFFIX)]
|
|
|
|
if not target:
|
|
return None, False
|
|
|
|
p = self._query(f"filter('{header}',labels(hdrs,{target}{CC_LIB_SUFFIX}))")
|
|
filter_res = [line for line in p.stdout.splitlines() if line.startswith("//")]
|
|
if filter_res == []:
|
|
return target, False
|
|
return target, True
|
|
|
|
def _get_build_file(self, target: str) -> Optional[str]:
|
|
p = self._query(f"buildfiles({target})")
|
|
for line in p.stdout.splitlines():
|
|
if line.startswith("//src"):
|
|
return line.strip()
|
|
|
|
return None
|
|
|
|
def _check_dep_exists(self, fix_target: str, dep: str) -> bool:
|
|
p = self._query(f"filter({dep}$,deps({fix_target}))")
|
|
for line in p.stdout.splitlines():
|
|
if line.startswith("//") and line.split()[0] == dep:
|
|
return True
|
|
|
|
return False
|
|
|
|
def _fix_target(self, target: str) -> Dict:
|
|
target_fixes = {"fixes": {}, "cycles": {}}
|
|
orphaned_headers = []
|
|
for hdr in self._find_misplaced_headers(target):
|
|
new_dep, has_header = self._find_header_target(hdr)
|
|
if not new_dep:
|
|
orphaned_headers.append(hdr)
|
|
continue
|
|
|
|
buildozer_cmds = [f"buildozer 'remove hdrs {hdr}' {target}"]
|
|
if not has_header:
|
|
buildozer_cmds += [f"buildozer 'add hdrs {hdr}' {new_dep}"]
|
|
if self._check_dep_exists(target, new_dep):
|
|
new_dep = None
|
|
else:
|
|
buildozer_cmds += [f"buildozer 'add deps {new_dep}' {target}"]
|
|
move_header(target, hdr, new_dep, has_header)
|
|
p = self._build(target)
|
|
if p.returncode == 0:
|
|
target_fixes["fixes"][hdr] = buildozer_cmds
|
|
elif p.returncode == 1 and "cycle in dependency graph" in p.stdout:
|
|
target_fixes["cycles"][hdr] = buildozer_cmds
|
|
else:
|
|
print("Unexpected bazel failure.")
|
|
undo_header_move(target, hdr, new_dep, has_header)
|
|
|
|
print(f"Orphaned headers for {target}")
|
|
print("\n".join(orphaned_headers))
|
|
return target_fixes
|
|
|
|
def _evaluate_target_expression(self, target_exp: str) -> List[str]:
|
|
p = self._query(
|
|
f"filter('.*{CC_LIB_SUFFIX}$',kind(cc_library,deps({target_exp}, 1)))",
|
|
["--noimplicit_deps"],
|
|
)
|
|
return [
|
|
line.split()[0][: -len(CC_LIB_SUFFIX)]
|
|
for line in p.stdout.splitlines()
|
|
if line.startswith("//")
|
|
]
|
|
|
|
def fix_targets(self, target_exp: str) -> Dict:
|
|
fixes = {}
|
|
for target in self._evaluate_target_expression(target_exp):
|
|
fixes[target] = self._fix_target(target)
|
|
|
|
return fixes
|
|
|
|
|
|
def main(
|
|
target_exp: Annotated[str, typer.Argument()],
|
|
output_file: Annotated[str, typer.Option()] = "",
|
|
copy_format: Annotated[bool, typer.Option()] = False,
|
|
):
|
|
hf = HeaderFixer()
|
|
fixes = hf.fix_targets(target_exp)
|
|
json_output = pprint.pformat(json.dumps(fixes), compact=False).replace("'", '"')
|
|
if output_file:
|
|
with open(output_file, "w") as f:
|
|
print(json_output, filename, file=f)
|
|
elif copy_format:
|
|
useful_print(fixes)
|
|
else:
|
|
print(json_output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
typer.run(main)
|