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 logging
     20 import os
     21 import re
     22 
     23 
     24 ALL_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.
     35 FUTURE_API_LEVEL = 10000
     36 
     37 
     38 def logger():
     39     """Return the main logger for this module."""
     40     return logging.getLogger(__name__)
     41 
     42 
     43 def 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 
     58 def 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 
     64 def 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 
     77 def 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 
     82 def 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 
    102 def 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 
    117 def 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 
    139 def 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 
    159 class ParseError(RuntimeError):
    160     """An error that occurred while parsing a symbol file."""
    161     pass
    162 
    163 
    164 class 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 
    184 class 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 
    194 class 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 
    274 class 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 
    329 def 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 
    356 def 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 
    376 if __name__ == '__main__':
    377     main()
    378