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 logging 20import os 21import re 22 23 24ALL_ARCHITECTURES = ( 25 'arm', 26 'arm64', 27 'mips', 28 'mips64', 29 'x86', 30 'x86_64', 31) 32 33 34# Arbitrary magic number. We use the same one in api-level.h for this purpose. 35FUTURE_API_LEVEL = 10000 36 37 38def logger(): 39 """Return the main logger for this module.""" 40 return logging.getLogger(__name__) 41 42 43def api_level_arg(api_str): 44 """Parses an API level, handling the "current" special case. 45 46 Args: 47 api_str: (string) Either a numeric API level or "current". 48 49 Returns: 50 (int) FUTURE_API_LEVEL if `api_str` is "current", else `api_str` parsed 51 as an integer. 52 """ 53 if api_str == "current": 54 return FUTURE_API_LEVEL 55 return int(api_str) 56 57 58def get_tags(line): 59 """Returns a list of all tags on this line.""" 60 _, _, all_tags = line.strip().partition('#') 61 return [e for e in re.split(r'\s+', all_tags) if e.strip()] 62 63 64def get_tag_value(tag): 65 """Returns the value of a key/value tag. 66 67 Raises: 68 ValueError: Tag is not a key/value type tag. 69 70 Returns: Value part of tag as a string. 71 """ 72 if '=' not in tag: 73 raise ValueError('Not a key/value tag: ' + tag) 74 return tag.partition('=')[2] 75 76 77def version_is_private(version): 78 """Returns True if the version name should be treated as private.""" 79 return version.endswith('_PRIVATE') or version.endswith('_PLATFORM') 80 81 82def should_omit_version(name, tags, arch, api, vndk): 83 """Returns True if the version section should be ommitted. 84 85 We want to omit any sections that do not have any symbols we'll have in the 86 stub library. Sections that contain entirely future symbols or only symbols 87 for certain architectures. 88 """ 89 if version_is_private(name): 90 return True 91 if 'platform-only' in tags: 92 return True 93 if 'vndk' in tags and not vndk: 94 return True 95 if not symbol_in_arch(tags, arch): 96 return True 97 if not symbol_in_api(tags, arch, api): 98 return True 99 return False 100 101 102def symbol_in_arch(tags, arch): 103 """Returns true if the symbol is present for the given architecture.""" 104 has_arch_tags = False 105 for tag in tags: 106 if tag == arch: 107 return True 108 if tag in ALL_ARCHITECTURES: 109 has_arch_tags = True 110 111 # If there were no arch tags, the symbol is available for all 112 # architectures. If there were any arch tags, the symbol is only available 113 # for the tagged architectures. 114 return not has_arch_tags 115 116 117def symbol_in_api(tags, arch, api): 118 """Returns true if the symbol is present for the given API level.""" 119 introduced_tag = None 120 arch_specific = False 121 for tag in tags: 122 # If there is an arch-specific tag, it should override the common one. 123 if tag.startswith('introduced=') and not arch_specific: 124 introduced_tag = tag 125 elif tag.startswith('introduced-' + arch + '='): 126 introduced_tag = tag 127 arch_specific = True 128 elif tag == 'future': 129 return api == FUTURE_API_LEVEL 130 131 if introduced_tag is None: 132 # We found no "introduced" tags, so the symbol has always been 133 # available. 134 return True 135 136 return api >= int(get_tag_value(introduced_tag)) 137 138 139def symbol_versioned_in_api(tags, api): 140 """Returns true if the symbol should be versioned for the given API. 141 142 This models the `versioned=API` tag. This should be a very uncommonly 143 needed tag, and is really only needed to fix versioning mistakes that are 144 already out in the wild. 145 146 For example, some of libc's __aeabi_* functions were originally placed in 147 the private version, but that was incorrect. They are now in LIBC_N, but 148 when building against any version prior to N we need the symbol to be 149 unversioned (otherwise it won't resolve on M where it is private). 150 """ 151 for tag in tags: 152 if tag.startswith('versioned='): 153 return api >= int(get_tag_value(tag)) 154 # If there is no "versioned" tag, the tag has been versioned for as long as 155 # it was introduced. 156 return True 157 158 159class ParseError(RuntimeError): 160 """An error that occurred while parsing a symbol file.""" 161 pass 162 163 164class Version(object): 165 """A version block of a symbol file.""" 166 def __init__(self, name, base, tags, symbols): 167 self.name = name 168 self.base = base 169 self.tags = tags 170 self.symbols = symbols 171 172 def __eq__(self, other): 173 if self.name != other.name: 174 return False 175 if self.base != other.base: 176 return False 177 if self.tags != other.tags: 178 return False 179 if self.symbols != other.symbols: 180 return False 181 return True 182 183 184class Symbol(object): 185 """A symbol definition from a symbol file.""" 186 def __init__(self, name, tags): 187 self.name = name 188 self.tags = tags 189 190 def __eq__(self, other): 191 return self.name == other.name and set(self.tags) == set(other.tags) 192 193 194class SymbolFileParser(object): 195 """Parses NDK symbol files.""" 196 def __init__(self, input_file): 197 self.input_file = input_file 198 self.current_line = None 199 200 def parse(self): 201 """Parses the symbol file and returns a list of Version objects.""" 202 versions = [] 203 while self.next_line() != '': 204 if '{' in self.current_line: 205 versions.append(self.parse_version()) 206 else: 207 raise ParseError( 208 'Unexpected contents at top level: ' + self.current_line) 209 return versions 210 211 def parse_version(self): 212 """Parses a single version section and returns a Version object.""" 213 name = self.current_line.split('{')[0].strip() 214 tags = get_tags(self.current_line) 215 symbols = [] 216 global_scope = True 217 while self.next_line() != '': 218 if '}' in self.current_line: 219 # Line is something like '} BASE; # tags'. Both base and tags 220 # are optional here. 221 base = self.current_line.partition('}')[2] 222 base = base.partition('#')[0].strip() 223 if not base.endswith(';'): 224 raise ParseError( 225 'Unterminated version block (expected ;).') 226 base = base.rstrip(';').rstrip() 227 if base == '': 228 base = None 229 return Version(name, base, tags, symbols) 230 elif ':' in self.current_line: 231 visibility = self.current_line.split(':')[0].strip() 232 if visibility == 'local': 233 global_scope = False 234 elif visibility == 'global': 235 global_scope = True 236 else: 237 raise ParseError('Unknown visiblity label: ' + visibility) 238 elif global_scope: 239 symbols.append(self.parse_symbol()) 240 else: 241 # We're in a hidden scope. Ignore everything. 242 pass 243 raise ParseError('Unexpected EOF in version block.') 244 245 def parse_symbol(self): 246 """Parses a single symbol line and returns a Symbol object.""" 247 if ';' not in self.current_line: 248 raise ParseError( 249 'Expected ; to terminate symbol: ' + self.current_line) 250 if '*' in self.current_line: 251 raise ParseError( 252 'Wildcard global symbols are not permitted.') 253 # Line is now in the format "<symbol-name>; # tags" 254 name, _, _ = self.current_line.strip().partition(';') 255 tags = get_tags(self.current_line) 256 return Symbol(name, tags) 257 258 def next_line(self): 259 """Returns the next non-empty non-comment line. 260 261 A return value of '' indicates EOF. 262 """ 263 line = self.input_file.readline() 264 while line.strip() == '' or line.strip().startswith('#'): 265 line = self.input_file.readline() 266 267 # We want to skip empty lines, but '' indicates EOF. 268 if line == '': 269 break 270 self.current_line = line 271 return self.current_line 272 273 274class Generator(object): 275 """Output generator that writes stub source files and version scripts.""" 276 def __init__(self, src_file, version_script, arch, api, vndk): 277 self.src_file = src_file 278 self.version_script = version_script 279 self.arch = arch 280 self.api = api 281 self.vndk = vndk 282 283 def write(self, versions): 284 """Writes all symbol data to the output files.""" 285 for version in versions: 286 self.write_version(version) 287 288 def write_version(self, version): 289 """Writes a single version block's data to the output files.""" 290 name = version.name 291 tags = version.tags 292 if should_omit_version(name, tags, self.arch, self.api, self.vndk): 293 return 294 295 section_versioned = symbol_versioned_in_api(tags, self.api) 296 version_empty = True 297 pruned_symbols = [] 298 for symbol in version.symbols: 299 if not self.vndk and 'vndk' in symbol.tags: 300 continue 301 if not symbol_in_arch(symbol.tags, self.arch): 302 continue 303 if not symbol_in_api(symbol.tags, self.arch, self.api): 304 continue 305 306 if symbol_versioned_in_api(symbol.tags, self.api): 307 version_empty = False 308 pruned_symbols.append(symbol) 309 310 if len(pruned_symbols) > 0: 311 if not version_empty and section_versioned: 312 self.version_script.write(version.name + ' {\n') 313 self.version_script.write(' global:\n') 314 for symbol in pruned_symbols: 315 emit_version = symbol_versioned_in_api(symbol.tags, self.api) 316 if section_versioned and emit_version: 317 self.version_script.write(' ' + symbol.name + ';\n') 318 319 if 'var' in symbol.tags: 320 self.src_file.write('int {} = 0;\n'.format(symbol.name)) 321 else: 322 self.src_file.write('void {}() {{}}\n'.format(symbol.name)) 323 324 if not version_empty and section_versioned: 325 base = '' if version.base is None else ' ' + version.base 326 self.version_script.write('}' + base + ';\n') 327 328 329def parse_args(): 330 """Parses and returns command line arguments.""" 331 parser = argparse.ArgumentParser() 332 333 parser.add_argument('-v', '--verbose', action='count', default=0) 334 335 parser.add_argument( 336 '--api', type=api_level_arg, required=True, 337 help='API level being targeted.') 338 parser.add_argument( 339 '--arch', choices=ALL_ARCHITECTURES, required=True, 340 help='Architecture being targeted.') 341 parser.add_argument( 342 '--vndk', action='store_true', help='Use the VNDK variant.') 343 344 parser.add_argument( 345 'symbol_file', type=os.path.realpath, help='Path to symbol file.') 346 parser.add_argument( 347 'stub_src', type=os.path.realpath, 348 help='Path to output stub source file.') 349 parser.add_argument( 350 'version_script', type=os.path.realpath, 351 help='Path to output version script.') 352 353 return parser.parse_args() 354 355 356def main(): 357 """Program entry point.""" 358 args = parse_args() 359 360 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG) 361 verbosity = args.verbose 362 if verbosity > 2: 363 verbosity = 2 364 logging.basicConfig(level=verbose_map[verbosity]) 365 366 with open(args.symbol_file) as symbol_file: 367 versions = SymbolFileParser(symbol_file).parse() 368 369 with open(args.stub_src, 'w') as src_file: 370 with open(args.version_script, 'w') as version_file: 371 generator = Generator(src_file, version_file, args.arch, args.api, 372 args.vndk) 373 generator.write(versions) 374 375 376if __name__ == '__main__': 377 main() 378