blob: 6ade912ceceae263e8797f4b261bea10384ebc40 [file] [log] [blame]
#!/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())