| #!/usr/bin/env python3 |
| # Copyright 2017 Obsidian Research Corp. |
| # Licensed under BSD (MIT variant) or GPLv2. See COPYING. |
| """check-build - Run static checks on a build""" |
| from __future__ import print_function |
| import argparse |
| import inspect |
| import os |
| import re |
| import shutil |
| import subprocess |
| import tempfile |
| import sys |
| import copy |
| import shlex |
| import pipes |
| from contextlib import contextmanager; |
| from pkg_resources import parse_version |
| |
| def get_src_dir(): |
| """Get the source directory using git""" |
| git_top = subprocess.check_output(["git","rev-parse","--git-dir"]).decode().strip(); |
| if git_top == ".git": |
| return "."; |
| return os.path.dirname(git_top); |
| |
| def get_package_version(args): |
| """Return PACKAGE_VERSION from CMake""" |
| with open(os.path.join(args.SRC,"CMakeLists.txt")) as F: |
| for ln in F: |
| g = re.match(r'^set\(PACKAGE_VERSION "(.+)"\)',ln) |
| if g is None: |
| continue; |
| return g.group(1); |
| raise RuntimeError("Could not find version"); |
| |
| @contextmanager |
| def inDirectory(dir): |
| cdir = os.getcwd(); |
| try: |
| os.chdir(dir); |
| yield True; |
| finally: |
| os.chdir(cdir); |
| |
| @contextmanager |
| def private_tmp(): |
| """Simple version of Python 3's tempfile.TemporaryDirectory""" |
| dfn = tempfile.mkdtemp(); |
| try: |
| yield dfn; |
| finally: |
| shutil.rmtree(dfn); |
| |
| # ------------------------------------------------------------------------- |
| |
| def get_symbol_vers(fn,exported=True): |
| """Return the symbol version suffixes from the ELF file, eg IB_VERBS_1.0, etc""" |
| syms = subprocess.check_output(["readelf","--wide","-s",fn]).decode(); |
| |
| go = False; |
| res = set(); |
| for I in syms.splitlines(): |
| if I.startswith("Symbol table '.dynsym'"): |
| go = True; |
| continue; |
| |
| if I.startswith(" ") and go: |
| itms = I.split(); |
| if exported: |
| if (len(itms) == 8 and itms[3] == "OBJECT" and |
| itms[4] == "GLOBAL" and itms[6] == "ABS"): |
| res.add(itms[7]); |
| else: |
| if (len(itms) >= 8 and itms[3] == "FUNC" and |
| itms[4] == "GLOBAL" and itms[6] == "UND"): |
| res.add(itms[7]); |
| else: |
| go = False; |
| if not res: |
| raise ValueError("Failed to read ELF symbol versions from %r"%(fn)); |
| return res; |
| |
| def symver_parse_version(ver): |
| return parse_version(ver.partition("_")[-1]) |
| |
| def check_lib_symver(args,fn): |
| g = re.match(r"lib([^.]+)\.so\.(\d+)\.(\d+)\.(.*)",fn); |
| if g.group(4) != args.PACKAGE_VERSION: |
| raise ValueError("Shared Library filename %r does not have the package version %r (%r)"%( |
| fn,args.PACKAGE_VERSION,g.groups())); |
| |
| # umad/etc used the wrong symbol version name when they moved to soname 3.0 |
| if g.group(1) == "ibumad": |
| newest_symver = "%s_%s.%s"%(g.group(1).upper(),'1',g.group(3)); |
| elif g.group(1) == "ibmad": |
| newest_symver = "%s_%s.%s"%(g.group(1).upper(),'1',g.group(3)); |
| elif g.group(1) == "ibnetdisc": |
| newest_symver = "%s_%s.%s"%(g.group(1).upper(),'1',g.group(3)); |
| else: |
| newest_symver = "%s_%s.%s"%(g.group(1).upper(),g.group(2),g.group(3)); |
| |
| syms = get_symbol_vers(fn); |
| if newest_symver not in syms: |
| raise ValueError("Symbol version %r implied by filename %r not in ELF (%r)"%( |
| newest_symver,fn,syms)); |
| |
| # The private symbol tag should also be older than the package version |
| private = set(I for I in syms if "PRIVATE" in I) |
| if len(private) > 1: |
| raise ValueError("Too many private symbol versions in ELF %r (%r)"%(fn,private)); |
| if private: |
| private_rel = list(private)[0].split('_')[-1]; |
| if private_rel > args.PACKAGE_VERSION: |
| raise ValueError("Private Symbol Version %r is newer than the package version %r"%( |
| private,args.PACKAGE_VERSION)); |
| |
| syms = list(syms - private); |
| syms.sort(key=symver_parse_version) |
| if newest_symver != syms[-1]: |
| raise ValueError("Symbol version %r implied by filename %r not the newest in ELF (%r)"%( |
| newest_symver,fn,syms)); |
| |
| def test_lib_names(args): |
| """Check that the library filename matches the symbol versions""" |
| libd = os.path.join(args.BUILD,"lib"); |
| |
| # List of shlibs that follow the ABI guidelines |
| libs = {}; |
| with inDirectory(libd): |
| for fn in os.listdir("."): |
| if os.path.islink(fn): |
| lfn = os.readlink(fn); |
| if not os.path.islink(lfn): |
| check_lib_symver(args,lfn); |
| # ------------------------------------------------------------------------- |
| |
| def check_verbs_abi(args,fn): |
| g = re.match(r"lib([^-]+)-rdmav(\d+).so",fn); |
| if g is None: |
| raise ValueError("Provider library has unknown file name format %r"%(fn)); |
| |
| private_ver = int(g.group(2)); |
| syms = get_symbol_vers(fn,exported=False); |
| syms = {I.partition("@")[2] for I in syms}; |
| assert "IBVERBS_PRIVATE_%u"%(private_ver) in syms; |
| assert len([I for I in syms if I.startswith("IBVERBS_PRIVATE")]) == 1; |
| |
| def test_verbs_private(args): |
| """Check that the IBVERBS_PRIVATE symbols match the library name, eg that the |
| map file and the cmake stuff are in sync.""" |
| libd = os.path.join(args.BUILD,"lib"); |
| with inDirectory(libd): |
| for fn in os.listdir("."): |
| if not os.path.islink(fn) and "rdmav" in fn and fn.endswith(".so"): |
| check_verbs_abi(args,fn); |
| |
| # ------------------------------------------------------------------------- |
| |
| def check_abi(args,fn): |
| g1 = re.match(r"lib([^.]+).so\.(.+)\.(.+)",fn); |
| g2 = re.match(r"lib([^.]+).so\.(.+\..+)",fn); |
| if g1 is None or g2 is None: |
| raise ValueError("Library has unknown file name format %r"%(fn)); |
| |
| ref_fn = os.path.join(args.SRC,"ABI",g1.group(1) + ".dump"); |
| cur_fn = os.path.join(args.SRC,"ABI","current-" + g1.group(1) + ".dump"); |
| subprocess.check_call(["abi-dumper", |
| "-lver",g2.group(1), |
| fn, |
| "-o",cur_fn]); |
| |
| if not os.path.exists(ref_fn): |
| print("ABI file does not exist for %r"%(ref_fn), file=sys.stderr); |
| return False; |
| |
| subprocess.check_call(["abi-compliance-checker", |
| "-l",g1.group(1), |
| "-old",ref_fn, |
| "-new",cur_fn]); |
| |
| return True; |
| |
| def test_verbs_uapi(args): |
| """Compare the ABI output from 'abi-dumper' between what is present in git and |
| what was built in this tree. This allows us to detect changes in ABI on |
| the -stable branch.""" |
| |
| # User must provide the ABI dir in the source tree |
| if not os.path.isdir(os.path.join(args.SRC,"ABI")): |
| print("ABI check skipped, no ABI/ directory."); |
| return; |
| |
| libd = os.path.join(args.BUILD,"lib"); |
| success = True; |
| with inDirectory(libd): |
| for fn in os.listdir("."): |
| if not os.path.islink(fn) and re.match(r"lib.+\.so\..+\..+",fn): |
| success = success & check_abi(args,fn); |
| |
| assert success == True; |
| |
| # ------------------------------------------------------------------------- |
| |
| def is_obsolete(fn): |
| """True if the header is obsolete and should not be compiled anyhow.""" |
| with open(fn) as F: |
| for ln in F.readlines(): |
| if re.search(r"#warning.*This header is obsolete",ln): |
| return True; |
| return False; |
| |
| def is_fixup(fn): |
| """True if this is a fixup header, fixup headers are exempted because they |
| required includes are not the same for kernel headers (eg netinet/in.h)""" |
| if os.path.islink(fn): |
| return "buildlib/fixup-include/" in os.readlink(fn); |
| return False; |
| |
| def get_headers(incdir): |
| includes = set(); |
| for root,dirs,files in os.walk(incdir): |
| for I in files: |
| if I.endswith(".h"): |
| includes.add(os.path.join(root,I)); |
| return includes; |
| |
| def compile_test_headers(tmpd,incdir,includes,with_cxx=False): |
| cppflags = subprocess.check_output(["pkg-config","libnl-3.0","--cflags-only-I"]).decode().strip(); |
| cppflags = "-I %s %s"%(incdir,cppflags) |
| with open(os.path.join(tmpd,"build.ninja"),"wt") as F: |
| print("rule comp", file=F); |
| print(" command = %s -Werror -c %s $in -o $out"%(args.CC,cppflags), file=F); |
| print(" description=Header check for $in", file=F); |
| print("rule comp_cxx", file=F); |
| print(" command = %s -Werror -c %s $in -o $out"%(args.CXX,cppflags), file=F); |
| print(" description=Header C++ check for $in", file=F); |
| count = 0; |
| for I in sorted(includes): |
| if is_obsolete(I) or is_fixup(I): |
| continue; |
| print("build %s : comp %s"%("out%d.o"%(count),I), file=F); |
| print("default %s"%("out%d.o"%(count)), file=F); |
| print("build %s : comp_cxx %s"%("outxx%d.o"%(count),I), file=F); |
| if with_cxx: |
| print("default %s"%("outxx%d.o"%(count)), file=F); |
| count = count + 1; |
| subprocess.check_call(["ninja"],cwd=tmpd); |
| |
| def test_published_headers(args): |
| """Test that every header file can be included on its own, and has no obvious |
| implicit dependencies. This is intended as a first pass check of the public |
| installed API headers""" |
| incdir = os.path.abspath(os.path.join(args.BUILD,"include")); |
| includes = get_headers(incdir); |
| |
| # Make a little ninja file to compile each header |
| with private_tmp() as tmpd: |
| compile_test_headers(tmpd,incdir,includes); |
| |
| # ------------------------------------------------------------------------- |
| |
| allowed_uapi_headers = { |
| # This header is installed in all supported distributions |
| "rdma/ib_user_sa.h", |
| "rdma/ib_user_verbs.h", |
| "linux/stddef.h", |
| } |
| |
| non_cxx_headers = { |
| "infiniband/arch.h", |
| "infiniband/ib.h", |
| "infiniband/ib_user_ioctl_verbs.h", |
| "infiniband/ibnetdisc_osd.h", |
| "infiniband/mad_osd.h", |
| "infiniband/mlx5_api.h", |
| "infiniband/mlx5_user_ioctl_verbs.h", |
| "infiniband/opcode.h", |
| "infiniband/sa-kern-abi.h", |
| "infiniband/sa.h", |
| "infiniband/verbs_api.h", |
| "rdma/rdma_cma_abi.h", |
| } |
| |
| def test_installed_headers(args): |
| """This test also checks that the public headers can be compiled on their own, |
| but goes further and confirms that the public headers do not depend on any |
| internal headers, or kernel kAPI headers.""" |
| with private_tmp() as tmpd: |
| env = copy.deepcopy(os.environ); |
| env["DESTDIR"] = tmpd; |
| subprocess.check_output(["ninja","install"],env=env,cwd=args.BUILD); |
| |
| includes = get_headers(tmpd); |
| incdir = os.path.commonprefix(list(includes)); |
| rincludes = {I[len(incdir):] for I in includes}; |
| |
| bincdir = os.path.abspath(os.path.join(args.BUILD,"include")); |
| all_includes = set(); |
| for I in get_headers(bincdir): |
| if not is_fixup(I) and not is_obsolete(I): |
| all_includes.add(I[len(bincdir)+1:]); |
| |
| # Drop error includes for any include file that is internal, this way |
| # when we compile the public headers any include of an internal header |
| # will fail. |
| for I in sorted(all_includes - rincludes): |
| if I in allowed_uapi_headers: |
| continue; |
| |
| I = os.path.join(incdir,I) |
| dfn = os.path.dirname(I); |
| if not os.path.isdir(dfn): |
| os.makedirs(dfn); |
| assert not os.path.exists(I); |
| with open(I,"w") as F: |
| print('#error "Private internal header"', file=F); |
| |
| # Roughly check that the headers have the extern "C" for C++ |
| # compilation. |
| for I in sorted(rincludes - non_cxx_headers): |
| with open(os.path.join(incdir,I)) as F: |
| if 'extern "C" {' not in F.read(): |
| raise ValueError("No extern C in %r"%(I)); |
| |
| compile_test_headers(tmpd,incdir,includes,with_cxx=True); |
| |
| # ------------------------------------------------------------------------- |
| |
| |
| def get_symbol_names(fn): |
| """Return the defined, public, symbols from a ELF shlib""" |
| syms = subprocess.check_output(["readelf", "--wide", "-s", fn]).decode() |
| go = False |
| res = set() |
| for I in syms.splitlines(): |
| if I.startswith("Symbol table '.dynsym'"): |
| go = True |
| continue |
| |
| if I.startswith(" ") and go: |
| g = re.match( |
| r"\s+\d+:\s+[0-9a-f]+\s+\d+.*(?:FUNC|OBJECT)\s+GLOBAL\s+DEFAULT\s+\d+\s+(\S+)@@(\S+)$", |
| I) |
| if not g or "PRIVATE" in g.group(2): |
| continue |
| res.add(g.group(1)) |
| else: |
| go = False |
| |
| return res |
| |
| |
| def get_cc_args_from_pkgconfig(args, name, static): |
| """Get the compile arguments from pkg-config for the named librarary""" |
| os.environ["PKG_CONFIG_PATH"] = os.path.join(args.BUILD, "lib", |
| "pkgconfig") |
| flags = ["pkg-config", "--errors-to-stdout", "--cflags", "--libs"] |
| if static: |
| flags.append("--static") |
| opts = subprocess.check_output(flags + ["lib" + name]).decode() |
| opts = shlex.split(opts) |
| |
| opts.insert(0, "-Wall") |
| opts.insert(0, "-Werror") |
| opts.insert(0, "-L%s" % (os.path.join(args.BUILD, "lib"))) |
| opts.insert(1, "-I%s" % (os.path.join(args.BUILD, "include"))) |
| if not static: |
| return opts |
| |
| # Only static link the pkg-config stuff, otherwise we get warnings about |
| # static linking portions of glibc that need NSS. |
| opts.insert(0, "-Wl,-Bstatic") |
| opts.append("-Wl,-Bdynamic") |
| |
| # We need this extra libpthread/m because libnl's pkgconfig file is |
| # broken and doesn't include the private libraries it requires. :( |
| if "-lnl-3" in opts: |
| opts.append("-lm") |
| opts.append("-lpthread") |
| |
| # Put glibc associated libraries out side the static link section, |
| if "-lpthread" in opts: |
| while "-lpthread" in opts: |
| opts.remove("-lpthread") |
| opts.append("-lpthread") |
| if "-lm" in opts: |
| while "-lm" in opts: |
| opts.remove("-lm") |
| opts.append("-lm") |
| return opts |
| |
| |
| def compile_ninja(args, Fninja, name, cfn, opts): |
| print(""" |
| rule comp_{name} |
| command = {CC} -Wall -o $out $in {opts} |
| description = Compile and link $out |
| build {name} : comp_{name} {cfn} |
| default {name}""".format( |
| name=name, |
| CC=args.CC, |
| cfn=cfn, |
| opts=" ".join(pipes.quote(I) for I in opts)), file=Fninja) |
| |
| |
| def get_providers(args): |
| """Return a list of provider names""" |
| return set( |
| I for I in os.listdir(os.path.join(args.SRC, "providers")) |
| if not I.startswith(".")) |
| |
| |
| def check_static_lib(args, tmpd, Fninja, static_lib, shared_lib, name): |
| syms = get_symbol_names(shared_lib) |
| if not syms: |
| return |
| |
| cfn = os.path.join(tmpd, "%s-test.c" % (name)) |
| with open(cfn, "wt") as F: |
| F.write("#include <stdio.h>\n") |
| for I in syms: |
| F.write("extern void %s(void);\n" % (I)) |
| F.write("int main(int argc,const char *argv[]) {\n") |
| for I in syms: |
| F.write('printf("%%p",&%s);\n' % (I)) |
| F.write("return 0; }\n") |
| |
| compile_ninja(args, Fninja, "%s-static-out" % (name), cfn, |
| get_cc_args_from_pkgconfig(args, name, static=True)) |
| compile_ninja(args, Fninja, "%s-shared-out" % (name), cfn, |
| get_cc_args_from_pkgconfig(args, name, static=False)) |
| |
| |
| def check_static_providers(args, tmpd, Fninja): |
| """Test that expected values for RDMA_STATIC_PROVIDERS are accepted and the |
| link works""" |
| cfn = os.path.join(tmpd, "provider-test.c") |
| with open(cfn, "wt") as F: |
| F.write("#include <infiniband/verbs.h>\n") |
| F.write("int main(int argc,const char *argv[]) {\n") |
| F.write('ibv_get_device_list(NULL);\n') |
| F.write("return 0; }\n") |
| |
| opts = get_cc_args_from_pkgconfig( |
| args, "ibverbs", static=True) |
| |
| providers = get_providers(args) |
| for I in sorted(providers | { |
| "none", |
| "all", |
| }): |
| compile_ninja(args, Fninja, "providers-%s-static-out" % (I), cfn, |
| ["-DRDMA_STATIC_PROVIDERS=%s" % (I)] + opts) |
| |
| compile_ninja( |
| args, Fninja, "providers-static-out", cfn, |
| ["-DRDMA_STATIC_PROVIDERS=%s" % (",".join(providers))] + opts) |
| |
| |
| def test_static_libs(args): |
| """Compile then link statically and dynamically a dummy program that touches |
| every symbol in the libraries using pkgconfig output to guide the link |
| options. This tests that pkgconfig is setup properly and that all the |
| magic with incorporating the internal libraries for static linking has |
| done its job.""" |
| libd = os.path.join(args.BUILD, "lib") |
| success = True |
| libs = [] |
| with inDirectory(libd): |
| fns = set(fn for fn in os.listdir(".") if not os.path.islink(fn)) |
| for static_lib in fns: |
| g = re.match(r"lib(.+)\.a$", static_lib) |
| if g: |
| for shared_lib in fns: |
| if re.match(r"lib%s.*\.so" % (g.group(1)), shared_lib): |
| libs.append((os.path.join(libd, static_lib), |
| os.path.join(libd, shared_lib), |
| g.group(1))) |
| break |
| else: |
| raise ValueError( |
| "Failed to find matching shared library for %r" % |
| (static_lib)) |
| |
| with private_tmp() as tmpd: |
| with open(os.path.join(tmpd, "build.ninja"), "wt") as Fninja: |
| for I in libs: |
| check_static_lib(args, tmpd, Fninja, I[0], I[1], I[2]) |
| check_static_providers(args, tmpd, Fninja) |
| subprocess.check_call(["ninja"], cwd=tmpd) |
| |
| |
| # ------------------------------------------------------------------------- |
| |
| parser = argparse.ArgumentParser(description='Run build time tests') |
| parser.add_argument("--build",default=os.getcwd(),dest="BUILD", |
| help="Build directory to inpsect"); |
| parser.add_argument("--src",default=None,dest="SRC", |
| help="Top of the source tree"); |
| parser.add_argument("--cc",default="cc",dest="CC", |
| help="C compiler to use"); |
| parser.add_argument("--cxx",default="c++",dest="CXX", |
| help="C++ compiler to use"); |
| args = parser.parse_args(); |
| |
| if args.SRC is None: |
| args.SRC = get_src_dir(); |
| args.SRC = os.path.abspath(args.SRC); |
| args.PACKAGE_VERSION = get_package_version(args); |
| |
| funcs = globals(); |
| for k,v in list(funcs.items()): |
| if k.startswith("test_") and inspect.isfunction(v): |
| v(args); |