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