blob: 78544c391cb9b8bad81e228ad3754e681626ec5b [file] [log] [blame]
Dan Albert914449f2016-06-17 16:45:24 -07001#!/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."""
18import argparse
19import os
20import re
21
22
23ALL_ARCHITECTURES = (
24 'arm',
25 'arm64',
26 'mips',
27 'mips64',
28 'x86',
29 'x86_64',
30)
31
32
33class Scope(object):
34 """Enum for version script scope.
35
36 Top: Top level of the file.
37 Global: In a version and visibility section where symbols should be visible
38 to the NDK.
39 Local: In a visibility section of a public version where symbols should be
40 hidden to the NDK.
41 Private: In a version where symbols should not be visible to the NDK.
42 """
43 Top = 1
44 Global = 2
45 Local = 3
46 Private = 4
47
48
49class Stack(object):
50 """Basic stack implementation."""
51 def __init__(self):
52 self.stack = []
53
54 def push(self, obj):
55 """Push an item on to the stack."""
56 self.stack.append(obj)
57
58 def pop(self):
59 """Remove and return the item on the top of the stack."""
60 return self.stack.pop()
61
62 @property
63 def top(self):
64 """Return the top of the stack."""
65 return self.stack[-1]
66
67
Dan Alberta85042a2016-07-28 16:58:27 -070068def get_tags(line):
69 """Returns a list of all tags on this line."""
70 _, _, all_tags = line.strip().partition('#')
71 return re.split(r'\s+', all_tags)
72
73
Dan Albert914449f2016-06-17 16:45:24 -070074def version_is_private(version):
75 """Returns True if the version name should be treated as private."""
76 return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
77
78
79def enter_version(scope, line, version_file):
80 """Enters a new version block scope."""
81 if scope.top != Scope.Top:
82 raise RuntimeError('Encountered nested version block.')
83
84 # Entering a new version block. By convention symbols with versions ending
85 # with "_PRIVATE" or "_PLATFORM" are not included in the NDK.
86 version_name = line.split('{')[0].strip()
Dan Alberta85042a2016-07-28 16:58:27 -070087 tags = get_tags(line)
88 if version_is_private(version_name) or 'future' in tags:
Dan Albert914449f2016-06-17 16:45:24 -070089 scope.push(Scope.Private)
90 else:
91 scope.push(Scope.Global) # By default symbols are visible.
92 version_file.write(line)
93
94
95def leave_version(scope, line, version_file):
96 """Leave a version block scope."""
97 # There is no close to a visibility section, just the end of the version or
98 # a new visiblity section.
99 assert scope.top in (Scope.Global, Scope.Local, Scope.Private)
100 if scope.top != Scope.Private:
101 version_file.write(line)
102 scope.pop()
103 assert scope.top == Scope.Top
104
105
106def enter_visibility(scope, line, version_file):
107 """Enters a new visibility block scope."""
108 leave_visibility(scope)
109 version_file.write(line)
110 visibility = line.split(':')[0].strip()
111 if visibility == 'local':
112 scope.push(Scope.Local)
113 elif visibility == 'global':
114 scope.push(Scope.Global)
115 else:
116 raise RuntimeError('Unknown visiblity label: ' + visibility)
117
118
119def leave_visibility(scope):
120 """Leaves a visibility block scope."""
121 assert scope.top in (Scope.Global, Scope.Local)
122 scope.pop()
123 assert scope.top == Scope.Top
124
125
126def handle_top_scope(scope, line, version_file):
127 """Processes a line in the top level scope."""
128 if '{' in line:
129 enter_version(scope, line, version_file)
130 else:
131 raise RuntimeError('Unexpected contents at top level: ' + line)
132
133
134def handle_private_scope(scope, line, version_file):
135 """Eats all input."""
136 if '}' in line:
137 leave_version(scope, line, version_file)
138
139
140def handle_local_scope(scope, line, version_file):
141 """Passes through input."""
142 if ':' in line:
143 enter_visibility(scope, line, version_file)
144 elif '}' in line:
145 leave_version(scope, line, version_file)
146 else:
147 version_file.write(line)
148
149
150def symbol_in_arch(tags, arch):
151 """Returns true if the symbol is present for the given architecture."""
152 has_arch_tags = False
153 for tag in tags:
154 if tag == arch:
155 return True
156 if tag in ALL_ARCHITECTURES:
157 has_arch_tags = True
158
159 # If there were no arch tags, the symbol is available for all
160 # architectures. If there were any arch tags, the symbol is only available
161 # for the tagged architectures.
162 return not has_arch_tags
163
164
165def symbol_in_version(tags, arch, version):
166 """Returns true if the symbol is present for the given version."""
167 introduced_tag = None
168 arch_specific = False
169 for tag in tags:
170 # If there is an arch-specific tag, it should override the common one.
171 if tag.startswith('introduced=') and not arch_specific:
172 introduced_tag = tag
173 elif tag.startswith('introduced-' + arch + '='):
174 introduced_tag = tag
175 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700176 elif tag == 'future':
177 # This symbol is not in any released API level.
178 # TODO(danalbert): These need to be emitted for version == current.
179 # That's not a construct we have yet, so just skip it for now.
180 return False
Dan Albert914449f2016-06-17 16:45:24 -0700181
182 if introduced_tag is None:
183 # We found no "introduced" tags, so the symbol has always been
184 # available.
185 return True
186
187 # The tag is a key=value pair, and we only care about the value now.
188 _, _, version_str = introduced_tag.partition('=')
189 return version >= int(version_str)
190
191
192def handle_global_scope(scope, line, src_file, version_file, arch, api):
193 """Emits present symbols to the version file and stub source file."""
194 if ':' in line:
195 enter_visibility(scope, line, version_file)
196 return
197 if '}' in line:
198 leave_version(scope, line, version_file)
199 return
200
201 if ';' not in line:
202 raise RuntimeError('Expected ; to terminate symbol: ' + line)
203 if '*' in line:
204 raise RuntimeError('Wildcard global symbols are not permitted.')
205
206 # Line is now in the format "<symbol-name>; # tags"
207 # Tags are whitespace separated.
208 symbol_name, _, rest = line.strip().partition(';')
Dan Alberta85042a2016-07-28 16:58:27 -0700209 tags = get_tags(line)
Dan Albert914449f2016-06-17 16:45:24 -0700210
211 if not symbol_in_arch(tags, arch):
212 return
213 if not symbol_in_version(tags, arch, api):
214 return
215
216 if 'var' in tags:
217 src_file.write('int {} = 0;\n'.format(symbol_name))
218 else:
219 src_file.write('void {}() {{}}\n'.format(symbol_name))
220 version_file.write(line)
221
222
223def generate(symbol_file, src_file, version_file, arch, api):
224 """Generates the stub source file and version script."""
225 scope = Stack()
226 scope.push(Scope.Top)
227 for line in symbol_file:
228 if line.strip() == '' or line.strip().startswith('#'):
229 version_file.write(line)
230 elif scope.top == Scope.Top:
231 handle_top_scope(scope, line, version_file)
232 elif scope.top == Scope.Private:
233 handle_private_scope(scope, line, version_file)
234 elif scope.top == Scope.Local:
235 handle_local_scope(scope, line, version_file)
236 elif scope.top == Scope.Global:
237 handle_global_scope(scope, line, src_file, version_file, arch, api)
238
239
240def parse_args():
241 """Parses and returns command line arguments."""
242 parser = argparse.ArgumentParser()
243
244 parser.add_argument('--api', type=int, help='API level being targeted.')
245 parser.add_argument(
246 '--arch', choices=ALL_ARCHITECTURES,
247 help='Architecture being targeted.')
248
249 parser.add_argument(
250 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
251 parser.add_argument(
252 'stub_src', type=os.path.realpath,
253 help='Path to output stub source file.')
254 parser.add_argument(
255 'version_script', type=os.path.realpath,
256 help='Path to output version script.')
257
258 return parser.parse_args()
259
260
261def main():
262 """Program entry point."""
263 args = parse_args()
264
265 with open(args.symbol_file) as symbol_file:
266 with open(args.stub_src, 'w') as src_file:
267 with open(args.version_script, 'w') as version_file:
268 generate(symbol_file, src_file, version_file, args.arch,
269 args.api)
270
271
272if __name__ == '__main__':
273 main()