| # 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 |