• 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 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