blob: 60fd9a140fea79bc6b0d25cc2dbfd1d16dd203df [file]
#!/usr/bin/env python3
# Copyright (C) 2026 The Android Open Source Project
#
# Licensed 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.
"""Verifies trace_processor plugin dependency graph rules.
Two rules are enforced by inspecting the JSON GN build description:
1. Targets outside src/trace_processor/plugins/ must not depend on any
plugin target. The only allowed dependers of plugin targets from
non-plugin code are the leaf consumer targets in
ALLOWED_NON_PLUGIN_DEPENDERS (the trace_processor lib/unittests
and the root :perfetto_benchmarks executable).
2. plugin -> plugin GN deps must be matched by a #include of the
dependee plugin's public header in the depender plugin's sources.
The public header is plugins/<name>/<name>.h. Including the header
is what forces the C++ Plugin<Self, ...> template parameter to
resolve.
"""
from __future__ import print_function
import argparse
import os
import re
import sys
from typing import Dict, List, Optional, Set, Tuple
import gn_utils
PLUGIN_PATH_PREFIX = 'src/trace_processor/plugins/'
ALLOWED_NON_PLUGIN_DEPENDERS = (
'//src/trace_processor:lib',
'//src/trace_processor:unittests',
'//:perfetto_benchmarks',
)
INCLUDE_RE = re.compile(r'^\s*#include\s+"([^"]+)"')
def label_dir_and_name(label: str) -> Tuple[str, str]:
"""Splits a GN label like //foo/bar:baz into ('foo/bar', 'baz')."""
no_tc = gn_utils.label_without_toolchain(label)
assert no_tc.startswith('//'), no_tc
body = no_tc[2:]
if ':' in body:
d, n = body.split(':', 1)
return d, n
return body, os.path.basename(body)
def plugin_name_for_dir(dir_path: str) -> Optional[str]:
"""For src/trace_processor/plugins/<name>[/sub] returns <name>."""
if not dir_path.startswith(PLUGIN_PATH_PREFIX):
return None
rest = dir_path[len(PLUGIN_PATH_PREFIX):]
if not rest:
return None
return rest.split('/', 1)[0]
def header_included_in_files(header: str, files: Set[str]) -> bool:
for rel in files:
abs_path = os.path.join(gn_utils.repo_root(), rel)
try:
with open(abs_path, 'r', encoding='utf-8') as f:
for line in f:
m = INCLUDE_RE.match(line)
if m and m.group(1) == header:
return True
except (OSError, UnicodeDecodeError):
continue
return False
def check(desc: Dict) -> List[str]:
rule1_violations: Set[Tuple[str, str]] = set()
plugin_to_plugin: Set[Tuple[str, str]] = set()
plugin_sources: Dict[str, Set[str]] = {}
for label, target in desc.items():
src_dir, _ = label_dir_and_name(label)
src_label = gn_utils.label_without_toolchain(label)
src_plugin = plugin_name_for_dir(src_dir)
if src_plugin is not None:
bucket = plugin_sources.setdefault(src_plugin, set())
for s in list(target.get('sources', [])) + list(target.get('public', [])):
if not s.startswith('//'):
continue
if s.endswith('.h') or s.endswith('.cc'):
bucket.add(gn_utils.label_to_path(s))
for dep in target.get('deps', []):
dep_dir, _ = label_dir_and_name(dep)
dst_plugin = plugin_name_for_dir(dep_dir)
if dst_plugin is None:
continue
if src_plugin is not None:
if src_plugin == dst_plugin:
continue
plugin_to_plugin.add((src_plugin, dst_plugin))
elif src_label in ALLOWED_NON_PLUGIN_DEPENDERS:
continue
else:
rule1_violations.add((src_label, gn_utils.label_without_toolchain(dep)))
errors: List[str] = []
allowed = ' or '.join(ALLOWED_NON_PLUGIN_DEPENDERS)
for label, dep in sorted(rule1_violations):
errors.append(
f'target "{label}" depends on plugin target "{dep}". '
f'Only plugin targets and {allowed} may depend on plugin targets.')
for src, dst in sorted(plugin_to_plugin):
expected = f'{PLUGIN_PATH_PREFIX}{dst}/{dst}.h'
if not header_included_in_files(expected, plugin_sources.get(src, set())):
errors.append(
f'plugin "{src}" GN-depends on plugin "{dst}" but no source '
f'file in {PLUGIN_PATH_PREFIX}{src}/ includes "{expected}". '
f'plugin->plugin deps must be matched by a Plugin<{src}, {dst}> '
f'template parameter, which requires including the dependee '
f"plugin's public header.")
return errors
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'--out',
help='use an existing GN out directory instead of creating a temp one.')
args = parser.parse_args()
if args.out:
desc = gn_utils.load_build_description(args.out)
errors = check(desc)
else:
with gn_utils.BuildDescription('') as bd:
errors = check(bd.desc)
for e in errors:
print(e, file=sys.stderr)
return 1 if errors else 0
if __name__ == '__main__':
sys.exit(main())