#!/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 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() if version_is_private(version_name): 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 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 "; # tags" # Tags are whitespace separated. symbol_name, _, rest = line.strip().partition(';') _, _, all_tags = rest.partition('#') tags = re.split(r'\s+', all_tags) 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()