Home | History | Annotate | Download | only in cc
      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()
    446