From: Jason Gunthorpe <jgg@xxxxxxxxxxxx> Once rdma-core started to introduce ccan and util internal libraries the static libraries stopped working in a sane way. We need to include the internal libraries within the static ones and sanitize the symbol names to get things back to where they used to be. Add a post-processing script that figures out what to do and invokes binutils to generate clean static libraries. Signed-off-by: Jason Gunthorpe <jgg@xxxxxxxxxxxx> --- CMakeLists.txt | 2 + buildlib/cbuild | 1 + buildlib/rdma_functions.cmake | 72 ++++++--- buildlib/sanitize_static_lib.py | 269 ++++++++++++++++++++++++++++++++ 4 files changed, 319 insertions(+), 25 deletions(-) create mode 100644 buildlib/sanitize_static_lib.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cccf85fab7133..94f3736b5b3ad6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,8 @@ set(BUILD_INCLUDE ${CMAKE_BINARY_DIR}/include) set(BUILD_BIN ${CMAKE_BINARY_DIR}/bin) # Libraries set(BUILD_LIB ${CMAKE_BINARY_DIR}/lib) +# Static library pre-processing +set(BUILD_STATIC_LIB ${CMAKE_BINARY_DIR}/lib/statics) # Used for IN_PLACE configuration set(BUILD_ETC ${CMAKE_BINARY_DIR}/etc) diff --git a/buildlib/cbuild b/buildlib/cbuild index 0e28aff94807f6..1cc54f313ca148 100755 --- a/buildlib/cbuild +++ b/buildlib/cbuild @@ -102,6 +102,7 @@ class centos6(YumEnvironment): 'make', 'pkgconfig', 'python', + 'python-argparse', 'rpm-build', 'valgrind-devel', }; diff --git a/buildlib/rdma_functions.cmake b/buildlib/rdma_functions.cmake index f7c83d106d2aa5..990c052ffc1311 100644 --- a/buildlib/rdma_functions.cmake +++ b/buildlib/rdma_functions.cmake @@ -4,12 +4,26 @@ # Helper functions for use in the sub CMakeLists files to make them simpler # and more uniform. -# Global list of pairs of (SHARED STATIC) libary target names +# Global list of tuples of (SHARED STATIC MAP) library target names set(RDMA_STATIC_LIBS "" CACHE INTERNAL "Doc" FORCE) set(COMMON_LIBS_PIC ccan_pic rdma_util_pic) set(COMMON_LIBS ccan rdma_util) +function(rdma_public_static_lib SHLIB STATICLIB VERSION_SCRIPT) + if (NOT IS_ABSOLUTE ${VERSION_SCRIPT}) + set(VERSION_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/${VERSION_SCRIPT}") + endif() + + set_target_properties(${STATICLIB} PROPERTIES + OUTPUT_NAME ${SHLIB} + ARCHIVE_OUTPUT_DIRECTORY "${BUILD_STATIC_LIB}") + target_compile_definitions(${STATICLIB} PRIVATE _STATIC_LIBRARY_BUILD_=1) + + list(APPEND RDMA_STATIC_LIBS ${SHLIB} ${STATICLIB} ${VERSION_SCRIPT}) + set(RDMA_STATIC_LIBS "${RDMA_STATIC_LIBS}" CACHE INTERNAL "") +endfunction() + function(rdma_make_dir DDIR) if(NOT EXISTS "${DDIR}/") execute_process(COMMAND "${CMAKE_COMMAND}" "-E" "make_directory" @@ -88,14 +102,8 @@ function(rdma_library DEST VERSION_SCRIPT SOVERSION VERSION) # Create a static library if (ENABLE_STATIC) add_library(${DEST}-static STATIC ${ARGN}) - set_target_properties(${DEST}-static PROPERTIES - OUTPUT_NAME ${DEST} - ARCHIVE_OUTPUT_DIRECTORY "${BUILD_LIB}") - target_compile_definitions(${DEST}-static PRIVATE _STATIC_LIBRARY_BUILD_=1) - install(TARGETS ${DEST}-static DESTINATION "${CMAKE_INSTALL_LIBDIR}") - - list(APPEND RDMA_STATIC_LIBS ${DEST} ${DEST}-static) - set(RDMA_STATIC_LIBS "${RDMA_STATIC_LIBS}" CACHE INTERNAL "") + target_link_libraries(${DEST}-static LINK ${COMMON_LIBS}) + rdma_public_static_lib(${DEST} ${DEST}-static ${VERSION_SCRIPT}) endif() # Create a shared library @@ -125,13 +133,7 @@ function(rdma_shared_provider DEST VERSION_SCRIPT SOVERSION VERSION) # Create a static provider library if (ENABLE_STATIC) add_library(${DEST}-static STATIC ${ARGN}) - set_target_properties(${DEST}-static PROPERTIES OUTPUT_NAME ${DEST}) - set_target_properties(${DEST}-static PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${BUILD_LIB}") - target_compile_definitions(${DEST}-static PRIVATE _STATIC_LIBRARY_BUILD_=1) - install(TARGETS ${DEST}-static DESTINATION "${CMAKE_INSTALL_LIBDIR}") - - list(APPEND RDMA_STATIC_LIBS ${DEST} ${DEST}-static) - set(RDMA_STATIC_LIBS "${RDMA_STATIC_LIBS}" CACHE INTERNAL "") + rdma_public_static_lib(${DEST} ${DEST}-static ${VERSION_SCRIPT}) endif() # Create the plugin shared library @@ -177,12 +179,8 @@ function(rdma_provider DEST) # but we don't have any directions on how to make static linking work.. if (ENABLE_STATIC) add_library(${DEST} STATIC ${ARGN}) - set_target_properties(${DEST} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${BUILD_LIB}") - target_compile_definitions(${DEST} PRIVATE _STATIC_LIBRARY_BUILD_=1) - install(TARGETS ${DEST} DESTINATION "${CMAKE_INSTALL_LIBDIR}") - - list(APPEND RDMA_STATIC_LIBS "${DEST}-rdmav${IBVERBS_PABI_VERSION}" ${DEST}) - set(RDMA_STATIC_LIBS "${RDMA_STATIC_LIBS}" CACHE INTERNAL "") + rdma_public_static_lib("${DEST}-rdmav${IBVERBS_PABI_VERSION}" ${DEST} ${BUILDLIB}/provider.map) + set_target_properties(${DEST} PROPERTIES OUTPUT_NAME ${DEST}) endif() # Create the plugin shared library @@ -234,18 +232,20 @@ function(rdma_test_executable EXEC) endfunction() # Finalize the setup of the static libraries by copying the meta information -# from the shared and setting up the libtool .la files. +# from the shared to static and setting up the static builder function(rdma_finalize_libs) list(LENGTH RDMA_STATIC_LIBS LEN) - if (LEN LESS 2) + if (LEN LESS 3) return() endif() math(EXPR LEN ${LEN}-1) - foreach(I RANGE 0 ${LEN} 2) + foreach(I RANGE 0 ${LEN} 3) list(GET RDMA_STATIC_LIBS ${I} SHARED) math(EXPR I ${I}+1) list(GET RDMA_STATIC_LIBS ${I} STATIC) + math(EXPR I ${I}+1) + list(GET RDMA_STATIC_LIBS ${I} MAP) # PUBLIC libraries set(LIBS "") @@ -263,7 +263,29 @@ function(rdma_finalize_libs) set_target_properties(${STATIC} PROPERTIES LINK_LIBRARIES "${TMP}") list(APPEND LIBS "${TMP}") endif() + + set(ARGS ${ARGS} --map "${MAP}" --lib "$<TARGET_FILE:${STATIC}>") + set(DEPENDS ${DEPENDS} ${STATIC} ${MAP}) + + get_target_property(TMP ${STATIC} OUTPUT_NAME) + set(OUTPUTS ${OUTPUTS} "${BUILD_LIB}/lib${TMP}.a") + install(FILES "${BUILD_LIB}/lib${TMP}.a" DESTINATION "${CMAKE_INSTALL_LIBDIR}") + endforeach() + + foreach(STATIC ${COMMON_LIBS}) + set(ARGS ${ARGS} --internal_lib "$<TARGET_FILE:${STATIC}>") + set(DEPENDS ${DEPENDS} ${STATIC}) endforeach() + + add_custom_command( + OUTPUT ${OUTPUTS} + COMMAND "${PYTHON_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/buildlib/sanitize_static_lib.py" + --version ${PACKAGE_VERSION} + --ar "${CMAKE_AR}" --nm "${CMAKE_NM}" --objcopy "${CMAKE_OBJCOPY}" ${ARGS} + DEPENDS ${DEPENDS} "${CMAKE_SOURCE_DIR}/buildlib/sanitize_static_lib.py" + COMMENT "Building distributable static libraries" + VERBATIM) + add_custom_target("make_static" ALL DEPENDS ${OUTPUTS}) endfunction() # Generate a pkg-config file diff --git a/buildlib/sanitize_static_lib.py b/buildlib/sanitize_static_lib.py new file mode 100644 index 00000000000000..86027ccc817457 --- /dev/null +++ b/buildlib/sanitize_static_lib.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +# Copyright (c) 2018 Mellanox Technologies, Ltd. All rights reserved. +# Licensed under BSD (MIT variant) or GPLv2. See COPYING. +"""This tool is used to create installable versions of the static libraries in rdma-core. + +This is complicated because rdma-core was not designed with static libraries +in mind and relies on the dynamic linker to hide a variety of internal +details. + +The build uses several internal utility libraries across the providers and the +libraries. When building statically these libraries have to become inlined +into the various main libraries. This script figures out which static +libraries should include which internal libraries and inlines them +appropriately. + +rdma-core is not careful to use globally unique names throughout all the +libraries and all the providers. Normally the map file in the dynamic linker +will hide these external symbols. This script does something similar for static +linking by analyzing the libraries and map files then renaming internal +symbols with a globally unique prefix. + +This is far too complicated to handle internally with cmake, so we have cmake +produce the nearly completed libraries, then process them here using bintuils, +and finally produce the final installation ready libraries.""" + +import collections +import subprocess +import argparse +import tempfile +import itertools +import sys +import os +import re + +SymVer = collections.namedtuple( + "SymVer", ["version", "prior_version", "globals", "locals"]) + +try: + from tempfile import TemporaryDirectory +except ImportError: + import shutil + import tempfile + + # From /usr/lib/python3/dist-packages/setuptools/py31compat.py + class TemporaryDirectory(object): + def __init__(self): + self.name = None + self.name = tempfile.mkdtemp() + + def __enter__(self): + return self.name + + def __exit__(self, exctype, excvalue, exctrace): + try: + shutil.rmtree(self.name, True) + except OSError: + pass + self.name = None + + +try: + from subprocess import check_output +except ImportError: + # From /usr/lib/python2.7/subprocess.py + def check_output(*popenargs, **kwargs): + if 'stdout' in kwargs: + raise ValueError( + 'stdout argument not allowed, it will be overridden.') + process = subprocess.Popen( + stdout=subprocess.PIPE, *popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + raise CalledProcessError(retcode, cmd, output=output) + return output + + subprocess.check_output = check_output + + +def parse_stanza(version, prior_version, lines): + gbl = [] + local = [] + cur = None + + cur = 0 + for I in re.finditer( + r"\s*(?:(global:)|(local:)(\s*\*\s*;)|(?:(\w+)\s*;))", + lines, + flags=re.DOTALL | re.MULTILINE): + if I.group(1): # global + lst = gbl + if I.group(2): # local + lst = local + if I.group(3): # wildcard + lst.append("*") + assert (cur is not gbl) + if I.group(4): # symbol name + lst.append(I.group(4)) + + assert cur == I.start() + cur = I.end() + assert cur == len(lines) + + return SymVer(version or "", prior_version or "", gbl, local) + + +def load_map(fn): + """This is a lame regex based parser for GNU linker map files. It asserts if + the map file is invalid. It returns a list of the global symbols""" + with open(fn, "rt") as F: + lines = F.read() + p = re.compile(r"/\*.*?\*/", flags=re.DOTALL) + lines = re.sub(p, "", lines) + lines = lines.strip() + + # Extract each stanza + res = [] + cur = 0 + for I in re.finditer( + r"\s*(?:(\S+)\s+)?{(.*?)\s*}(\s*\S+)?\s*;", + lines, + flags=re.DOTALL | re.MULTILINE): + assert cur == I.start() + res.append(parse_stanza(I.group(1), I.group(3), I.group(2))) + cur = I.end() + assert cur == len(lines) + + return res + + +class Lib(object): + def __init__(self, libfn, tmpdir): + self.libfn = os.path.basename(libfn) + self.objdir = os.path.join(tmpdir, self.libfn) + self.final_objdir = os.path.join(tmpdir, "r-" + self.libfn) + self.final_lib = os.path.join(os.path.dirname(libfn), "..", self.libfn) + self.needs = set() + self.needed = set() + + os.makedirs(self.objdir) + os.makedirs(self.final_objdir) + + subprocess.check_call([args.ar, "x", libfn], cwd=self.objdir) + self.objects = [I for I in os.listdir(self.objdir)] + self.get_syms() + + def get_syms(self): + """Read the definedsymbols from each object file""" + self.syms = set() + self.needed_syms = set() + for I in self.objects: + I = os.path.join(self.objdir, I) + syms = subprocess.check_output([args.nm, "--defined-only", I]) + for ln in syms.decode().splitlines(): + ln = ln.split() + if ln[1].isupper(): + self.syms.add(ln[2]) + + syms = subprocess.check_output([args.nm, "--undefined-only", I]) + for ln in syms.decode().splitlines(): + ln = ln.split() + if ln[0].isupper(): + self.needed_syms.add(ln[1]) + + def rename_syms(self, rename_fn): + """Invoke objcopy on all the objects to rename their symbols""" + for I in self.objects: + subprocess.check_call([ + args.objcopy, + "--redefine-syms=%s" % (rename_fn), + os.path.join(self.objdir, I), + os.path.join(self.final_objdir, I) + ]) + + def incorporate_internal(self, internal_libs): + """If this library requires an internal library then we want to inline it into + this lib when we reconstruct it.""" + for lib in self.needs.intersection(internal_libs): + self.objects.extend( + os.path.join(lib.final_objdir, I) for I in lib.objects) + + def finalize(self): + """Write out the now modified library""" + try: + os.unlink(self.final_lib) + except OSError: + pass + subprocess.check_call( + [args.ar, "qsc", self.final_lib] + + [os.path.join(self.final_objdir, I) for I in self.objects]) + + +def compute_graph(libs): + """Look at the symbols each library provides vs the symbols each library needs + and organize the libraries into a graph.""" + for a, b in itertools.permutations(libs, 2): + if not a.syms.isdisjoint(b.needed_syms): + b.needs.add(a) + a.needed.add(b) + + # Use transitivity to prune the needs list + def prune(cur_lib, to_prune): + for I in cur_lib.needed: + I.needs.discard(to_prune) + to_prune.needed.discard(I) + prune(I, to_prune) + + for cur_lib in libs: + for I in list(cur_lib.needed): + prune(I, cur_lib) + + +parser = argparse.ArgumentParser( + description='Generate static libraries for distribution') +parser.add_argument( + "--map", + dest="maps", + action="append", + help="List of map files defining all the public symbols", + default=[]) +parser.add_argument( + "--lib", dest="libs", action="append", help="The input static libraries") +parser.add_argument( + "--internal_lib", + dest="internal_libs", + action="append", + help= + "The internal static libraries, these will be merged into other libraries") +parser.add_argument( + "--version", action="store", help="Package version number", required=True) +parser.add_argument("--ar", action="store", help="ar tool", required=True) +parser.add_argument("--nm", action="store", help="nm tool", required=True) +parser.add_argument( + "--objcopy", action="store", help="objcopy tool", required=True) +args = parser.parse_args() + +global_syms = set() +for fn in sorted(set(args.maps)): + for I in load_map(fn): + # Private symbols in libibverbs are also mangled for maximum safety. + if "PRIVATE" not in I.version: + global_syms.update(I.globals) + +with TemporaryDirectory() as tmpdir: + libs = set(Lib(fn, tmpdir) for fn in args.libs) + internal_libs = set(Lib(fn, tmpdir) for fn in args.internal_libs) + all_libs = libs | internal_libs + + all_syms = set() + for I in all_libs: + all_syms.update(I.syms) + compute_graph(all_libs) + + # Generate a redefine file for objcopy that will sanitize the internal names + prefix = re.sub(r"\W", "_", args.version) + redefine_fn = os.path.join(tmpdir, "redefine") + with open(redefine_fn, "wt") as F: + for I in sorted(all_syms - global_syms): + F.write("%s rdmacore%s_%s\n" % (I, prefix, I)) + + for I in all_libs: + I.rename_syms(redefine_fn) + + for I in libs: + I.incorporate_internal(internal_libs) + I.finalize() -- 2.19.1