123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- #!/usr/bin/env python
- #
- # Copyright (C) 2016 The Android Open Source Project
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- #
- """Generates source for stub shared libraries for the NDK."""
- import argparse
- import json
- import logging
- import os
- import re
- import sys
- ALL_ARCHITECTURES = (
- 'arm',
- 'arm64',
- 'x86',
- 'x86_64',
- )
- # Arbitrary magic number. We use the same one in api-level.h for this purpose.
- FUTURE_API_LEVEL = 10000
- def logger():
- """Return the main logger for this module."""
- return logging.getLogger(__name__)
- def get_tags(line):
- """Returns a list of all tags on this line."""
- _, _, all_tags = line.strip().partition('#')
- return [e for e in re.split(r'\s+', all_tags) if e.strip()]
- def is_api_level_tag(tag):
- """Returns true if this tag has an API level that may need decoding."""
- if tag.startswith('introduced='):
- return True
- if tag.startswith('introduced-'):
- return True
- if tag.startswith('versioned='):
- return True
- return False
- def decode_api_level_tags(tags, api_map):
- """Decodes API level code names in a list of tags.
- Raises:
- ParseError: An unknown version name was found in a tag.
- """
- for idx, tag in enumerate(tags):
- if not is_api_level_tag(tag):
- continue
- name, value = split_tag(tag)
- try:
- decoded = str(decode_api_level(value, api_map))
- tags[idx] = '='.join([name, decoded])
- except KeyError:
- raise ParseError('Unknown version name in tag: {}'.format(tag))
- return tags
- def split_tag(tag):
- """Returns a key/value tuple of the tag.
- Raises:
- ValueError: Tag is not a key/value type tag.
- Returns: Tuple of (key, value) of the tag. Both components are strings.
- """
- if '=' not in tag:
- raise ValueError('Not a key/value tag: ' + tag)
- key, _, value = tag.partition('=')
- return key, value
- def get_tag_value(tag):
- """Returns the value of a key/value tag.
- Raises:
- ValueError: Tag is not a key/value type tag.
- Returns: Value part of tag as a string.
- """
- return split_tag(tag)[1]
- def version_is_private(version):
- """Returns True if the version name should be treated as private."""
- return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
- def should_omit_version(version, arch, api, llndk, apex):
- """Returns True if the version section should be ommitted.
- We want to omit any sections that do not have any symbols we'll have in the
- stub library. Sections that contain entirely future symbols or only symbols
- for certain architectures.
- """
- if version_is_private(version.name):
- return True
- if 'platform-only' in version.tags:
- return True
- no_llndk_no_apex = 'llndk' not in version.tags and 'apex' not in version.tags
- keep = no_llndk_no_apex or \
- ('llndk' in version.tags and llndk) or \
- ('apex' in version.tags and apex)
- if not keep:
- return True
- if not symbol_in_arch(version.tags, arch):
- return True
- if not symbol_in_api(version.tags, arch, api):
- return True
- return False
- def should_omit_symbol(symbol, arch, api, llndk, apex):
- """Returns True if the symbol should be omitted."""
- no_llndk_no_apex = 'llndk' not in symbol.tags and 'apex' not in symbol.tags
- keep = no_llndk_no_apex or \
- ('llndk' in symbol.tags and llndk) or \
- ('apex' in symbol.tags and apex)
- if not keep:
- return True
- if not symbol_in_arch(symbol.tags, arch):
- return True
- if not symbol_in_api(symbol.tags, arch, api):
- return True
- return False
- def symbol_in_arch(tags, arch):
- """Returns true if the symbol is present for the given architecture."""
- has_arch_tags = False
- for tag in tags:
- if tag == arch:
- return True
- if tag in ALL_ARCHITECTURES:
- has_arch_tags = True
- # If there were no arch tags, the symbol is available for all
- # architectures. If there were any arch tags, the symbol is only available
- # for the tagged architectures.
- return not has_arch_tags
- def symbol_in_api(tags, arch, api):
- """Returns true if the symbol is present for the given API level."""
- introduced_tag = None
- arch_specific = False
- for tag in tags:
- # If there is an arch-specific tag, it should override the common one.
- if tag.startswith('introduced=') and not arch_specific:
- introduced_tag = tag
- elif tag.startswith('introduced-' + arch + '='):
- introduced_tag = tag
- arch_specific = True
- elif tag == 'future':
- return api == FUTURE_API_LEVEL
- if introduced_tag is None:
- # We found no "introduced" tags, so the symbol has always been
- # available.
- return True
- return api >= int(get_tag_value(introduced_tag))
- def symbol_versioned_in_api(tags, api):
- """Returns true if the symbol should be versioned for the given API.
- This models the `versioned=API` tag. This should be a very uncommonly
- needed tag, and is really only needed to fix versioning mistakes that are
- already out in the wild.
- For example, some of libc's __aeabi_* functions were originally placed in
- the private version, but that was incorrect. They are now in LIBC_N, but
- when building against any version prior to N we need the symbol to be
- unversioned (otherwise it won't resolve on M where it is private).
- """
- for tag in tags:
- if tag.startswith('versioned='):
- return api >= int(get_tag_value(tag))
- # If there is no "versioned" tag, the tag has been versioned for as long as
- # it was introduced.
- return True
- class ParseError(RuntimeError):
- """An error that occurred while parsing a symbol file."""
- pass
- class MultiplyDefinedSymbolError(RuntimeError):
- """A symbol name was multiply defined."""
- def __init__(self, multiply_defined_symbols):
- super(MultiplyDefinedSymbolError, self).__init__(
- 'Version script contains multiple definitions for: {}'.format(
- ', '.join(multiply_defined_symbols)))
- self.multiply_defined_symbols = multiply_defined_symbols
- class Version(object):
- """A version block of a symbol file."""
- def __init__(self, name, base, tags, symbols):
- self.name = name
- self.base = base
- self.tags = tags
- self.symbols = symbols
- def __eq__(self, other):
- if self.name != other.name:
- return False
- if self.base != other.base:
- return False
- if self.tags != other.tags:
- return False
- if self.symbols != other.symbols:
- return False
- return True
- class Symbol(object):
- """A symbol definition from a symbol file."""
- def __init__(self, name, tags):
- self.name = name
- self.tags = tags
- def __eq__(self, other):
- return self.name == other.name and set(self.tags) == set(other.tags)
- class SymbolFileParser(object):
- """Parses NDK symbol files."""
- def __init__(self, input_file, api_map, arch, api, llndk, apex):
- self.input_file = input_file
- self.api_map = api_map
- self.arch = arch
- self.api = api
- self.llndk = llndk
- self.apex = apex
- self.current_line = None
- def parse(self):
- """Parses the symbol file and returns a list of Version objects."""
- versions = []
- while self.next_line() != '':
- if '{' in self.current_line:
- versions.append(self.parse_version())
- else:
- raise ParseError(
- 'Unexpected contents at top level: ' + self.current_line)
- self.check_no_duplicate_symbols(versions)
- return versions
- def check_no_duplicate_symbols(self, versions):
- """Raises errors for multiply defined symbols.
- This situation is the normal case when symbol versioning is actually
- used, but this script doesn't currently handle that. The error message
- will be a not necessarily obvious "error: redefition of 'foo'" from
- stub.c, so it's better for us to catch this situation and raise a
- better error.
- """
- symbol_names = set()
- multiply_defined_symbols = set()
- for version in versions:
- if should_omit_version(version, self.arch, self.api, self.llndk, self.apex):
- continue
- for symbol in version.symbols:
- if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex):
- continue
- if symbol.name in symbol_names:
- multiply_defined_symbols.add(symbol.name)
- symbol_names.add(symbol.name)
- if multiply_defined_symbols:
- raise MultiplyDefinedSymbolError(
- sorted(list(multiply_defined_symbols)))
- def parse_version(self):
- """Parses a single version section and returns a Version object."""
- name = self.current_line.split('{')[0].strip()
- tags = get_tags(self.current_line)
- tags = decode_api_level_tags(tags, self.api_map)
- symbols = []
- global_scope = True
- cpp_symbols = False
- while self.next_line() != '':
- if '}' in self.current_line:
- # Line is something like '} BASE; # tags'. Both base and tags
- # are optional here.
- base = self.current_line.partition('}')[2]
- base = base.partition('#')[0].strip()
- if not base.endswith(';'):
- raise ParseError(
- 'Unterminated version/export "C++" block (expected ;).')
- if cpp_symbols:
- cpp_symbols = False
- else:
- base = base.rstrip(';').rstrip()
- if base == '':
- base = None
- return Version(name, base, tags, symbols)
- elif 'extern "C++" {' in self.current_line:
- cpp_symbols = True
- elif not cpp_symbols and ':' in self.current_line:
- visibility = self.current_line.split(':')[0].strip()
- if visibility == 'local':
- global_scope = False
- elif visibility == 'global':
- global_scope = True
- else:
- raise ParseError('Unknown visiblity label: ' + visibility)
- elif global_scope and not cpp_symbols:
- symbols.append(self.parse_symbol())
- else:
- # We're in a hidden scope or in 'extern "C++"' block. Ignore
- # everything.
- pass
- raise ParseError('Unexpected EOF in version block.')
- def parse_symbol(self):
- """Parses a single symbol line and returns a Symbol object."""
- if ';' not in self.current_line:
- raise ParseError(
- 'Expected ; to terminate symbol: ' + self.current_line)
- if '*' in self.current_line:
- raise ParseError(
- 'Wildcard global symbols are not permitted.')
- # Line is now in the format "<symbol-name>; # tags"
- name, _, _ = self.current_line.strip().partition(';')
- tags = get_tags(self.current_line)
- tags = decode_api_level_tags(tags, self.api_map)
- return Symbol(name, tags)
- def next_line(self):
- """Returns the next non-empty non-comment line.
- A return value of '' indicates EOF.
- """
- line = self.input_file.readline()
- while line.strip() == '' or line.strip().startswith('#'):
- line = self.input_file.readline()
- # We want to skip empty lines, but '' indicates EOF.
- if line == '':
- break
- self.current_line = line
- return self.current_line
- class Generator(object):
- """Output generator that writes stub source files and version scripts."""
- def __init__(self, src_file, version_script, arch, api, llndk, apex):
- self.src_file = src_file
- self.version_script = version_script
- self.arch = arch
- self.api = api
- self.llndk = llndk
- self.apex = apex
- def write(self, versions):
- """Writes all symbol data to the output files."""
- for version in versions:
- self.write_version(version)
- def write_version(self, version):
- """Writes a single version block's data to the output files."""
- if should_omit_version(version, self.arch, self.api, self.llndk, self.apex):
- return
- section_versioned = symbol_versioned_in_api(version.tags, self.api)
- version_empty = True
- pruned_symbols = []
- for symbol in version.symbols:
- if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex):
- continue
- if symbol_versioned_in_api(symbol.tags, self.api):
- version_empty = False
- pruned_symbols.append(symbol)
- if len(pruned_symbols) > 0:
- if not version_empty and section_versioned:
- self.version_script.write(version.name + ' {\n')
- self.version_script.write(' global:\n')
- for symbol in pruned_symbols:
- emit_version = symbol_versioned_in_api(symbol.tags, self.api)
- if section_versioned and emit_version:
- self.version_script.write(' ' + symbol.name + ';\n')
- weak = ''
- if 'weak' in symbol.tags:
- weak = '__attribute__((weak)) '
- if 'var' in symbol.tags:
- self.src_file.write('{}int {} = 0;\n'.format(
- weak, symbol.name))
- else:
- self.src_file.write('{}void {}() {{}}\n'.format(
- weak, symbol.name))
- if not version_empty and section_versioned:
- base = '' if version.base is None else ' ' + version.base
- self.version_script.write('}' + base + ';\n')
- def decode_api_level(api, api_map):
- """Decodes the API level argument into the API level number.
- For the average case, this just decodes the integer value from the string,
- but for unreleased APIs we need to translate from the API codename (like
- "O") to the future API level for that codename.
- """
- try:
- return int(api)
- except ValueError:
- pass
- if api == "current":
- return FUTURE_API_LEVEL
- return api_map[api]
- def parse_args():
- """Parses and returns command line arguments."""
- parser = argparse.ArgumentParser()
- parser.add_argument('-v', '--verbose', action='count', default=0)
- parser.add_argument(
- '--api', required=True, help='API level being targeted.')
- parser.add_argument(
- '--arch', choices=ALL_ARCHITECTURES, required=True,
- help='Architecture being targeted.')
- parser.add_argument(
- '--llndk', action='store_true', help='Use the LLNDK variant.')
- parser.add_argument(
- '--apex', action='store_true', help='Use the APEX variant.')
- parser.add_argument(
- '--api-map', type=os.path.realpath, required=True,
- help='Path to the API level map JSON file.')
- parser.add_argument(
- 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
- parser.add_argument(
- 'stub_src', type=os.path.realpath,
- help='Path to output stub source file.')
- parser.add_argument(
- 'version_script', type=os.path.realpath,
- help='Path to output version script.')
- return parser.parse_args()
- def main():
- """Program entry point."""
- args = parse_args()
- with open(args.api_map) as map_file:
- api_map = json.load(map_file)
- api = decode_api_level(args.api, api_map)
- verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
- verbosity = args.verbose
- if verbosity > 2:
- verbosity = 2
- logging.basicConfig(level=verbose_map[verbosity])
- with open(args.symbol_file) as symbol_file:
- try:
- versions = SymbolFileParser(symbol_file, api_map, args.arch, api,
- args.llndk, args.apex).parse()
- except MultiplyDefinedSymbolError as ex:
- sys.exit('{}: error: {}'.format(args.symbol_file, ex))
- with open(args.stub_src, 'w') as src_file:
- with open(args.version_script, 'w') as version_file:
- generator = Generator(src_file, version_file, args.arch, api,
- args.llndk, args.apex)
- generator.write(versions)
- if __name__ == '__main__':
- main()
|