| #!/usr/bin/env python3 |
| |
| # Copyright 2016 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import os |
| import os.path |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| |
| # The path to `whole_archive`. |
| sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) |
| |
| import whole_archive |
| |
| # Prefix for all custom linker driver arguments. |
| LINKER_DRIVER_ARG_PREFIX = '-Wcrl,' |
| # Linker action to create a directory and pass it to the linker as |
| # `-object_path_lto`. Special-cased since it has to run before the link. |
| OBJECT_PATH_LTO = 'object_path_lto' |
| |
| # The linker_driver.py is responsible for forwarding a linker invocation to |
| # the compiler driver, while processing special arguments itself. |
| # |
| # Usage: linker_driver.py clang++ main.o -L. -llib -o prog -Wcrl,dsym,out |
| # |
| # On Mac, the logical step of linking is handled by three discrete tools to |
| # perform the image link, debug info link, and strip. The linker_driver.py |
| # combines these three steps into a single tool. |
| # |
| # The command passed to the linker_driver.py should be the compiler driver |
| # invocation for the linker. It is first invoked unaltered (except for the |
| # removal of the special driver arguments, described below). Then the driver |
| # performs additional actions, based on these arguments: |
| # |
| # -Wcrl,installnametoolpath,<install_name_tool_path> |
| # Sets the path to the `install_name_tool` to run with |
| # -Wcrl,installnametool, in which case `xcrun` is not used to invoke it. |
| # |
| # -Wcrl,installnametool,<arguments,...> |
| # After invoking the linker, this will run install_name_tool on the linker's |
| # output. |arguments| are comma-separated arguments to be passed to the |
| # install_name_tool command. |
| # |
| # -Wcrl,dsym,<dsym_path_prefix> |
| # After invoking the linker, this will run `dsymutil` on the linker's |
| # output, producing a dSYM bundle, stored at dsym_path_prefix. As an |
| # example, if the linker driver were invoked with: |
| # "... -o out/gn/obj/foo/libbar.dylib ... -Wcrl,dsym,out/gn ..." |
| # The resulting dSYM would be out/gn/libbar.dylib.dSYM/. |
| # |
| # -Wcrl,dsymutilpath,<dsymutil_path> |
| # Sets the path to the dsymutil to run with -Wcrl,dsym, in which case |
| # `xcrun` is not used to invoke it. |
| # |
| # -Wcrl,unstripped,<unstripped_path_prefix> |
| # After invoking the linker, and before strip, this will save a copy of |
| # the unstripped linker output in the directory unstripped_path_prefix. |
| # |
| # -Wcrl,strip,<strip_arguments> |
| # After invoking the linker, and optionally dsymutil, this will run |
| # the strip command on the linker's output. strip_arguments are |
| # comma-separated arguments to be passed to the strip command. |
| # |
| # -Wcrl,strippath,<strip_path> |
| # Sets the path to the strip to run with -Wcrl,strip, in which case |
| # `xcrun` is not used to invoke it. |
| # -Wcrl,object_path_lto |
| # Creates temporary directory for LTO object files. |
| |
| |
| class LinkerDriver(object): |
| def __init__(self, args): |
| """Creates a new linker driver. |
| |
| Args: |
| args: list of string, Arguments to the script. |
| """ |
| if len(args) < 2: |
| raise RuntimeError("Usage: linker_driver.py [linker-invocation]") |
| self._args = args |
| |
| # List of linker driver actions. **The sort order of this list affects |
| # the order in which the actions are invoked.** |
| # The first item in the tuple is the argument's -Wcrl,<sub_argument> |
| # and the second is the function to invoke. |
| self._actions = [ |
| ('installnametoolpath,', self.set_install_name_tool_path), |
| ('installnametool,', self.run_install_name_tool), |
| ('dsymutilpath,', self.set_dsymutil_path), |
| ('dsym,', self.run_dsymutil), |
| ('unstripped,', self.run_save_unstripped), |
| ('strippath,', self.set_strip_path), |
| ('strip,', self.run_strip), |
| ] |
| |
| # Linker driver actions can modify the these values. |
| self._install_name_tool_cmd = ['xcrun', 'install_name_tool'] |
| self._dsymutil_cmd = ['xcrun', 'dsymutil'] |
| self._strip_cmd = ['xcrun', 'strip'] |
| |
| # The linker output file, lazily computed in self._get_linker_output(). |
| self._linker_output = None |
| # The temporary directory for intermediate LTO object files. If it |
| # exists, it will clean itself up on script exit. |
| self._object_path_lto = None |
| |
| def run(self): |
| """Runs the linker driver, separating out the main compiler driver's |
| arguments from the ones handled by this class. It then invokes the |
| required tools, starting with the compiler driver to produce the linker |
| output. |
| """ |
| # Collect arguments to the linker driver (this script) and remove them |
| # from the arguments being passed to the compiler driver. |
| linker_driver_actions = {} |
| compiler_driver_args = [] |
| for index, arg in enumerate(self._args[1:]): |
| if arg.startswith(LINKER_DRIVER_ARG_PREFIX): |
| # Convert driver actions into a map of name => lambda to invoke. |
| driver_action = self._process_driver_arg(arg) |
| assert driver_action[0] not in linker_driver_actions |
| linker_driver_actions[driver_action[0]] = driver_action[1] |
| else: |
| compiler_driver_args.append(arg) |
| |
| if self._object_path_lto is not None: |
| compiler_driver_args.append('-Wl,-object_path_lto,{}'.format( |
| self._object_path_lto.name)) |
| if self._get_linker_output() is None: |
| raise ValueError( |
| 'Could not find path to linker output (-o or --output)') |
| |
| # We want to link rlibs as --whole-archive if they are part of a unit |
| # test target. This is determined by switch |
| # `-LinkWrapper,add-whole-archive`. |
| compiler_driver_args = whole_archive.wrap_with_whole_archive( |
| compiler_driver_args) |
| |
| linker_driver_outputs = [self._get_linker_output()] |
| |
| try: |
| # Zero the mtime in OSO fields for deterministic builds. |
| # https://crbug.com/330262. |
| env = os.environ.copy() |
| env['ZERO_AR_DATE'] = '1' |
| # Run the linker by invoking the compiler driver. |
| subprocess.check_call(compiler_driver_args, env=env) |
| |
| # Run the linker driver actions, in the order specified by the |
| # actions list. |
| for action in self._actions: |
| name = action[0] |
| if name in linker_driver_actions: |
| linker_driver_outputs += linker_driver_actions[name]() |
| except: |
| # If a linker driver action failed, remove all the outputs to make |
| # the build step atomic. |
| map(_remove_path, linker_driver_outputs) |
| |
| # Re-report the original failure. |
| raise |
| |
| def _get_linker_output(self): |
| """Returns the value of the output argument to the linker.""" |
| if not self._linker_output: |
| for index, arg in enumerate(self._args): |
| if arg in ('-o', '-output', '--output'): |
| self._linker_output = self._args[index + 1] |
| break |
| return self._linker_output |
| |
| def _process_driver_arg(self, arg): |
| """Processes a linker driver argument and returns a tuple containing the |
| name and unary lambda to invoke for that linker driver action. |
| |
| Args: |
| arg: string, The linker driver argument. |
| |
| Returns: |
| A 2-tuple: |
| 0: The driver action name, as in |self._actions|. |
| 1: A lambda that calls the linker driver action with its direct |
| argument and returns a list of outputs from the action. |
| """ |
| if not arg.startswith(LINKER_DRIVER_ARG_PREFIX): |
| raise ValueError('%s is not a linker driver argument' % (arg, )) |
| |
| sub_arg = arg[len(LINKER_DRIVER_ARG_PREFIX):] |
| # Special-cased, since it needs to run before the link. |
| # TODO(lgrey): Remove if/when we start running `dsymutil` |
| # through the clang driver. See https://crbug.com/1324104 |
| if sub_arg == OBJECT_PATH_LTO: |
| self._object_path_lto = tempfile.TemporaryDirectory( |
| dir=os.getcwd()) |
| return (OBJECT_PATH_LTO, lambda: []) |
| |
| for driver_action in self._actions: |
| (name, action) = driver_action |
| if sub_arg.startswith(name): |
| return (name, lambda: action(sub_arg[len(name):])) |
| |
| raise ValueError('Unknown linker driver argument: %s' % (arg, )) |
| |
| def set_install_name_tool_path(self, install_name_tool_path): |
| """Linker driver action for -Wcrl,installnametoolpath,<path>. |
| |
| Sets the invocation command for install_name_tool, which allows the |
| caller to specify an alternate path. This action is always |
| processed before the run_install_name_tool action. |
| |
| Args: |
| install_name_tool_path: string, The path to the install_name_tool |
| binary to run |
| |
| Returns: |
| No output - this step is run purely for its side-effect. |
| """ |
| self._install_name_tool_cmd = [install_name_tool_path] |
| return [] |
| |
| def run_install_name_tool(self, args_string): |
| """Linker driver action for -Wcrl,installnametool,<args>. Invokes |
| install_name_tool on the linker's output. |
| |
| Args: |
| args_string: string, Comma-separated arguments for |
| `install_name_tool`. |
| |
| Returns: |
| No output - this step is run purely for its side-effect. |
| """ |
| command = list(self._install_name_tool_cmd) |
| command.extend(args_string.split(',')) |
| command.append(self._get_linker_output()) |
| subprocess.check_call(command) |
| return [] |
| |
| def run_dsymutil(self, dsym_path_prefix): |
| """Linker driver action for -Wcrl,dsym,<dsym-path-prefix>. Invokes |
| dsymutil on the linker's output and produces a dsym file at |dsym_file| |
| path. |
| |
| Args: |
| dsym_path_prefix: string, The path at which the dsymutil output |
| should be located. |
| |
| Returns: |
| list of string, Build step outputs. |
| """ |
| if not len(dsym_path_prefix): |
| raise ValueError('Unspecified dSYM output file') |
| |
| linker_output = self._get_linker_output() |
| base = os.path.basename(linker_output) |
| dsym_out = os.path.join(dsym_path_prefix, base + '.dSYM') |
| |
| # Remove old dSYMs before invoking dsymutil. |
| _remove_path(dsym_out) |
| |
| tools_paths = _find_tools_paths(self._args) |
| if os.environ.get('PATH'): |
| tools_paths.append(os.environ['PATH']) |
| dsymutil_env = os.environ.copy() |
| dsymutil_env['PATH'] = ':'.join(tools_paths) |
| subprocess.check_call(self._dsymutil_cmd + |
| ['-o', dsym_out, linker_output], |
| env=dsymutil_env) |
| return [dsym_out] |
| |
| def set_dsymutil_path(self, dsymutil_path): |
| """Linker driver action for -Wcrl,dsymutilpath,<dsymutil_path>. |
| |
| Sets the invocation command for dsymutil, which allows the caller to |
| specify an alternate dsymutil. This action is always processed before |
| the RunDsymUtil action. |
| |
| Args: |
| dsymutil_path: string, The path to the dsymutil binary to run |
| |
| Returns: |
| No output - this step is run purely for its side-effect. |
| """ |
| self._dsymutil_cmd = [dsymutil_path] |
| return [] |
| |
| def run_save_unstripped(self, unstripped_path_prefix): |
| """Linker driver action for -Wcrl,unstripped,<unstripped_path_prefix>. |
| Copies the linker output to |unstripped_path_prefix| before stripping. |
| |
| Args: |
| unstripped_path_prefix: string, The path at which the unstripped |
| output should be located. |
| |
| Returns: |
| list of string, Build step outputs. |
| """ |
| if not len(unstripped_path_prefix): |
| raise ValueError('Unspecified unstripped output file') |
| |
| base = os.path.basename(self._get_linker_output()) |
| unstripped_out = os.path.join(unstripped_path_prefix, |
| base + '.unstripped') |
| |
| shutil.copyfile(self._get_linker_output(), unstripped_out) |
| return [unstripped_out] |
| |
| def run_strip(self, strip_args_string): |
| """Linker driver action for -Wcrl,strip,<strip_arguments>. |
| |
| Args: |
| strip_args_string: string, Comma-separated arguments for `strip`. |
| |
| Returns: |
| list of string, Build step outputs. |
| """ |
| strip_command = list(self._strip_cmd) |
| if len(strip_args_string) > 0: |
| strip_command += strip_args_string.split(',') |
| strip_command.append(self._get_linker_output()) |
| subprocess.check_call(strip_command) |
| return [] |
| |
| def set_strip_path(self, strip_path): |
| """Linker driver action for -Wcrl,strippath,<strip_path>. |
| |
| Sets the invocation command for strip, which allows the caller to |
| specify an alternate strip. This action is always processed before the |
| RunStrip action. |
| |
| Args: |
| strip_path: string, The path to the strip binary to run |
| |
| Returns: |
| No output - this step is run purely for its side-effect. |
| """ |
| self._strip_cmd = [strip_path] |
| return [] |
| |
| |
| def _find_tools_paths(full_args): |
| """Finds all paths where the script should look for additional tools.""" |
| paths = [] |
| for idx, arg in enumerate(full_args): |
| if arg in ['-B', '--prefix']: |
| paths.append(full_args[idx + 1]) |
| elif arg.startswith('-B'): |
| paths.append(arg[2:]) |
| elif arg.startswith('--prefix='): |
| paths.append(arg[9:]) |
| return paths |
| |
| |
| def _remove_path(path): |
| """Removes the file or directory at |path| if it exists.""" |
| if os.path.exists(path): |
| if os.path.isdir(path): |
| shutil.rmtree(path) |
| else: |
| os.unlink(path) |
| |
| |
| if __name__ == '__main__': |
| LinkerDriver(sys.argv).run() |
| sys.exit(0) |