1#!/usr/bin/env python3 2 3# Copyright 2016 The Chromium Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7import os 8import os.path 9import shutil 10import subprocess 11import sys 12import tempfile 13 14# The path to `whole_archive`. 15sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) 16 17import whole_archive 18 19# Prefix for all custom linker driver arguments. 20LINKER_DRIVER_ARG_PREFIX = '-Wcrl,' 21# Linker action to create a directory and pass it to the linker as 22# `-object_path_lto`. Special-cased since it has to run before the link. 23OBJECT_PATH_LTO = 'object_path_lto' 24 25# The linker_driver.py is responsible for forwarding a linker invocation to 26# the compiler driver, while processing special arguments itself. 27# 28# Usage: linker_driver.py clang++ main.o -L. -llib -o prog -Wcrl,dsym,out 29# 30# On Mac, the logical step of linking is handled by three discrete tools to 31# perform the image link, debug info link, and strip. The linker_driver.py 32# combines these three steps into a single tool. 33# 34# The command passed to the linker_driver.py should be the compiler driver 35# invocation for the linker. It is first invoked unaltered (except for the 36# removal of the special driver arguments, described below). Then the driver 37# performs additional actions, based on these arguments: 38# 39# -Wcrl,installnametoolpath,<install_name_tool_path> 40# Sets the path to the `install_name_tool` to run with 41# -Wcrl,installnametool, in which case `xcrun` is not used to invoke it. 42# 43# -Wcrl,installnametool,<arguments,...> 44# After invoking the linker, this will run install_name_tool on the linker's 45# output. |arguments| are comma-separated arguments to be passed to the 46# install_name_tool command. 47# 48# -Wcrl,dsym,<dsym_path_prefix> 49# After invoking the linker, this will run `dsymutil` on the linker's 50# output, producing a dSYM bundle, stored at dsym_path_prefix. As an 51# example, if the linker driver were invoked with: 52# "... -o out/gn/obj/foo/libbar.dylib ... -Wcrl,dsym,out/gn ..." 53# The resulting dSYM would be out/gn/libbar.dylib.dSYM/. 54# 55# -Wcrl,dsymutilpath,<dsymutil_path> 56# Sets the path to the dsymutil to run with -Wcrl,dsym, in which case 57# `xcrun` is not used to invoke it. 58# 59# -Wcrl,unstripped,<unstripped_path_prefix> 60# After invoking the linker, and before strip, this will save a copy of 61# the unstripped linker output in the directory unstripped_path_prefix. 62# 63# -Wcrl,strip,<strip_arguments> 64# After invoking the linker, and optionally dsymutil, this will run 65# the strip command on the linker's output. strip_arguments are 66# comma-separated arguments to be passed to the strip command. 67# 68# -Wcrl,strippath,<strip_path> 69# Sets the path to the strip to run with -Wcrl,strip, in which case 70# `xcrun` is not used to invoke it. 71# -Wcrl,object_path_lto 72# Creates temporary directory for LTO object files. 73 74 75class LinkerDriver(object): 76 def __init__(self, args): 77 """Creates a new linker driver. 78 79 Args: 80 args: list of string, Arguments to the script. 81 """ 82 if len(args) < 2: 83 raise RuntimeError("Usage: linker_driver.py [linker-invocation]") 84 self._args = args 85 86 # List of linker driver actions. **The sort order of this list affects 87 # the order in which the actions are invoked.** 88 # The first item in the tuple is the argument's -Wcrl,<sub_argument> 89 # and the second is the function to invoke. 90 self._actions = [ 91 ('installnametoolpath,', self.set_install_name_tool_path), 92 ('installnametool,', self.run_install_name_tool), 93 ('dsymutilpath,', self.set_dsymutil_path), 94 ('dsym,', self.run_dsymutil), 95 ('unstripped,', self.run_save_unstripped), 96 ('strippath,', self.set_strip_path), 97 ('strip,', self.run_strip), 98 ] 99 100 # Linker driver actions can modify the these values. 101 self._install_name_tool_cmd = ['xcrun', 'install_name_tool'] 102 self._dsymutil_cmd = ['xcrun', 'dsymutil'] 103 self._strip_cmd = ['xcrun', 'strip'] 104 105 # The linker output file, lazily computed in self._get_linker_output(). 106 self._linker_output = None 107 # The temporary directory for intermediate LTO object files. If it 108 # exists, it will clean itself up on script exit. 109 self._object_path_lto = None 110 111 def run(self): 112 """Runs the linker driver, separating out the main compiler driver's 113 arguments from the ones handled by this class. It then invokes the 114 required tools, starting with the compiler driver to produce the linker 115 output. 116 """ 117 # Collect arguments to the linker driver (this script) and remove them 118 # from the arguments being passed to the compiler driver. 119 linker_driver_actions = {} 120 compiler_driver_args = [] 121 for index, arg in enumerate(self._args[1:]): 122 if arg.startswith(LINKER_DRIVER_ARG_PREFIX): 123 # Convert driver actions into a map of name => lambda to invoke. 124 driver_action = self._process_driver_arg(arg) 125 assert driver_action[0] not in linker_driver_actions 126 linker_driver_actions[driver_action[0]] = driver_action[1] 127 else: 128 compiler_driver_args.append(arg) 129 130 if self._object_path_lto is not None: 131 compiler_driver_args.append('-Wl,-object_path_lto,{}'.format( 132 self._object_path_lto.name)) 133 if self._get_linker_output() is None: 134 raise ValueError( 135 'Could not find path to linker output (-o or --output)') 136 137 # We want to link rlibs as --whole-archive if they are part of a unit 138 # test target. This is determined by switch 139 # `-LinkWrapper,add-whole-archive`. 140 compiler_driver_args = whole_archive.wrap_with_whole_archive( 141 compiler_driver_args) 142 143 linker_driver_outputs = [self._get_linker_output()] 144 145 try: 146 # Zero the mtime in OSO fields for deterministic builds. 147 # https://crbug.com/330262. 148 env = os.environ.copy() 149 env['ZERO_AR_DATE'] = '1' 150 # Run the linker by invoking the compiler driver. 151 subprocess.check_call(compiler_driver_args, env=env) 152 153 # Run the linker driver actions, in the order specified by the 154 # actions list. 155 for action in self._actions: 156 name = action[0] 157 if name in linker_driver_actions: 158 linker_driver_outputs += linker_driver_actions[name]() 159 except: 160 # If a linker driver action failed, remove all the outputs to make 161 # the build step atomic. 162 map(_remove_path, linker_driver_outputs) 163 164 # Re-report the original failure. 165 raise 166 167 def _get_linker_output(self): 168 """Returns the value of the output argument to the linker.""" 169 if not self._linker_output: 170 for index, arg in enumerate(self._args): 171 if arg in ('-o', '-output', '--output'): 172 self._linker_output = self._args[index + 1] 173 break 174 return self._linker_output 175 176 def _process_driver_arg(self, arg): 177 """Processes a linker driver argument and returns a tuple containing the 178 name and unary lambda to invoke for that linker driver action. 179 180 Args: 181 arg: string, The linker driver argument. 182 183 Returns: 184 A 2-tuple: 185 0: The driver action name, as in |self._actions|. 186 1: A lambda that calls the linker driver action with its direct 187 argument and returns a list of outputs from the action. 188 """ 189 if not arg.startswith(LINKER_DRIVER_ARG_PREFIX): 190 raise ValueError('%s is not a linker driver argument' % (arg, )) 191 192 sub_arg = arg[len(LINKER_DRIVER_ARG_PREFIX):] 193 # Special-cased, since it needs to run before the link. 194 # TODO(lgrey): Remove if/when we start running `dsymutil` 195 # through the clang driver. See https://crbug.com/1324104 196 if sub_arg == OBJECT_PATH_LTO: 197 self._object_path_lto = tempfile.TemporaryDirectory( 198 dir=os.getcwd()) 199 return (OBJECT_PATH_LTO, lambda: []) 200 201 for driver_action in self._actions: 202 (name, action) = driver_action 203 if sub_arg.startswith(name): 204 return (name, lambda: action(sub_arg[len(name):])) 205 206 raise ValueError('Unknown linker driver argument: %s' % (arg, )) 207 208 def set_install_name_tool_path(self, install_name_tool_path): 209 """Linker driver action for -Wcrl,installnametoolpath,<path>. 210 211 Sets the invocation command for install_name_tool, which allows the 212 caller to specify an alternate path. This action is always 213 processed before the run_install_name_tool action. 214 215 Args: 216 install_name_tool_path: string, The path to the install_name_tool 217 binary to run 218 219 Returns: 220 No output - this step is run purely for its side-effect. 221 """ 222 self._install_name_tool_cmd = [install_name_tool_path] 223 return [] 224 225 def run_install_name_tool(self, args_string): 226 """Linker driver action for -Wcrl,installnametool,<args>. Invokes 227 install_name_tool on the linker's output. 228 229 Args: 230 args_string: string, Comma-separated arguments for 231 `install_name_tool`. 232 233 Returns: 234 No output - this step is run purely for its side-effect. 235 """ 236 command = list(self._install_name_tool_cmd) 237 command.extend(args_string.split(',')) 238 command.append(self._get_linker_output()) 239 subprocess.check_call(command) 240 return [] 241 242 def run_dsymutil(self, dsym_path_prefix): 243 """Linker driver action for -Wcrl,dsym,<dsym-path-prefix>. Invokes 244 dsymutil on the linker's output and produces a dsym file at |dsym_file| 245 path. 246 247 Args: 248 dsym_path_prefix: string, The path at which the dsymutil output 249 should be located. 250 251 Returns: 252 list of string, Build step outputs. 253 """ 254 if not len(dsym_path_prefix): 255 raise ValueError('Unspecified dSYM output file') 256 257 linker_output = self._get_linker_output() 258 base = os.path.basename(linker_output) 259 dsym_out = os.path.join(dsym_path_prefix, base + '.dSYM') 260 261 # Remove old dSYMs before invoking dsymutil. 262 _remove_path(dsym_out) 263 264 tools_paths = _find_tools_paths(self._args) 265 if os.environ.get('PATH'): 266 tools_paths.append(os.environ['PATH']) 267 dsymutil_env = os.environ.copy() 268 dsymutil_env['PATH'] = ':'.join(tools_paths) 269 subprocess.check_call(self._dsymutil_cmd + 270 ['-o', dsym_out, linker_output], 271 env=dsymutil_env) 272 return [dsym_out] 273 274 def set_dsymutil_path(self, dsymutil_path): 275 """Linker driver action for -Wcrl,dsymutilpath,<dsymutil_path>. 276 277 Sets the invocation command for dsymutil, which allows the caller to 278 specify an alternate dsymutil. This action is always processed before 279 the RunDsymUtil action. 280 281 Args: 282 dsymutil_path: string, The path to the dsymutil binary to run 283 284 Returns: 285 No output - this step is run purely for its side-effect. 286 """ 287 self._dsymutil_cmd = [dsymutil_path] 288 return [] 289 290 def run_save_unstripped(self, unstripped_path_prefix): 291 """Linker driver action for -Wcrl,unstripped,<unstripped_path_prefix>. 292 Copies the linker output to |unstripped_path_prefix| before stripping. 293 294 Args: 295 unstripped_path_prefix: string, The path at which the unstripped 296 output should be located. 297 298 Returns: 299 list of string, Build step outputs. 300 """ 301 if not len(unstripped_path_prefix): 302 raise ValueError('Unspecified unstripped output file') 303 304 base = os.path.basename(self._get_linker_output()) 305 unstripped_out = os.path.join(unstripped_path_prefix, 306 base + '.unstripped') 307 308 shutil.copyfile(self._get_linker_output(), unstripped_out) 309 return [unstripped_out] 310 311 def run_strip(self, strip_args_string): 312 """Linker driver action for -Wcrl,strip,<strip_arguments>. 313 314 Args: 315 strip_args_string: string, Comma-separated arguments for `strip`. 316 317 Returns: 318 list of string, Build step outputs. 319 """ 320 strip_command = list(self._strip_cmd) 321 if len(strip_args_string) > 0: 322 strip_command += strip_args_string.split(',') 323 strip_command.append(self._get_linker_output()) 324 subprocess.check_call(strip_command) 325 return [] 326 327 def set_strip_path(self, strip_path): 328 """Linker driver action for -Wcrl,strippath,<strip_path>. 329 330 Sets the invocation command for strip, which allows the caller to 331 specify an alternate strip. This action is always processed before the 332 RunStrip action. 333 334 Args: 335 strip_path: string, The path to the strip binary to run 336 337 Returns: 338 No output - this step is run purely for its side-effect. 339 """ 340 self._strip_cmd = [strip_path] 341 return [] 342 343 344def _find_tools_paths(full_args): 345 """Finds all paths where the script should look for additional tools.""" 346 paths = [] 347 for idx, arg in enumerate(full_args): 348 if arg in ['-B', '--prefix']: 349 paths.append(full_args[idx + 1]) 350 elif arg.startswith('-B'): 351 paths.append(arg[2:]) 352 elif arg.startswith('--prefix='): 353 paths.append(arg[9:]) 354 return paths 355 356 357def _remove_path(path): 358 """Removes the file or directory at |path| if it exists.""" 359 if os.path.exists(path): 360 if os.path.isdir(path): 361 shutil.rmtree(path) 362 else: 363 os.unlink(path) 364 365 366if __name__ == '__main__': 367 LinkerDriver(sys.argv).run() 368 sys.exit(0) 369