github.com/westcoastroms/westcoastroms-build@v0.0.0-20190928114312-2350e5a73030/build/soong/cc/gen_stub_libs.py (about) 1 #!/usr/bin/env python 2 # 3 # Copyright (C) 2016 The Android Open Source Project 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 # 17 """Generates source for stub shared libraries for the NDK.""" 18 import argparse 19 import json 20 import logging 21 import os 22 import re 23 24 25 ALL_ARCHITECTURES = ( 26 'arm', 27 'arm64', 28 'mips', 29 'mips64', 30 'x86', 31 'x86_64', 32 ) 33 34 35 # Arbitrary magic number. We use the same one in api-level.h for this purpose. 36 FUTURE_API_LEVEL = 10000 37 38 39 def logger(): 40 """Return the main logger for this module.""" 41 return logging.getLogger(__name__) 42 43 44 def get_tags(line): 45 """Returns a list of all tags on this line.""" 46 _, _, all_tags = line.strip().partition('#') 47 return [e for e in re.split(r'\s+', all_tags) if e.strip()] 48 49 50 def is_api_level_tag(tag): 51 """Returns true if this tag has an API level that may need decoding.""" 52 if tag.startswith('introduced='): 53 return True 54 if tag.startswith('introduced-'): 55 return True 56 if tag.startswith('versioned='): 57 return True 58 return False 59 60 61 def decode_api_level_tags(tags, api_map): 62 """Decodes API level code names in a list of tags. 63 64 Raises: 65 ParseError: An unknown version name was found in a tag. 66 """ 67 for idx, tag in enumerate(tags): 68 if not is_api_level_tag(tag): 69 continue 70 name, value = split_tag(tag) 71 72 try: 73 decoded = str(decode_api_level(value, api_map)) 74 tags[idx] = '='.join([name, decoded]) 75 except KeyError: 76 raise ParseError('Unknown version name in tag: {}'.format(tag)) 77 return tags 78 79 80 def split_tag(tag): 81 """Returns a key/value tuple of the tag. 82 83 Raises: 84 ValueError: Tag is not a key/value type tag. 85 86 Returns: Tuple of (key, value) of the tag. Both components are strings. 87 """ 88 if '=' not in tag: 89 raise ValueError('Not a key/value tag: ' + tag) 90 key, _, value = tag.partition('=') 91 return key, value 92 93 94 def get_tag_value(tag): 95 """Returns the value of a key/value tag. 96 97 Raises: 98 ValueError: Tag is not a key/value type tag. 99 100 Returns: Value part of tag as a string. 101 """ 102 return split_tag(tag)[1] 103 104 105 def version_is_private(version): 106 """Returns True if the version name should be treated as private.""" 107 return version.endswith('_PRIVATE') or version.endswith('_PLATFORM') 108 109 110 def should_omit_version(name, tags, arch, api, vndk): 111 """Returns True if the version section should be ommitted. 112 113 We want to omit any sections that do not have any symbols we'll have in the 114 stub library. Sections that contain entirely future symbols or only symbols 115 for certain architectures. 116 """ 117 if version_is_private(name): 118 return True 119 if 'platform-only' in tags: 120 return True 121 if 'vndk' in tags and not vndk: 122 return True 123 if not symbol_in_arch(tags, arch): 124 return True 125 if not symbol_in_api(tags, arch, api): 126 return True 127 return False 128 129 130 def symbol_in_arch(tags, arch): 131 """Returns true if the symbol is present for the given architecture.""" 132 has_arch_tags = False 133 for tag in tags: 134 if tag == arch: 135 return True 136 if tag in ALL_ARCHITECTURES: 137 has_arch_tags = True 138 139 # If there were no arch tags, the symbol is available for all 140 # architectures. If there were any arch tags, the symbol is only available 141 # for the tagged architectures. 142 return not has_arch_tags 143 144 145 def symbol_in_api(tags, arch, api): 146 """Returns true if the symbol is present for the given API level.""" 147 introduced_tag = None 148 arch_specific = False 149 for tag in tags: 150 # If there is an arch-specific tag, it should override the common one. 151 if tag.startswith('introduced=') and not arch_specific: 152 introduced_tag = tag 153 elif tag.startswith('introduced-' + arch + '='): 154 introduced_tag = tag 155 arch_specific = True 156 elif tag == 'future': 157 return api == FUTURE_API_LEVEL 158 159 if introduced_tag is None: 160 # We found no "introduced" tags, so the symbol has always been 161 # available. 162 return True 163 164 return api >= int(get_tag_value(introduced_tag)) 165 166 167 def symbol_versioned_in_api(tags, api): 168 """Returns true if the symbol should be versioned for the given API. 169 170 This models the `versioned=API` tag. This should be a very uncommonly 171 needed tag, and is really only needed to fix versioning mistakes that are 172 already out in the wild. 173 174 For example, some of libc's __aeabi_* functions were originally placed in 175 the private version, but that was incorrect. They are now in LIBC_N, but 176 when building against any version prior to N we need the symbol to be 177 unversioned (otherwise it won't resolve on M where it is private). 178 """ 179 for tag in tags: 180 if tag.startswith('versioned='): 181 return api >= int(get_tag_value(tag)) 182 # If there is no "versioned" tag, the tag has been versioned for as long as 183 # it was introduced. 184 return True 185 186 187 class ParseError(RuntimeError): 188 """An error that occurred while parsing a symbol file.""" 189 pass 190 191 192 class Version(object): 193 """A version block of a symbol file.""" 194 def __init__(self, name, base, tags, symbols): 195 self.name = name 196 self.base = base 197 self.tags = tags 198 self.symbols = symbols 199 200 def __eq__(self, other): 201 if self.name != other.name: 202 return False 203 if self.base != other.base: 204 return False 205 if self.tags != other.tags: 206 return False 207 if self.symbols != other.symbols: 208 return False 209 return True 210 211 212 class Symbol(object): 213 """A symbol definition from a symbol file.""" 214 def __init__(self, name, tags): 215 self.name = name 216 self.tags = tags 217 218 def __eq__(self, other): 219 return self.name == other.name and set(self.tags) == set(other.tags) 220 221 222 class SymbolFileParser(object): 223 """Parses NDK symbol files.""" 224 def __init__(self, input_file, api_map): 225 self.input_file = input_file 226 self.api_map = api_map 227 self.current_line = None 228 229 def parse(self): 230 """Parses the symbol file and returns a list of Version objects.""" 231 versions = [] 232 while self.next_line() != '': 233 if '{' in self.current_line: 234 versions.append(self.parse_version()) 235 else: 236 raise ParseError( 237 'Unexpected contents at top level: ' + self.current_line) 238 return versions 239 240 def parse_version(self): 241 """Parses a single version section and returns a Version object.""" 242 name = self.current_line.split('{')[0].strip() 243 tags = get_tags(self.current_line) 244 tags = decode_api_level_tags(tags, self.api_map) 245 symbols = [] 246 global_scope = True 247 cpp_symbols = False 248 while self.next_line() != '': 249 if '}' in self.current_line: 250 # Line is something like '} BASE; # tags'. Both base and tags 251 # are optional here. 252 base = self.current_line.partition('}')[2] 253 base = base.partition('#')[0].strip() 254 if not base.endswith(';'): 255 raise ParseError( 256 'Unterminated version/export "C++" block (expected ;).') 257 if cpp_symbols: 258 cpp_symbols = False 259 else: 260 base = base.rstrip(';').rstrip() 261 if base == '': 262 base = None 263 return Version(name, base, tags, symbols) 264 elif 'extern "C++" {' in self.current_line: 265 cpp_symbols = True 266 elif not cpp_symbols and ':' in self.current_line: 267 visibility = self.current_line.split(':')[0].strip() 268 if visibility == 'local': 269 global_scope = False 270 elif visibility == 'global': 271 global_scope = True 272 else: 273 raise ParseError('Unknown visiblity label: ' + visibility) 274 elif global_scope and not cpp_symbols: 275 symbols.append(self.parse_symbol()) 276 else: 277 # We're in a hidden scope or in 'extern "C++"' block. Ignore everything. 278 pass 279 raise ParseError('Unexpected EOF in version block.') 280 281 def parse_symbol(self): 282 """Parses a single symbol line and returns a Symbol object.""" 283 if ';' not in self.current_line: 284 raise ParseError( 285 'Expected ; to terminate symbol: ' + self.current_line) 286 if '*' in self.current_line: 287 raise ParseError( 288 'Wildcard global symbols are not permitted.') 289 # Line is now in the format "<symbol-name>; # tags" 290 name, _, _ = self.current_line.strip().partition(';') 291 tags = get_tags(self.current_line) 292 tags = decode_api_level_tags(tags, self.api_map) 293 return Symbol(name, tags) 294 295 def next_line(self): 296 """Returns the next non-empty non-comment line. 297 298 A return value of '' indicates EOF. 299 """ 300 line = self.input_file.readline() 301 while line.strip() == '' or line.strip().startswith('#'): 302 line = self.input_file.readline() 303 304 # We want to skip empty lines, but '' indicates EOF. 305 if line == '': 306 break 307 self.current_line = line 308 return self.current_line 309 310 311 class Generator(object): 312 """Output generator that writes stub source files and version scripts.""" 313 def __init__(self, src_file, version_script, arch, api, vndk): 314 self.src_file = src_file 315 self.version_script = version_script 316 self.arch = arch 317 self.api = api 318 self.vndk = vndk 319 320 def write(self, versions): 321 """Writes all symbol data to the output files.""" 322 for version in versions: 323 self.write_version(version) 324 325 def write_version(self, version): 326 """Writes a single version block's data to the output files.""" 327 name = version.name 328 tags = version.tags 329 if should_omit_version(name, tags, self.arch, self.api, self.vndk): 330 return 331 332 section_versioned = symbol_versioned_in_api(tags, self.api) 333 version_empty = True 334 pruned_symbols = [] 335 for symbol in version.symbols: 336 if not self.vndk and 'vndk' in symbol.tags: 337 continue 338 if not symbol_in_arch(symbol.tags, self.arch): 339 continue 340 if not symbol_in_api(symbol.tags, self.arch, self.api): 341 continue 342 343 if symbol_versioned_in_api(symbol.tags, self.api): 344 version_empty = False 345 pruned_symbols.append(symbol) 346 347 if len(pruned_symbols) > 0: 348 if not version_empty and section_versioned: 349 self.version_script.write(version.name + ' {\n') 350 self.version_script.write(' global:\n') 351 for symbol in pruned_symbols: 352 emit_version = symbol_versioned_in_api(symbol.tags, self.api) 353 if section_versioned and emit_version: 354 self.version_script.write(' ' + symbol.name + ';\n') 355 356 weak = '' 357 if 'weak' in symbol.tags: 358 weak = '__attribute__((weak)) ' 359 360 if 'var' in symbol.tags: 361 self.src_file.write('{}int {} = 0;\n'.format( 362 weak, symbol.name)) 363 else: 364 self.src_file.write('{}void {}() {{}}\n'.format( 365 weak, symbol.name)) 366 367 if not version_empty and section_versioned: 368 base = '' if version.base is None else ' ' + version.base 369 self.version_script.write('}' + base + ';\n') 370 371 372 def decode_api_level(api, api_map): 373 """Decodes the API level argument into the API level number. 374 375 For the average case, this just decodes the integer value from the string, 376 but for unreleased APIs we need to translate from the API codename (like 377 "O") to the future API level for that codename. 378 """ 379 try: 380 return int(api) 381 except ValueError: 382 pass 383 384 if api == "current": 385 return FUTURE_API_LEVEL 386 387 return api_map[api] 388 389 390 def parse_args(): 391 """Parses and returns command line arguments.""" 392 parser = argparse.ArgumentParser() 393 394 parser.add_argument('-v', '--verbose', action='count', default=0) 395 396 parser.add_argument( 397 '--api', required=True, help='API level being targeted.') 398 parser.add_argument( 399 '--arch', choices=ALL_ARCHITECTURES, required=True, 400 help='Architecture being targeted.') 401 parser.add_argument( 402 '--vndk', action='store_true', help='Use the VNDK variant.') 403 404 parser.add_argument( 405 '--api-map', type=os.path.realpath, required=True, 406 help='Path to the API level map JSON file.') 407 408 parser.add_argument( 409 'symbol_file', type=os.path.realpath, help='Path to symbol file.') 410 parser.add_argument( 411 'stub_src', type=os.path.realpath, 412 help='Path to output stub source file.') 413 parser.add_argument( 414 'version_script', type=os.path.realpath, 415 help='Path to output version script.') 416 417 return parser.parse_args() 418 419 420 def main(): 421 """Program entry point.""" 422 args = parse_args() 423 424 with open(args.api_map) as map_file: 425 api_map = json.load(map_file) 426 api = decode_api_level(args.api, api_map) 427 428 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG) 429 verbosity = args.verbose 430 if verbosity > 2: 431 verbosity = 2 432 logging.basicConfig(level=verbose_map[verbosity]) 433 434 with open(args.symbol_file) as symbol_file: 435 versions = SymbolFileParser(symbol_file, api_map).parse() 436 437 with open(args.stub_src, 'w') as src_file: 438 with open(args.version_script, 'w') as version_file: 439 generator = Generator(src_file, version_file, args.arch, api, 440 args.vndk) 441 generator.write(versions) 442 443 444 if __name__ == '__main__': 445 main()