#!/usr/bin/env python # Copyright 2016 The Chromium Authors. All rights reserved. # 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 # On mac, the values of these globals are modified when parsing -Wcrl, flags. On # ios, the script uses the defaults. DSYMUTIL_INVOKE = ['xcrun', 'dsymutil'] STRIP_INVOKE = ['xcrun', 'strip'] # Setting this flag will emit a deterministic binary by stripping dates from the # N_OSO field. DETERMINISTIC_FLAG = '--deterministic' # 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,dsym, # 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, # Sets the path to the dsymutil to run with -Wcrl,dsym, in which case # `xcrun` is not used to invoke it. # # -Wcrl,unstripped, # 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, # 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, # Sets the path to the strip to run with -Wcrl,strip, in which case # `xcrun` is not used to invoke it. def Main(args): """Main function for the linker driver. Separates out the arguments for the main compiler driver and the linker driver, then invokes all the required tools. Args: args: list of string, Arguments to the script. """ if len(args) < 2: raise RuntimeError("Usage: linker_driver.py [linker-invocation]") # 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 = [] deterministic = False for arg in args[1:]: if arg.startswith(_LINKER_DRIVER_ARG_PREFIX): # Convert driver actions into a map of name => lambda to invoke. driver_action = ProcessLinkerDriverArg(arg) assert driver_action[0] not in linker_driver_actions linker_driver_actions[driver_action[0]] = driver_action[1] elif arg == DETERMINISTIC_FLAG: deterministic = True else: compiler_driver_args.append(arg) linker_driver_outputs = [_FindLinkerOutput(compiler_driver_args)] try: # Zero the mtime in OSO fields for deterministic builds. # https://crbug.com/330262. env = os.environ.copy() if deterministic: 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 _LINKER_DRIVER_ACTIONS: name = action[0] if name in linker_driver_actions: linker_driver_outputs += linker_driver_actions[name](args) except: # If a linker driver action failed, remove all the outputs to make the # build step atomic. map(_RemovePath, linker_driver_outputs) # Re-report the original failure. raise def ProcessLinkerDriverArg(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 _LINKER_DRIVER_ACTIONS. 1: An 1-ary lambda that takes the full list of arguments passed to Main(). The lambda should call the linker driver action that corresponds to the argument and return 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):] for driver_action in _LINKER_DRIVER_ACTIONS: (name, action) = driver_action if sub_arg.startswith(name): return (name, lambda full_args: action(sub_arg[len(name):], full_args)) raise ValueError('Unknown linker driver argument: %s' % (arg, )) def RunDsymUtil(dsym_path_prefix, full_args): """Linker driver action for -Wcrl,dsym,. 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. full_args: list of string, Full argument list for the linker driver. Returns: list of string, Build step outputs. """ if not len(dsym_path_prefix): raise ValueError('Unspecified dSYM output file') linker_out = _FindLinkerOutput(full_args) base = os.path.basename(linker_out) dsym_out = os.path.join(dsym_path_prefix, base + '.dSYM') # Remove old dSYMs before invoking dsymutil. _RemovePath(dsym_out) tools_paths = _FindToolsPaths(full_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(DSYMUTIL_INVOKE + ['-o', dsym_out, linker_out], env=dsymutil_env) return [dsym_out] def SetDsymutilPath(dsymutil_path, full_args): """Linker driver action for -Wcrl,dsymutilpath,. 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 full_args: list of string, Full argument list for the linker driver. Returns: No output - this step is run purely for its side-effect. """ global DSYMUTIL_INVOKE DSYMUTIL_INVOKE = [dsymutil_path] return [] def RunSaveUnstripped(unstripped_path_prefix, full_args): """Linker driver action for -Wcrl,unstripped,. 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. full_args: list of string, Full argument list for the linker driver. Returns: list of string, Build step outputs. """ if not len(unstripped_path_prefix): raise ValueError('Unspecified unstripped output file') linker_out = _FindLinkerOutput(full_args) base = os.path.basename(linker_out) unstripped_out = os.path.join(unstripped_path_prefix, base + '.unstripped') shutil.copyfile(linker_out, unstripped_out) return [unstripped_out] def RunStrip(strip_args_string, full_args): """Linker driver action for -Wcrl,strip,. Args: strip_args_string: string, Comma-separated arguments for `strip`. full_args: list of string, Full arguments for the linker driver. Returns: list of string, Build step outputs. """ strip_command = list(STRIP_INVOKE) if len(strip_args_string) > 0: strip_command += strip_args_string.split(',') strip_command.append(_FindLinkerOutput(full_args)) subprocess.check_call(strip_command) return [] def SetStripPath(strip_path, full_args): """Linker driver action for -Wcrl,strippath,. 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 full_args: list of string, Full argument list for the linker driver. Returns: No output - this step is run purely for its side-effect. """ global STRIP_INVOKE STRIP_INVOKE = [strip_path] return [] def _FindLinkerOutput(full_args): """Finds the output of the linker by looking for the output flag in its argument list. As this is a required linker argument, raises an error if it cannot be found. """ # The linker_driver.py script may be used to wrap either the compiler linker # (uses -o to configure the output) or lipo (uses -output to configure the # output). Since wrapping the compiler linker is the most likely possibility # use try/except and fallback to checking for -output if -o is not found. try: output_flag_index = full_args.index('-o') except ValueError: output_flag_index = full_args.index('-output') return full_args[output_flag_index + 1] def _FindToolsPaths(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 _RemovePath(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) _LINKER_DRIVER_ARG_PREFIX = '-Wcrl,' """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, and the second is the function to invoke. """ _LINKER_DRIVER_ACTIONS = [ ('dsymutilpath,', SetDsymutilPath), ('dsym,', RunDsymUtil), ('unstripped,', RunSaveUnstripped), ('strippath,', SetStripPath), ('strip,', RunStrip), ] if __name__ == '__main__': Main(sys.argv) sys.exit(0)