blob: 4a8bd66f5f4089ff51c3195c31160da1df549108 [file] [log] [blame]
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import pathlib
import re
from graph import Header
from graph import IncludeDir
_MODULE_START = re.compile(
r'^(\s*)(?:explicit )?((?:framework )?)module ([^\s.{]+)', flags=re.I)
_HEADER = re.compile(
r'\s+(exclude header|textual header|umbrella header|header|umbrella) "([^"]*)"'
)
_EXTERN_MODULE = re.compile(r'^(\s*)extern module ([^ ]*) "([^"]*)"')
def _parse_modulemap(include_dir: pathlib.Path, mm_path: pathlib.Path,
include_kind: IncludeDir, modules: dict[str, pathlib.Path],
headers: set[Header]):
# The modulemap's paths are relative to the modulemap, but the include's
# paths are relative to d.
rel = str(mm_path.parent.relative_to(include_dir))
rel_prefix = '' if rel == '.' else f'{rel}/'
framework_layers = []
def absolute(rel: str, check_exist=True):
# We don't have / support three layers of nesting of frameworks.
assert len(framework_layers) < 3
if include_kind != IncludeDir.Framework:
path = mm_path.parent / rel
# Frameworks have very specific path requirements.
elif len(framework_layers) == 1:
path = include_dir / 'Headers' / rel
elif len(framework_layers) == 2:
path = include_dir / f'Frameworks/{framework_layers[1]}.framework/Headers' / rel
if check_exist:
assert path.is_file(), path
return path
def relative(rel: str):
if include_kind == IncludeDir.Framework:
# In the event of foo.framework/Frameworks/bar.framework/..., it's bar
return f'{framework_layers[-1]}/{rel}'
else:
return rel_prefix + rel
current_module = None
with mm_path.open() as f:
for line in f:
match = _MODULE_START.match(line)
if match is not None:
indent, is_framework, mod = match.groups()
if not indent:
# It must be a root module
current_module = mod
modules[current_module] = mm_path
if is_framework:
# This is a bit hacky, but frameworks don't go deeper than two layers.
if not indent:
framework_layers = [mod]
else:
framework_layers = [framework_layers[0], mod]
header = _HEADER.search(line)
if header is not None:
kind, name = header.groups()
if kind in ['header', 'umbrella header', 'textual header']:
headers.add(
Header(
root_module=current_module,
include_dir=include_kind,
rel=relative(name),
abs=absolute(name),
textual=kind == 'textual header',
umbrella=kind == 'umbrella header',
))
if kind == 'umbrella':
if len(framework_layers) == 2:
# In case of submodules, reroot at the submodule
# a is an arbitrary filename that we discard with parents.
mod_root = absolute('a', must_exist=False).parents[1]
elif include_kind == IncludeDir.Framework:
mod_root = include_dir
else:
mod_root = mm_path.parent
for path in mod_root.glob(f"{name}/*.h"):
# We need a way to calculate what the name *should* have been.
rel = str(
path.relative_to(mod_root)).removeprefix('Headers').lstrip('/')
headers.add(
Header(
root_module=current_module,
include_dir=include_kind,
rel=relative(rel),
abs=path,
textual=False,
))
extern = _EXTERN_MODULE.match(line)
if extern is not None:
indent, extern_module_name, modulemap = extern.groups()
# The same module can be defined in multiple files. If it is, we can use
# the root module.modulemap's extern module foo "foo.modulemap" to
# resolve which one is the canonical definition.
if current_module is None:
modules[extern_module_name] = include_dir / modulemap
submap_headers = set()
submap_modules = {}
_parse_modulemap(
include_dir,
include_dir / modulemap,
include_kind,
modules=modules if current_module is None else submap_modules,
headers=submap_headers)
for k, v in submap_modules.items():
if k not in submap_modules:
submap_modules[k] = v
# For module foo { extern module bar }, although the module is bar, the
# compilation unit is foo
if indent:
for hdr in submap_headers:
hdr.root_module = current_module
headers.update(submap_headers)
def calculate_modules(
include_kinds: list[tuple[pathlib.Path, IncludeDir]]
) -> tuple[dict[str, pathlib.Path], set[Header]]:
"""Calculates modules and the headers contained within.
Args:
include_kinds: A list of include dirs
Returns:
A mapping from module names to modulemaps, and headers defined by modulemaps
"""
modules = {}
headers = set()
for d, kind in include_kinds:
if kind == IncludeDir.Framework:
# For the semantics of frameworks, see
# https://clang.llvm.org/docs/Modules.html#module-declaration
for modulemap in d.glob("**/Modules/module.modulemap"):
if 'Versions' not in modulemap.parts:
_parse_modulemap(modulemap.parents[1],
modulemap,
include_kind=kind,
modules=modules,
headers=headers)
else:
# One level deep is sufficient for the apple sysroot.
# ** doesn't work because otherwise it includes the things referenced by
# the root module.modulemap
for modulemap in d.glob('*/module.modulemap'):
_parse_modulemap(d,
modulemap,
include_kind=kind,
modules=modules,
headers=headers)
# Intentionally place this after the previous parse_modulemap so that we
# override the modules.
if (d / 'module.modulemap').is_file():
_parse_modulemap(d,
d / 'module.modulemap',
include_kind=kind,
modules=modules,
headers=headers)
return modules, headers