blob: f5d8a1a37152473d25109284d42d015d6af96a89 [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 tempfile
import cl_utils
from pathlib import Path
from typing import AbstractSet, 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 tok.startswith("@"):
yield RustcInput(file=Path(tok[1:]), 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 _ScanDepsCommandTransformer(object):
"""Implementation detail of rustc_dep_only_command()."""
def __init__(self, emit_metadata: bool, depfile_name: str):
self._emit_metadata = emit_metadata
self._depfile_name = depfile_name
self._replaced_emit = False
self._aux_rspfiles = []
@property
def emit_metadata(self) -> bool:
return self._emit_metadata
@property
def depfile_name(self) -> str:
return self._depfile_name
@property
def _replacement_emit_args(self) -> Sequence[str]:
# Remove outputs like emit=link.
if self.emit_metadata:
# Need to tell the compiler driver that we are only interested
# in the deps needed for generating metadata.
return [
f"--emit=metadata,dep-info={self.depfile_name}",
"-Z",
"binary-dep-depinfo",
]
else:
return [
f"--emit=dep-info={self.depfile_name}",
"-Z",
"binary-dep-depinfo",
]
def _rustc_dep_only_command_impl(
self, command_tokens: Iterable[str]
) -> Iterable[str]:
"""Generate a command that only produces a depfile.
This also accumulates a set of temporary transformed rspfiles
that the caller is responsible for cleaning up.
Yields:
possibly transformed command token
"""
handle_optarg = None
output_arg = False
# Use the original command (without response files expanded)
# to avoid command length limits.
# Because of this, this transformation on --emit only works
# when the --emit argument is not buried inside a response file.
# When targeting metadata, for --externs, replace .rlib with
# corresponding .rmeta if it exists (not required).
for tok in command_tokens:
if handle_optarg == "--extern":
handle_optarg = None
if not self.emit_metadata:
yield tok
continue
lib, sep, path = tok.partition("=")
if sep != "=": # --extern foo (without path)
yield tok
continue
# Assume .so files are proc_macros, which are needed for
# dep scanning; rmeta alone won't suffice.
if path.endswith(".so"):
yield tok
continue
rmeta = Path(path).with_suffix(".rmeta")
# evaluating metadata only requires other .rmeta files
if rmeta.exists():
yield f"{lib}={rmeta}"
else:
yield tok
continue
if handle_optarg == "-o":
handle_optarg = None
if self.emit_metadata:
yield str(Path(tok).with_suffix(".rmeta"))
else:
yield tok
continue
# Replace the first --emit encountered in the original command tokens
if tok.startswith("--emit"):
if self._replaced_emit:
pass
else:
self._replaced_emit = True
for arg in self._replacement_emit_args:
yield arg
continue
if tok in {"--extern", "-o"}:
handle_optarg = tok
yield tok
continue
# Check for response files, recursively descend into them.
prefix_matched = False
for prefix in ("@shell:", "@"):
# Apparently shell-argfiles do not want a trailing newline,
# because that will result in an '' filename arg.
if tok.startswith(prefix):
prefix_matched = True
# recursively descend into response files, and rewrite them
# into new copies as needed.
orig_rspfile = Path(tok.removeprefix(prefix))
yield self._transform_rspfile_arg(prefix, orig_rspfile)
break
if prefix_matched:
continue
# else
yield tok
def _transform_rspfile_arg(self, prefix: str, orig_rspfile: Path) -> str:
"""Transform the contents of a @rspfile into a new one.
Args:
prefix: either "@" or "@shell:" part of the response file argument.
orig_rspfile: the original response file referenced.
Returns:
Possibly modified command-line argument for a response file.
"""
rspfile_contents = orig_rspfile.read_text()
if not rspfile_contents:
# Don't bother changing empty files.
# This also avoids accidentally adding a blank line
# which the compiler may interpret as ''.
return f"{prefix}{orig_rspfile}"
new_rspfile_lines = list(
self._rustc_dep_only_command_rspfile_lines(
rspfile_contents.splitlines()
)
)
# Use a temporary file with a random suffix, in case
# multiple concurrent invocations reference the same rspfile.
# It is the caller's responsibility to clean up each of these
# tempfiles.
_, new_rspfile = tempfile.mkstemp(
dir=str(orig_rspfile.parent),
prefix=orig_rspfile.name + ".aux.",
text=True,
)
# Keep absolute/relative path consistent with original.
new_rspfile_path = Path(new_rspfile)
if not orig_rspfile.is_absolute():
new_rspfile_path = cl_utils.relpath(new_rspfile_path, Path("."))
new_rspfile_path.write_text("\n".join(new_rspfile_lines) + "\n")
# Tell the caller to clean-up this transformed rspfile.
self._aux_rspfiles.append(new_rspfile_path)
return f"{prefix}{new_rspfile_path}"
def _rustc_dep_only_command_rspfile_lines(
self,
rspfile_lines: Iterable[str],
) -> Iterable[str]:
new_lines = []
# transform rspfile one line at a time
for line in rspfile_lines:
yield self._rustc_dep_only_command_rspfile_line(line)
def _rustc_dep_only_command_rspfile_line(
self,
rspfile_line: str,
) -> str:
"""Transforms a single line of a rspfile for a dep-scanning command."""
# Note: use space split instead of shlex.split because quotes
# are to be interpreted literally.
return " ".join(
self._rustc_dep_only_command_impl(
command_tokens=rspfile_line.split(" ")
)
)
def rustc_dep_only_command(
command_tokens: Iterable[str],
emit_metadata: bool,
depfile_name: str,
) -> Tuple[Sequence[str], Sequence[Path]]:
"""Generate a command that only produces a depfile.
This implementation handles response files recursively,
by transforming them as they are encountered into new auxiliary
response files referenced in the new command.
Args:
command_tokens: original rustc command
emit_metadata: if True, command is expected to generate
separate metadata.
depfile_name: name of the new depfile to emit (different
from the depfile in the original command).
Returns:
* transformed command tokens, for dep-scanning
* paths of rewritten temporary response files (containing
possibly transformed command tokens), that the caller should
clean up automatically.
"""
transformer = _ScanDepsCommandTransformer(
emit_metadata=emit_metadata,
depfile_name=depfile_name,
)
aux_toks = list(transformer._rustc_dep_only_command_impl(command_tokens))
aux_rspfiles = transformer._aux_rspfiles
if not transformer._replaced_emit:
return aux_toks + transformer._replacement_emit_args, aux_rspfiles
return aux_toks, aux_rspfiles
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._original_command = command
self._working_dir = (working_dir or Path(os.curdir)).absolute()
# expand response files
rsp_files = []
self._rsp_expanded_command = list(
cl_utils.expand_response_files(self.original_command, rsp_files)
)
self._response_files = set(rsp_files)
# analyze response-file-expanded command using canonical expanded-flag form
(
self._attributes,
remaining_args,
) = _RUSTC_COMMAND_SCANNER.parse_known_args(
list(
cl_utils.expand_fused_flags(
self.rsp_expanded_command, _RUSTC_FUSED_FLAGS
)
)
)
self._compiler = find_compiler_from_command(remaining_args)
compiler_arg_index = remaining_args.index(str(self._compiler))
self._env = [
tok for tok in remaining_args[:compiler_arg_index] if "=" in tok
]
self._crate_type = parse_crate_type(self._attributes.crate_type)
self._direct_inputs = list(
find_direct_inputs(remaining_args[compiler_arg_index:])
)
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 left == "--soname":
# `-Clink-arg=--soname=foo.so` is intended to set the DT_SONAME
# for the shared object and is not an input or output file that
# needs to be relativized.
continue
if is_linkable(arg):
self._link_arg_files.append(arg)
@property
def env(self) -> Sequence[str]:
return self._env
@property
def original_command(self) -> Sequence[str]:
return self._original_command
@property
def rsp_expanded_command(self) -> Sequence[str]:
return self._rsp_expanded_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 response_files(self) -> AbstractSet[Path]:
return self._response_files
@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 emit_metadata(self) -> bool:
return "metadata" 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 rmeta(self) -> Optional[Path]:
if self.emit_metadata and self.output_file:
return Path(
cl_utils.last_value_of_dict_flag(
self.emit, "metadata", self._output_file_base + ".rmeta"
)
)
return 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_metadata:
yield self.rmeta
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_with_rspfiles(
self, depfile_name: str
) -> Tuple[Sequence[str], Sequence[Path]]:
"""Generate a command that only produces a depfile.
Returns:
modified command
rewritten response files (which appear in the modified command)
"""
return rustc_dep_only_command(
command_tokens=self.original_command,
emit_metadata=self.emit_metadata,
depfile_name=depfile_name,
)
# TODO: write a main() routine just for printing debug info about a compile
# command