| #!/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. |
| """Wraps a Rust compile command for remote execution (reclient, RBE). |
| |
| Given a Rust compile command, this script |
| 1) identifies all inputs needed to execute the command hermetically (remotely). |
| 2) identifies all outputs to be retrieved from remote execution. |
| 3) composes a `rewrapper` (reclient) command to request remote execution. |
| This includes downloading remotely produced artifacts. |
| 4) forwards stdout/stderr back to the local environment |
| |
| This script was ported over from build/rbe/rustc-remote-wrapper.sh. |
| |
| For full usage, see `rustc_remote_wrapper.py -h`. |
| """ |
| |
| import argparse |
| import glob |
| import os |
| import subprocess |
| import stat |
| import sys |
| |
| import cl_utils |
| import depfile |
| import fuchsia |
| import linker |
| import rustc |
| import remote_action |
| |
| from pathlib import Path |
| from typing import Any, Iterable, Optional, Sequence |
| |
| _SCRIPT_BASENAME = Path(__file__).name |
| |
| # standard '755' executable permissions |
| _EXEC_PERMS = ( |
| stat.S_IRWXU | (stat.S_IRGRP | stat.S_IXGRP) | (stat.S_IROTH | stat.S_IXOTH) |
| ) |
| |
| |
| def msg(text: str): |
| print(f"[{_SCRIPT_BASENAME}] {text}") |
| |
| |
| # string.removeprefix() only appeared in python 3.9 |
| def remove_prefix(text: str, prefix: str) -> str: |
| if text.startswith(prefix): |
| return text[len(prefix) :] |
| return text |
| |
| |
| # This is needed in some places to workaround b/203540556 (reclient). |
| def remove_dot_slash_prefix(text: str) -> str: |
| return remove_prefix(text, "./") |
| |
| |
| # string.removesuffix() only appeared in python 3.9 |
| def remove_suffix(text: str, suffix: str) -> str: |
| if text.endswith(suffix): |
| return text[: -len(suffix)] |
| return text |
| |
| |
| # Defined for convenient mocking. |
| def _readlines_from_file(path: Path) -> Sequence[str]: |
| return path.read_text().splitlines(keepends=True) |
| |
| |
| # Defined for convenient mocking. |
| def _make_local_depfile(command: Sequence[str]) -> int: |
| return subprocess.call(command) |
| |
| |
| # Defined for convenient mocking. |
| def _tool_is_executable(tool: Path) -> bool: |
| return os.access(tool, os.X_OK) |
| |
| |
| # Defined for convenient mocking. |
| def _libcxx_isfile(libcxx: Path) -> bool: |
| return libcxx.is_file() |
| |
| |
| # Defined for convenient mocking. |
| def _env_file_exists(path: Path) -> bool: |
| return path.exists() |
| |
| |
| def _flatten_comma_list_to_paths(items: Iterable[str]) -> Iterable[Path]: |
| for item in cl_utils.flatten_comma_list(items): |
| yield Path(item) |
| |
| |
| def _main_arg_parser() -> argparse.ArgumentParser: |
| """Construct the argument parser, called by main().""" |
| parser = argparse.ArgumentParser( |
| description="Prepares a Rust compile command for remote execution.", |
| argument_default=[], |
| ) |
| remote_action.inherit_main_arg_parser_flags(parser) |
| |
| # There could be multiple variants of the standard C++ library to choose |
| # from. Use the one that corresponds to the compiler options. |
| parser.add_argument( |
| "--cxx-stdlibdir", |
| type=Path, |
| default=None, |
| help="Where to find libc++ (from clang)", |
| ) |
| |
| return parser |
| |
| |
| _MAIN_ARG_PARSER = _main_arg_parser() |
| |
| |
| def relativize_paths(paths: Iterable[Path], start: Path) -> Iterable[Path]: |
| for p in paths: |
| if p.is_absolute(): |
| yield cl_utils.relpath(p, start=start) |
| else: |
| yield p # Paths are already normalized upon construction |
| |
| |
| def accompany_rlib_with_so(deps: Iterable[Path]) -> Iterable[Path]: |
| """Expand list to include .so files. |
| |
| Some Rust libraries come with both .rlib and .so (like libstd), however, |
| the depfile generator fails to list the .so file in some cases, |
| which causes the build to silently fallback to static linking when |
| dynamic linking is requested and intended. This can result in a mismatch |
| between local and remote building. |
| See https://github.com/rust-lang/rust/issues/90106 |
| Workaround (https://fxbug.dev/42167956): check for existence of .so and include it. |
| |
| Yields: |
| original sequence, plus potentially additional .so libs. |
| """ |
| for dep in deps: |
| yield dep |
| if dep.suffix == ".rlib": |
| so_file = dep.with_suffix(".so") # replaces .rlib with .so |
| if so_file.is_file(): |
| yield so_file |
| |
| |
| def expand_deps_for_rlib_compile(paths: Iterable[Path]) -> Iterable[Path]: |
| """Expand a list of rmeta deps to be suitable for full (rlib) compilation. |
| |
| Depending on the rustc options used for scanning for dependencies, |
| the deps may contain list the .rmeta metadata file for some |
| entries (because only metadata is needed for fast operations like |
| dep-scanning). However, for the purposes of compiling a real .rlib |
| (or other rust artifact) we need to interpret this as requiring the |
| corresponding .rlib. |
| The implied .rlib need not necessarily exist on disk with real |
| contents; it is permitted to be a download stub from an earlier |
| remote build. |
| |
| Yields: |
| possibly modified deps. |
| """ |
| for path in paths: |
| yield path |
| if path.name.endswith(".rmeta"): |
| f = path.with_suffix(".rlib") |
| if f.exists(): # ok to exist as download stub |
| yield f |
| continue |
| |
| # .so proc macros already appear as deps in the depfile |
| # so there is no need to repeat them here. |
| pass |
| |
| |
| class RemoteInputProcessingError(RuntimeError): |
| def __init__(self, message: str): |
| super().__init__(message) |
| |
| |
| def _filter_local_command(args: Iterable[str]) -> Sequence[str]: |
| return list(cl_utils.strip_option_prefix(args, "--local-only")) |
| |
| |
| class RustRemoteAction(object): |
| def __init__( |
| self, |
| argv: Sequence[str], |
| exec_root: Optional[Path] = None, |
| working_dir: Optional[Path] = None, |
| host_platform: str = None, |
| auto_reproxy: bool = True, # Ok to disable during unit-tests |
| ): |
| 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._rust_action = rustc.RustAction( |
| command=filtered_command, |
| working_dir=self.working_dir, |
| ) |
| |
| # TODO: check for missing required flags, like --target |
| |
| self._local_only = self._main_args.local or self._remote_disqualified() |
| |
| self._cleanup_files: Sequence[Path] = [] |
| self._prepare_status: int = None # 0 means success, like an exit code |
| self._remote_action = None # will be set by prepare() step |
| |
| @property |
| def _c_sysroot_is_outside_exec_root(self) -> bool: |
| c_sysroot_dir = self.c_sysroot |
| if not c_sysroot_dir: |
| return False # not applicable |
| |
| if not c_sysroot_dir.is_absolute(): |
| # C sysroot is relative to the working directory |
| return False |
| |
| # Check all absolute path parents. |
| return self.exec_root not in c_sysroot_dir.parents |
| |
| def _remote_disqualified(self): |
| """Detects when the action cannot run remotely for various reasons.""" |
| # If the C sysroot is outside of exec_root, (e.g. an absolute path |
| # like /Library/Developer/... for Mac OS SDKs) then the command |
| # will only work locally. |
| if self.needs_linker: |
| if self._c_sysroot_is_outside_exec_root: |
| self.vmsg( |
| f"Forcing local execution because C sysroot ({self.c_sysroot}) is outside of exec_root ({self.exec_root})." |
| ) |
| return True |
| |
| # TODO: procedural macros need to target the host AND the remote |
| # platform to be usable both locally and remotely. |
| # For now, only build with procedural macros locally. |
| if any( |
| path.suffix == ".dylib" |
| for path in self._rust_action.externs.values() |
| ): |
| self.vmsg( |
| f"Forcing local execution because one of the externs points to a .dylib, which does not work in the remote environment." |
| ) |
| return True |
| |
| return False |
| |
| @property |
| def verbose(self) -> bool: |
| return self._main_args.verbose |
| |
| def vmsg(self, text: str): |
| if self.verbose: |
| msg(text) |
| |
| @property |
| def dry_run(self) -> bool: |
| return self._main_args.dry_run |
| |
| @property |
| def check_determinism(self) -> bool: |
| return self._main_args.check_determinism |
| |
| @property |
| def determinism_attempts(self) -> int: |
| return self._main_args.determinism_attempts |
| |
| @property |
| def miscomparison_export_dir(self) -> Optional[Path]: |
| if self._main_args.miscomparison_export_dir: |
| return self.working_dir / self._main_args.miscomparison_export_dir |
| return None |
| |
| @property |
| def clang_cxx_stdlibdir(self) -> Optional[Path]: |
| return self._main_args.cxx_stdlibdir |
| |
| @property |
| def local_only(self) -> bool: |
| """If the conditions are not right for remote execution, disable, |
| regardless of configuration. |
| """ |
| return self._local_only |
| |
| @property |
| def label(self) -> str: |
| return self._main_args.label |
| |
| def vmsg(self, text: str): |
| if self.verbose: |
| msg(text) |
| |
| def value_verbose(self, desc: str, value: str) -> str: |
| """In verbose mode, print and forward value.""" |
| self.vmsg(f"{desc}: {value}") |
| return value |
| |
| def yield_verbose(self, desc: str, items: Iterable[Any]) -> Iterable[Any]: |
| """In verbose mode, print and forward items. |
| |
| Args: |
| desc: text description of what is being printed. |
| items: stream of any type of object that is str-able. |
| |
| Yields: |
| items, unchanged |
| """ |
| if self.verbose: |
| msg(f"{desc}: {{") |
| for item in items: |
| print(f" {item}") # one item per line |
| yield item |
| print(f"}} # {desc}") |
| else: |
| yield from items |
| |
| @property |
| def working_dir(self) -> Path: |
| return self._working_dir |
| |
| @property |
| def exec_root(self) -> Path: |
| return self._exec_root |
| |
| @property |
| def label(self) -> Optional[str]: |
| return self._main_args.label |
| |
| @property |
| def host_platform(self) -> str: |
| return self._host_platform |
| |
| @property |
| def exec_root_rel(self) -> Path: |
| # relpath can handle cases that Path.relative_to() cannot. |
| return cl_utils.relpath(self.exec_root, start=self.working_dir) |
| |
| @property |
| def build_subdir(self) -> Path: # relative |
| """This is the relative path from the exec_root to the current working dir.""" |
| return self.working_dir.relative_to(self.exec_root) |
| |
| @property |
| def crate_type(self) -> rustc.CrateType: |
| return self._rust_action.crate_type |
| |
| @property |
| def needs_linker(self) -> bool: |
| return self._rust_action.needs_linker |
| |
| @property |
| def target(self) -> Optional[str]: |
| return self._rust_action.target |
| |
| @property |
| def rust_sysroot(self) -> Optional[Path]: |
| return self._rust_action.rust_sysroot |
| |
| @property |
| def c_sysroot(self) -> Optional[Path]: |
| return self._rust_action.c_sysroot |
| |
| @property |
| def linker(self) -> Optional[Path]: |
| return self._rust_action.linker |
| |
| @property |
| def local_clang_toolchain_dir(self) -> Path: |
| """Infer the clang toolchain dir based on the linker location. |
| |
| Returns: |
| Path to a toolchain dir, relative to the working dir. |
| """ |
| linker = self.linker # a relative path |
| if not linker: |
| return self.exec_root_rel / fuchsia.REMOTE_CLANG_SUBDIR |
| # We want TOOLDIR from a path like TOOLDIR/bin/TOOL. |
| # This follows a typical clang install structure. |
| return linker.parent.parent |
| |
| @property |
| def remote_clang_toolchain_dir(self) -> Path: # relative |
| return fuchsia.remote_executable(self.local_clang_toolchain_dir) |
| |
| @property |
| def depfile(self) -> Optional[Path]: |
| return self._rust_action.depfile |
| |
| @property |
| def primary_output(self) -> Path: |
| return self._rust_action.output_file |
| |
| @property |
| def host_compiler(self) -> Path: |
| return self._rust_action.compiler |
| |
| @property |
| def remote_compiler(self) -> Path: |
| return fuchsia.remote_executable(self.host_compiler) |
| |
| @property |
| def host_matches_remote(self) -> bool: |
| return self.remote_compiler == self.host_compiler |
| |
| @property |
| def original_command(self) -> Sequence[str]: |
| return self._rust_action.original_command |
| |
| def _replace_with_remote_compiler(self, tok) -> Optional[str]: |
| if tok == str(self.host_compiler): |
| return str(self.remote_compiler) |
| |
| @staticmethod |
| def _replace_with_remote_linker(tok) -> Optional[str]: |
| """Replace the host platform linker with the remote platform one.""" |
| return cl_utils.match_prefix_transform_suffix( |
| tok, "-Clinker=", lambda x: str(fuchsia.remote_executable(Path(x))) |
| ) |
| |
| @staticmethod |
| def _normalize_libcxx(tok) -> Optional[str]: |
| # A path like ".../bin/../lib/libc++a" needs to be normalized |
| # so that the remote linker does not fail when looking for a |
| # non-existent "bin" part of the path. |
| prefix = "-Clink-arg=" |
| if tok.startswith(prefix) and tok.endswith(".a") and ".." in tok: |
| right = tok[len(prefix) :] |
| # We do not want the Path.resolve() behavior of following |
| # symlinks and checking for existence; we truly want simple |
| # os.path.normpath() behavior. |
| normpath = os.path.normpath(right) |
| return prefix + normpath |
| |
| @property |
| def local_depfile(self) -> Path: |
| return Path(str(self.depfile) + ".nolink") |
| |
| @property |
| def dep_only_command(self) -> Sequence[str]: |
| return cl_utils.auto_env_prefix_command( |
| _filter_local_command( |
| self._rust_action.dep_only_command(self.local_depfile) |
| ) |
| ) |
| |
| def remote_compile_command(self) -> Iterable[str]: |
| """Transforms a local command into the remotely executed command. |
| |
| Note: that response files are preserved, so tokens inside response files |
| cannot be modified. |
| """ |
| for tok in self.original_command: |
| # Apply the first matching transform on each token. |
| replacement = self._replace_with_remote_compiler(tok) |
| if replacement is not None: |
| yield replacement |
| continue |
| |
| replacement = self._replace_with_remote_linker(tok) |
| if replacement is not None: |
| yield replacement |
| continue |
| |
| replacement = self._normalize_libcxx(tok) |
| if replacement is not None: |
| yield replacement |
| continue |
| |
| # When using -fuse-ld, also hint at the full path to the |
| # linker tool that is expected, without having to prepend |
| # PATH in the remote environment. |
| # This is only needed for remote cross-compilation. |
| if ( |
| tok.startswith("-Clink-arg=-fuse-ld=") |
| and not self.host_matches_remote |
| ): |
| yield tok |
| yield f"-Clink-arg=--ld-path={self.remote_ld_path}" |
| continue |
| |
| # else |
| yield tok |
| |
| # TODO(https://fxbug.dev/42057067): relocate rust sysroot to |
| # be indepedent of host-platform to make remote commands |
| # match for better caching. |
| # The rust sysroot is home to the standard libraries for |
| # all target platforms. |
| # Fuchsia currently uses the default sysroot location which is |
| # based on the *host* compiler path, but the remote compiler's |
| # default sysroot will be different. |
| # Inform the remote compiler to use the location of the sysroot |
| # of the *host* compiler. |
| fuchsia_use_host_rust_sysroot = self._rust_action.default_rust_sysroot() |
| yield f"--sysroot={fuchsia_use_host_rust_sysroot}" |
| |
| def _cleanup(self): |
| for f in self._cleanup_files: |
| f.unlink(missing_ok=True) # does remove or rmdir |
| |
| def _local_depfile_inputs(self) -> Iterable[Path]: |
| # Generate a local depfile for the purposes of discovering |
| # all transitive inputs. |
| self._cleanup_files.append(self.local_depfile) |
| |
| dep_only_command = self.dep_only_command |
| cmd_str = cl_utils.command_quoted_str(dep_only_command) |
| self.vmsg(f"scan-deps-only command: {cmd_str}") |
| dep_status = _make_local_depfile(dep_only_command) |
| if dep_status != 0: |
| self._prepare_status = dep_status |
| if self.verbose: |
| # This is really a lot of information, which is only interesting |
| # in limited circumstances. So we don't print it unless requested. |
| raise RemoteInputProcessingError( |
| f"Error: Local generation of depfile failed (exit={dep_status}): {cmd_str}" |
| ) |
| else: |
| raise RemoteInputProcessingError( |
| f"Error: Local generation of depfile failed (exit={dep_status}), use --verbose to see the command line." |
| ) |
| |
| # There is a phony dep for each input that is needed. |
| deps = list( |
| depfile.parse_lines(_readlines_from_file(self.local_depfile)) |
| ) |
| target_paths = [dep.target_paths for dep in deps if dep.is_phony] |
| remote_depfile_inputs = [ |
| target for paths in target_paths for target in paths |
| ] |
| |
| # Expand some .rmeta deps for .rlib compilation. |
| expanded_remote_inputs = list( |
| expand_deps_for_rlib_compile(remote_depfile_inputs) |
| ) |
| |
| # TODO: if needed, transform the rust std lib paths, depending on |
| # Fuchsia directory layout of Rust prebuilt libs. |
| # See remap_remote_rust_lib() in rustc-remote-wrapper.sh |
| # for one possible implementation. |
| yield from self.yield_verbose( |
| "depfile inputs", |
| relativize_paths( |
| accompany_rlib_with_so(expanded_remote_inputs), self.working_dir |
| ), |
| ) |
| |
| def _remote_compiler_inputs(self) -> Iterable[Path]: |
| # remote compiler is an input |
| remote_compiler = self.remote_compiler |
| if not _tool_is_executable(remote_compiler): |
| raise RemoteInputProcessingError( |
| f"Remote compilation requires {remote_compiler} to be available for uploading, but it is missing." |
| ) |
| yield self.value_verbose("remote compiler", remote_compiler) |
| yield from self._remote_rustc_shlibs() |
| |
| def _remote_rustc_shlibs(self) -> Iterable[Path]: |
| """Find remote compiler shared libraries. |
| |
| Yields: |
| shared library paths relative to current working dir. |
| """ |
| if self.host_platform == fuchsia.REMOTE_PLATFORM: |
| # remote and host execution environments match |
| yield from self.yield_verbose( |
| "remote compiler shlibs (detected)", |
| relativize_paths( |
| remote_action.host_tool_nonsystem_shlibs( |
| self.remote_compiler |
| ), |
| self.working_dir, |
| ), |
| ) |
| else: |
| yield from self._assumed_remote_rustc_shlibs() |
| |
| def _assumed_remote_rustc_shlibs(self) -> Iterable[Path]: |
| # KLUDGE: the host's binutils may not be able to analyze the remote |
| # executable (ELF), so hardcode what we think the shlibs are. |
| yield from self.yield_verbose( |
| "remote compiler shlibs (guessed)", |
| fuchsia.remote_rustc_shlibs(self.exec_root_rel), |
| ) |
| |
| def _rust_stdlib_libunwind_inputs(self) -> Iterable[Path]: |
| # The majority of stdlibs already appear in dep-info and are uploaded |
| # as needed. However, libunwind.a is not listed, but is directly |
| # needed by code emitted by rustc. Listing this here works around a |
| # missing upload issue, and adheres to the guidance of listing files |
| # instead of whole directories. |
| if not self.target: |
| return |
| libunwind_a = ( |
| self.rust_sysroot |
| / fuchsia.rust_stdlib_subdir( # relative to self.working_dir |
| target_triple=self.target |
| ) |
| / "libunwind.a" |
| ) |
| if libunwind_a.exists(): |
| yield self.value_verbose("libunwind", libunwind_a) |
| |
| def _inputs_from_env(self) -> Iterable[Path]: |
| """Scan command environment variables for references to inputs files. |
| |
| If a variable value looks like a path and it points to something |
| that exists, assume it is needed for remote execution. |
| """ |
| for e in self._rust_action.env: |
| key, sep, value = e.partition("=") |
| if sep == "=": |
| try: |
| p = Path(value) |
| if _env_file_exists(p): |
| yield cl_utils.relpath(p, start=self.working_dir) |
| except ValueError: # value is not a Path, ignore it |
| pass |
| |
| def _remote_inputs(self) -> Iterable[Path]: |
| """Remote inputs are relative to current working dir.""" |
| yield self.value_verbose( |
| "top source", self._rust_action.direct_sources[0] |
| ) |
| |
| # Pass along response files without altering the original command. |
| yield from self._rust_action.response_files |
| |
| yield from self._local_depfile_inputs() |
| |
| yield from self._remote_compiler_inputs() |
| |
| yield from self._rust_stdlib_libunwind_inputs() |
| |
| # Indirect dependencies (libraries) |
| yield from self.yield_verbose( |
| "extern libs", self._rust_action.extern_paths() |
| ) |
| |
| # Prefer to have the build system's command specify additional |
| # --remote-inputs instead of trying to infer them. |
| # yield from self.yield_verbose('env var files', self._inputs_from_env()) |
| |
| # Link arguments like static libs are checked for existence |
| # but not necessarily used until linking a binary. |
| # This is why we need to includes link_arg_files unconditionally, |
| # whereas the linker tools themselves can be dropped until linking binaries. |
| yield from self.yield_verbose( |
| "link arg files", self._rust_action.link_arg_files |
| ) |
| |
| if self.needs_linker: |
| yield from self._remote_linker_inputs() |
| |
| yield from self.yield_verbose( |
| "forwarded inputs", |
| _flatten_comma_list_to_paths(self._main_args.inputs), |
| ) |
| |
| def _remote_output_files(self) -> Iterable[Path]: |
| """Remote output files are relative to current working dir.""" |
| yield self.value_verbose("main output", self.primary_output) |
| |
| depfile = self.depfile |
| if depfile: |
| yield self.value_verbose("depfile", depfile) |
| |
| yield from self.yield_verbose( |
| "extra compiler outputs", self._rust_action.extra_output_files() |
| ) |
| |
| yield from self.yield_verbose( |
| "forwarded output files", |
| _flatten_comma_list_to_paths(self._main_args.output_files), |
| ) |
| |
| def _remote_output_dirs(self) -> Iterable[Path]: |
| yield from self.yield_verbose( |
| "forwarded output dirs", |
| _flatten_comma_list_to_paths(self._main_args.output_directories), |
| ) |
| |
| @property |
| def remote_options(self) -> Sequence[str]: |
| """rewrapper remote execution options.""" |
| fixed_remote_options = [ |
| # type=tool says we are providing a custom tool (Rust compiler), and |
| # thus, own the logic for providing explicit inputs. |
| # shallow=true works around an issue where racing mode downloads |
| # incorrectly |
| # toolname=rustc just helps classify the remote action type |
| "--labels=type=tool,shallow=true,toolname=rustc", |
| # --canonicalize_working_dir: coerce the output dir to a constant. |
| # This requires that the command be insensitive to output dir, and |
| # that its outputs do not leak the remote output dir. |
| # Ensuring that the results reproduce consistently across different |
| # build directories helps with caching. |
| "--canonicalize_working_dir=true", |
| ] |
| |
| # _main_remote_options should be allowed to override the |
| # fixed_remote_options |
| return fixed_remote_options + self._main_remote_options |
| |
| def prepare(self) -> int: |
| """Setup the remote action, but do not execute it. |
| |
| Returns: |
| exit code, 0 for success |
| """ |
| # cache the preparation |
| if self._prepare_status is not None: |
| return self._prepare_status |
| |
| # inputs and outputs are relative to current working dir |
| try: |
| remote_inputs = list(self._remote_inputs()) |
| except RemoteInputProcessingError as e: |
| msg(e) |
| return 1 |
| |
| remote_output_files = list(self._remote_output_files()) |
| remote_output_dirs = list(self._remote_output_dirs()) |
| |
| # Interpret --download_outputs=false as a request to avoid |
| # downloading only the primary rustc output (often the largest |
| # artifact). In other words, always download *all* other outputs, |
| # including the depfile and emitted llvm-ir (if applicable). |
| # The depfile *must* be downloaded because it is consumed by ninja. |
| |
| translated_remote_options = [] |
| for opt in self.remote_options: |
| if opt == "--download_outputs=false": |
| translated_remote_options.append( |
| f"--download_regex=-{self.primary_output}$" |
| ) |
| else: |
| translated_remote_options.append(opt) |
| |
| self._remote_action = remote_action.remote_action_from_args( |
| main_args=self._main_args, |
| remote_options=translated_remote_options, |
| command=list(self.remote_compile_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, |
| ) |
| self._prepare_status = 0 # exit code success |
| return self._prepare_status |
| |
| @property |
| def remote_action(self) -> remote_action.RemoteAction: |
| return self._remote_action |
| |
| @property |
| def remote_linker(self) -> Optional[Path]: |
| if not self.linker: |
| return None |
| return fuchsia.remote_executable(self.linker) |
| |
| @property |
| def target_linker_prefix(self) -> Optional[str]: |
| if not self.target: |
| return None |
| if "darwin" in self.target: |
| return "ld64" |
| return "ld" # most cases |
| |
| @property |
| def remote_ld_path(self) -> Optional[Path]: |
| ld = self._rust_action.use_ld # e.g. "lld" |
| if not ld: |
| return None |
| prefix = self.target_linker_prefix |
| if not prefix: |
| return None |
| return self.remote_linker.parent / f"{prefix}.{ld}" |
| |
| def _remote_linker_executables(self) -> Iterable[Path]: |
| if self.linker: |
| remote_linker = self.remote_linker |
| yield self.value_verbose("remote linker", remote_linker) |
| |
| # ld.lld -> lld, but the symlink is required for the clang |
| # linker driver to be able to use lld. |
| # Nowadays, lld -> llvm. |
| # ld.LINKER is expected to be in the remote environment PATH. |
| # See ToolChain::GetLinkerPath() in llvm-project:clang/lib/Driver/ToolChain.cpp |
| ld = self._rust_action.use_ld # e.g. "lld" |
| if ld: |
| yield self.value_verbose( |
| "remote clang -fuse-ld", self.remote_ld_path |
| ) |
| |
| def _remote_libcxx(self, clang_lib_triple: str) -> Iterable[Path]: |
| if self.clang_cxx_stdlibdir: # override, selecting for variant |
| libcxx_remote = self.clang_cxx_stdlibdir / "libc++.a" |
| else: |
| libcxx_remote = fuchsia.clang_libcxx_static( |
| self.remote_clang_toolchain_dir, clang_lib_triple |
| ) |
| if _libcxx_isfile(libcxx_remote): |
| yield self.value_verbose("remote libc++", libcxx_remote) |
| |
| def _remote_clang_runtime_libs( |
| self, clang_lib_triple: str |
| ) -> Iterable[Path]: |
| # clang runtime lib dir |
| rt_libdir_remote = list( |
| fuchsia.clang_runtime_libdirs( |
| self.remote_clang_toolchain_dir, clang_lib_triple |
| ) |
| ) |
| # if none found, that's ok. |
| if len(rt_libdir_remote) == 1: |
| yield self.value_verbose( |
| "remote runtime libdir", rt_libdir_remote[0] |
| ) |
| if len(rt_libdir_remote) > 1: |
| raise RemoteInputProcessingError( |
| f"Found more than one clang runtime lib dir (don't know which one to use): {rt_libdir_remote}" |
| ) |
| |
| def _c_sysroot_files(self) -> Iterable[Path]: |
| # sysroot files |
| if not self.target: |
| return |
| c_sysroot_dir = self.c_sysroot |
| if not c_sysroot_dir: |
| return |
| # if sysroot points outside of exec_root, stop |
| if self._c_sysroot_is_outside_exec_root: |
| return |
| sysroot_triple = fuchsia.rustc_target_to_sysroot_triple(self.target) |
| if c_sysroot_dir: |
| # Some sysroot files are linker scripts to be expanded. |
| if sysroot_triple: |
| search_paths = [ |
| c_sysroot_dir / "usr/lib" / sysroot_triple, |
| c_sysroot_dir / "lib" / sysroot_triple, |
| ] |
| else: |
| search_paths = [c_sysroot_dir / "lib"] |
| |
| link = linker.LinkerInvocation( |
| working_dir_abs=self.working_dir, search_paths=search_paths |
| ) |
| |
| lld = self.host_compiler.parent / "ld.lld" # co-located with clang |
| |
| def linker_script_expander(paths: Sequence[Path]) -> Iterable[Path]: |
| if lld.exists(): |
| yield from link.expand_using_lld(lld=lld, inputs=paths) |
| else: |
| for path in paths: |
| yield from link.expand_possible_linker_script(path) |
| |
| yield from self.yield_verbose( |
| "C sysroot files", |
| fuchsia.c_sysroot_files( |
| sysroot_dir=c_sysroot_dir, |
| sysroot_triple=sysroot_triple, |
| with_libgcc=self._rust_action.want_sysroot_libgcc, |
| linker_script_expander=linker_script_expander, |
| ), |
| ) |
| |
| def _remote_linker_inputs(self) -> Iterable[Path]: |
| if self.linker: |
| yield from self._remote_linker_executables() |
| |
| if self.target: |
| clang_lib_triple = fuchsia.rustc_target_to_clang_target( |
| self.target |
| ) |
| yield from self._remote_libcxx(clang_lib_triple) |
| yield from self._remote_clang_runtime_libs(clang_lib_triple) |
| |
| # --crate-type cdylib needs rust-lld (hard-coding this is a hack) |
| # This will always be linux-x64, even when cross-compiling, because |
| # that is the only RBE remote backend option available. |
| if self.crate_type == rustc.CrateType.CDYLIB: |
| yield self.value_verbose( |
| "remote rust-lld", |
| fuchsia.remote_rustc_to_rust_lld_path(self.remote_compiler), |
| ) |
| |
| yield from self._c_sysroot_files() |
| |
| def _depfile_exists(self) -> bool: |
| # Defined for easy mocking. |
| return self.depfile and self.depfile.exists() |
| |
| def _post_remote_success_action(self) -> int: |
| if self._depfile_exists(): |
| self._rewrite_remote_or_local_depfile() |
| |
| if ( |
| self.remote_action.skipping_some_download |
| and self._rust_action.main_output_is_executable |
| ): |
| # TODO(b/285030257): This is a workaround to a problem where |
| # download stubs need the appropriate execution permissions |
| # to be set, so that remote execution inputs get the same |
| # permissions. This is important for tools that mirror execution |
| # bits from inputs to outputs, like llvm-objcopy. |
| # Once re-client presents permission information in the |
| # --action_log (reproxy remote_metadata), this workaround can |
| # be replaced with a more generalized solution. |
| if remote_action.is_download_stub_file(self.primary_output): |
| self.primary_output.chmod(_EXEC_PERMS) |
| return 0 |
| |
| def _rewrite_remote_or_local_depfile(self): |
| """Rewrite depfile without working dir absolute paths. |
| |
| TEMPORARY WORKAROUND until upstream fix lands: |
| https://github.com/pest-parser/pest/pull/522 |
| Rewrite the depfile if it contains any absolute paths from the remote |
| build; paths should be relative to the root_build_dir. |
| |
| Assume that the output dir is two levels down from the exec_root. |
| |
| When using the `canonicalize_working_dir` rewrapper option, |
| the output directory is coerced to a predictable 'set_by_reclient' constant. |
| See https://source.corp.google.com/foundry-x-re-client/internal/pkg/reproxy/action.go;l=131 |
| It is still possible for a tool to leak absolute paths, which could |
| expose that constant in returned artifacts. |
| We forgive this for depfiles, but other artifacts should be verified |
| separately. |
| """ |
| # It is possible for this to run after local execution with |
| # exec_strategy=local,racing,remote_local_fallback, so the logic |
| # herein should accommodate both possibilities. |
| # In the future, it might be possible to determine whether the local |
| # or remote result was used from self.action_log. |
| self.vmsg(f"Rewriting the (remote or local) depfile {self.depfile}") |
| remote_action.rewrite_depfile( |
| dep_file=self.working_dir / self.depfile, # in-place |
| transform=self.remote_action._relativize_remote_or_local_deps, |
| ) |
| |
| def run_remote(self) -> int: |
| prepare_status = self.prepare() |
| if prepare_status != 0: |
| return prepare_status |
| |
| try: |
| return self.remote_action.run_with_main_args(self._main_args) |
| |
| finally: |
| self._cleanup() |
| |
| def run_local(self) -> int: |
| # Even if this is running locally, some of intermediate inputs |
| # have have come from remote actions that opted not to |
| # download their outputs (and left only download stubs). |
| # To be safe, verify the inputs and download as needed. |
| |
| prepare_status = self.prepare() |
| if prepare_status != 0: |
| return prepare_status |
| |
| # We know in our build system configuration that the following |
| # file suffixes could have come from remote builds. |
| # It is ok for this set to be conservatively inclusive. |
| # Eliminate inputs that come from the project root ../.., |
| # because those are sources or prebuilts. |
| remote_artifact_suffixes = {".o", ".a", ".so", ".dylib", ".rlib"} |
| download_statuses = self.remote_action.download_inputs( |
| lambda path: path.suffix in remote_artifact_suffixes |
| and not str(path).startswith(str(self.remote_action.exec_root_rel)) |
| ) |
| for path, status in download_statuses.items(): |
| if status.returncode != 0: |
| msg(f"Failed to download input: {path}\n{status.stderr_text}") |
| return status.returncode |
| |
| # don't bother with remote action preparation |
| # or any of the remote action features. |
| export_dir = self.miscomparison_export_dir |
| command = cl_utils.auto_env_prefix_command( |
| _filter_local_command(self.original_command) |
| ) |
| if self.check_determinism: |
| self.vmsg("Comparing two local runs of the original command.") |
| |
| output_files = list(self._remote_output_files()) |
| |
| max_attempts = self.determinism_attempts |
| # For b/328756843, increase repetition count for hard-to-reproduce cases. |
| override_attempts = fuchsia.determinism_repetitions(output_files) |
| if override_attempts is not None: |
| msg( |
| f"Notice: Overriding number of determinism repetitions: {override_attempts}" |
| ) |
| max_attempts = override_attempts |
| |
| command = fuchsia.check_determinism_command( |
| exec_root=self.exec_root_rel, |
| outputs=output_files, |
| command=command, |
| max_attempts=max_attempts, |
| miscomparison_export_dir=( |
| export_dir / self.build_subdir if export_dir else None |
| ), |
| label=self.label, |
| ) |
| |
| exit_code = subprocess.call(command, cwd=self.working_dir) |
| |
| # Optional: on determinism failures, copy data. |
| if exit_code != 0 and self.check_determinism and export_dir: |
| # Compute the inputs that would have been used for remote execution |
| if not self.remote_action: |
| self.prepare() |
| # Check determinism script already copies outputs, we just copy the inputs. |
| with cl_utils.chdir_cm(self.exec_root): |
| for f in self.remote_action.inputs_relative_to_project_root: |
| cl_utils.copy_preserve_subpath(f, export_dir) |
| |
| return exit_code |
| |
| def run(self) -> int: |
| if self.local_only: |
| return self.run_local() |
| else: |
| return self.run_remote() |
| |
| |
| def main(argv: Sequence[str]) -> int: |
| rust_remote_action = RustRemoteAction( |
| argv, # [remote options] -- rustc-compile-command... |
| exec_root=remote_action.PROJECT_ROOT, |
| working_dir=Path(os.curdir), |
| host_platform=fuchsia.HOST_PREBUILT_PLATFORM, |
| ) |
| return rust_remote_action.run() |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv[1:])) |