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