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