| #!/usr/bin/env python |
| |
| # 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. |
| """Modularize modularizes a platform.""" |
| |
| import argparse |
| import concurrent.futures |
| import logging |
| import pathlib |
| import re |
| import subprocess |
| import sys |
| import traceback |
| |
| from compiler import Compiler |
| from config import fix_graph |
| from graph import IncludeDir |
| from graph import run_build |
| from platforms import Cpu |
| from platforms import Os |
| import render |
| |
| SOURCE_ROOT = pathlib.Path(__file__).parents[3].resolve() |
| _OS_OPTS = '|'.join(os.value for os in Os) |
| _CPU_OPTS = '|'.join(cpu.value for cpu in Cpu) |
| _PLATFORM = re.compile('^' + ''.join([ |
| f'(?P<os>{_OS_OPTS})', |
| f'(?:-(?P<cpu>{_CPU_OPTS}))?', |
| r'(?P<xcode_suffix>_xcode\d+)?', |
| ]) + '$') |
| |
| |
| def main(args): |
| logging.basicConfig(level=logging.getLevelNamesMapping()[args.verbosity]) |
| existing_platforms = [ |
| f.name for f in (SOURCE_ROOT / 'build/modules').iterdir() |
| ] |
| |
| filter = { |
| 'os': args.os, |
| 'cpu': args.cpu, |
| } |
| |
| calls = {} |
| for platform in existing_platforms: |
| match = _PLATFORM.match(platform) |
| if match is None: |
| continue |
| |
| props = match.groupdict() |
| os = Os(props['os']) |
| # If no CPU is provided, we support all CPUs with a single BUILD.gn. |
| # But we do need to be consistent about the CPU we target, so we arbitrarily |
| # pick x64 since it's probably the most supported CPU. |
| cpu = Cpu(props['cpu'] or 'x64') |
| |
| # We want to reconstruct the platform to dedupe xcode versions, and ensure |
| # that we always have the correct cpu arch. |
| platform = f"{os.value}-{cpu.value}" |
| |
| skip = False |
| for k, v in filter.items(): |
| if v is not None and props[k] != v: |
| skip = True |
| if skip: |
| continue |
| out_dir = SOURCE_ROOT / 'out' / platform |
| args_gn = out_dir / 'args.gn' |
| if not args_gn.exists(): |
| out_dir.mkdir(exist_ok=True, parents=True) |
| args_gn.write_text('\n'.join([ |
| # These are required |
| f'target_os = "{os.value}"', |
| f'target_cpu = "{cpu.value}"', |
| # This is required (see README.md) |
| 'use_clang_modules = true', |
| # This is strongly recommended, otherwise you may accidentally |
| # test manual modules. |
| 'use_autogenerated_modules = true', |
| # Some useful defaults. User is free to change these. |
| 'use_remoteexec = true', |
| 'symbol_level = 0', |
| 'is_debug = false', |
| 'running_modularize = true', |
| ])) |
| |
| error_log = None if args.error_log is None else args.error_log / platform |
| calls[platform] = dict( |
| out_dir=out_dir, |
| error_log=error_log, |
| use_cache=args.cache, |
| compile=args.compile, |
| os=os, |
| cpu=cpu, |
| ) |
| |
| if not calls: |
| print('No matching platforms. Try copying an existing one', file=sys.stderr) |
| elif len(calls) == 1: |
| _modularize(**next(iter(calls.values()))) |
| return |
| |
| # Use a ProcessPoolExecutor rather than a ThreadPoolExecutor because: |
| # * No shared state between instances |
| # * GIL prevents a performance benefit from a thread pool executor. |
| with concurrent.futures.ProcessPoolExecutor() as executor: |
| futures = { |
| k: executor.submit(_modularize, **kwargs) |
| for k, kwargs in calls.items() |
| } |
| |
| success = True |
| for platform, future in sorted(futures.items()): |
| exc = future.exception() |
| if exc is not None: |
| success = False |
| print(f'{platform} raised an exception:', file=sys.stderr) |
| traceback.print_exception(exc) |
| if not success: |
| exit(1) |
| |
| |
| def _modularize(out_dir: pathlib.Path, error_log: pathlib.Path | None, |
| use_cache: bool, compile: bool, cpu: Cpu, os: Os): |
| # Modularize requires gn gen to have been run at least once. |
| if not (out_dir / 'build.ninja').is_file(): |
| subprocess.run(['gn', 'gen', out_dir], check=True) |
| compiler = Compiler( |
| source_root=SOURCE_ROOT, |
| gn_out=out_dir, |
| error_dir=error_log, |
| use_cache=use_cache, |
| cpu=cpu, |
| os=os, |
| ) |
| |
| if compile: |
| ps, files = compiler.compile_one(compile) |
| print('stderr:', ps.stderr.decode('utf-8'), file=sys.stderr) |
| print('Files used:') |
| print('\n'.join(sorted(map(str, files)))) |
| print('Setting breakpoint to allow further debugging') |
| breakpoint() |
| return |
| |
| graph = compiler.compile_all() |
| replacements = fix_graph(graph, compiler) |
| targets = run_build(graph) |
| |
| platform = (out_dir / 'gen/module_platform.txt').read_text() |
| logging.info('Detected platform %s', platform) |
| |
| out_dir = SOURCE_ROOT / 'build/modules' / platform |
| out_dir.mkdir(exist_ok=True, parents=False) |
| if compiler.sysroot_dir == IncludeDir.Sysroot: |
| render.render_modulemap(out_dir=out_dir, |
| replacements=replacements, |
| targets=targets) |
| render.render_build_gn( |
| out_dir=out_dir, |
| targets=targets, |
| compiler=compiler, |
| ) |
| |
| |
| def _optional_path(s: str) -> pathlib.Path | None: |
| if s: |
| return pathlib.Path(s).resolve() |
| |
| |
| if __name__ == '__main__': |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| '--cpu', |
| help='Only update platforms matching this cpu', |
| type=lambda x: None if x is None else Cpu(x), |
| default=None, |
| choices=[cpu.value for cpu in Cpu], |
| ) |
| parser.add_argument( |
| '--os', |
| help='Only update platforms matching this cpu', |
| type=lambda x: None if x is None else Os(x), |
| default=None, |
| choices=[os.value for os in Os], |
| ) |
| |
| # Make it required so the user understands how compilation works. |
| cache = parser.add_mutually_exclusive_group(required=True) |
| cache.add_argument( |
| '--cache', |
| action='store_true', |
| help='Enable caching. Will attempt to reuse the compilation results.') |
| cache.add_argument( |
| '--no-cache', |
| action='store_false', |
| dest='cache', |
| help='Disable caching. Will attempt to recompile the whole libcxx, ' + |
| 'builtins, and sysroot on every invocation') |
| |
| parser.add_argument( |
| '--compile', |
| help='Compile a single header file (eg. --compile=sys/types.h) instead ' + |
| 'of the whole sysroot. Useful for debugging.', |
| ) |
| |
| parser.add_argument('--error-log', type=_optional_path) |
| |
| parser.add_argument( |
| '--verbosity', |
| help='Verbosity of logging', |
| default='INFO', |
| choices=logging.getLevelNamesMapping().keys(), |
| type=lambda x: x.upper(), |
| ) |
| |
| main(parser.parse_args()) |