1# functions for handling ABI checking of libraries 2 3import os 4import sys 5import re 6import fnmatch 7 8from waflib import Options, Utils, Logs, Task, Build, Errors 9from waflib.TaskGen import feature, before, after 10from wafsamba import samba_utils 11 12# these type maps cope with platform specific names for common types 13# please add new type mappings into the list below 14abi_type_maps = { 15 '_Bool' : 'bool', 16 'struct __va_list_tag *' : 'va_list' 17 } 18 19version_key = lambda x: list(map(int, x.split("."))) 20 21def normalise_signature(sig): 22 '''normalise a signature from gdb''' 23 sig = sig.strip() 24 sig = re.sub('^\$[0-9]+\s=\s\{(.+)\}$', r'\1', sig) 25 sig = re.sub('^\$[0-9]+\s=\s\{(.+)\}(\s0x[0-9a-f]+\s<\w+>)+$', r'\1', sig) 26 sig = re.sub('^\$[0-9]+\s=\s(0x[0-9a-f]+)\s?(<\w+>)?$', r'\1', sig) 27 sig = re.sub('0x[0-9a-f]+', '0xXXXX', sig) 28 sig = re.sub('", <incomplete sequence (\\\\[a-z0-9]+)>', r'\1"', sig) 29 30 for t in abi_type_maps: 31 # we need to cope with non-word characters in mapped types 32 m = t 33 m = m.replace('*', '\*') 34 if m[-1].isalnum() or m[-1] == '_': 35 m += '\\b' 36 if m[0].isalnum() or m[0] == '_': 37 m = '\\b' + m 38 sig = re.sub(m, abi_type_maps[t], sig) 39 return sig 40 41 42def normalise_varargs(sig): 43 '''cope with older versions of gdb''' 44 sig = re.sub(',\s\.\.\.', '', sig) 45 return sig 46 47 48def parse_sigs(sigs, abi_match): 49 '''parse ABI signatures file''' 50 abi_match = samba_utils.TO_LIST(abi_match) 51 ret = {} 52 a = sigs.split('\n') 53 for s in a: 54 if s.find(':') == -1: 55 continue 56 sa = s.split(':') 57 if abi_match: 58 matched = False 59 negative = False 60 for p in abi_match: 61 if p[0] == '!' and fnmatch.fnmatch(sa[0], p[1:]): 62 negative = True 63 break 64 elif fnmatch.fnmatch(sa[0], p): 65 matched = True 66 break 67 if (not matched) and negative: 68 continue 69 Logs.debug("%s -> %s" % (sa[1], normalise_signature(sa[1]))) 70 ret[sa[0]] = normalise_signature(sa[1]) 71 return ret 72 73def save_sigs(sig_file, parsed_sigs): 74 '''save ABI signatures to a file''' 75 sigs = "".join('%s: %s\n' % (s, parsed_sigs[s]) for s in sorted(parsed_sigs.keys())) 76 return samba_utils.save_file(sig_file, sigs, create_dir=True) 77 78 79def abi_check_task(self): 80 '''check if the ABI has changed''' 81 abi_gen = self.ABI_GEN 82 83 libpath = self.inputs[0].abspath(self.env) 84 libname = os.path.basename(libpath) 85 86 sigs = samba_utils.get_string(Utils.cmd_output([abi_gen, libpath])) 87 parsed_sigs = parse_sigs(sigs, self.ABI_MATCH) 88 89 sig_file = self.ABI_FILE 90 91 old_sigs = samba_utils.load_file(sig_file) 92 if old_sigs is None or Options.options.ABI_UPDATE: 93 if not save_sigs(sig_file, parsed_sigs): 94 raise Errors.WafError('Failed to save ABI file "%s"' % sig_file) 95 Logs.warn('Generated ABI signatures %s' % sig_file) 96 return 97 98 parsed_old_sigs = parse_sigs(old_sigs, self.ABI_MATCH) 99 100 # check all old sigs 101 got_error = False 102 for s in parsed_old_sigs: 103 if not s in parsed_sigs: 104 Logs.error('%s: symbol %s has been removed - please update major version\n\tsignature: %s' % ( 105 libname, s, parsed_old_sigs[s])) 106 got_error = True 107 elif normalise_varargs(parsed_old_sigs[s]) != normalise_varargs(parsed_sigs[s]): 108 Logs.error('%s: symbol %s has changed - please update major version\n\told_signature: %s\n\tnew_signature: %s' % ( 109 libname, s, parsed_old_sigs[s], parsed_sigs[s])) 110 got_error = True 111 112 for s in parsed_sigs: 113 if not s in parsed_old_sigs: 114 Logs.error('%s: symbol %s has been added - please mark it _PRIVATE_ or update minor version\n\tsignature: %s' % ( 115 libname, s, parsed_sigs[s])) 116 got_error = True 117 118 if got_error: 119 raise Errors.WafError('ABI for %s has changed - please fix library version then build with --abi-update\nSee http://wiki.samba.org/index.php/Waf#ABI_Checking for more information\nIf you have not changed any ABI, and your platform always gives this error, please configure with --abi-check-disable to skip this check' % libname) 120 121 122t = Task.task_factory('abi_check', abi_check_task, color='BLUE', ext_in='.bin') 123t.quiet = True 124# allow "waf --abi-check" to force re-checking the ABI 125if '--abi-check' in sys.argv: 126 t.always_run = True 127 128@after('apply_link') 129@feature('abi_check') 130def abi_check(self): 131 '''check that ABI matches saved signatures''' 132 env = self.bld.env 133 if not env.ABI_CHECK or self.abi_directory is None: 134 return 135 136 # if the platform doesn't support -fvisibility=hidden then the ABI 137 # checks become fairly meaningless 138 if not env.HAVE_VISIBILITY_ATTR: 139 return 140 141 topsrc = self.bld.srcnode.abspath() 142 abi_gen = os.path.join(topsrc, 'buildtools/scripts/abi_gen.sh') 143 144 abi_file = "%s/%s-%s.sigs" % (self.abi_directory, self.version_libname, 145 self.vnum) 146 147 tsk = self.create_task('abi_check', self.link_task.outputs[0]) 148 tsk.ABI_FILE = abi_file 149 tsk.ABI_MATCH = self.abi_match 150 tsk.ABI_GEN = abi_gen 151 152 153def abi_process_file(fname, version, symmap): 154 '''process one ABI file, adding new symbols to the symmap''' 155 for line in Utils.readf(fname).splitlines(): 156 symname = line.split(":")[0] 157 if not symname in symmap: 158 symmap[symname] = version 159 160 161def abi_write_vscript(f, libname, current_version, versions, symmap, abi_match): 162 """Write a vscript file for a library in --version-script format. 163 164 :param f: File-like object to write to 165 :param libname: Name of the library, uppercased 166 :param current_version: Current version 167 :param versions: Versions to consider 168 :param symmap: Dictionary mapping symbols -> version 169 :param abi_match: List of symbols considered to be public in the current 170 version 171 """ 172 173 invmap = {} 174 for s in symmap: 175 invmap.setdefault(symmap[s], []).append(s) 176 177 last_key = "" 178 versions = sorted(versions, key=version_key) 179 for k in versions: 180 symver = "%s_%s" % (libname, k) 181 if symver == current_version: 182 break 183 f.write("%s {\n" % symver) 184 if k in sorted(invmap.keys()): 185 f.write("\tglobal:\n") 186 for s in invmap.get(k, []): 187 f.write("\t\t%s;\n" % s); 188 f.write("}%s;\n\n" % last_key) 189 last_key = " %s" % symver 190 f.write("%s {\n" % current_version) 191 local_abi = list(filter(lambda x: x[0] == '!', abi_match)) 192 global_abi = list(filter(lambda x: x[0] != '!', abi_match)) 193 f.write("\tglobal:\n") 194 if len(global_abi) > 0: 195 for x in global_abi: 196 f.write("\t\t%s;\n" % x) 197 else: 198 f.write("\t\t*;\n") 199 # Always hide symbols that must be local if exist 200 local_abi.extend(["!_end", "!__bss_start", "!_edata"]) 201 f.write("\tlocal:\n") 202 for x in local_abi: 203 f.write("\t\t%s;\n" % x[1:]) 204 if global_abi != ["*"]: 205 if len(global_abi) > 0: 206 f.write("\t\t*;\n") 207 f.write("};\n") 208 209 210def abi_build_vscript(task): 211 '''generate a vscript file for our public libraries''' 212 213 tgt = task.outputs[0].bldpath(task.env) 214 215 symmap = {} 216 versions = [] 217 for f in task.inputs: 218 fname = f.abspath(task.env) 219 basename = os.path.basename(fname) 220 version = basename[len(task.env.LIBNAME)+1:-len(".sigs")] 221 versions.append(version) 222 abi_process_file(fname, version, symmap) 223 f = open(tgt, mode='w') 224 try: 225 abi_write_vscript(f, task.env.LIBNAME, task.env.VERSION, versions, 226 symmap, task.env.ABI_MATCH) 227 finally: 228 f.close() 229 230 231def ABI_VSCRIPT(bld, libname, abi_directory, version, vscript, abi_match=None): 232 '''generate a vscript file for our public libraries''' 233 if abi_directory: 234 source = bld.path.ant_glob('%s/%s-[0-9]*.sigs' % (abi_directory, libname), flat=True) 235 def abi_file_key(path): 236 return version_key(path[:-len(".sigs")].rsplit("-")[-1]) 237 source = sorted(source.split(), key=abi_file_key) 238 else: 239 source = '' 240 241 libname = os.path.basename(libname) 242 version = os.path.basename(version) 243 libname = libname.replace("-", "_").replace("+","_").upper() 244 version = version.replace("-", "_").replace("+","_").upper() 245 246 t = bld.SAMBA_GENERATOR(vscript, 247 rule=abi_build_vscript, 248 source=source, 249 group='vscripts', 250 target=vscript) 251 if abi_match is None: 252 abi_match = ["*"] 253 else: 254 abi_match = samba_utils.TO_LIST(abi_match) 255 t.env.ABI_MATCH = abi_match 256 t.env.VERSION = version 257 t.env.LIBNAME = libname 258 t.vars = ['LIBNAME', 'VERSION', 'ABI_MATCH'] 259Build.BuildContext.ABI_VSCRIPT = ABI_VSCRIPT 260