| #!/usr/bin/env python3.8 |
| # 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. |
| |
| """Relativize shell command arguments to be relative. |
| |
| This script helps convert commands with absolute paths to equivalent |
| commands using relative paths. Paths are transformed blindly without |
| verifying existence or validity. |
| This helps reclient gather arguments under a common --exec_root directory. |
| Note, however, that this script is unaware of exec_root; it is the |
| responsibility of the invoker to make sure all path arguments fall |
| under a common exec_root. |
| |
| NOTE: This file is copied over from fuchsia.git:build/rbe/relativize_args.py |
| which also comes with unit-tests. |
| """ |
| |
| import argparse |
| import os |
| import subprocess |
| import sys |
| from pathlib import Path |
| from typing import Callable, Sequence |
| |
| _SCRIPT_BASENAME = os.path.basename(__file__) |
| |
| |
| def msg(text: str): |
| print(f"[{_SCRIPT_BASENAME}] {text}") |
| |
| |
| def split_transform_join( |
| token: str, sep: str, transform: Callable[[str], str] |
| ) -> str: |
| return sep.join(transform(x) for x in token.split(sep)) |
| |
| |
| def lexically_rewrite_token(token: str, transform: Callable[[str], str]) -> str: |
| """Lexically replaces substrings between delimiters. |
| |
| This is useful for transforming substrings of text. |
| |
| This can transform "--foo=bar,baz" into |
| f("--foo") + "=" + f("bar") + "," + f("baz") |
| |
| Args: |
| token: text to transform, like a shell token. |
| transform: text transformation. |
| |
| Returns: |
| text with substrings transformed. |
| """ |
| |
| def inner_transform(text: str) -> str: |
| return split_transform_join(text, ",", transform) |
| |
| return split_transform_join(token, "=", inner_transform) |
| |
| |
| def greatest_path_parent(p: Path) -> Path: |
| assert p.is_absolute() |
| return Path(*p.parts[:2]) # keep the leading '/' and the first component |
| |
| |
| def relativize_path(arg: str, start: Path) -> str: |
| """Convert a path or path substring to relative. |
| |
| Args: |
| arg: string that is a path or may contain a path. |
| start: result paths are relative to this directory (absolute). |
| |
| Returns: |
| possibly transformed arg with relative paths. |
| """ |
| start_abs = start.absolute() |
| # Handle known compiler flags like -I/abs/path, -L/abs/path |
| # Such flags are fused to their arguments without a delimiter. |
| for flag in ("-I", "-L", "-isystem"): |
| if arg.startswith(flag): |
| suffix = arg[len(flag):] |
| return flag + relativize_path(suffix, start=start_abs) |
| |
| try_path = Path(arg) |
| # Windows-style flags look like absolute paths, e.g. /Foo |
| # so we leave those alone by checking for existence. |
| # Only check the existence of the greatest parent, because some paths |
| # may refer to outputs that do not exist yet. |
| if try_path.is_absolute() and greatest_path_parent(try_path).exists(): |
| # Can't use Path.relative_to() because arguments |
| # aren't guaranteed to be subdir of the other. |
| return os.path.relpath(arg, start=start_abs) |
| |
| return arg |
| |
| |
| def relativize_command(command: Sequence[str], |
| working_dir: Path) -> Sequence[str]: |
| """Transform a command to use relative paths. |
| |
| Args: |
| command: the command to transform, sequence of shell tokens. |
| working_dir: result paths are relative to this (absolute). |
| |
| Returns: |
| command using relative paths |
| """ |
| relativized_command = [] |
| # Subprocess calls do not work for commands that start with VAR=VALUE |
| # environment variables, which is remedied by prefixing with 'env'. |
| if command and "=" in command[0]: |
| relativized_command += ["/usr/bin/env"] |
| |
| relativized_command += [ |
| lexically_rewrite_token(tok, lambda x: relativize_path(x, working_dir)) |
| for tok in command |
| ] |
| |
| return relativized_command |
| |
| |
| def main_arg_parser() -> argparse.ArgumentParser: |
| """Construct the argument parser, called by main().""" |
| parser = argparse.ArgumentParser( |
| description="Transforms a command to use relative paths.", |
| argument_default=[], |
| ) |
| parser.add_argument( |
| "--verbose", |
| action="store_true", |
| default=False, |
| help="Print information rewritten command.", |
| ) |
| parser.add_argument( |
| "--dry-run", |
| action="store_true", |
| default=False, |
| help="Show transformed command and exit.", |
| ) |
| parser.add_argument( |
| "--cwd", |
| type=Path, |
| default=Path(os.curdir), |
| help="Override the current working dir for relative paths.", |
| ) |
| parser.add_argument( |
| "--disable", |
| action="store_false", |
| dest="enable", |
| default=True, |
| help="If disabled, run the original command as-is.", |
| ) |
| |
| # Positional args are the command and arguments to run. |
| parser.add_argument("command", nargs="*", help="The command to run") |
| return parser |
| |
| |
| def main(argv: Sequence[str]) -> None: |
| parser = main_arg_parser() |
| args = parser.parse_args(argv) |
| |
| command = args.command |
| relativized_command = relativize_command( |
| command=command, working_dir=args.cwd |
| ) |
| |
| cmd_str = " ".join(relativized_command) |
| if args.verbose or args.dry_run: |
| msg(f"Relativized command: {cmd_str}") |
| |
| if args.dry_run: |
| return 0 |
| |
| if not args.enable: |
| return subprocess.call(command) |
| |
| exit_code = subprocess.call(relativized_command) |
| if exit_code != 0: |
| msg(f"*** Relativized command failed (exit={exit_code}): {cmd_str}") |
| return exit_code |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv[1:])) |