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()