blob: ae8b17b4f17382672feac6f982d4ac5bcf4e19fd [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 Albertc42458e2016-07-29 13:05:39 -070074def get_tag_value(tag):
75 """Returns the value of a key/value tag.
76
77 Raises:
78 ValueError: Tag is not a key/value type tag.
79
80 Returns: Value part of tag as a string.
81 """
82 if '=' not in tag:
83 raise ValueError('Not a key/value tag: ' + tag)
84 return tag.partition('=')[2]
85
86
Dan Albert914449f2016-06-17 16:45:24 -070087def version_is_private(version):
88 """Returns True if the version name should be treated as private."""
89 return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
90
91
Dan Albert08532b62016-07-28 18:09:47 -070092def should_omit_version(name, tags, arch, api):
93 """Returns True if the version section should be ommitted.
94
95 We want to omit any sections that do not have any symbols we'll have in the
96 stub library. Sections that contain entirely future symbols or only symbols
97 for certain architectures.
98 """
99 if version_is_private(name):
100 return True
101 if not symbol_in_arch(tags, arch):
102 return True
Dan Albertc42458e2016-07-29 13:05:39 -0700103 if not symbol_in_api(tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -0700104 return True
105 return False
106
107
108def enter_version(scope, line, version_file, arch, api):
Dan Albert914449f2016-06-17 16:45:24 -0700109 """Enters a new version block scope."""
110 if scope.top != Scope.Top:
111 raise RuntimeError('Encountered nested version block.')
112
113 # Entering a new version block. By convention symbols with versions ending
114 # with "_PRIVATE" or "_PLATFORM" are not included in the NDK.
115 version_name = line.split('{')[0].strip()
Dan Alberta85042a2016-07-28 16:58:27 -0700116 tags = get_tags(line)
Dan Albert08532b62016-07-28 18:09:47 -0700117 if should_omit_version(version_name, tags, arch, api):
Dan Albert914449f2016-06-17 16:45:24 -0700118 scope.push(Scope.Private)
119 else:
120 scope.push(Scope.Global) # By default symbols are visible.
121 version_file.write(line)
122
123
124def leave_version(scope, line, version_file):
125 """Leave a version block scope."""
126 # There is no close to a visibility section, just the end of the version or
127 # a new visiblity section.
128 assert scope.top in (Scope.Global, Scope.Local, Scope.Private)
129 if scope.top != Scope.Private:
130 version_file.write(line)
131 scope.pop()
132 assert scope.top == Scope.Top
133
134
135def enter_visibility(scope, line, version_file):
136 """Enters a new visibility block scope."""
137 leave_visibility(scope)
138 version_file.write(line)
139 visibility = line.split(':')[0].strip()
140 if visibility == 'local':
141 scope.push(Scope.Local)
142 elif visibility == 'global':
143 scope.push(Scope.Global)
144 else:
145 raise RuntimeError('Unknown visiblity label: ' + visibility)
146
147
148def leave_visibility(scope):
149 """Leaves a visibility block scope."""
150 assert scope.top in (Scope.Global, Scope.Local)
151 scope.pop()
152 assert scope.top == Scope.Top
153
154
Dan Albert08532b62016-07-28 18:09:47 -0700155def handle_top_scope(scope, line, version_file, arch, api):
Dan Albert914449f2016-06-17 16:45:24 -0700156 """Processes a line in the top level scope."""
157 if '{' in line:
Dan Albert08532b62016-07-28 18:09:47 -0700158 enter_version(scope, line, version_file, arch, api)
Dan Albert914449f2016-06-17 16:45:24 -0700159 else:
160 raise RuntimeError('Unexpected contents at top level: ' + line)
161
162
163def handle_private_scope(scope, line, version_file):
164 """Eats all input."""
165 if '}' in line:
166 leave_version(scope, line, version_file)
167
168
169def handle_local_scope(scope, line, version_file):
170 """Passes through input."""
171 if ':' in line:
172 enter_visibility(scope, line, version_file)
173 elif '}' in line:
174 leave_version(scope, line, version_file)
175 else:
176 version_file.write(line)
177
178
179def symbol_in_arch(tags, arch):
180 """Returns true if the symbol is present for the given architecture."""
181 has_arch_tags = False
182 for tag in tags:
183 if tag == arch:
184 return True
185 if tag in ALL_ARCHITECTURES:
186 has_arch_tags = True
187
188 # If there were no arch tags, the symbol is available for all
189 # architectures. If there were any arch tags, the symbol is only available
190 # for the tagged architectures.
191 return not has_arch_tags
192
193
Dan Albertc42458e2016-07-29 13:05:39 -0700194def symbol_in_api(tags, arch, api):
195 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700196 introduced_tag = None
197 arch_specific = False
198 for tag in tags:
199 # If there is an arch-specific tag, it should override the common one.
200 if tag.startswith('introduced=') and not arch_specific:
201 introduced_tag = tag
202 elif tag.startswith('introduced-' + arch + '='):
203 introduced_tag = tag
204 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700205 elif tag == 'future':
206 # This symbol is not in any released API level.
Dan Albertc42458e2016-07-29 13:05:39 -0700207 # TODO(danalbert): These need to be emitted for api == current.
Dan Alberta85042a2016-07-28 16:58:27 -0700208 # That's not a construct we have yet, so just skip it for now.
209 return False
Dan Albert914449f2016-06-17 16:45:24 -0700210
211 if introduced_tag is None:
212 # We found no "introduced" tags, so the symbol has always been
213 # available.
214 return True
215
Dan Albertc42458e2016-07-29 13:05:39 -0700216 return api >= int(get_tag_value(introduced_tag))
217
218
219def symbol_versioned_in_api(tags, api):
220 """Returns true if the symbol should be versioned for the given API.
221
222 This models the `versioned=API` tag. This should be a very uncommonly
223 needed tag, and is really only needed to fix versioning mistakes that are
224 already out in the wild.
225
226 For example, some of libc's __aeabi_* functions were originally placed in
227 the private version, but that was incorrect. They are now in LIBC_N, but
228 when building against any version prior to N we need the symbol to be
229 unversioned (otherwise it won't resolve on M where it is private).
230 """
231 for tag in tags:
232 if tag.startswith('versioned='):
233 return api >= int(get_tag_value(tag))
234 # If there is no "versioned" tag, the tag has been versioned for as long as
235 # it was introduced.
236 return True
237
Dan Albert914449f2016-06-17 16:45:24 -0700238
239
240def handle_global_scope(scope, line, src_file, version_file, arch, api):
241 """Emits present symbols to the version file and stub source file."""
242 if ':' in line:
243 enter_visibility(scope, line, version_file)
244 return
245 if '}' in line:
246 leave_version(scope, line, version_file)
247 return
248
249 if ';' not in line:
250 raise RuntimeError('Expected ; to terminate symbol: ' + line)
251 if '*' in line:
252 raise RuntimeError('Wildcard global symbols are not permitted.')
253
254 # Line is now in the format "<symbol-name>; # tags"
255 # Tags are whitespace separated.
Dan Albertc42458e2016-07-29 13:05:39 -0700256 symbol_name, _, _ = line.strip().partition(';')
Dan Alberta85042a2016-07-28 16:58:27 -0700257 tags = get_tags(line)
Dan Albert914449f2016-06-17 16:45:24 -0700258
259 if not symbol_in_arch(tags, arch):
260 return
Dan Albertc42458e2016-07-29 13:05:39 -0700261 if not symbol_in_api(tags, arch, api):
Dan Albert914449f2016-06-17 16:45:24 -0700262 return
263
264 if 'var' in tags:
265 src_file.write('int {} = 0;\n'.format(symbol_name))
266 else:
267 src_file.write('void {}() {{}}\n'.format(symbol_name))
Dan Albertc42458e2016-07-29 13:05:39 -0700268
269 if symbol_versioned_in_api(tags, api):
270 version_file.write(line)
Dan Albert914449f2016-06-17 16:45:24 -0700271
272
273def generate(symbol_file, src_file, version_file, arch, api):
274 """Generates the stub source file and version script."""
275 scope = Stack()
276 scope.push(Scope.Top)
277 for line in symbol_file:
278 if line.strip() == '' or line.strip().startswith('#'):
279 version_file.write(line)
280 elif scope.top == Scope.Top:
Dan Albert08532b62016-07-28 18:09:47 -0700281 handle_top_scope(scope, line, version_file, arch, api)
Dan Albert914449f2016-06-17 16:45:24 -0700282 elif scope.top == Scope.Private:
283 handle_private_scope(scope, line, version_file)
284 elif scope.top == Scope.Local:
285 handle_local_scope(scope, line, version_file)
286 elif scope.top == Scope.Global:
287 handle_global_scope(scope, line, src_file, version_file, arch, api)
288
289
290def parse_args():
291 """Parses and returns command line arguments."""
292 parser = argparse.ArgumentParser()
293
294 parser.add_argument('--api', type=int, help='API level being targeted.')
295 parser.add_argument(
296 '--arch', choices=ALL_ARCHITECTURES,
297 help='Architecture being targeted.')
298
299 parser.add_argument(
300 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
301 parser.add_argument(
302 'stub_src', type=os.path.realpath,
303 help='Path to output stub source file.')
304 parser.add_argument(
305 'version_script', type=os.path.realpath,
306 help='Path to output version script.')
307
308 return parser.parse_args()
309
310
311def main():
312 """Program entry point."""
313 args = parse_args()
314
315 with open(args.symbol_file) as symbol_file:
316 with open(args.stub_src, 'w') as src_file:
317 with open(args.version_script, 'w') as version_file:
318 generate(symbol_file, src_file, version_file, args.arch,
319 args.api)
320
321
322if __name__ == '__main__':
323 main()