# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Helpers for determining Component of directories."""
import functools
import os
import re
_METADATA_FILENAME = 'DIR_METADATA'
_METADATA_COMPONENT_REGEX = re.compile(r'^\s*component:\s*"(.*?)"',
re.MULTILINE)
_METADATA_MIXINS_REGEX = re.compile(r'^\s*mixins:\s*"(.*?)"', re.MULTILINE)
# Paths that are missing metadata, and where it's hard to add (e.g. code in
# other repositories.
_COMPONENT_DEFAULTS = {
os.path.join('third_party', 'webrtc'): 'Blink>WebRTC',
os.path.join('logging', 'rtc_event_log'): 'Blink>WebRTC', # Generated files
os.path.join('modules'): 'Blink>WebRTC', # Generated files
}
def _SafeRead(path):
try:
with open(path) as f:
return f.read()
except IOError:
# Need to catch both FileNotFoundError and NotADirectoryError since
# source_paths for .aar files look like: /path/to/lib.aar/path/within/zip
return ''
@functools.lru_cache
class _ComponentLookupContext:
def __init__(self, source_directory, component_overrides):
self._mixins_cache = {}
self._dir_cache = _COMPONENT_DEFAULTS.copy()
self._source_directory = source_directory
self._component_overrides = component_overrides
def ComponentForSourcePath(self, source_path):
return self._ComponentForDirectory(os.path.dirname(source_path))
def _ParseComponentFromMetadata(self, path):
"""Extracts Component from DIR_METADATA."""
result = self._mixins_cache.get(path)
if result is not None:
return result
data = _SafeRead(path)
m = _METADATA_COMPONENT_REGEX.search(data)
if m:
result = m.group(1)
else:
# Recurse on mixins.
self._mixins_cache[path] = '' # Guard against cycles.
result = ''
for mixin_path in _METADATA_MIXINS_REGEX.findall(data):
if mixin_path.startswith('//'):
mixin_path = os.path.join(self._source_directory, mixin_path[2:])
else:
logging.warning('Found non-ablsolute mixin path in %s', path)
continue
result = self._ParseComponentFromMetadata(mixin_path)
if result:
break
self._mixins_cache[path] = result
return result
def _ComponentForDirectory(self, directory):
"""Searches all parent directories for COMPONENT in OWNERS files.
Args:
directory: Path of directory to start searching from relative to
|source_directory|.
Returns:
COMPONENT belonging to |path|, or empty string if not found.
"""
assert not os.path.isabs(directory)
component = self._dir_cache.get(directory)
if component is not None:
return component
for prefix, component in self._component_overrides:
if directory.startswith(prefix):
result = component
break
else:
metadata_path = os.path.join(self._source_directory, directory,
_METADATA_FILENAME)
result = self._ParseComponentFromMetadata(metadata_path)
if not result:
parent_directory = os.path.dirname(directory)
if parent_directory:
result = self._ComponentForDirectory(parent_directory)
self._dir_cache[directory] = result
return result
def PopulateComponents(raw_symbols, source_directory, component_overrides,
default_component):
"""Populates the |component| field based on |source_path|.
Symbols without a |source_path| are skipped.
Args:
raw_symbols: list of Symbol objects.
source_directory: Directory to use as the root.
component_overrides: Tuple of (source path prefix, component) tuples.
default_component: Component to use when none was found.
"""
# Convert to tuple for lru_cache.
context = _ComponentLookupContext(source_directory, component_overrides)
for symbol in raw_symbols:
found_component = ''
if symbol.source_path:
found_component = context.ComponentForSourcePath(symbol.source_path)
if not found_component and symbol.object_path:
# Some generated files and not put under their target_gen_dir (common
# grit _resources_maps.cc files). So also look at object path.
found_component = context.ComponentForSourcePath(symbol.object_path)
symbol.component = found_component or default_component