| #!/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 os |
| import re |
| |
| |
| ALL_ARCHITECTURES = ( |
| 'arm', |
| 'arm64', |
| 'mips', |
| 'mips64', |
| 'x86', |
| 'x86_64', |
| ) |
| |
| |
| class Scope(object): |
| """Enum for version script scope. |
| |
| Top: Top level of the file. |
| Global: In a version and visibility section where symbols should be visible |
| to the NDK. |
| Local: In a visibility section of a public version where symbols should be |
| hidden to the NDK. |
| Private: In a version where symbols should not be visible to the NDK. |
| """ |
| Top = 1 |
| Global = 2 |
| Local = 3 |
| Private = 4 |
| |
| |
| class Stack(object): |
| """Basic stack implementation.""" |
| def __init__(self): |
| self.stack = [] |
| |
| def push(self, obj): |
| """Push an item on to the stack.""" |
| self.stack.append(obj) |
| |
| def pop(self): |
| """Remove and return the item on the top of the stack.""" |
| return self.stack.pop() |
| |
| @property |
| def top(self): |
| """Return the top of the stack.""" |
| return self.stack[-1] |
| |
| |
| def get_tags(line): |
| """Returns a list of all tags on this line.""" |
| _, _, all_tags = line.strip().partition('#') |
| return re.split(r'\s+', all_tags) |
| |
| |
| 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 enter_version(scope, line, version_file): |
| """Enters a new version block scope.""" |
| if scope.top != Scope.Top: |
| raise RuntimeError('Encountered nested version block.') |
| |
| # Entering a new version block. By convention symbols with versions ending |
| # with "_PRIVATE" or "_PLATFORM" are not included in the NDK. |
| version_name = line.split('{')[0].strip() |
| tags = get_tags(line) |
| if version_is_private(version_name) or 'future' in tags: |
| scope.push(Scope.Private) |
| else: |
| scope.push(Scope.Global) # By default symbols are visible. |
| version_file.write(line) |
| |
| |
| def leave_version(scope, line, version_file): |
| """Leave a version block scope.""" |
| # There is no close to a visibility section, just the end of the version or |
| # a new visiblity section. |
| assert scope.top in (Scope.Global, Scope.Local, Scope.Private) |
| if scope.top != Scope.Private: |
| version_file.write(line) |
| scope.pop() |
| assert scope.top == Scope.Top |
| |
| |
| def enter_visibility(scope, line, version_file): |
| """Enters a new visibility block scope.""" |
| leave_visibility(scope) |
| version_file.write(line) |
| visibility = line.split(':')[0].strip() |
| if visibility == 'local': |
| scope.push(Scope.Local) |
| elif visibility == 'global': |
| scope.push(Scope.Global) |
| else: |
| raise RuntimeError('Unknown visiblity label: ' + visibility) |
| |
| |
| def leave_visibility(scope): |
| """Leaves a visibility block scope.""" |
| assert scope.top in (Scope.Global, Scope.Local) |
| scope.pop() |
| assert scope.top == Scope.Top |
| |
| |
| def handle_top_scope(scope, line, version_file): |
| """Processes a line in the top level scope.""" |
| if '{' in line: |
| enter_version(scope, line, version_file) |
| else: |
| raise RuntimeError('Unexpected contents at top level: ' + line) |
| |
| |
| def handle_private_scope(scope, line, version_file): |
| """Eats all input.""" |
| if '}' in line: |
| leave_version(scope, line, version_file) |
| |
| |
| def handle_local_scope(scope, line, version_file): |
| """Passes through input.""" |
| if ':' in line: |
| enter_visibility(scope, line, version_file) |
| elif '}' in line: |
| leave_version(scope, line, version_file) |
| else: |
| version_file.write(line) |
| |
| |
| 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_version(tags, arch, version): |
| """Returns true if the symbol is present for the given version.""" |
| 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': |
| # This symbol is not in any released API level. |
| # TODO(danalbert): These need to be emitted for version == current. |
| # That's not a construct we have yet, so just skip it for now. |
| return False |
| |
| if introduced_tag is None: |
| # We found no "introduced" tags, so the symbol has always been |
| # available. |
| return True |
| |
| # The tag is a key=value pair, and we only care about the value now. |
| _, _, version_str = introduced_tag.partition('=') |
| return version >= int(version_str) |
| |
| |
| def handle_global_scope(scope, line, src_file, version_file, arch, api): |
| """Emits present symbols to the version file and stub source file.""" |
| if ':' in line: |
| enter_visibility(scope, line, version_file) |
| return |
| if '}' in line: |
| leave_version(scope, line, version_file) |
| return |
| |
| if ';' not in line: |
| raise RuntimeError('Expected ; to terminate symbol: ' + line) |
| if '*' in line: |
| raise RuntimeError('Wildcard global symbols are not permitted.') |
| |
| # Line is now in the format "<symbol-name>; # tags" |
| # Tags are whitespace separated. |
| symbol_name, _, rest = line.strip().partition(';') |
| tags = get_tags(line) |
| |
| if not symbol_in_arch(tags, arch): |
| return |
| if not symbol_in_version(tags, arch, api): |
| return |
| |
| if 'var' in tags: |
| src_file.write('int {} = 0;\n'.format(symbol_name)) |
| else: |
| src_file.write('void {}() {{}}\n'.format(symbol_name)) |
| version_file.write(line) |
| |
| |
| def generate(symbol_file, src_file, version_file, arch, api): |
| """Generates the stub source file and version script.""" |
| scope = Stack() |
| scope.push(Scope.Top) |
| for line in symbol_file: |
| if line.strip() == '' or line.strip().startswith('#'): |
| version_file.write(line) |
| elif scope.top == Scope.Top: |
| handle_top_scope(scope, line, version_file) |
| elif scope.top == Scope.Private: |
| handle_private_scope(scope, line, version_file) |
| elif scope.top == Scope.Local: |
| handle_local_scope(scope, line, version_file) |
| elif scope.top == Scope.Global: |
| handle_global_scope(scope, line, src_file, version_file, arch, api) |
| |
| |
| def parse_args(): |
| """Parses and returns command line arguments.""" |
| parser = argparse.ArgumentParser() |
| |
| parser.add_argument('--api', type=int, help='API level being targeted.') |
| parser.add_argument( |
| '--arch', choices=ALL_ARCHITECTURES, |
| help='Architecture being targeted.') |
| |
| 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.symbol_file) as symbol_file: |
| with open(args.stub_src, 'w') as src_file: |
| with open(args.version_script, 'w') as version_file: |
| generate(symbol_file, src_file, version_file, args.arch, |
| args.api) |
| |
| |
| if __name__ == '__main__': |
| main() |