blob: 6a0b7d8a12b8394df5c319c448f86828e806178c [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 collections
import dataclasses
import enum
import os
import cl_utils
from pathlib import Path
from typing import Dict, Iterable, Optional, Sequence, Tuple
_RUSTC_FUSED_FLAGS = {'-C', '-L', '-Z'}
def _remove_prefix(base: str, prefix: str) -> str:
# str.removeprefix is only available in Python 3.9+
if base.startswith(prefix):
return base[len(prefix):]
return base
def _remove_suffix(base: str, suffix: str) -> str:
# str.removesuffix is only available in Python 3.9+
if base.endswith(suffix):
return base[:-len(suffix)]
return base
def _rustc_command_scanner() -> argparse.ArgumentParser:
"""Analyze rustc command attributes.
Flags should already be canonicalized through `expand_fused_flags()`.
"""
parser = argparse.ArgumentParser(
description="Detects Rust compilation attributes",
argument_default=None,
add_help=False,
)
parser.add_argument(
"-o", # required
type=Path,
dest="output",
default=None,
metavar="FILE",
help="compiler output",
)
parser.add_argument(
"--crate-type",
type=str,
default=None,
metavar="TYPE",
help="Rust compiler output crate type",
)
parser.add_argument(
"--sysroot",
type=Path,
default=None,
help="compiler sysroot, where target-specific rustlibs can be found",
)
parser.add_argument(
"--target",
type=str,
default=None,
help="target platform",
)
parser.add_argument(
"--emit",
action='append',
default=[],
help=
"Types of outputs to produce. Values can be 'key' or 'key=value' form.",
)
parser.add_argument(
"--extern",
action='append',
default=[],
metavar="LIB:PATH",
help="Specify where transitive dependencies can be found",
)
for f in _RUSTC_FUSED_FLAGS:
parser.add_argument(
f,
type=str,
action='append',
dest="{}_flags".format(f.lstrip('-')),
default=[],
help=f"All {f}* flags",
)
return parser
_RUSTC_COMMAND_SCANNER = _rustc_command_scanner()
class CrateType(enum.Enum):
UNKNOWN = 0
RLIB = 1 # "rlib"
BINARY = 2 # "bin"
CDYLIB = 3 # "cdylib"
DYLIB = 4 # "dylib"
PROC_MACRO = 5 # "proc-macro"
STATICLIB = 6 # "staticlib"
def parse_crate_type(crate_type) -> CrateType:
return {
"rlib": CrateType.RLIB,
"bin": CrateType.BINARY,
"cdylib": CrateType.CDYLIB,
"dylib": CrateType.DYLIB,
"proc-macro": CrateType.PROC_MACRO,
"staticlib": CrateType.STATICLIB,
}.get(crate_type, CrateType.UNKNOWN)
class InputType(enum.Enum):
SOURCE = 0
LINKABLE = 1 # Any of: .a, .so, .dylib
@dataclasses.dataclass
class RustcInput(object):
file: Path
type: InputType
def is_linkable(f: str) -> bool:
# f might not be a Path
return any(
f.endswith(suffix)
for suffix in ('.a', '.o', '.so', '.dylib', '.so.debug', '.ld'))
def find_direct_inputs(command: Iterable[str]) -> Iterable[RustcInput]:
for tok in command:
if tok.endswith('.rs'):
yield RustcInput(file=Path(tok), type=InputType.SOURCE)
elif is_linkable(tok):
yield RustcInput(file=Path(tok), type=InputType.LINKABLE)
def find_compiler_from_command(command: Iterable[str]) -> Path:
for tok in command:
if 'rustc' in tok:
return Path(tok)
return None # or raise error
class RustAction(object):
"""This is a rustc compile action."""
def __init__(self, command: Sequence[str], working_dir: Path = None):
# keep a copy of the original command
self._command = command
self._working_dir = (working_dir or Path(os.curdir)).absolute()
# analyze command using canonical expanded form
self._attributes, remaining_args = _RUSTC_COMMAND_SCANNER.parse_known_args(
list(
cl_utils.expand_fused_flags(self._command, _RUSTC_FUSED_FLAGS)))
self._compiler = find_compiler_from_command(remaining_args)
self._env = [
tok for tok in
remaining_args[:remaining_args.index(str(self._compiler))]
if '=' in tok
]
self._crate_type = parse_crate_type(self._attributes.crate_type)
self._direct_inputs = list(find_direct_inputs(remaining_args))
self._C_flags = cl_utils.keyed_flags_to_values_dict(
self._attributes.C_flags)
self._Z_flags = cl_utils.keyed_flags_to_values_dict(
self._attributes.Z_flags)
self._L_flags = cl_utils.keyed_flags_to_values_dict(
self._attributes.L_flags)
self._emit = cl_utils.keyed_flags_to_values_dict(
cl_utils.flatten_comma_list(self._attributes.emit))
raw_extern = cl_utils.keyed_flags_to_values_dict(
cl_utils.flatten_comma_list(self._attributes.extern),
convert_type=Path)
self._extern: Dict[str, Path] = {
k: v[-1] # last value wins
for k, v in raw_extern.items()
if v # ignore empty lists
}
# post-process some flags
self._c_sysroot: Path = None
self._use_ld: Path = None
self._want_sysroot_libgcc = False
self._link_arg_files: Sequence[Path] = []
for arg in self._link_arg_flags:
if arg == '-lgcc':
self._want_sysroot_libgcc = True
continue
left, sep, right = arg.partition('=')
if left == '--sysroot':
self._c_sysroot = Path(right)
continue
if left == '-fuse-ld':
self._use_ld = Path(right)
continue
if is_linkable(arg):
self._link_arg_files.append(arg)
@property
def env(self) -> Sequence[str]:
return self._env
@property
def command(self) -> Sequence[str]:
"""The original command."""
return self._command
@property
def working_dir(self) -> Path:
return self._working_dir
@property
def output_file(self) -> Optional[Path]:
return self._attributes.output # usually this is the -o file
@property
def compiler(self) -> Path:
return self._compiler
@property
def crate_type(self) -> CrateType:
return self._crate_type
@property
def needs_linker(self) -> bool:
return self.crate_type in {
CrateType.BINARY, CrateType.PROC_MACRO, CrateType.DYLIB,
CrateType.CDYLIB
}
@property
def main_output_is_executable(self) -> bool:
return self.crate_type in {
CrateType.BINARY, CrateType.DYLIB, CrateType.CDYLIB
}
@property
def want_sysroot_libgcc(self) -> bool:
return self._want_sysroot_libgcc
@property
def target(self) -> Optional[str]:
return self._attributes.target
@property
def direct_sources(self) -> Sequence[Path]:
return [
s.file for s in self._direct_inputs if s.type == InputType.SOURCE
]
@property
def emit(self) -> Dict[str, str]:
return self._emit
@property
def emit_llvm_ir(self) -> bool:
return 'llvm-ir' in self.emit
@property
def emit_llvm_bc(self) -> bool:
return 'llvm-bc' in self.emit
@property
def save_analysis(self) -> bool:
return cl_utils.last_value_of_dict_flag(
self._Z_flags, 'save-analysis', 'no') == 'yes'
@property
def llvm_time_trace(self) -> bool:
return 'llvm-time-trace' in self._Z_flags
@property
def extra_filename(self) -> str:
return cl_utils.last_value_of_dict_flag(self._C_flags, 'extra-filename')
@property
def depfile(self) -> Optional[Path]:
d = cl_utils.last_value_of_dict_flag(self.emit, 'dep-info', '')
return Path(d) if d else None
@property
def use_ld(self) -> Optional[Path]:
return self._use_ld
@property
def linker(self) -> Optional[Path]:
d = cl_utils.last_value_of_dict_flag(self._C_flags, 'linker')
return Path(d) if d else None
@property
def _link_arg_flags(self) -> Sequence[str]:
return self._C_flags.get('link-arg', [])
@property
def link_arg_files(self) -> Sequence[Path]:
return [Path(p) for p in self._link_arg_files]
@property
def link_map_output(self) -> Optional[Path]:
# The linker can produce a .map output file.
for arg in self._C_flags.get('link-args', []):
if arg.startswith('--Map='):
return Path(_remove_prefix(arg, '--Map='))
return None
def default_rust_sysroot(self) -> Path:
"""This is the relative location of rust sysroot, when unspecified."""
command = [str(self.compiler), '--print', 'sysroot']
result = cl_utils.subprocess_call(
command, cwd=self.working_dir, quiet=True)
if result.returncode != 0:
raise RuntimeError('Error: unable to infer default rust sysroot')
# expect one line with the absolute path to the sysroot
sysroot_abs = Path(result.stdout[0].strip())
sysroot_rel = cl_utils.relpath(sysroot_abs, start=self.working_dir)
return sysroot_rel
@property
def rust_sysroot(self) -> Path:
"""This is where the target rustlibs for all platforms live."""
return self._attributes.sysroot or self.default_rust_sysroot()
@property
def c_sysroot(self) -> Optional[Path]:
return self._c_sysroot
@property
def native(self) -> Sequence[Path]:
return [Path(p) for p in self._L_flags.get('native', [])]
@property
def native_link_arg_files(self) -> Iterable[Path]:
for path in self.native:
if path.is_dir():
# TODO: debug print
pass
elif path.is_file():
yield path
# caller might need to prepend $build_dir
@property
def explicit_link_arg_files(self) -> Sequence[Path]:
return [
s.file for s in self._direct_inputs if s.type == InputType.LINKABLE
]
@property
def externs(self) -> Dict[str, Path]:
return self._extern
def extern_paths(self) -> Iterable[Path]:
yield from self.externs.values()
@property
def _output_file_base(self) -> str:
"""Removes any .rlib or .exe suffix to get the stem name."""
# Returning str instead of Path because caller most likely
# wants to append something to the result to form a Path name.
if not self.output_file:
raise RuntimeError(
'Cannot infer stem name without a named -o output file')
return str(self.output_file.parent / self.output_file.stem)
@property
def _auxiliary_output_path(self) -> str:
# Returning str instead of Path because caller most likely
# wants to append something to the result to form a Path name.
return self._output_file_base + self.extra_filename
def extra_output_files(self) -> Iterable[Path]:
base = self._auxiliary_output_path
if self.emit_llvm_ir:
yield Path(base + '.ll')
if self.emit_llvm_bc:
yield Path(base + '.bc')
if self.save_analysis:
# Path() construction already normalizes away any leading './'
analysis_file = Path(
'save-analysis-temp',
Path(self._auxiliary_output_path + '.json').name)
yield analysis_file
if self.llvm_time_trace:
trace_file = Path(self._output_file_base + '.llvm_timings.json')
yield trace_file
link_map = self.link_map_output
if link_map:
yield link_map
def dep_only_command(self, depfile_name: str) -> Iterable[str]:
"""Generate a command that only produces a depfile."""
new_emit_args = [
f'--emit=dep-info={depfile_name}', '-Z', 'binary-dep-depinfo'
]
replaced_emit = False
for tok in self.command:
if tok.startswith('--emit'): # replace the original emit
if replaced_emit:
pass
else:
replaced_emit = True
yield from new_emit_args
else:
yield tok
# if we haven't seen emit yet, add it
if not replaced_emit:
yield from new_emit_args
# TODO: write a main() routine just for printing debug info about a compile
# command