xref: /aosp_15_r20/external/angle/build/toolchain/apple/linker_driver.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
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