• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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