blob: 531c4d063a95ef3bc14b6c84ffe85e189627e58e [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2022 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""C++ compilation commands and attributes.
"""
import argparse
import dataclasses
import enum
import cl_utils
from pathlib import Path
from typing import Iterable, Optional, Sequence, Tuple
def _remove_suffix(text: str, suffix: str) -> str:
"""string.removesuffix is in Python 3.9+"""
if text.endswith(suffix):
return text[:-len(suffix)]
return text
def _cxx_command_scanner() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Detects C++ compilation attributes (clang, gcc)",
argument_default=[],
add_help=False,
)
parser.add_argument(
"-o",
type=Path,
dest="output",
default=None,
metavar="FILE",
help="compiler output",
required=True,
)
parser.add_argument(
"--sysroot",
type=Path,
default=None,
help="compiler sysroot",
)
parser.add_argument(
"--target",
type=str,
default="",
help="target platform",
)
parser.add_argument(
"-fprofile-list",
type=Path,
dest="profile_list",
default=None,
metavar="FILE",
help="profile list",
)
parser.add_argument(
"-fcrash-diagnostics-dir",
type=Path,
dest="crash_diagnostics_dir",
default=None,
metavar="DIR",
help="additional directory where clang produces crash reports",
)
return parser
_CXX_COMMAND_SCANNER = _cxx_command_scanner()
class Compiler(enum.Enum):
OTHER = 0
CLANG = 1
GCC = 2
class SourceLanguage(enum.Enum):
UNKNOWN = 0
C = 1
CXX = 2 # C++
OBJC = 3 # Objective-C
ASM = 4
@dataclasses.dataclass
class Source(object):
file: Path
dialect: SourceLanguage
def _compile_action_sources(command: Iterable[str]) -> Iterable[Source]:
for tok in command:
if tok.endswith('.c'):
yield Source(file=Path(tok), dialect=SourceLanguage.C)
if tok.endswith('.cc') or tok.endswith('.cxx') or tok.endswith('.cpp'):
yield Source(file=Path(tok), dialect=SourceLanguage.CXX)
if tok.endswith('.s') or tok.endswith('.S'):
yield Source(file=Path(tok), dialect=SourceLanguage.ASM)
if tok.endswith('.mm'):
yield Source(file=Path(tok), dialect=SourceLanguage.OBJC)
def _infer_dialect_from_sources(sources: Iterable[Source]) -> SourceLanguage:
# check by source file extension first
for s in sources:
return s.dialect
return SourceLanguage.UNKNOWN
@dataclasses.dataclass
class CompilerTool(object):
tool: Path
type: Compiler
def _find_compiler_from_command(command: Iterable[str]) -> Optional[Path]:
for tok in command:
if 'clang' in tok: # matches clang++
return CompilerTool(tool=Path(tok), type=Compiler.CLANG)
# 'g++' matches 'clang++', so this clause must come second:
if 'gcc' in tok or 'g++' in tok:
return CompilerTool(tool=Path(tok), type=Compiler.GCC)
return None # or raise error
def _c_preprocess_arg_parser() -> argparse.ArgumentParser:
"""The sole purpose of this is to filter out preprocessing flags.
We do not actually care about the parameters of these flags.
"""
parser = argparse.ArgumentParser(
description="Detects C-preprocessing attributes",
argument_default=[], # many of the options are repeatable
add_help=False,
)
parser.add_argument(
"-D",
type=str,
dest="defines",
action='append',
default=[],
help="preprocessing defines",
)
parser.add_argument(
"-I",
type=Path,
dest="includes",
action='append',
default=[],
metavar="DIR",
help="preprocessing include paths",
)
parser.add_argument(
"-L",
type=Path,
dest="libdirs",
action='append',
default=[],
metavar="DIR",
help="linking search paths",
)
parser.add_argument(
"-U",
type=str,
dest="undefines",
action='append',
default=[],
help="preprocessing undefines",
)
parser.add_argument(
"-isystem",
type=Path,
action='append',
default=[],
metavar="DIR",
help="system include paths",
)
parser.add_argument(
"--sysroot",
type=Path,
help="compiler sysroot",
)
parser.add_argument(
"-stdlib",
type=str,
help="C++ standard library",
)
parser.add_argument(
"-include",
type=Path,
action='append',
default=[],
metavar="FILE",
help="prepend include file",
)
parser.add_argument(
"-M",
action="store_true",
default=False,
help="output make rule",
)
parser.add_argument(
"-MM",
action="store_true",
default=False,
help="output make rule without system dirs",
)
parser.add_argument(
"-MG",
action="store_true",
default=False,
help="tolerate missing generated headers",
)
parser.add_argument(
"-MP",
action="store_true",
default=False,
help="deps include phony targets",
)
parser.add_argument(
"-MD",
action="store_true",
default=False,
help="make dependencies",
)
parser.add_argument(
"-MMD",
action="store_true",
default=False,
help="make dependencies, without system headers",
)
parser.add_argument(
"-MF",
type=Path,
dest="depfile",
default=None,
help="name depfile",
metavar="DEPFILE",
)
parser.add_argument(
"-MT",
type=Path,
help="rename dependency target",
)
parser.add_argument(
"-MQ",
type=Path,
help="rename dependency target (quoted)",
)
parser.add_argument(
"-undef",
action="store_true",
default=False,
help="no predefined macros",
)
return parser
_C_PREPROCESS_ARG_PARSER = _c_preprocess_arg_parser()
# These are flags that are joined with their arguments with
# no separator (no '=' or space).
_CPP_FUSED_FLAGS = {'-I', '-D', '-L', '-U', '-isystem'}
class CxxAction(object):
"""Attributes of C/C++ (or dialect) compilation command.
Suitable for compilers: clang, gcc
"""
def __init__(self, command: Sequence[str]):
self._command = command # keep a copy of the original command
self._attributes, remaining_args = _CXX_COMMAND_SCANNER.parse_known_args(
command)
self._compiler = _find_compiler_from_command(remaining_args)
self._sources = list(_compile_action_sources(remaining_args))
self._dialect = _infer_dialect_from_sources(self._sources)
canonical_command = cl_utils.expand_fused_flags(
self._command, _CPP_FUSED_FLAGS)
self._cpp_attributes, self._command_without_cpp_options = _C_PREPROCESS_ARG_PARSER.parse_known_args(
canonical_command)
@property
def command(self) -> Sequence[str]:
return self._command
@property
def output_file(self) -> Optional[Path]:
return self._attributes.output # usually this is the -o file
@property
def crash_diagnostics_dir(self) -> Optional[Path]:
return self._attributes.crash_diagnostics_dir
@property
def compiler(self) -> CompilerTool:
return self._compiler
@property
def depfile(self) -> Optional[Path]:
return self._cpp_attributes.depfile
@property
def target(self) -> str:
return self._attributes.target
@property
def sysroot(self) -> Path:
return self._attributes.sysroot
@property
def sources(self) -> Sequence[Source]:
return self._sources
@property
def compiler_is_clang(self) -> bool:
return self.compiler.type == Compiler.CLANG
@property
def compiler_is_gcc(self) -> bool:
return self.compiler.type == Compiler.GCC
@property
def dialect_is_c(self) -> bool:
return self._dialect == SourceLanguage.C
@property
def dialect_is_cxx(self) -> bool:
return self._dialect == SourceLanguage.CXX
@property
def profile_list(self) -> Optional[Path]:
return self._attributes.profile_list
@property
def preprocessed_output(self) -> Path:
if self._dialect == SourceLanguage.CXX:
pp_ext = '.ii'
else:
pp_ext = '.i'
# replaces .o with .i or .ii
return self.output_file.with_suffix(pp_ext)
@property
def uses_macos_sdk(self) -> bool:
return str(self.sysroot).startswith('/Library/Developer/')
# TODO: scan command for absolute paths (C++-specific)
def input_files(self) -> Iterable[Path]:
"""Files known to be inputs based on flags."""
# Note: reclient already infers many C++ inputs in its own
# input processor, so it is not necessary to list them
# for remote actions (but it does not hurt).
for s in self.sources:
yield s.file
if self.profile_list:
yield self.profile_list
def output_files(self) -> Iterable[Path]:
# Note: reclient already infers many C++ outputs in its own
# input processor, so it is not necessary to list them
# for remote actions (but it does not hurt).
if self.output_file:
yield self.output_file # This should be first, for naming purposes
# TODO: intermediate outputs from -save-temps, like .i, .s
def output_dirs(self) -> Iterable[Path]:
if self.crash_diagnostics_dir:
yield self.crash_diagnostics_dir
def _preprocess_only_command(self) -> Iterable[str]:
# replace the output with a preprocessor output
for tok in self._command:
if tok == str(self.output_file):
yield str(self.preprocessed_output)
else:
# TODO: discard irrelevant flags, like linker flags
yield tok
# -E tells the compiler to stop after preprocessing
yield '-E'
# -fno-blocks works around in issue where preprocessing includes
# blocks-featured code when it is not wanted.
if self.compiler_is_clang:
yield '-fno-blocks'
def _compile_with_preprocessed_input_command(self) -> Iterable[str]:
# replace the first named source file with the preprocessed output
used_preprocessed_input = False
for tok in self._command_without_cpp_options:
if tok.endswith('.c') or tok.endswith('.cc') or tok.endswith(
'.cxx') or tok.endswith('.cpp'):
if used_preprocessed_input: # ignore other sources after the first
continue
yield str(self.preprocessed_output)
used_preprocessed_input = True
continue
# everything else is kept in the compile command
yield tok
def split_preprocessing(self) -> Tuple[Sequence[str], Sequence[str]]:
"""Create separate preprocessing and compile commands.
Returns:
C-preprocessing command, and
modified compile command using preprocessed input
"""
return (
list(self._preprocess_only_command()),
list(self._compile_with_preprocessed_input_command()),
)
# TODO: write a main() routine just for printing debug info about a compile
# command