create_recursive_library_for_cmake.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. # Custom script is necessary because CMake does not yet support creating static libraries combined with dependencies
  2. # https://gitlab.kitware.com/cmake/cmake/-/issues/22975
  3. #
  4. # This script is intended to be used set as a CXX_LINKER_LAUNCHER property for recursive library targets.
  5. # It parses the linking command and transforms it to archiving commands combining static libraries from dependencies.
  6. import argparse
  7. import os
  8. import re
  9. import shlex
  10. import subprocess
  11. import sys
  12. import tempfile
  13. class Opts(object):
  14. def __init__(self, args):
  15. argparser = argparse.ArgumentParser(allow_abbrev=False)
  16. argparser.add_argument('--project-binary-dir', required=True)
  17. argparser.add_argument('--cmake-ar', required=True)
  18. argparser.add_argument('--cmake-ranlib', required=True)
  19. argparser.add_argument('--cmake-host-system-name', required=True)
  20. argparser.add_argument('--cmake-cxx-standard-libraries')
  21. argparser.add_argument('--global-part-suffix', required=True)
  22. self.parsed_args, other_args = argparser.parse_known_args(args=args)
  23. if len(other_args) < 2:
  24. # must contain at least '--linking-cmdline' and orginal linking tool name
  25. raise Exception('not enough arguments')
  26. if other_args[0] != '--linking-cmdline':
  27. raise Exception("expected '--linking-cmdline' arg, got {}".format(other_args[0]))
  28. self.is_msvc_compatible_linker = other_args[1].endswith('\\link.exe') or other_args[1].endswith('\\lld-link.exe')
  29. is_host_system_windows = self.parsed_args.cmake_host_system_name == 'Windows'
  30. std_libraries_to_exclude_from_input = (
  31. set(self.parsed_args.cmake_cxx_standard_libraries.split())
  32. if self.parsed_args.cmake_cxx_standard_libraries is not None
  33. else set()
  34. )
  35. msvc_preserved_option_prefixes = [
  36. 'machine:',
  37. 'nodefaultlib',
  38. 'nologo',
  39. ]
  40. self.preserved_options = []
  41. # these variables can contain paths absolute or relative to project_binary_dir
  42. self.global_libs_and_objects_input = []
  43. self.non_global_libs_input = []
  44. self.output = None
  45. def is_external_library(path):
  46. """
  47. Check whether this library has been built in this CMake project or came from Conan-provided dependencies
  48. (these use absolute paths).
  49. If it is a library that is added from some other path (like CUDA) return True
  50. """
  51. return not (os.path.exists(path) or os.path.exists(os.path.join(self.parsed_args.project_binary_dir, path)))
  52. def process_input(args):
  53. i = 0
  54. is_in_whole_archive = False
  55. while i < len(args):
  56. arg = args[i]
  57. if is_host_system_windows and ((arg[0] == '/') or (arg[0] == '-')):
  58. arg_wo_specifier_lower = arg[1:].lower()
  59. if arg_wo_specifier_lower.startswith('out:'):
  60. self.output = arg[len('/out:') :]
  61. elif arg_wo_specifier_lower.startswith('wholearchive:'):
  62. lib_path = arg[len('/wholearchive:') :]
  63. if not is_external_library(lib_path):
  64. self.global_libs_and_objects_input.append(lib_path)
  65. else:
  66. for preserved_option_prefix in msvc_preserved_option_prefixes:
  67. if arg_wo_specifier_lower.startswith(preserved_option_prefix):
  68. self.preserved_options.append(arg)
  69. break
  70. # other flags are non-linking related and just ignored
  71. elif arg[0] == '-':
  72. if arg == '-o':
  73. if (i + 1) >= len(args):
  74. raise Exception('-o flag without an argument')
  75. self.output = args[i + 1]
  76. i += 1
  77. elif arg == '-Wl,--whole-archive':
  78. is_in_whole_archive = True
  79. elif arg == '-Wl,--no-whole-archive':
  80. is_in_whole_archive = False
  81. elif arg.startswith('-Wl,-force_load,'):
  82. lib_path = arg[len('-Wl,-force_load,') :]
  83. if not is_external_library(lib_path):
  84. self.global_libs_and_objects_input.append(lib_path)
  85. elif arg == '-isysroot':
  86. i += 1
  87. # other flags are non-linking related and just ignored
  88. elif arg[0] == '@':
  89. # response file with args
  90. with open(arg[1:]) as response_file:
  91. parsed_args = shlex.shlex(response_file, posix=False, punctuation_chars=False)
  92. parsed_args.whitespace_split = True
  93. args_in_response_file = list(arg.strip('"') for arg in parsed_args)
  94. process_input(args_in_response_file)
  95. elif not is_external_library(arg):
  96. if is_in_whole_archive or arg.endswith('.o') or arg.endswith('.obj'):
  97. self.global_libs_and_objects_input.append(arg)
  98. elif arg not in std_libraries_to_exclude_from_input:
  99. self.non_global_libs_input.append(arg)
  100. i += 1
  101. process_input(other_args[2:])
  102. if self.output is None:
  103. raise Exception("No output specified")
  104. if (len(self.global_libs_and_objects_input) == 0) and (len(self.non_global_libs_input) == 0):
  105. raise Exception("List of input objects and libraries is empty")
  106. class FilesCombiner(object):
  107. def __init__(self, opts):
  108. self.opts = opts
  109. archiver_tool_path = opts.parsed_args.cmake_ar
  110. if sys.platform.startswith('darwin'):
  111. # force LIBTOOL even if CMAKE_AR is defined because 'ar' under Darwin does not contain the necessary options
  112. arch_type = 'LIBTOOL'
  113. archiver_tool_path = 'libtool'
  114. elif opts.is_msvc_compatible_linker:
  115. arch_type = 'LIB'
  116. elif re.match(r'^(|.*/)llvm\-ar(\-[\d])?', opts.parsed_args.cmake_ar):
  117. arch_type = 'LLVM_AR'
  118. elif re.match(r'^(|.*/)(gcc\-)?ar(\-[\d])?', opts.parsed_args.cmake_ar):
  119. arch_type = 'GNU_AR'
  120. else:
  121. raise Exception('Unsupported arch type for CMAKE_AR={}'.format(opts.parsed_args.cmake_ar))
  122. self.archiving_cmd_prefix = [
  123. sys.executable,
  124. os.path.join(os.path.dirname(os.path.abspath(__file__)), 'link_lib.py'),
  125. archiver_tool_path,
  126. arch_type,
  127. 'gnu', # llvm_ar_format, used only if arch_type == 'LLVM_AR'
  128. opts.parsed_args.project_binary_dir,
  129. 'None', # plugin. Unused for now
  130. ]
  131. # the remaining archiving cmd args are [output, .. input .. ]
  132. def do(self, output, input_list):
  133. input_file_path = None
  134. try:
  135. if self.opts.is_msvc_compatible_linker:
  136. # use response file for input (because of Windows cmdline length limitations)
  137. # can't use NamedTemporaryFile because of permissions issues on Windows
  138. input_file_fd, input_file_path = tempfile.mkstemp()
  139. try:
  140. input_file = os.fdopen(input_file_fd, 'w')
  141. for input in input_list:
  142. if ' ' in input:
  143. input_file.write('"{}" '.format(input))
  144. else:
  145. input_file.write('{} '.format(input))
  146. input_file.flush()
  147. finally:
  148. os.close(input_file_fd)
  149. input_args = ['@' + input_file_path]
  150. else:
  151. input_args = input_list
  152. cmd = self.archiving_cmd_prefix + [output] + self.opts.preserved_options + input_args
  153. subprocess.check_call(cmd)
  154. finally:
  155. if input_file_path is not None:
  156. os.remove(input_file_path)
  157. if not self.opts.is_msvc_compatible_linker:
  158. subprocess.check_call([self.opts.parsed_args.cmake_ranlib, output])
  159. if __name__ == "__main__":
  160. opts = Opts(sys.argv[1:])
  161. output_prefix, output_ext = os.path.splitext(opts.output)
  162. globals_output = output_prefix + opts.parsed_args.global_part_suffix + output_ext
  163. if os.path.exists(globals_output):
  164. os.remove(globals_output)
  165. if os.path.exists(opts.output):
  166. os.remove(opts.output)
  167. files_combiner = FilesCombiner(opts)
  168. if len(opts.global_libs_and_objects_input) > 0:
  169. files_combiner.do(globals_output, opts.global_libs_and_objects_input)
  170. if len(opts.non_global_libs_input) > 0:
  171. files_combiner.do(opts.output, opts.non_global_libs_input)