blob: f87e719643bf386fb539f9eed3b26a280b3c5a78 [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.
"""Remotely compile C and C++.
This script functions as a standalone executable.
Usage:
$0 [remote options...] -- compile-comand...
"""
import argparse
import os
import subprocess
import sys
import cxx
import cl_utils
import depfile
import fuchsia
import remote_action
from pathlib import Path
from typing import Any, Iterable, Optional, Sequence
_SCRIPT_BASENAME = Path(__file__).name
_SCRIPT_DIR = Path(__file__).parent
def msg(text: str):
print(f'[{_SCRIPT_BASENAME}] {text}')
REMOTE_COMPILER_SWAPPER = _SCRIPT_DIR / "cxx-swap-remote-compiler.sh"
def _main_arg_parser() -> argparse.ArgumentParser:
"""Construct the argument parser, called by main()."""
parser = argparse.ArgumentParser(
description="Prepares a C++ command for remote execution.",
argument_default=[],
add_help=True, # Want this to exit after printing --help
)
remote_action.inherit_main_arg_parser_flags(parser)
group = parser.add_argument_group(
title="C++ remote wrapper",
description="C++ remote action options",
)
group.add_argument(
"--cpp-strategy",
type=str,
choices=['auto', 'local', 'integrated'],
default='auto',
metavar='STRATEGY',
help="""Configure how C-preprocessing is done.
integrated: preprocess and compile in a single step,
local: preprocess locally, compile remotely,
auto (default): one of the above, chosen by the script automatically.
""",
)
return parser
_MAIN_ARG_PARSER = _main_arg_parser()
def check_missing_remote_tools(
compiler_type: cxx.Compiler,
project_root: Path = None) -> Iterable[Path]:
"""Check for the existence of tools needed for remote execution.
Args:
compiled_type (cxx.Compiler): clang or gcc
project_root: location of project root from which the compilers
can be found. Path may be absolute or relative.
Yields:
Paths to missing but required tool directories.
"""
root = project_root or remote_action.PROJECT_ROOT_REL
required_remote_tools = []
if compiler_type == cxx.Compiler.CLANG:
required_remote_tools.append(fuchsia.REMOTE_CLANG_SUBDIR)
if compiler_type == cxx.Compiler.GCC:
required_remote_tools.append(fuchsia.REMOTE_GCC_SUBDIR)
for d in required_remote_tools:
if not (root / d).is_dir():
yield d
class CxxRemoteAction(object):
def __init__(
self,
argv: Sequence[str],
exec_root: Path = None,
working_dir: Path = None,
host_platform: str = None,
auto_reproxy: bool = True, # can disable for unit-testing
):
self._working_dir = (working_dir or Path(os.curdir)).absolute()
self._exec_root = (exec_root or remote_action.PROJECT_ROOT).absolute()
self._host_platform = host_platform or fuchsia.HOST_PREBUILT_PLATFORM
# Propagate --remote-flag=... options to the remote prefix,
# as if they appeared before '--'.
# Forwarded rewrapper options with values must be written as '--flag=value',
# not '--flag value' because argparse doesn't know what unhandled flags
# expect values.
main_argv, filtered_command = remote_action.forward_remote_flags(argv)
# forward all unknown flags to rewrapper
# --help here will result in early exit()
self._main_args, self._main_remote_options = _MAIN_ARG_PARSER.parse_known_args(
main_argv)
# Re-launch with reproxy if needed.
if auto_reproxy:
remote_action.auto_relaunch_with_reproxy(
script=Path(__file__), argv=argv, args=self._main_args)
if not filtered_command: # there is no command, bail out early
return
self._cxx_action = cxx.CxxAction(command=filtered_command)
# Determine whether this action can be done remotely.
self._local_only = self._main_args.local or self._detect_local_only()
self._local_preprocess_command = None
self._cpp_strategy = self._resolve_cpp_strategy()
self._prepare_status = None
self._cleanup_files = [] # Sequence[Path]
self._remote_action = None
@property
def compiler_path(self) -> Path:
return self.cxx_action.compiler.tool
@property
def compiler_type(self) -> cxx.Compiler:
return self.cxx_action.compiler.type
@property
def depfile(self) -> Optional[Path]:
return self.cxx_action.depfile
def _depfile_exists(self) -> bool:
# Defined for easy precise mocking.
return self.depfile and self.depfile.exists()
def check_preconditions(self):
if not self.cxx_action.target and self.cxx_action.compiler_is_clang:
raise Exception(
"For remote compiling with clang, an explicit --target is required, but is missing."
)
# check for required remote tools
missing_required_tools = list(
check_missing_remote_tools(self.compiler_type, self.exec_root_rel))
if missing_required_tools:
raise Exception(
f"Missing the following tools needed for remote compiling C++: {missing_required_tools}. See tqr/563535 for how to fetch the needed packages."
)
@property
def command_line_inputs(self) -> Sequence[Path]:
return [
Path(p)
for p in cl_utils.flatten_comma_list(self._main_args.inputs)
]
@property
def command_line_output_files(self) -> Sequence[Path]:
return [
Path(p)
for p in cl_utils.flatten_comma_list(self._main_args.output_files)
]
@property
def command_line_output_dirs(self) -> Sequence[Path]:
return [
Path(p) for p in cl_utils.flatten_comma_list(
self._main_args.output_directories)
]
def _post_remote_success_action(self) -> int:
# Remotely generated gcc depfiles may contain absolute paths
# that are not suitable for local use. Rewrite them.
if self.compiler_type == cxx.Compiler.GCC and self._depfile_exists():
self._rewrite_remote_depfile()
# TODO: if downloads were skipped, need to force-download depfile
return 0
def _rewrite_remote_depfile(self):
remote_action.rewrite_depfile(
dep_file=self.working_dir / self.depfile, # in-place
transform=self.remote_action._relativize_remote_deps,
)
def prepare(self) -> int:
"""Setup everything ahead of remote execution."""
assert not self.local_only, "This should not be reached in local-only mode."
if self._prepare_status is not None:
return self._prepare_status
self.check_preconditions()
# evaluate the separate preprocessing and compile-preprocessed command
# even if we won't use them.
self._local_preprocess_command, self._compile_preprocessed_command = self.cxx_action.split_preprocessing(
)
remote_inputs = list(
self.cxx_action.input_files()) + self.command_line_inputs
if self.cpp_strategy == 'local':
# preprocess locally, then compile the result remotely
preprocessed_source = self.cxx_action.preprocessed_output
self._cleanup_files.append(preprocessed_source)
remote_inputs.append(preprocessed_source)
cpp_status = self.preprocess_locally()
remote_command = self._compile_preprocessed_command
if cpp_status != 0:
return cpp_status
elif self.cpp_strategy == 'integrated':
# preprocess driven by the compiler, done remotely
remote_command = self.cxx_action.command
# TODO: might need -Wno-constant-logical-operand to workaround
# ZX_DEBUG_ASSERT.
# Prepare remote compile action
remote_output_dirs = list(
self.cxx_action.output_dirs()) + self.command_line_output_dirs
remote_options = [
"--labels=type=compile,compiler=clang,lang=cpp", # TODO: gcc?
"--canonicalize_working_dir=true",
] + self._main_remote_options # allow forwarded options to override defaults
# The output file is inferred automatically by rewrapper in C++ mode,
# but naming it explicitly here makes it easier for RemoteAction
# to use the output file name for other auxiliary files.
remote_output_files = list(
self.cxx_action.output_files()) + self.command_line_output_files
# Workaround b/239101612: missing gcc support libexec binaries for remote build
if self.compiler_type == cxx.Compiler.GCC:
remote_inputs.extend(
list(fuchsia.gcc_support_tools(self.compiler_path)))
# Support for remote cross-compilation:
if self.host_platform != fuchsia.REMOTE_PLATFORM:
# compiler path is relative to current working dir
compiler_swapper_rel = os.path.relpath(
REMOTE_COMPILER_SWAPPER, start=self.working_dir)
remote_inputs.extend([self.remote_compiler, compiler_swapper_rel])
# Let --remote_wrapper apply the prefix to the command remotely.
remote_options.append(f'--remote_wrapper={compiler_swapper_rel}')
self.vprintlist('remote inputs', remote_inputs)
self.vprintlist('remote output files', remote_output_files)
self.vprintlist('remote output dirs', remote_output_dirs)
self.vprintlist('rewrapper options', remote_options)
downloads = []
if self.depfile: # always fetch the depfile
downloads.append(self.depfile)
self._remote_action = remote_action.remote_action_from_args(
main_args=self._main_args,
remote_options=remote_options,
command=remote_command,
inputs=remote_inputs,
output_files=remote_output_files,
output_dirs=remote_output_dirs,
working_dir=self.working_dir,
exec_root=self.exec_root,
post_remote_run_success_action=self._post_remote_success_action,
downloads=downloads,
)
self._prepare_status = 0
return self._prepare_status
@property
def remote_action(self) -> remote_action.RemoteAction:
return self._remote_action
@property
def working_dir(self) -> Path:
return self._working_dir
@property
def exec_root(self) -> Path:
return self._exec_root
@property
def exec_root_rel(self) -> Path:
return cl_utils.relpath(self.exec_root, start=self.working_dir)
@property
def host_platform(self) -> str:
return self._host_platform
@property
def verbose(self) -> bool:
return self._main_args.verbose
@property
def dry_run(self) -> bool:
return self._main_args.dry_run
def vmsg(self, text: str):
if self.verbose:
msg(text)
def vprintlist(self, desc: str, items: Iterable[Any]):
"""In verbose mode, print elements.
Args:
desc: text description of what is being printed.
items: stream of any type of object that is str-able.
"""
if self.verbose:
msg(f'{desc}: {{')
for item in items:
text = str(item)
print(f' {text}')
print(f'}} # {desc}')
def _resolve_cpp_strategy(self) -> str:
"""Resolve preprocessing strategy to 'local' or 'integrated'."""
cpp_strategy = self._main_args.cpp_strategy
if cpp_strategy == 'auto':
if self.cxx_action.uses_macos_sdk:
# Mac SDK headers reside outside of exec_root,
# which doesn't work for remote compiling.
cpp_strategy = 'local'
else:
cpp_strategy = 'integrated'
return cpp_strategy
@property
def cpp_strategy(self) -> str:
return self._cpp_strategy
@property
def original_compile_command(self) -> Sequence[str]:
return self.cxx_action.command
def _detect_local_only(self) -> bool:
if self.cxx_action.sources:
first_source = self.cxx_action.sources[0]
if first_source.file.name.endswith('.S'):
# Compiling un-preprocessed assembly is not supported remotely.
return True
elif first_source.dialect not in {cxx.SourceLanguage.C,
cxx.SourceLanguage.CXX}:
# e.g. Obj-C must be compiled locally
return True
return False
@property
def local_only(self) -> bool:
return self._local_only
@property
def cxx_action(self) -> cxx.CxxAction:
return self._cxx_action
@property
def local_preprocess_command(self) -> Sequence[str]:
return self._local_preprocess_command
@property
def remote_compiler(self) -> Path:
return fuchsia.remote_executable(self.cxx_action.compiler.tool)
def _run_locally(self) -> int:
return subprocess.call(self.original_compile_command)
def preprocess_locally(self) -> int:
# Locally preprocess if needed
local_cpp_cmd = cl_utils.command_quoted_str(
self.local_preprocess_command)
if self.dry_run:
msg(f"[dry-run only] {local_cpp_cmd}")
return 0
self.vmsg(f"Local C-preprocessing: {local_cpp_cmd}")
cpp_status = subprocess.call(self.local_preprocess_command)
if cpp_status != 0:
print(
f"*** Local C-preprocessing failed (exit={cpp_status}): {local_cpp_cmd}"
)
return cpp_status
def _run_remote_action(self) -> int:
return self.remote_action.run_with_main_args(self._main_args)
def run(self) -> int:
if self.local_only:
return self._run_locally()
prepare_status = self.prepare()
if prepare_status != 0:
return prepare_status
# Remote compile C++
try:
return self._run_remote_action()
# TODO: normalize absolute paths in remotely generated depfile (gcc)
finally:
if not self._main_args.save_temps:
self._cleanup()
def _cleanup(self):
for f in self._cleanup_files:
f.unlink()
def main(argv: Sequence[str]) -> int:
cxx_remote_action = CxxRemoteAction(
argv, # [remote options] -- C-compile-command...
exec_root=remote_action.PROJECT_ROOT,
working_dir=Path(os.curdir),
host_platform=fuchsia.HOST_PREBUILT_PLATFORM,
)
return cxx_remote_action.run()
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))