tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

linker_driver.py (20976B)


      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 
      7 import os
      8 import os.path
      9 import re
     10 import shutil
     11 import subprocess
     12 import sys
     13 import tempfile
     14 
     15 # The path to `whole_archive`.
     16 sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
     17 
     18 import whole_archive
     19 
     20 # Prefix for all custom linker driver arguments.
     21 LINKER_DRIVER_ARG_PREFIX = '-Wcrl,'
     22 LINKER_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 
     88 class 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 
    520 def _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 
    533 def _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 
    542 if __name__ == '__main__':
    543    LinkerDriver(sys.argv).run()
    544    sys.exit(0)