blob: 4b7c244e545a1fcaffbc4b77e82208297747613c [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
Dan Albert08532b62016-07-28 18:09:47 -070079def should_omit_version(name, tags, arch, api):
80 """Returns True if the version section should be ommitted.
81
82 We want to omit any sections that do not have any symbols we'll have in the
83 stub library. Sections that contain entirely future symbols or only symbols
84 for certain architectures.
85 """
86 if version_is_private(name):
87 return True
88 if not symbol_in_arch(tags, arch):
89 return True
90 if not symbol_in_version(tags, arch, api):
91 return True
92 return False
93
94
95def enter_version(scope, line, version_file, arch, api):
Dan Albert914449f2016-06-17 16:45:24 -070096 """Enters a new version block scope."""
97 if scope.top != Scope.Top:
98 raise RuntimeError('Encountered nested version block.')
99
100 # Entering a new version block. By convention symbols with versions ending
101 # with "_PRIVATE" or "_PLATFORM" are not included in the NDK.
102 version_name = line.split('{')[0].strip()
Dan Alberta85042a2016-07-28 16:58:27 -0700103 tags = get_tags(line)
Dan Albert08532b62016-07-28 18:09:47 -0700104 if should_omit_version(version_name, tags, arch, api):
Dan Albert914449f2016-06-17 16:45:24 -0700105 scope.push(Scope.Private)
106 else:
107 scope.push(Scope.Global) # By default symbols are visible.
108 version_file.write(line)
109
110
111def leave_version(scope, line, version_file):
112 """Leave a version block scope."""
113 # There is no close to a visibility section, just the end of the version or
114 # a new visiblity section.
115 assert scope.top in (Scope.Global, Scope.Local, Scope.Private)
116 if scope.top != Scope.Private:
117 version_file.write(line)
118 scope.pop()
119 assert scope.top == Scope.Top
120
121
122def enter_visibility(scope, line, version_file):
123 """Enters a new visibility block scope."""
124 leave_visibility(scope)
125 version_file.write(line)
126 visibility = line.split(':')[0].strip()
127 if visibility == 'local':
128 scope.push(Scope.Local)
129 elif visibility == 'global':
130 scope.push(Scope.Global)
131 else:
132 raise RuntimeError('Unknown visiblity label: ' + visibility)
133
134
135def leave_visibility(scope):
136 """Leaves a visibility block scope."""
137 assert scope.top in (Scope.Global, Scope.Local)
138 scope.pop()
139 assert scope.top == Scope.Top
140
141
Dan Albert08532b62016-07-28 18:09:47 -0700142def handle_top_scope(scope, line, version_file, arch, api):
Dan Albert914449f2016-06-17 16:45:24 -0700143 """Processes a line in the top level scope."""
144 if '{' in line:
Dan Albert08532b62016-07-28 18:09:47 -0700145 enter_version(scope, line, version_file, arch, api)
Dan Albert914449f2016-06-17 16:45:24 -0700146 else:
147 raise RuntimeError('Unexpected contents at top level: ' + line)
148
149
150def handle_private_scope(scope, line, version_file):
151 """Eats all input."""
152 if '}' in line:
153 leave_version(scope, line, version_file)
154
155
156def handle_local_scope(scope, line, version_file):
157 """Passes through input."""
158 if ':' in line:
159 enter_visibility(scope, line, version_file)
160 elif '}' in line:
161 leave_version(scope, line, version_file)
162 else:
163 version_file.write(line)
164
165
166def symbol_in_arch(tags, arch):
167 """Returns true if the symbol is present for the given architecture."""
168 has_arch_tags = False
169 for tag in tags:
170 if tag == arch:
171 return True
172 if tag in ALL_ARCHITECTURES:
173 has_arch_tags = True
174
175 # If there were no arch tags, the symbol is available for all
176 # architectures. If there were any arch tags, the symbol is only available
177 # for the tagged architectures.
178 return not has_arch_tags
179
180
181def symbol_in_version(tags, arch, version):
182 """Returns true if the symbol is present for the given version."""
183 introduced_tag = None
184 arch_specific = False
185 for tag in tags:
186 # If there is an arch-specific tag, it should override the common one.
187 if tag.startswith('introduced=') and not arch_specific:
188 introduced_tag = tag
189 elif tag.startswith('introduced-' + arch + '='):
190 introduced_tag = tag
191 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700192 elif tag == 'future':
193 # This symbol is not in any released API level.
194 # TODO(danalbert): These need to be emitted for version == current.
195 # That's not a construct we have yet, so just skip it for now.
196 return False
Dan Albert914449f2016-06-17 16:45:24 -0700197
198 if introduced_tag is None:
199 # We found no "introduced" tags, so the symbol has always been
200 # available.
201 return True
202
203 # The tag is a key=value pair, and we only care about the value now.
204 _, _, version_str = introduced_tag.partition('=')
205 return version >= int(version_str)
206
207
208def handle_global_scope(scope, line, src_file, version_file, arch, api):
209 """Emits present symbols to the version file and stub source file."""
210 if ':' in line:
211 enter_visibility(scope, line, version_file)
212 return
213 if '}' in line:
214 leave_version(scope, line, version_file)
215 return
216
217 if ';' not in line:
218 raise RuntimeError('Expected ; to terminate symbol: ' + line)
219 if '*' in line:
220 raise RuntimeError('Wildcard global symbols are not permitted.')
221
222 # Line is now in the format "<symbol-name>; # tags"
223 # Tags are whitespace separated.
224 symbol_name, _, rest = line.strip().partition(';')
Dan Alberta85042a2016-07-28 16:58:27 -0700225 tags = get_tags(line)
Dan Albert914449f2016-06-17 16:45:24 -0700226
227 if not symbol_in_arch(tags, arch):
228 return
229 if not symbol_in_version(tags, arch, api):
230 return
231
232 if 'var' in tags:
233 src_file.write('int {} = 0;\n'.format(symbol_name))
234 else:
235 src_file.write('void {}() {{}}\n'.format(symbol_name))
236 version_file.write(line)
237
238
239def generate(symbol_file, src_file, version_file, arch, api):
240 """Generates the stub source file and version script."""
241 scope = Stack()
242 scope.push(Scope.Top)
243 for line in symbol_file:
244 if line.strip() == '' or line.strip().startswith('#'):
245 version_file.write(line)
246 elif scope.top == Scope.Top:
Dan Albert08532b62016-07-28 18:09:47 -0700247 handle_top_scope(scope, line, version_file, arch, api)
Dan Albert914449f2016-06-17 16:45:24 -0700248 elif scope.top == Scope.Private:
249 handle_private_scope(scope, line, version_file)
250 elif scope.top == Scope.Local:
251 handle_local_scope(scope, line, version_file)
252 elif scope.top == Scope.Global:
253 handle_global_scope(scope, line, src_file, version_file, arch, api)
254
255
256def parse_args():
257 """Parses and returns command line arguments."""
258 parser = argparse.ArgumentParser()
259
260 parser.add_argument('--api', type=int, help='API level being targeted.')
261 parser.add_argument(
262 '--arch', choices=ALL_ARCHITECTURES,
263 help='Architecture being targeted.')
264
265 parser.add_argument(
266 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
267 parser.add_argument(
268 'stub_src', type=os.path.realpath,
269 help='Path to output stub source file.')
270 parser.add_argument(
271 'version_script', type=os.path.realpath,
272 help='Path to output version script.')
273
274 return parser.parse_args()
275
276
277def main():
278 """Program entry point."""
279 args = parse_args()
280
281 with open(args.symbol_file) as symbol_file:
282 with open(args.stub_src, 'w') as src_file:
283 with open(args.version_script, 'w') as version_file:
284 generate(symbol_file, src_file, version_file, args.arch,
285 args.api)
286
287
288if __name__ == '__main__':
289 main()