blob: 4906ea243b441eb56e8e3a3ec57dfe763e14e79c [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
Dan Albert49927d22017-03-28 15:00:46 -070019import json
Dan Albert8bdccb92016-07-29 13:06:22 -070020import logging
Dan Albert914449f2016-06-17 16:45:24 -070021import os
22import re
Dan Albert756f2d02018-10-09 16:36:03 -070023import sys
Dan Albert914449f2016-06-17 16:45:24 -070024
25
26ALL_ARCHITECTURES = (
27 'arm',
28 'arm64',
29 'mips',
30 'mips64',
31 'x86',
32 'x86_64',
33)
34
35
Dan Albertfd86e9e2016-11-08 13:35:12 -080036# Arbitrary magic number. We use the same one in api-level.h for this purpose.
37FUTURE_API_LEVEL = 10000
38
39
Dan Albert8bdccb92016-07-29 13:06:22 -070040def logger():
41 """Return the main logger for this module."""
42 return logging.getLogger(__name__)
Dan Albert914449f2016-06-17 16:45:24 -070043
44
Dan Alberta85042a2016-07-28 16:58:27 -070045def get_tags(line):
46 """Returns a list of all tags on this line."""
47 _, _, all_tags = line.strip().partition('#')
Dan Albert8bdccb92016-07-29 13:06:22 -070048 return [e for e in re.split(r'\s+', all_tags) if e.strip()]
Dan Alberta85042a2016-07-28 16:58:27 -070049
50
Dan Albert3f6fb2d2017-03-28 16:04:25 -070051def is_api_level_tag(tag):
52 """Returns true if this tag has an API level that may need decoding."""
53 if tag.startswith('introduced='):
54 return True
55 if tag.startswith('introduced-'):
56 return True
57 if tag.startswith('versioned='):
58 return True
59 return False
60
61
62def decode_api_level_tags(tags, api_map):
63 """Decodes API level code names in a list of tags.
64
65 Raises:
66 ParseError: An unknown version name was found in a tag.
67 """
68 for idx, tag in enumerate(tags):
69 if not is_api_level_tag(tag):
70 continue
71 name, value = split_tag(tag)
72
73 try:
74 decoded = str(decode_api_level(value, api_map))
75 tags[idx] = '='.join([name, decoded])
76 except KeyError:
77 raise ParseError('Unknown version name in tag: {}'.format(tag))
78 return tags
79
80
81def split_tag(tag):
82 """Returns a key/value tuple of the tag.
83
84 Raises:
85 ValueError: Tag is not a key/value type tag.
86
87 Returns: Tuple of (key, value) of the tag. Both components are strings.
88 """
89 if '=' not in tag:
90 raise ValueError('Not a key/value tag: ' + tag)
91 key, _, value = tag.partition('=')
92 return key, value
93
94
Dan Albertc42458e2016-07-29 13:05:39 -070095def get_tag_value(tag):
96 """Returns the value of a key/value tag.
97
98 Raises:
99 ValueError: Tag is not a key/value type tag.
100
101 Returns: Value part of tag as a string.
102 """
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700103 return split_tag(tag)[1]
Dan Albertc42458e2016-07-29 13:05:39 -0700104
105
Dan Albert914449f2016-06-17 16:45:24 -0700106def version_is_private(version):
107 """Returns True if the version name should be treated as private."""
108 return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
109
110
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900111def should_omit_version(version, arch, api, vndk, apex):
Dan Albert08532b62016-07-28 18:09:47 -0700112 """Returns True if the version section should be ommitted.
113
114 We want to omit any sections that do not have any symbols we'll have in the
115 stub library. Sections that contain entirely future symbols or only symbols
116 for certain architectures.
117 """
Dan Albert756f2d02018-10-09 16:36:03 -0700118 if version_is_private(version.name):
Dan Albert08532b62016-07-28 18:09:47 -0700119 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700120 if 'platform-only' in version.tags:
Dan Albert300cb2f2016-11-04 14:52:30 -0700121 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700122 if 'vndk' in version.tags and not vndk:
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700123 return True
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900124 if 'apex' in version.tags and not apex:
125 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700126 if not symbol_in_arch(version.tags, arch):
Dan Albert08532b62016-07-28 18:09:47 -0700127 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700128 if not symbol_in_api(version.tags, arch, api):
129 return True
130 return False
131
132
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900133def should_omit_symbol(symbol, arch, api, vndk, apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700134 """Returns True if the symbol should be omitted."""
135 if not vndk and 'vndk' in symbol.tags:
136 return True
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900137 if not apex and 'apex' in symbol.tags:
138 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700139 if not symbol_in_arch(symbol.tags, arch):
140 return True
141 if not symbol_in_api(symbol.tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -0700142 return True
143 return False
144
145
Dan Albert914449f2016-06-17 16:45:24 -0700146def symbol_in_arch(tags, arch):
147 """Returns true if the symbol is present for the given architecture."""
148 has_arch_tags = False
149 for tag in tags:
150 if tag == arch:
151 return True
152 if tag in ALL_ARCHITECTURES:
153 has_arch_tags = True
154
155 # If there were no arch tags, the symbol is available for all
156 # architectures. If there were any arch tags, the symbol is only available
157 # for the tagged architectures.
158 return not has_arch_tags
159
160
Dan Albertc42458e2016-07-29 13:05:39 -0700161def symbol_in_api(tags, arch, api):
162 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700163 introduced_tag = None
164 arch_specific = False
165 for tag in tags:
166 # If there is an arch-specific tag, it should override the common one.
167 if tag.startswith('introduced=') and not arch_specific:
168 introduced_tag = tag
169 elif tag.startswith('introduced-' + arch + '='):
170 introduced_tag = tag
171 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700172 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800173 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700174
175 if introduced_tag is None:
176 # We found no "introduced" tags, so the symbol has always been
177 # available.
178 return True
179
Dan Albertc42458e2016-07-29 13:05:39 -0700180 return api >= int(get_tag_value(introduced_tag))
181
182
183def symbol_versioned_in_api(tags, api):
184 """Returns true if the symbol should be versioned for the given API.
185
186 This models the `versioned=API` tag. This should be a very uncommonly
187 needed tag, and is really only needed to fix versioning mistakes that are
188 already out in the wild.
189
190 For example, some of libc's __aeabi_* functions were originally placed in
191 the private version, but that was incorrect. They are now in LIBC_N, but
192 when building against any version prior to N we need the symbol to be
193 unversioned (otherwise it won't resolve on M where it is private).
194 """
195 for tag in tags:
196 if tag.startswith('versioned='):
197 return api >= int(get_tag_value(tag))
198 # If there is no "versioned" tag, the tag has been versioned for as long as
199 # it was introduced.
200 return True
201
Dan Albert914449f2016-06-17 16:45:24 -0700202
Dan Albert8bdccb92016-07-29 13:06:22 -0700203class ParseError(RuntimeError):
204 """An error that occurred while parsing a symbol file."""
205 pass
Dan Albert914449f2016-06-17 16:45:24 -0700206
207
Dan Albert756f2d02018-10-09 16:36:03 -0700208class MultiplyDefinedSymbolError(RuntimeError):
209 """A symbol name was multiply defined."""
210 def __init__(self, multiply_defined_symbols):
211 super(MultiplyDefinedSymbolError, self).__init__(
212 'Version script contains multiple definitions for: {}'.format(
213 ', '.join(multiply_defined_symbols)))
214 self.multiply_defined_symbols = multiply_defined_symbols
215
216
Dan Albert8bdccb92016-07-29 13:06:22 -0700217class Version(object):
218 """A version block of a symbol file."""
219 def __init__(self, name, base, tags, symbols):
220 self.name = name
221 self.base = base
222 self.tags = tags
223 self.symbols = symbols
224
225 def __eq__(self, other):
226 if self.name != other.name:
227 return False
228 if self.base != other.base:
229 return False
230 if self.tags != other.tags:
231 return False
232 if self.symbols != other.symbols:
233 return False
234 return True
235
236
237class Symbol(object):
238 """A symbol definition from a symbol file."""
239 def __init__(self, name, tags):
240 self.name = name
241 self.tags = tags
242
243 def __eq__(self, other):
244 return self.name == other.name and set(self.tags) == set(other.tags)
245
Dan Albert8bdccb92016-07-29 13:06:22 -0700246class SymbolFileParser(object):
247 """Parses NDK symbol files."""
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900248 def __init__(self, input_file, api_map, arch, api, vndk, apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700249 self.input_file = input_file
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700250 self.api_map = api_map
Dan Albert756f2d02018-10-09 16:36:03 -0700251 self.arch = arch
252 self.api = api
253 self.vndk = vndk
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900254 self.apex = apex
Dan Albert8bdccb92016-07-29 13:06:22 -0700255 self.current_line = None
256
257 def parse(self):
258 """Parses the symbol file and returns a list of Version objects."""
259 versions = []
260 while self.next_line() != '':
261 if '{' in self.current_line:
262 versions.append(self.parse_version())
263 else:
264 raise ParseError(
265 'Unexpected contents at top level: ' + self.current_line)
Dan Albert756f2d02018-10-09 16:36:03 -0700266
267 self.check_no_duplicate_symbols(versions)
Dan Albert8bdccb92016-07-29 13:06:22 -0700268 return versions
269
Dan Albert756f2d02018-10-09 16:36:03 -0700270 def check_no_duplicate_symbols(self, versions):
271 """Raises errors for multiply defined symbols.
272
273 This situation is the normal case when symbol versioning is actually
274 used, but this script doesn't currently handle that. The error message
275 will be a not necessarily obvious "error: redefition of 'foo'" from
276 stub.c, so it's better for us to catch this situation and raise a
277 better error.
278 """
279 symbol_names = set()
280 multiply_defined_symbols = set()
281 for version in versions:
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900282 if should_omit_version(version, self.arch, self.api, self.vndk, self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700283 continue
284
285 for symbol in version.symbols:
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900286 if should_omit_symbol(symbol, self.arch, self.api, self.vndk, self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700287 continue
288
289 if symbol.name in symbol_names:
290 multiply_defined_symbols.add(symbol.name)
291 symbol_names.add(symbol.name)
292 if multiply_defined_symbols:
293 raise MultiplyDefinedSymbolError(
294 sorted(list(multiply_defined_symbols)))
295
Dan Albert8bdccb92016-07-29 13:06:22 -0700296 def parse_version(self):
297 """Parses a single version section and returns a Version object."""
298 name = self.current_line.split('{')[0].strip()
299 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700300 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700301 symbols = []
302 global_scope = True
dimitry2be7fa92017-11-21 17:47:33 +0100303 cpp_symbols = False
Dan Albert8bdccb92016-07-29 13:06:22 -0700304 while self.next_line() != '':
305 if '}' in self.current_line:
306 # Line is something like '} BASE; # tags'. Both base and tags
307 # are optional here.
308 base = self.current_line.partition('}')[2]
309 base = base.partition('#')[0].strip()
310 if not base.endswith(';'):
311 raise ParseError(
dimitry2be7fa92017-11-21 17:47:33 +0100312 'Unterminated version/export "C++" block (expected ;).')
313 if cpp_symbols:
314 cpp_symbols = False
315 else:
316 base = base.rstrip(';').rstrip()
317 if base == '':
318 base = None
319 return Version(name, base, tags, symbols)
320 elif 'extern "C++" {' in self.current_line:
321 cpp_symbols = True
322 elif not cpp_symbols and ':' in self.current_line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700323 visibility = self.current_line.split(':')[0].strip()
324 if visibility == 'local':
325 global_scope = False
326 elif visibility == 'global':
327 global_scope = True
328 else:
329 raise ParseError('Unknown visiblity label: ' + visibility)
dimitry2be7fa92017-11-21 17:47:33 +0100330 elif global_scope and not cpp_symbols:
Dan Albert8bdccb92016-07-29 13:06:22 -0700331 symbols.append(self.parse_symbol())
332 else:
Dan Albertf50b6ce2018-09-25 13:39:25 -0700333 # We're in a hidden scope or in 'extern "C++"' block. Ignore
334 # everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700335 pass
336 raise ParseError('Unexpected EOF in version block.')
337
338 def parse_symbol(self):
339 """Parses a single symbol line and returns a Symbol object."""
340 if ';' not in self.current_line:
341 raise ParseError(
342 'Expected ; to terminate symbol: ' + self.current_line)
343 if '*' in self.current_line:
344 raise ParseError(
345 'Wildcard global symbols are not permitted.')
346 # Line is now in the format "<symbol-name>; # tags"
347 name, _, _ = self.current_line.strip().partition(';')
348 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700349 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700350 return Symbol(name, tags)
351
352 def next_line(self):
353 """Returns the next non-empty non-comment line.
354
355 A return value of '' indicates EOF.
356 """
357 line = self.input_file.readline()
358 while line.strip() == '' or line.strip().startswith('#'):
359 line = self.input_file.readline()
360
361 # We want to skip empty lines, but '' indicates EOF.
362 if line == '':
363 break
364 self.current_line = line
365 return self.current_line
366
367
368class Generator(object):
369 """Output generator that writes stub source files and version scripts."""
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900370 def __init__(self, src_file, version_script, arch, api, vndk, apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700371 self.src_file = src_file
372 self.version_script = version_script
373 self.arch = arch
374 self.api = api
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700375 self.vndk = vndk
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900376 self.apex = apex
Dan Albert8bdccb92016-07-29 13:06:22 -0700377
378 def write(self, versions):
379 """Writes all symbol data to the output files."""
380 for version in versions:
381 self.write_version(version)
382
383 def write_version(self, version):
384 """Writes a single version block's data to the output files."""
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900385 if should_omit_version(version, self.arch, self.api, self.vndk, self.apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700386 return
387
Dan Albert756f2d02018-10-09 16:36:03 -0700388 section_versioned = symbol_versioned_in_api(version.tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700389 version_empty = True
390 pruned_symbols = []
391 for symbol in version.symbols:
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900392 if should_omit_symbol(symbol, self.arch, self.api, self.vndk, self.apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700393 continue
394
395 if symbol_versioned_in_api(symbol.tags, self.api):
396 version_empty = False
397 pruned_symbols.append(symbol)
398
399 if len(pruned_symbols) > 0:
Dan Albertae452cc2017-01-03 14:27:41 -0800400 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700401 self.version_script.write(version.name + ' {\n')
402 self.version_script.write(' global:\n')
403 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800404 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
405 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700406 self.version_script.write(' ' + symbol.name + ';\n')
407
Dan Albertf55f0782017-07-28 11:00:22 -0700408 weak = ''
409 if 'weak' in symbol.tags:
410 weak = '__attribute__((weak)) '
411
Dan Albert8bdccb92016-07-29 13:06:22 -0700412 if 'var' in symbol.tags:
Dan Albertf55f0782017-07-28 11:00:22 -0700413 self.src_file.write('{}int {} = 0;\n'.format(
414 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700415 else:
Dan Albertf55f0782017-07-28 11:00:22 -0700416 self.src_file.write('{}void {}() {{}}\n'.format(
417 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700418
Dan Albertae452cc2017-01-03 14:27:41 -0800419 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700420 base = '' if version.base is None else ' ' + version.base
421 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700422
423
Dan Albert49927d22017-03-28 15:00:46 -0700424def decode_api_level(api, api_map):
425 """Decodes the API level argument into the API level number.
426
427 For the average case, this just decodes the integer value from the string,
428 but for unreleased APIs we need to translate from the API codename (like
429 "O") to the future API level for that codename.
430 """
431 try:
432 return int(api)
433 except ValueError:
434 pass
435
436 if api == "current":
437 return FUTURE_API_LEVEL
438
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700439 return api_map[api]
Dan Albert49927d22017-03-28 15:00:46 -0700440
441
Dan Albert914449f2016-06-17 16:45:24 -0700442def parse_args():
443 """Parses and returns command line arguments."""
444 parser = argparse.ArgumentParser()
445
Dan Albert8bdccb92016-07-29 13:06:22 -0700446 parser.add_argument('-v', '--verbose', action='count', default=0)
447
Dan Albert914449f2016-06-17 16:45:24 -0700448 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700449 '--api', required=True, help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700450 parser.add_argument(
451 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700452 help='Architecture being targeted.')
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700453 parser.add_argument(
454 '--vndk', action='store_true', help='Use the VNDK variant.')
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900455 parser.add_argument(
456 '--apex', action='store_true', help='Use the APEX variant.')
Dan Albert914449f2016-06-17 16:45:24 -0700457
458 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700459 '--api-map', type=os.path.realpath, required=True,
460 help='Path to the API level map JSON file.')
461
462 parser.add_argument(
Dan Albert914449f2016-06-17 16:45:24 -0700463 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
464 parser.add_argument(
465 'stub_src', type=os.path.realpath,
466 help='Path to output stub source file.')
467 parser.add_argument(
468 'version_script', type=os.path.realpath,
469 help='Path to output version script.')
470
471 return parser.parse_args()
472
473
474def main():
475 """Program entry point."""
476 args = parse_args()
477
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700478 with open(args.api_map) as map_file:
479 api_map = json.load(map_file)
480 api = decode_api_level(args.api, api_map)
Dan Albert49927d22017-03-28 15:00:46 -0700481
Dan Albert8bdccb92016-07-29 13:06:22 -0700482 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
483 verbosity = args.verbose
484 if verbosity > 2:
485 verbosity = 2
486 logging.basicConfig(level=verbose_map[verbosity])
487
Dan Albert914449f2016-06-17 16:45:24 -0700488 with open(args.symbol_file) as symbol_file:
Dan Albert756f2d02018-10-09 16:36:03 -0700489 try:
490 versions = SymbolFileParser(symbol_file, api_map, args.arch, api,
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900491 args.vndk, args.apex).parse()
Dan Albert756f2d02018-10-09 16:36:03 -0700492 except MultiplyDefinedSymbolError as ex:
493 sys.exit('{}: error: {}'.format(args.symbol_file, ex))
Dan Albert8bdccb92016-07-29 13:06:22 -0700494
495 with open(args.stub_src, 'w') as src_file:
496 with open(args.version_script, 'w') as version_file:
Dan Albert49927d22017-03-28 15:00:46 -0700497 generator = Generator(src_file, version_file, args.arch, api,
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900498 args.vndk, args.apex)
Dan Albert8bdccb92016-07-29 13:06:22 -0700499 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700500
501
502if __name__ == '__main__':
503 main()