2024-09-17 14:49:10 -05:00
""" 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
2024-10-10 10:59:18 -07:00
import subprocess
import sys
from typing import Annotated , Dict , List , Optional , Tuple
import typer
2024-09-17 14:49:10 -05:00
# 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
2024-11-08 15:45:17 -06:00
from buildscripts . client . jiraclient import JiraAuth , JiraClient
from buildscripts . util . codeowners_utils import Owners
JIRA_SERVER = " https://jira.mongodb.org "
2024-09-17 14:49:10 -05:00
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 ] )
2024-11-08 15:45:17 -06:00
# if add_header:
# bd_utils.bd_add([new_dep], "hdrs", [header])
2024-09-17 14:49:10 -05:00
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 ] )
2024-11-08 15:45:17 -06:00
# if remove_header:
# bd_utils.bd_remove([new_dep], "hdrs", [header])
# bd_utils.bd_add([fix_target], "hdrs", [header])
def new_filegroup ( header : str , issue_key : str ) - > str :
package = header . split ( " : " ) [ 0 ] + " :__pkg__ "
target_name = header . split ( " . " ) [ 0 ]
fg_name = target_name . split ( " : " ) [ 1 ] + " _hdrs "
fg_label = package . split ( " : " ) [ 0 ] + f " : { fg_name } "
bd_utils . bd_new ( package , " filegroup " , fg_name )
bd_utils . bd_add ( [ fg_label ] , " srcs " , [ header ] )
return fg_label
def fix_cycle ( fix_target : str , header : str , issue_key : str ) - > str :
fg_label = new_filegroup ( header , issue_key )
bd_utils . bd_add ( [ fix_target ] , " hdrs " , [ fg_label ] )
return fg_label
def todo_comment ( issue_key : str , header : str , new_dep : str , fg_label : str ) - > None :
comment = f " TODO( { issue_key } ): Remove cycle created by moving { header } to { new_dep } " . replace (
" " , " \ "
)
bd_utils . bd_comment ( [ fg_label ] , comment )
2024-09-17 14:49:10 -05:00
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 ) :
2025-03-31 11:05:00 -05:00
self . bazel_exec = " bazel "
2024-11-08 15:45:17 -06:00
auth = JiraAuth ( )
auth . pat = os . environ [ " JIRA_TOKEN " ]
self . jira_client = JiraClient ( JIRA_SERVER , auth , dry_run = False )
self . owners = Owners ( )
self . team_issues = { }
2024-09-17 14:49:10 -05:00
def _query (
self , query : str , config : bool = False , args : List [ str ] = [ ]
) - > subprocess . CompletedProcess :
2024-11-08 15:45:17 -06:00
query_cmd = " cquery "
2024-09-17 14:49:10 -05:00
p = subprocess . run (
2025-03-31 11:05:00 -05:00
[ self . bazel_exec , query_cmd ] + args + [ query ] ,
2024-09-17 14:49:10 -05:00
capture_output = True ,
text = True ,
check = True ,
)
return p
def _build ( self , target : str ) - > subprocess . CompletedProcess :
p = subprocess . run (
2025-03-31 11:05:00 -05:00
[ self . bazel_exec , " build " ] + [ target ] ,
2024-09-17 14:49:10 -05:00
capture_output = True ,
text = 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
2024-11-08 15:45:17 -06:00
# ignore if cpp file of respective header is a src of our fix target
if new_dep == target :
continue
2024-09-17 14:49:10 -05:00
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 ) :
2024-11-08 15:45:17 -06:00
print ( f " Dep { new_dep } is already a dependency " )
2024-09-17 14:49:10 -05:00
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
2024-11-08 15:45:17 -06:00
elif p . returncode == 1 and " cycle in dependency graph " in p . stderr :
2024-09-17 14:49:10 -05:00
target_fixes [ " cycles " ] [ hdr ] = buildozer_cmds
2024-11-08 15:45:17 -06:00
issue_key = self . _create_jira_ticket ( hdr )
fg_label = fix_cycle ( target , hdr , issue_key )
todo_comment ( issue_key , hdr , new_dep , fg_label )
undo_header_move ( target , hdr , new_dep , has_header )
2024-09-17 14:49:10 -05:00
else :
print ( " Unexpected bazel failure. " )
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 ( " // " )
]
2024-11-08 15:45:17 -06:00
def _create_jira_ticket ( self , header : str ) - > str :
2024-11-26 11:27:52 -08:00
summary = " Fix cycle created by " + header
2024-11-08 15:45:17 -06:00
header_file_path = header . replace ( " : " , " / " ) [ 2 : ]
assigned_teams = self . owners . get_jira_team_owner ( header_file_path )
if not assigned_teams :
assigned_teams = [ " Build " ]
teams_key = " , " . join ( sorted ( assigned_teams ) )
if teams_key in self . team_issues :
description = header
issue = self . team_issues [ teams_key ]
# Add new header to description
issue . update ( description = ( issue . fields . description or " " ) + " \n " + description )
else :
description = (
" [Header relocation info|https://github.com/10gen/mongo/blob/master/bazel/docs/header_cycle_resolution.md] \n Please resolve dependency issues with the following headers: \n "
+ header
)
issue = self . jira_client . create_issue (
issue_type = " Bug " ,
summary = summary ,
description = description ,
assigned_teams = assigned_teams ,
jira_project = " SERVER " ,
)
self . team_issues [ teams_key ] = issue
if not issue :
return " "
return issue . key
2024-09-17 14:49:10 -05:00
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 )