symbolextractor.py 10 KB

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