1# Copyright 2013-2016 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15# This script extracts the symbols of a given shared library
16# into a file. If the symbols have not changed, the file is not
17# touched. This information is used to skip link steps if the
18# ABI has not changed.
19
20# This file is basically a reimplementation of
21# http://cgit.freedesktop.org/libreoffice/core/commit/?id=3213cd54b76bc80a6f0516aac75a48ff3b2ad67c
22
23import typing as T
24import os, sys
25from .. import mesonlib
26from .. import mlog
27from ..mesonlib import Popen_safe
28import argparse
29
30parser = argparse.ArgumentParser()
31
32parser.add_argument('--cross-host', default=None, dest='cross_host',
33                    help='cross compilation host platform')
34parser.add_argument('args', nargs='+')
35
36TOOL_WARNING_FILE = None
37RELINKING_WARNING = 'Relinking will always happen on source changes.'
38
39def dummy_syms(outfilename: str):
40    """Just touch it so relinking happens always."""
41    with open(outfilename, 'w'):
42        pass
43
44def write_if_changed(text: str, outfilename: str):
45    try:
46        with open(outfilename, 'r') as f:
47            oldtext = f.read()
48        if text == oldtext:
49            return
50    except FileNotFoundError:
51        pass
52    with open(outfilename, 'w') as f:
53        f.write(text)
54
55def print_tool_warning(tool: list, msg: str, stderr: str = None):
56    global TOOL_WARNING_FILE
57    if os.path.exists(TOOL_WARNING_FILE):
58        return
59    if len(tool) == 1:
60        tool = tool[0]
61    m = '{!r} {}. {}'.format(tool, msg, RELINKING_WARNING)
62    if stderr:
63        m += '\n' + stderr
64    mlog.warning(m)
65    # Write it out so we don't warn again
66    with open(TOOL_WARNING_FILE, 'w'):
67        pass
68
69def get_tool(name: str) -> T.List[str]:
70    evar = name.upper()
71    if evar in os.environ:
72        import shlex
73        return shlex.split(os.environ[evar])
74    return [name]
75
76def call_tool(name: str, args: T.List[str], **kwargs) -> str:
77    tool = get_tool(name)
78    try:
79        p, output, e = Popen_safe(tool + args, **kwargs)
80    except FileNotFoundError:
81        print_tool_warning(tool, 'not found')
82        return None
83    except PermissionError:
84        print_tool_warning(tool, 'not usable')
85        return None
86    if p.returncode != 0:
87        print_tool_warning(tool, 'does not work', e)
88        return None
89    return output
90
91def call_tool_nowarn(tool: T.List[str], **kwargs) -> T.Tuple[str, str]:
92    try:
93        p, output, e = Popen_safe(tool, **kwargs)
94    except FileNotFoundError:
95        return None, '{!r} not found\n'.format(tool[0])
96    except PermissionError:
97        return None, '{!r} not usable\n'.format(tool[0])
98    if p.returncode != 0:
99        return None, e
100    return output, None
101
102def gnu_syms(libfilename: str, outfilename: str):
103    # Get the name of the library
104    output = call_tool('readelf', ['-d', libfilename])
105    if not output:
106        dummy_syms(outfilename)
107        return
108    result = [x for x in output.split('\n') if 'SONAME' in x]
109    assert(len(result) <= 1)
110    # Get a list of all symbols exported
111    output = call_tool('nm', ['--dynamic', '--extern-only', '--defined-only',
112                              '--format=posix', libfilename])
113    if not output:
114        dummy_syms(outfilename)
115        return
116    for line in output.split('\n'):
117        if not line:
118            continue
119        line_split = line.split()
120        entry = line_split[0:2]
121        # Store the size of symbols pointing to data objects so we relink
122        # when those change, which is needed because of copy relocations
123        # https://github.com/mesonbuild/meson/pull/7132#issuecomment-628353702
124        if line_split[1].upper() in ('B', 'G', 'D') and len(line_split) >= 4:
125            entry += [line_split[3]]
126        result += [' '.join(entry)]
127    write_if_changed('\n'.join(result) + '\n', outfilename)
128
129def solaris_syms(libfilename: str, outfilename: str):
130    # gnu_syms() works with GNU nm & readelf, not Solaris nm & elfdump
131    origpath = os.environ['PATH']
132    os.environ['PATH'] = '/usr/gnu/bin:' + origpath
133    gnu_syms(libfilename, outfilename)
134    os.environ['PATH'] = origpath
135
136def osx_syms(libfilename: str, outfilename: str):
137    # Get the name of the library
138    output = call_tool('otool', ['-l', libfilename])
139    if not output:
140        dummy_syms(outfilename)
141        return
142    arr = output.split('\n')
143    for (i, val) in enumerate(arr):
144        if 'LC_ID_DYLIB' in val:
145            match = i
146            break
147    result = [arr[match + 2], arr[match + 5]] # Libreoffice stores all 5 lines but the others seem irrelevant.
148    # Get a list of all symbols exported
149    output = call_tool('nm', ['--extern-only', '--defined-only',
150                              '--format=posix', libfilename])
151    if not output:
152        dummy_syms(outfilename)
153        return
154    result += [' '.join(x.split()[0:2]) for x in output.split('\n')]
155    write_if_changed('\n'.join(result) + '\n', outfilename)
156
157def openbsd_syms(libfilename: str, outfilename: str):
158    # Get the name of the library
159    output = call_tool('readelf', ['-d', libfilename])
160    if not output:
161        dummy_syms(outfilename)
162        return
163    result = [x for x in output.split('\n') if 'SONAME' in x]
164    assert(len(result) <= 1)
165    # Get a list of all symbols exported
166    output = call_tool('nm', ['-D', '-P', '-g', libfilename])
167    if not output:
168        dummy_syms(outfilename)
169        return
170    # U = undefined (cope with the lack of --defined-only option)
171    result += [' '.join(x.split()[0:2]) for x in output.split('\n') if x and not x.endswith('U ')]
172    write_if_changed('\n'.join(result) + '\n', outfilename)
173
174def cygwin_syms(impfilename: str, outfilename: str):
175    # Get the name of the library
176    output = call_tool('dlltool', ['-I', impfilename])
177    if not output:
178        dummy_syms(outfilename)
179        return
180    result = [output]
181    # Get the list of all symbols exported
182    output = call_tool('nm', ['--extern-only', '--defined-only',
183                              '--format=posix', impfilename])
184    if not output:
185        dummy_syms(outfilename)
186        return
187    for line in output.split('\n'):
188        if ' T ' not in line:
189            continue
190        result.append(line.split(maxsplit=1)[0])
191    write_if_changed('\n'.join(result) + '\n', outfilename)
192
193def _get_implib_dllname(impfilename: str) -> T.Tuple[T.List[str], str]:
194    all_stderr = ''
195    # First try lib.exe, which is provided by MSVC. Then llvm-lib.exe, by LLVM
196    # for clang-cl.
197    #
198    # We cannot call get_tool on `lib` because it will look at the `LIB` env
199    # var which is the list of library paths MSVC will search for import
200    # libraries while linking.
201    for lib in (['lib'], get_tool('llvm-lib')):
202        output, e = call_tool_nowarn(lib + ['-list', impfilename])
203        if output:
204            # The output is a list of DLLs that each symbol exported by the import
205            # library is available in. We only build import libraries that point to
206            # a single DLL, so we can pick any of these. Pick the last one for
207            # simplicity. Also skip the last line, which is empty.
208            return output.split('\n')[-2:-1], None
209        all_stderr += e
210    # Next, try dlltool.exe which is provided by MinGW
211    output, e = call_tool_nowarn(get_tool('dlltool') + ['-I', impfilename])
212    if output:
213        return [output], None
214    all_stderr += e
215    return ([], all_stderr)
216
217def _get_implib_exports(impfilename: str) -> T.Tuple[T.List[str], str]:
218    all_stderr = ''
219    # Force dumpbin.exe to use en-US so we can parse its output
220    env = os.environ.copy()
221    env['VSLANG'] = '1033'
222    output, e = call_tool_nowarn(get_tool('dumpbin') + ['-exports', impfilename], env=env)
223    if output:
224        lines = output.split('\n')
225        start = lines.index('File Type: LIBRARY')
226        end = lines.index('  Summary')
227        return lines[start:end], None
228    all_stderr += e
229    # Next, try llvm-nm.exe provided by LLVM, then nm.exe provided by MinGW
230    for nm in ('llvm-nm', 'nm'):
231        output, e = call_tool_nowarn(get_tool(nm) + ['--extern-only', '--defined-only',
232                                                     '--format=posix', impfilename])
233        if output:
234            result = []
235            for line in output.split('\n'):
236                if ' T ' not in line or line.startswith('.text'):
237                    continue
238                result.append(line.split(maxsplit=1)[0])
239            return result, None
240        all_stderr += e
241    return ([], all_stderr)
242
243def windows_syms(impfilename: str, outfilename: str):
244    # Get the name of the library
245    result, e = _get_implib_dllname(impfilename)
246    if not result:
247        print_tool_warning('lib, llvm-lib, dlltool', 'do not work or were not found', e)
248        dummy_syms(outfilename)
249        return
250    # Get a list of all symbols exported
251    symbols, e = _get_implib_exports(impfilename)
252    if not symbols:
253        print_tool_warning('dumpbin, llvm-nm, nm', 'do not work or were not found', e)
254        dummy_syms(outfilename)
255        return
256    result += symbols
257    write_if_changed('\n'.join(result) + '\n', outfilename)
258
259def gen_symbols(libfilename: str, impfilename: str, outfilename: str, cross_host: str):
260    if cross_host is not None:
261        # In case of cross builds just always relink. In theory we could
262        # determine the correct toolset, but we would need to use the correct
263        # `nm`, `readelf`, etc, from the cross info which requires refactoring.
264        dummy_syms(outfilename)
265    elif mesonlib.is_linux() or mesonlib.is_hurd():
266        gnu_syms(libfilename, outfilename)
267    elif mesonlib.is_osx():
268        osx_syms(libfilename, outfilename)
269    elif mesonlib.is_openbsd():
270        openbsd_syms(libfilename, outfilename)
271    elif mesonlib.is_windows():
272        if os.path.isfile(impfilename):
273            windows_syms(impfilename, outfilename)
274        else:
275            # No import library. Not sure how the DLL is being used, so just
276            # rebuild everything that links to it every time.
277            dummy_syms(outfilename)
278    elif mesonlib.is_cygwin():
279        if os.path.isfile(impfilename):
280            cygwin_syms(impfilename, outfilename)
281        else:
282            # No import library. Not sure how the DLL is being used, so just
283            # rebuild everything that links to it every time.
284            dummy_syms(outfilename)
285    elif mesonlib.is_sunos():
286        solaris_syms(libfilename, outfilename)
287    else:
288        if not os.path.exists(TOOL_WARNING_FILE):
289            mlog.warning('Symbol extracting has not been implemented for this '
290                         'platform. ' + RELINKING_WARNING)
291            # Write it out so we don't warn again
292            with open(TOOL_WARNING_FILE, 'w'):
293                pass
294        dummy_syms(outfilename)
295
296def run(args):
297    global TOOL_WARNING_FILE
298    options = parser.parse_args(args)
299    if len(options.args) != 4:
300        print('symbolextractor.py <shared library file> <import library> <output file>')
301        sys.exit(1)
302    privdir = os.path.join(options.args[0], 'meson-private')
303    TOOL_WARNING_FILE = os.path.join(privdir, 'symbolextractor_tool_warning_printed')
304    libfile = options.args[1]
305    impfile = options.args[2] # Only used on Windows
306    outfile = options.args[3]
307    gen_symbols(libfile, impfile, outfile, options.cross_host)
308    return 0
309
310if __name__ == '__main__':
311    sys.exit(run(sys.argv[1:]))
312