# Custom script is necessary because CMake does not yet support creating static libraries combined with dependencies
# https://gitlab.kitware.com/cmake/cmake/-/issues/22975
#
# This script is intended to be used set as a CXX_LINKER_LAUNCHER property for recursive library targets.
# It parses the linking command and transforms it to archiving commands combining static libraries from dependencies.
import argparse
import os
import shlex
import subprocess
import sys
import tempfile
class Opts(object):
def __init__(self, args):
argparser = argparse.ArgumentParser(allow_abbrev=False)
argparser.add_argument('--cmake-binary-dir', required=True)
argparser.add_argument('--cmake-ar', required=True)
argparser.add_argument('--cmake-ranlib', required=True)
argparser.add_argument('--cmake-host-system-name', required=True)
argparser.add_argument('--cmake-cxx-standard-libraries')
argparser.add_argument('--global-part-suffix', required=True)
self.parsed_args, other_args = argparser.parse_known_args(args=args)
if len(other_args) < 2:
# must contain at least '--linking-cmdline' and orginal linking tool name
raise Exception('not enough arguments')
if other_args[0] != '--linking-cmdline':
raise Exception("expected '--linking-cmdline' arg, got {}".format(other_args[0]))
self.is_msvc_compatible_linker = other_args[1].endswith('\\link.exe') or other_args[1].endswith('\\lld-link.exe')
is_host_system_windows = self.parsed_args.cmake_host_system_name == 'Windows'
std_libraries_to_exclude_from_input = (
set(self.parsed_args.cmake_cxx_standard_libraries.split())
if self.parsed_args.cmake_cxx_standard_libraries is not None
else set()
)
msvc_preserved_option_prefixes = [
'machine:',
'nodefaultlib',
'nologo',
]
self.preserved_options = []
# these variables can contain paths absolute or relative to CMAKE_BINARY_DIR
self.global_libs_and_objects_input = []
self.non_global_libs_input = []
self.output = None
def is_external_library(path):
"""
Check whether this library has been built in this CMake project or came from Conan-provided dependencies
(these use absolute paths).
If it is a library that is added from some other path (like CUDA) return True
"""
return not (os.path.exists(path) or os.path.exists(os.path.join(self.parsed_args.cmake_binary_dir, path)))
def process_input(args):
i = 0
is_in_whole_archive = False
while i < len(args):
arg = args[i]
if is_host_system_windows and ((arg[0] == '/') or (arg[0] == '-')):
arg_wo_specifier_lower = arg[1:].lower()
if arg_wo_specifier_lower.startswith('out:'):
self.output = arg[len('/out:') :]
elif arg_wo_specifier_lower.startswith('wholearchive:'):
lib_path = arg[len('/wholearchive:') :]
if not is_external_library(lib_path):
self.global_libs_and_objects_input.append(lib_path)
else:
for preserved_option_prefix in msvc_preserved_option_prefixes:
if arg_wo_specifier_lower.startswith(preserved_option_prefix):
self.preserved_options.append(arg)
break
# other flags are non-linking related and just ignored
elif arg[0] == '-':
if arg == '-o':
if (i + 1) >= len(args):
raise Exception('-o flag without an argument')
self.output = args[i + 1]
i += 1
elif arg == '-Wl,--whole-archive':
is_in_whole_archive = True
elif arg == '-Wl,--no-whole-archive':
is_in_whole_archive = False
elif arg.startswith('-Wl,-force_load,'):
lib_path = arg[len('-Wl,-force_load,') :]
if not is_external_library(lib_path):
self.global_libs_and_objects_input.append(lib_path)
elif arg == '-isysroot':
i += 1
# other flags are non-linking related and just ignored
elif arg[0] == '@':
# response file with args
with open(arg[1:]) as response_file:
parsed_args = shlex.shlex(response_file, posix=False, punctuation_chars=False)
parsed_args.whitespace_split = True
args_in_response_file = list(arg.strip('"') for arg in parsed_args)
process_input(args_in_response_file)
elif not is_external_library(arg):
if is_in_whole_archive or arg.endswith('.o') or arg.endswith('.obj'):
self.global_libs_and_objects_input.append(arg)
elif arg not in std_libraries_to_exclude_from_input:
self.non_global_libs_input.append(arg)
i += 1
process_input(other_args[2:])
if self.output is None:
raise Exception("No output specified")
if (len(self.global_libs_and_objects_input) == 0) and (len(self.non_global_libs_input) == 0):
raise Exception("List of input objects and libraries is empty")
class FilesCombiner(object):
def __init__(self, opts):
self.opts = opts
archiver_tool_path = opts.parsed_args.cmake_ar
if sys.platform.startswith('darwin'):
# force LIBTOOL even if CMAKE_AR is defined because 'ar' under Darwin does not contain the necessary options
arch_type = 'LIBTOOL'
archiver_tool_path = 'libtool'
elif opts.is_msvc_compatible_linker:
arch_type = 'LIB'
elif opts.parsed_args.cmake_ar.endswith('llvm-ar'):
arch_type = 'LLVM_AR'
elif opts.parsed_args.cmake_ar.endswith('ar'):
arch_type = 'GNU_AR'
else:
raise Exception('Unsupported arch type for CMAKE_AR={}'.format(opts.parsed_args.cmake_ar))
self.archiving_cmd_prefix = [
sys.executable,
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'link_lib.py'),
archiver_tool_path,
arch_type,
'gnu', # llvm_ar_format, used only if arch_type == 'LLVM_AR'
opts.parsed_args.cmake_binary_dir,
'None', # plugin. Unused for now
]
# the remaining archiving cmd args are [output, .. input .. ]
def do(self, output, input_list):
input_file_path = None
try:
if self.opts.is_msvc_compatible_linker:
# use response file for input (because of Windows cmdline length limitations)
# can't use NamedTemporaryFile because of permissions issues on Windows
input_file_fd, input_file_path = tempfile.mkstemp()
try:
input_file = os.fdopen(input_file_fd, 'w')
for input in input_list:
if ' ' in input:
input_file.write('"{}" '.format(input))
else:
input_file.write('{} '.format(input))
input_file.flush()
finally:
os.close(input_file_fd)
input_args = ['@' + input_file_path]
else:
input_args = input_list
cmd = self.archiving_cmd_prefix + [output] + self.opts.preserved_options + input_args
subprocess.check_call(cmd)
finally:
if input_file_path is not None:
os.remove(input_file_path)
if not self.opts.is_msvc_compatible_linker:
subprocess.check_call([self.opts.parsed_args.cmake_ranlib, output])
if __name__ == "__main__":
opts = Opts(sys.argv[1:])
output_prefix, output_ext = os.path.splitext(opts.output)
globals_output = output_prefix + opts.parsed_args.global_part_suffix + output_ext
if os.path.exists(globals_output):
os.remove(globals_output)
if os.path.exists(opts.output):
os.remove(opts.output)
files_combiner = FilesCombiner(opts)
if len(opts.global_libs_and_objects_input) > 0:
files_combiner.do(globals_output, opts.global_libs_and_objects_input)
if len(opts.non_global_libs_input) > 0:
files_combiner.do(opts.output, opts.non_global_libs_input)