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 re 10import shutil 11import subprocess 12import sys 13import tempfile 14 15# The path to `whole_archive`. 16sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) 17 18import whole_archive 19 20# Prefix for all custom linker driver arguments. 21LINKER_DRIVER_ARG_PREFIX = '-Wcrl,' 22LINKER_DRIVER_COMPILER_ARG_PREFIX = '-Wcrl,driver,' 23 24# The linker_driver.py is responsible for forwarding a linker invocation to 25# the compiler driver, while processing special arguments itself. 26# 27# Usage: linker_driver.py -Wcrl,driver,clang++ main.o -L. -llib -o prog \ 28# -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 compiler driver invocation for the linker is specified by the following 35# required argument. 36# 37# -Wcrl,driver,<path_to_compiler_driver> 38# Specifies the path to the compiler driver. 39# 40# After running the compiler driver, the script performs additional actions, 41# based on these arguments: 42# 43# -Wcrl,installnametoolpath,<install_name_tool_path> 44# Sets the path to the `install_name_tool` to run with 45# -Wcrl,installnametool, in which case `xcrun` is not used to invoke it. 46# 47# -Wcrl,installnametool,<arguments,...> 48# After invoking the linker, this will run install_name_tool on the linker's 49# output. |arguments| are comma-separated arguments to be passed to the 50# install_name_tool command. 51# 52# -Wcrl,dsym,<dsym_path_prefix> 53# After invoking the linker, this will run `dsymutil` on the linker's 54# output, producing a dSYM bundle, stored at dsym_path_prefix. As an 55# example, if the linker driver were invoked with: 56# "... -o out/gn/obj/foo/libbar.dylib ... -Wcrl,dsym,out/gn ..." 57# The resulting dSYM would be out/gn/libbar.dylib.dSYM/. 58# 59# -Wcrl,dsymutilpath,<dsymutil_path> 60# Sets the path to the dsymutil to run with -Wcrl,dsym, in which case 61# `xcrun` is not used to invoke it. 62# 63# -Wcrl,unstripped,<unstripped_path_prefix> 64# After invoking the linker, and before strip, this will save a copy of 65# the unstripped linker output in the directory unstripped_path_prefix. 66# 67# -Wcrl,strip,<strip_arguments> 68# After invoking the linker, and optionally dsymutil, this will run 69# the strip command on the linker's output. strip_arguments are 70# comma-separated arguments to be passed to the strip command. 71# 72# -Wcrl,strippath,<strip_path> 73# Sets the path to the strip to run with -Wcrl,strip, in which case 74# `xcrun` is not used to invoke it. 75# -Wcrl,object_path_lto 76# Creates temporary directory for LTO object files. 77# 78# -Wcrl,otoolpath,<otool path> 79# Sets the path to the otool for solink process. 80# -Wcrl,nmpath,<nm path> 81# Sets the path to the nm for solink process. 82# 83# -Wcrl.tocname,<tocname> 84# Output TOC for solink. 85# It would be processed both before the linker (to check reexport 86# in old module) and after the linker (to produce TOC if needed). 87 88class LinkerDriver(object): 89 def __init__(self, args): 90 """Creates a new linker driver. 91 92 Args: 93 args: list of string, Arguments to the script. 94 """ 95 self._args = args 96 97 # List of linker driver pre-actions that need to run before the link. 98 # **The sort order of this list affects the order in which 99 # the actions are invoked.** 100 # The first item in the tuple is the argument's -Wcrl,<sub_argument> 101 # and the second is the function to invoke. 102 self._pre_actions = [ 103 ('object_path_lto', self.prepare_object_path_lto), 104 ('installnametoolpath,', self.set_install_name_tool_path), 105 ('dsymutilpath,', self.set_dsymutil_path), 106 ('strippath,', self.set_strip_path), 107 ('otoolpath,', self.set_otool_path), 108 ('nmpath,', self.set_nm_path), 109 ('tocname,', self.check_reexport_in_old_module), 110 ] 111 112 # List of linker driver actions. **The sort order of this list affects 113 # the order in which the actions are invoked.** 114 # The first item in the tuple is the argument's -Wcrl,<sub_argument> 115 # and the second is the function to invoke. 116 self._actions = [ 117 ('installnametool,', self.run_install_name_tool), 118 ('dsym,', self.run_dsymutil), 119 ('unstripped,', self.run_save_unstripped), 120 ('strip,', self.run_strip), 121 ('tocname,', self.output_toc), 122 ] 123 124 # Linker driver actions can modify the these values. 125 self._driver_path = None # Must be specified on the command line. 126 self._otool_cmd = ['xcrun', 'otool'] 127 self._nm_cmd = ['xcrun', 'nm'] 128 self._install_name_tool_cmd = ['xcrun', 'install_name_tool'] 129 self._dsymutil_cmd = ['xcrun', 'dsymutil'] 130 self._strip_cmd = ['xcrun', 'strip'] 131 132 # The linker output file, lazily computed in self._get_linker_output(). 133 self._linker_output = None 134 135 # may not need to reexport unless LC_REEXPORT_DYLIB is used. 136 self._reexport_in_old_module = False 137 138 139 def run(self): 140 """Runs the linker driver, separating out the main compiler driver's 141 arguments from the ones handled by this class. It then invokes the 142 required tools, starting with the compiler driver to produce the linker 143 output. 144 """ 145 # Collect arguments to the linker driver (this script) and remove them 146 # from the arguments being passed to the compiler driver. 147 self._linker_driver_actions = {} 148 self._linker_driver_pre_actions = {} 149 self._compiler_driver_args = [] 150 for index, arg in enumerate(self._args[1:]): 151 if arg.startswith(LINKER_DRIVER_COMPILER_ARG_PREFIX): 152 assert not self._driver_path 153 self._driver_path = arg[len(LINKER_DRIVER_COMPILER_ARG_PREFIX 154 ):] 155 elif arg.startswith(LINKER_DRIVER_ARG_PREFIX): 156 # Convert driver actions into a map of name => lambda to invoke. 157 self._process_driver_arg(arg) 158 else: 159 # TODO(crbug.com/40268754): On Apple, the linker command line 160 # produced by rustc for LTO includes these arguments, but the 161 # Apple linker doesn't accept them. 162 # Upstream bug: https://github.com/rust-lang/rust/issues/60059 163 BAD_RUSTC_ARGS = '-Wl,-plugin-opt=O[0-9],-plugin-opt=mcpu=.*' 164 if not re.match(BAD_RUSTC_ARGS, arg): 165 self._compiler_driver_args.append(arg) 166 167 if not self._driver_path: 168 raise RuntimeError( 169 "Usage: linker_driver.py -Wcrl,driver,<compiler-driver> " 170 "[linker-args]...") 171 172 if self._get_linker_output() is None: 173 raise ValueError( 174 'Could not find path to linker output (-o or --output)') 175 176 # We want to link rlibs as --whole-archive if they are part of a unit 177 # test target. This is determined by switch 178 # `-LinkWrapper,add-whole-archive`. 179 self._compiler_driver_args = whole_archive.wrap_with_whole_archive( 180 self._compiler_driver_args, is_apple=True) 181 182 linker_driver_outputs = [self._get_linker_output()] 183 184 try: 185 # Zero the mtime in OSO fields for deterministic builds. 186 # https://crbug.com/330262. 187 env = os.environ.copy() 188 env['ZERO_AR_DATE'] = '1' 189 190 # Run the driver pre-actions, in the order specified by the 191 # actions list. 192 for action in self._pre_actions: 193 name = action[0] 194 if name in self._linker_driver_pre_actions: 195 self._linker_driver_pre_actions[name]() 196 197 # Run the linker by invoking the compiler driver. 198 subprocess.check_call([self._driver_path] + 199 self._compiler_driver_args, 200 env=env) 201 202 # Run the linker driver actions, in the order specified by the 203 # actions list. 204 for action in self._actions: 205 name = action[0] 206 if name in self._linker_driver_actions: 207 linker_driver_outputs += self._linker_driver_actions[name]( 208 ) 209 except: 210 # If a linker driver action failed, remove all the outputs to make 211 # the build step atomic. 212 map(_remove_path, linker_driver_outputs) 213 214 # Re-report the original failure. 215 raise 216 217 def _get_linker_output(self): 218 """Returns the value of the output argument to the linker.""" 219 if not self._linker_output: 220 for index, arg in enumerate(self._args): 221 if arg in ('-o', '-output', '--output'): 222 self._linker_output = self._args[index + 1] 223 break 224 return self._linker_output 225 226 def _process_driver_arg(self, arg): 227 """Processes a linker driver argument and returns a tuple containing the 228 name and unary lambda to invoke for that linker driver action. 229 230 Args: 231 arg: string, The linker driver argument. 232 233 Returns: 234 A 2-tuple: 235 0: The driver action name, as in |self._actions|. 236 1: A lambda that calls the linker driver action with its direct 237 argument and returns a list of outputs from the action. 238 """ 239 if not arg.startswith(LINKER_DRIVER_ARG_PREFIX): 240 raise ValueError('%s is not a linker driver argument' % (arg, )) 241 242 sub_arg = arg[len(LINKER_DRIVER_ARG_PREFIX):] 243 244 found = False 245 for driver_action in self._pre_actions: 246 (pre_name, pre_action) = driver_action 247 if sub_arg.startswith(pre_name): 248 assert pre_name not in self._linker_driver_pre_actions, \ 249 f"Name '{pre_name}' found in linker driver pre actions" 250 self._linker_driver_pre_actions[pre_name] = \ 251 lambda: pre_action(sub_arg[len(pre_name):]) 252 # same sub_arg may be used in actions. 253 found = True 254 break 255 256 for driver_action in self._actions: 257 (name, action) = driver_action 258 if sub_arg.startswith(name): 259 assert name not in self._linker_driver_actions, \ 260 f"Name '{name}' found in linker driver actions" 261 self._linker_driver_actions[name] = \ 262 lambda: action(sub_arg[len(name):]) 263 return 264 265 if not found: 266 raise ValueError('Unknown linker driver argument: %s' % (arg, )) 267 268 def prepare_object_path_lto(self, arg): 269 """Linker driver pre-action for -Wcrl,object_path_lto. 270 271 Prepare object_path_lto path in temp directory. 272 """ 273 # TODO(lgrey): Remove if/when we start running `dsymutil` 274 # through the clang driver. See https://crbug.com/1324104 275 # The temporary directory for intermediate LTO object files. If it 276 # exists, it will clean itself up on script exit. 277 object_path_lto = tempfile.TemporaryDirectory(dir=os.getcwd()) 278 self._compiler_driver_args.append('-Wl,-object_path_lto,{}'.format( 279 os.path.relpath(object_path_lto.name))) 280 281 def check_reexport_in_old_module(self, tocname): 282 """Linker driver pre-action for -Wcrl,tocname,<path>. 283 284 Check whether it contains LC_REEXPORT_DYLIB in old module, so that 285 needs to ouptupt TOC file for solink even if the same TOC. 286 287 Returns: 288 True if old module have LC_REEXPORT_DYLIB 289 """ 290 if not os.path.exists(tocname): 291 return 292 dylib = self._get_linker_output() 293 if not os.path.exists(dylib): 294 return 295 p = subprocess.run(self._otool_cmd + ['-l', dylib], 296 capture_output=True) 297 if p.returncode != 0: 298 return 299 if re.match(rb'\s+cmd LC_REEXPORT_DYLIB$', p.stdout, re.MULTILINE): 300 self._reexport_in_old_module = True 301 302 def set_install_name_tool_path(self, install_name_tool_path): 303 """Linker driver pre-action for -Wcrl,installnametoolpath,<path>. 304 305 Sets the invocation command for install_name_tool, which allows the 306 caller to specify an alternate path. This action is always 307 processed before the run_install_name_tool action. 308 309 Args: 310 install_name_tool_path: string, The path to the install_name_tool 311 binary to run 312 """ 313 self._install_name_tool_cmd = [install_name_tool_path] 314 315 def run_install_name_tool(self, args_string): 316 """Linker driver action for -Wcrl,installnametool,<args>. Invokes 317 install_name_tool on the linker's output. 318 319 Args: 320 args_string: string, Comma-separated arguments for 321 `install_name_tool`. 322 323 Returns: 324 No output - this step is run purely for its side-effect. 325 """ 326 command = list(self._install_name_tool_cmd) 327 command.extend(args_string.split(',')) 328 command.append(self._get_linker_output()) 329 subprocess.check_call(command) 330 return [] 331 332 def run_dsymutil(self, dsym_path_prefix): 333 """Linker driver action for -Wcrl,dsym,<dsym-path-prefix>. Invokes 334 dsymutil on the linker's output and produces a dsym file at |dsym_file| 335 path. 336 337 Args: 338 dsym_path_prefix: string, The path at which the dsymutil output 339 should be located. 340 341 Returns: 342 list of string, Build step outputs. 343 """ 344 if not len(dsym_path_prefix): 345 raise ValueError('Unspecified dSYM output file') 346 347 linker_output = self._get_linker_output() 348 base = os.path.basename(linker_output) 349 dsym_out = os.path.join(dsym_path_prefix, base + '.dSYM') 350 351 # Remove old dSYMs before invoking dsymutil. 352 _remove_path(dsym_out) 353 354 tools_paths = _find_tools_paths(self._args) 355 if os.environ.get('PATH'): 356 tools_paths.append(os.environ['PATH']) 357 dsymutil_env = os.environ.copy() 358 dsymutil_env['PATH'] = ':'.join(tools_paths) 359 subprocess.check_call(self._dsymutil_cmd + 360 ['-o', dsym_out, linker_output], 361 env=dsymutil_env) 362 return [dsym_out] 363 364 def set_dsymutil_path(self, dsymutil_path): 365 """Linker driver pre-action for -Wcrl,dsymutilpath,<dsymutil_path>. 366 367 Sets the invocation command for dsymutil, which allows the caller to 368 specify an alternate dsymutil. This action is always processed before 369 the RunDsymUtil action. 370 371 Args: 372 dsymutil_path: string, The path to the dsymutil binary to run 373 """ 374 self._dsymutil_cmd = [dsymutil_path] 375 376 def run_save_unstripped(self, unstripped_path_prefix): 377 """Linker driver action for -Wcrl,unstripped,<unstripped_path_prefix>. 378 Copies the linker output to |unstripped_path_prefix| before stripping. 379 380 Args: 381 unstripped_path_prefix: string, The path at which the unstripped 382 output should be located. 383 384 Returns: 385 list of string, Build step outputs. 386 """ 387 if not len(unstripped_path_prefix): 388 raise ValueError('Unspecified unstripped output file') 389 390 base = os.path.basename(self._get_linker_output()) 391 unstripped_out = os.path.join(unstripped_path_prefix, 392 base + '.unstripped') 393 394 shutil.copyfile(self._get_linker_output(), unstripped_out) 395 return [unstripped_out] 396 397 def run_strip(self, strip_args_string): 398 """Linker driver action for -Wcrl,strip,<strip_arguments>. 399 400 Args: 401 strip_args_string: string, Comma-separated arguments for `strip`. 402 """ 403 strip_command = list(self._strip_cmd) 404 if len(strip_args_string) > 0: 405 strip_command += strip_args_string.split(',') 406 strip_command.append(self._get_linker_output()) 407 subprocess.check_call(strip_command) 408 return [] 409 410 def set_strip_path(self, strip_path): 411 """Linker driver pre-action for -Wcrl,strippath,<strip_path>. 412 413 Sets the invocation command for strip, which allows the caller to 414 specify an alternate strip. This action is always processed before the 415 RunStrip action. 416 417 Args: 418 strip_path: string, The path to the strip binary to run 419 """ 420 self._strip_cmd = [strip_path] 421 422 def set_otool_path(self, otool_path): 423 """Linker driver pre-action for -Wcrl,otoolpath,<otool_path>. 424 425 Sets the invocation command for otool. 426 427 Args: 428 otool_path: string. The path to the otool binary to run 429 430 """ 431 self._otool_cmd = [otool_path] 432 433 def set_nm_path(self, nm_path): 434 """Linker driver pre-action for -Wcrl,nmpath,<nm_path>. 435 436 Sets the invocation command for nm. 437 438 Args: 439 nm_path: string. The path to the nm binary to run 440 441 Returns: 442 No output - this step is run purely for its side-effect. 443 """ 444 self._nm_cmd = [nm_path] 445 446 def output_toc(self, tocname): 447 """Linker driver action for -Wcrl,tocname,<path>. 448 449 Produce *.TOC from linker output. 450 451 TODO(ukai): recursively collect symbols from all 'LC_REEXPORT_DYLIB'- 452 exported modules and present them all in the TOC, and 453 drop self._reexport_in_old_module. 454 455 Args: 456 tocname: string, The path to *.TOC file. 457 Returns: 458 list of string, TOC file as output. 459 """ 460 new_toc = self._extract_toc() 461 old_toc = None 462 if not self._reexport_in_old_module: 463 try: 464 with open(tocname, 'rb') as f: 465 old_toc = f.read() 466 except OSError: 467 pass 468 469 if self._reexport_in_old_module or new_toc != old_toc: 470 # TODO: use delete_on_close in python 3.12 or later. 471 with tempfile.NamedTemporaryFile(prefix=tocname + '.', 472 dir='.', 473 delete=False) as f: 474 f.write(new_toc) 475 f.close() 476 os.rename(f.name, tocname) 477 return [tocname] 478 479 def _extract_toc(self): 480 """Extract TOC from linker output. 481 482 Returns: 483 output contents in bytes. 484 """ 485 toc = b'' 486 dylib = self._get_linker_output() 487 out = subprocess.check_output(self._otool_cmd + ['-l', dylib]) 488 lines = out.split(b'\n') 489 found_id = False 490 for i, line in enumerate(lines): 491 # Too many LC_ID_DYLIBs? We didn’t understand something about 492 # the otool output. Raise an exception and die, rather than 493 # proceeding. 494 495 # Not any LC_ID_DYLIBs? Probably not an MH_DYLIB. Probably fine, we 496 # can proceed with ID-less TOC generation. 497 if line == b' cmd LC_ID_DYLIB': 498 if found_id: 499 raise ValueError('Too many LC_ID_DYLIBs in %s' % dylib) 500 toc += line + b'\n' 501 for j in range(5): 502 toc += lines[i + 1 + j] + b'\n' 503 found_id = True 504 505 # -U ignores undefined symbols 506 # -g display only global (external) symbols 507 # -p unsorted https://crrev.com/c/2173969 508 out = subprocess.check_output(self._nm_cmd + ['-Ugp', dylib]) 509 lines = out.split(b'\n') 510 for line in lines: 511 fields = line.split(b' ', 2) 512 if len(fields) < 3: 513 continue 514 # fields = (value, type, name) 515 # emit [type, name] 516 toc += b' '.join(fields[1:3]) + b'\n' 517 return toc 518 519 520def _find_tools_paths(full_args): 521 """Finds all paths where the script should look for additional tools.""" 522 paths = [] 523 for idx, arg in enumerate(full_args): 524 if arg in ['-B', '--prefix']: 525 paths.append(full_args[idx + 1]) 526 elif arg.startswith('-B'): 527 paths.append(arg[2:]) 528 elif arg.startswith('--prefix='): 529 paths.append(arg[9:]) 530 return paths 531 532 533def _remove_path(path): 534 """Removes the file or directory at |path| if it exists.""" 535 if os.path.exists(path): 536 if os.path.isdir(path): 537 shutil.rmtree(path) 538 else: 539 os.unlink(path) 540 541 542if __name__ == '__main__': 543 LinkerDriver(sys.argv).run() 544 sys.exit(0) 545