linker_driver.py 14 KB

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