1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# Copyright 2016 The Chromium Authors. All rights reserved.
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
12
13"""
14The linker_driver.py is responsible for forwarding a linker invocation to
15the compiler driver, while processing special arguments itself.
16
17Usage: linker_driver.py clang++ main.o -L. -llib -o prog -Wcrl,dsym,out
18
19On Mac, the logical step of linking is handled by three discrete tools to
20perform the image link, debug info link, and strip. The linker_driver.py
21combines these three steps into a single tool.
22
23The command passed to the linker_driver.py should be the compiler driver
24invocation for the linker. It is first invoked unaltered (except for the
25removal of the special driver arguments, described below). Then the driver
26performs additional actions, based on these arguments:
27
28  -Wcrl,dsym,<dsym_path_prefix>
29      After invoking the linker, this will run `dsymutil` on the linker's
30      output, producing a dSYM bundle, stored at dsym_path_prefix. As an
31      example, if the linker driver were invoked with:
32        "... -o out/gn/obj/foo/libbar.dylib ... -Wcrl,dsym,out/gn ..."
33      The resulting dSYM would be out/gn/libbar.dylib.dSYM/.
34
35  -Wcrl,unstripped,<unstripped_path_prefix>
36      After invoking the linker, and before strip, this will save a copy of
37      the unstripped linker output in the directory unstripped_path_prefix.
38
39  -Wcrl,strip,<strip_arguments>
40      After invoking the linker, and optionally dsymutil, this will run
41      the strip command on the linker's output. strip_arguments are
42      comma-separated arguments to be passed to the strip command.
43"""
44
45
46def main(args):
47    """main function for the linker driver. Separates out the arguments for
48    the main compiler driver and the linker driver, then invokes all the
49    required tools.
50
51    Args:
52      args: list of string, Arguments to the script.
53    """
54
55    if len(args) < 2:
56        raise RuntimeError("Usage: linker_driver.py [linker-invocation]")
57
58    i = 0
59    while i < len(args):
60        arg = args[i]
61        if arg == '--developer':
62            if i + 1 < len(args) and not args[i + 1].startswith('--'):
63                os.environ['DEVELOPER_DIR'] = args[i + 1]
64                del args[i:i + 2]
65            else:
66                i += 1
67        else:
68            i += 1
69
70    # Collect arguments to the linker driver (this script) and remove them from
71    # the arguments being passed to the compiler driver.
72    linker_driver_actions = {}
73    compiler_driver_args = []
74    for arg in args[1:]:
75        if arg.startswith(_LINKER_DRIVER_ARG_PREFIX):
76            # Convert driver actions into a map of name => lambda to invoke.
77            driver_action = process_linker_driver_arg(arg)
78            assert driver_action[0] not in linker_driver_actions
79            linker_driver_actions[driver_action[0]] = driver_action[1]
80        else:
81            compiler_driver_args.append(arg)
82
83    linker_driver_outputs = [_find_linker_output(compiler_driver_args)]
84
85    try:
86        # Run the linker by invoking the compiler driver.
87        subprocess.check_call(compiler_driver_args)
88
89        # Run the linker driver actions, in the order specified by the actions list.
90        for action in _LINKER_DRIVER_ACTIONS:
91            name = action[0]
92            if name in linker_driver_actions:
93                linker_driver_outputs += linker_driver_actions[name](args)
94    except:
95        # If a linker driver action failed, remove all the outputs to make the
96        # build step atomic.
97        map(_remove_path, linker_driver_outputs)
98
99        # Re-report the original failure.
100        raise
101
102
103def process_linker_driver_arg(arg):
104    """Processes a linker driver argument and returns a tuple containing the
105    name and unary lambda to invoke for that linker driver action.
106
107    Args:
108      arg: string, The linker driver argument.
109
110    Returns:
111      A 2-tuple:
112        0: The driver action name, as in _LINKER_DRIVER_ACTIONS.
113        1: An 1-ary lambda that takes the full list of arguments passed to
114           main(). The lambda should call the linker driver action that
115           corresponds to the argument and return a list of outputs from the
116           action.
117    """
118    if not arg.startswith(_LINKER_DRIVER_ARG_PREFIX):
119        raise ValueError('%s is not a linker driver argument' % (arg,))
120
121    sub_arg = arg[len(_LINKER_DRIVER_ARG_PREFIX):]
122
123    for driver_action in _LINKER_DRIVER_ACTIONS:
124        (name, action) = driver_action
125        if sub_arg.startswith(name):
126            return (name,
127                    lambda full_args: action(sub_arg[len(name):], full_args))
128
129    raise ValueError('Unknown linker driver argument: %s' % (arg,))
130
131
132def run_dsym_util(dsym_path_prefix, full_args):
133    """Linker driver action for -Wcrl,dsym,<dsym-path-prefix>. Invokes dsymutil
134    on the linker's output and produces a dsym file at |dsym_file| path.
135
136    Args:
137      dsym_path_prefix: string, The path at which the dsymutil output should be
138          located.
139      full_args: list of string, Full argument list for the linker driver.
140
141    Returns:
142        list of string, Build step outputs.
143    """
144    if not len(dsym_path_prefix):
145        raise ValueError('Unspecified dSYM output file')
146
147    linker_out = _find_linker_output(full_args)
148    base = os.path.basename(linker_out)
149    dsym_out = os.path.join(dsym_path_prefix, base + '.dSYM')
150
151    # Remove old dSYMs before invoking dsymutil.
152    _remove_path(dsym_out)
153    subprocess.check_call(['xcrun', 'dsymutil', '-o', dsym_out, linker_out])
154    return [dsym_out]
155
156
157def run_save_unstripped(unstripped_path_prefix, full_args):
158    """Linker driver action for -Wcrl,unstripped,<unstripped_path_prefix>. Copies
159    the linker output to |unstripped_path_prefix| before stripping.
160
161    Args:
162      unstripped_path_prefix: string, The path at which the unstripped output
163          should be located.
164      full_args: list of string, Full argument list for the linker driver.
165
166    Returns:
167      list of string, Build step outputs.
168    """
169    if not len(unstripped_path_prefix):
170        raise ValueError('Unspecified unstripped output file')
171
172    linker_out = _find_linker_output(full_args)
173    base = os.path.basename(linker_out)
174    unstripped_out = os.path.join(unstripped_path_prefix, base + '.unstripped')
175
176    shutil.copyfile(linker_out, unstripped_out)
177    return [unstripped_out]
178
179
180def run_strip(strip_args_string, full_args):
181    """Linker driver action for -Wcrl,strip,<strip_arguments>.
182
183    Args:
184        strip_args_string: string, Comma-separated arguments for `strip`.
185        full_args: list of string, Full arguments for the linker driver.
186
187    Returns:
188        list of string, Build step outputs.
189    """
190    strip_command = ['xcrun', 'strip']
191    if len(strip_args_string) > 0:
192        strip_command += strip_args_string.split(',')
193    strip_command.append(_find_linker_output(full_args))
194    subprocess.check_call(strip_command)
195    return []
196
197
198def _find_linker_output(full_args):
199    """Finds the output of the linker by looking for the output flag in its
200    argument list. As this is a required linker argument, raises an error if it
201    cannot be found.
202    """
203    # The linker_driver.py script may be used to wrap either the compiler linker
204    # (uses -o to configure the output) or lipo (uses -output to configure the
205    # output). Since wrapping the compiler linker is the most likely possibility
206    # use try/except and fallback to checking for -output if -o is not found.
207    try:
208        output_flag_index = full_args.index('-o')
209    except ValueError:
210        output_flag_index = full_args.index('-output')
211    return full_args[output_flag_index + 1]
212
213
214def _remove_path(path):
215    """Removes the file or directory at |path| if it exists."""
216    if os.path.exists(path):
217        if os.path.isdir(path):
218            shutil.rmtree(path)
219        else:
220            os.unlink(path)
221
222
223_LINKER_DRIVER_ARG_PREFIX = '-Wcrl,'
224
225"""List of linker driver actions. The sort order of this list affects the
226order in which the actions are invoked. The first item in the tuple is the
227argument's -Wcrl,<sub_argument> and the second is the function to invoke.
228"""
229_LINKER_DRIVER_ACTIONS = [
230    ('dsym,', run_dsym_util),
231    ('unstripped,', run_save_unstripped),
232    ('strip,', run_strip),
233]
234
235if __name__ == '__main__':
236    main(sys.argv)
237    sys.exit(0)
238