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