blob: d61dfbb073b700f59a322a181661f0e23fc6770e [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',
Dan Albert914449f2016-06-17 16:45:24 -070029 'x86',
30 'x86_64',
31)
32
33
Dan Albertfd86e9e2016-11-08 13:35:12 -080034# Arbitrary magic number. We use the same one in api-level.h for this purpose.
35FUTURE_API_LEVEL = 10000
36
37
Dan Albert8bdccb92016-07-29 13:06:22 -070038def logger():
39 """Return the main logger for this module."""
40 return logging.getLogger(__name__)
Dan Albert914449f2016-06-17 16:45:24 -070041
42
Dan Alberta85042a2016-07-28 16:58:27 -070043def get_tags(line):
44 """Returns a list of all tags on this line."""
45 _, _, all_tags = line.strip().partition('#')
Dan Albert8bdccb92016-07-29 13:06:22 -070046 return [e for e in re.split(r'\s+', all_tags) if e.strip()]
Dan Alberta85042a2016-07-28 16:58:27 -070047
48
Dan Albert3f6fb2d2017-03-28 16:04:25 -070049def is_api_level_tag(tag):
50 """Returns true if this tag has an API level that may need decoding."""
51 if tag.startswith('introduced='):
52 return True
53 if tag.startswith('introduced-'):
54 return True
55 if tag.startswith('versioned='):
56 return True
57 return False
58
59
60def decode_api_level_tags(tags, api_map):
61 """Decodes API level code names in a list of tags.
62
63 Raises:
64 ParseError: An unknown version name was found in a tag.
65 """
66 for idx, tag in enumerate(tags):
67 if not is_api_level_tag(tag):
68 continue
69 name, value = split_tag(tag)
70
71 try:
72 decoded = str(decode_api_level(value, api_map))
73 tags[idx] = '='.join([name, decoded])
74 except KeyError:
75 raise ParseError('Unknown version name in tag: {}'.format(tag))
76 return tags
77
78
79def split_tag(tag):
80 """Returns a key/value tuple of the tag.
81
82 Raises:
83 ValueError: Tag is not a key/value type tag.
84
85 Returns: Tuple of (key, value) of the tag. Both components are strings.
86 """
87 if '=' not in tag:
88 raise ValueError('Not a key/value tag: ' + tag)
89 key, _, value = tag.partition('=')
90 return key, value
91
92
Dan Albertc42458e2016-07-29 13:05:39 -070093def get_tag_value(tag):
94 """Returns the value of a key/value tag.
95
96 Raises:
97 ValueError: Tag is not a key/value type tag.
98
99 Returns: Value part of tag as a string.
100 """
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700101 return split_tag(tag)[1]
Dan Albertc42458e2016-07-29 13:05:39 -0700102
103
Dan Albert914449f2016-06-17 16:45:24 -0700104def version_is_private(version):
105 """Returns True if the version name should be treated as private."""
106 return version.endswith('_PRIVATE') or version.endswith('_PLATFORM')
107
108
Jiyong Park92d6bc12019-11-06 12:37:43 +0900109def should_omit_version(version, arch, api, llndk, apex):
Dan Albert08532b62016-07-28 18:09:47 -0700110 """Returns True if the version section should be ommitted.
111
112 We want to omit any sections that do not have any symbols we'll have in the
113 stub library. Sections that contain entirely future symbols or only symbols
114 for certain architectures.
115 """
Dan Albert756f2d02018-10-09 16:36:03 -0700116 if version_is_private(version.name):
Dan Albert08532b62016-07-28 18:09:47 -0700117 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700118 if 'platform-only' in version.tags:
Dan Albert300cb2f2016-11-04 14:52:30 -0700119 return True
Jiyong Park14317652019-02-08 20:34:32 +0900120
Jiyong Park92d6bc12019-11-06 12:37:43 +0900121 no_llndk_no_apex = 'llndk' not in version.tags and 'apex' not in version.tags
122 keep = no_llndk_no_apex or \
123 ('llndk' in version.tags and llndk) or \
Jiyong Park14317652019-02-08 20:34:32 +0900124 ('apex' in version.tags and apex)
125 if not keep:
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900126 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700127 if not symbol_in_arch(version.tags, arch):
Dan Albert08532b62016-07-28 18:09:47 -0700128 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700129 if not symbol_in_api(version.tags, arch, api):
130 return True
131 return False
132
133
Jiyong Park92d6bc12019-11-06 12:37:43 +0900134def should_omit_symbol(symbol, arch, api, llndk, apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700135 """Returns True if the symbol should be omitted."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900136 no_llndk_no_apex = 'llndk' not in symbol.tags and 'apex' not in symbol.tags
137 keep = no_llndk_no_apex or \
138 ('llndk' in symbol.tags and llndk) or \
Jiyong Park14317652019-02-08 20:34:32 +0900139 ('apex' in symbol.tags and apex)
140 if not keep:
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900141 return True
Dan Albert756f2d02018-10-09 16:36:03 -0700142 if not symbol_in_arch(symbol.tags, arch):
143 return True
144 if not symbol_in_api(symbol.tags, arch, api):
Dan Albert08532b62016-07-28 18:09:47 -0700145 return True
146 return False
147
148
Dan Albert914449f2016-06-17 16:45:24 -0700149def symbol_in_arch(tags, arch):
150 """Returns true if the symbol is present for the given architecture."""
151 has_arch_tags = False
152 for tag in tags:
153 if tag == arch:
154 return True
155 if tag in ALL_ARCHITECTURES:
156 has_arch_tags = True
157
158 # If there were no arch tags, the symbol is available for all
159 # architectures. If there were any arch tags, the symbol is only available
160 # for the tagged architectures.
161 return not has_arch_tags
162
163
Dan Albertc42458e2016-07-29 13:05:39 -0700164def symbol_in_api(tags, arch, api):
165 """Returns true if the symbol is present for the given API level."""
Dan Albert914449f2016-06-17 16:45:24 -0700166 introduced_tag = None
167 arch_specific = False
168 for tag in tags:
169 # If there is an arch-specific tag, it should override the common one.
170 if tag.startswith('introduced=') and not arch_specific:
171 introduced_tag = tag
172 elif tag.startswith('introduced-' + arch + '='):
173 introduced_tag = tag
174 arch_specific = True
Dan Alberta85042a2016-07-28 16:58:27 -0700175 elif tag == 'future':
Dan Albertfd86e9e2016-11-08 13:35:12 -0800176 return api == FUTURE_API_LEVEL
Dan Albert914449f2016-06-17 16:45:24 -0700177
178 if introduced_tag is None:
179 # We found no "introduced" tags, so the symbol has always been
180 # available.
181 return True
182
Dan Albertc42458e2016-07-29 13:05:39 -0700183 return api >= int(get_tag_value(introduced_tag))
184
185
186def symbol_versioned_in_api(tags, api):
187 """Returns true if the symbol should be versioned for the given API.
188
189 This models the `versioned=API` tag. This should be a very uncommonly
190 needed tag, and is really only needed to fix versioning mistakes that are
191 already out in the wild.
192
193 For example, some of libc's __aeabi_* functions were originally placed in
194 the private version, but that was incorrect. They are now in LIBC_N, but
195 when building against any version prior to N we need the symbol to be
196 unversioned (otherwise it won't resolve on M where it is private).
197 """
198 for tag in tags:
199 if tag.startswith('versioned='):
200 return api >= int(get_tag_value(tag))
201 # If there is no "versioned" tag, the tag has been versioned for as long as
202 # it was introduced.
203 return True
204
Dan Albert914449f2016-06-17 16:45:24 -0700205
Dan Albert8bdccb92016-07-29 13:06:22 -0700206class ParseError(RuntimeError):
207 """An error that occurred while parsing a symbol file."""
208 pass
Dan Albert914449f2016-06-17 16:45:24 -0700209
210
Dan Albert756f2d02018-10-09 16:36:03 -0700211class MultiplyDefinedSymbolError(RuntimeError):
212 """A symbol name was multiply defined."""
213 def __init__(self, multiply_defined_symbols):
214 super(MultiplyDefinedSymbolError, self).__init__(
215 'Version script contains multiple definitions for: {}'.format(
216 ', '.join(multiply_defined_symbols)))
217 self.multiply_defined_symbols = multiply_defined_symbols
218
219
Dan Albert8bdccb92016-07-29 13:06:22 -0700220class Version(object):
221 """A version block of a symbol file."""
222 def __init__(self, name, base, tags, symbols):
223 self.name = name
224 self.base = base
225 self.tags = tags
226 self.symbols = symbols
227
228 def __eq__(self, other):
229 if self.name != other.name:
230 return False
231 if self.base != other.base:
232 return False
233 if self.tags != other.tags:
234 return False
235 if self.symbols != other.symbols:
236 return False
237 return True
238
239
240class Symbol(object):
241 """A symbol definition from a symbol file."""
242 def __init__(self, name, tags):
243 self.name = name
244 self.tags = tags
245
246 def __eq__(self, other):
247 return self.name == other.name and set(self.tags) == set(other.tags)
248
sophiezb858c6d2020-05-06 15:57:32 -0700249
Dan Albert8bdccb92016-07-29 13:06:22 -0700250class SymbolFileParser(object):
251 """Parses NDK symbol files."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900252 def __init__(self, input_file, api_map, arch, api, llndk, apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700253 self.input_file = input_file
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700254 self.api_map = api_map
Dan Albert756f2d02018-10-09 16:36:03 -0700255 self.arch = arch
256 self.api = api
Jiyong Park92d6bc12019-11-06 12:37:43 +0900257 self.llndk = llndk
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900258 self.apex = apex
Dan Albert8bdccb92016-07-29 13:06:22 -0700259 self.current_line = None
260
261 def parse(self):
262 """Parses the symbol file and returns a list of Version objects."""
263 versions = []
264 while self.next_line() != '':
265 if '{' in self.current_line:
266 versions.append(self.parse_version())
267 else:
268 raise ParseError(
269 'Unexpected contents at top level: ' + self.current_line)
Dan Albert756f2d02018-10-09 16:36:03 -0700270
271 self.check_no_duplicate_symbols(versions)
Dan Albert8bdccb92016-07-29 13:06:22 -0700272 return versions
273
Dan Albert756f2d02018-10-09 16:36:03 -0700274 def check_no_duplicate_symbols(self, versions):
275 """Raises errors for multiply defined symbols.
276
277 This situation is the normal case when symbol versioning is actually
278 used, but this script doesn't currently handle that. The error message
279 will be a not necessarily obvious "error: redefition of 'foo'" from
280 stub.c, so it's better for us to catch this situation and raise a
281 better error.
282 """
283 symbol_names = set()
284 multiply_defined_symbols = set()
285 for version in versions:
Jiyong Park92d6bc12019-11-06 12:37:43 +0900286 if should_omit_version(version, self.arch, self.api, self.llndk, self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700287 continue
288
289 for symbol in version.symbols:
Jiyong Park92d6bc12019-11-06 12:37:43 +0900290 if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex):
Dan Albert756f2d02018-10-09 16:36:03 -0700291 continue
292
293 if symbol.name in symbol_names:
294 multiply_defined_symbols.add(symbol.name)
295 symbol_names.add(symbol.name)
296 if multiply_defined_symbols:
297 raise MultiplyDefinedSymbolError(
298 sorted(list(multiply_defined_symbols)))
299
Dan Albert8bdccb92016-07-29 13:06:22 -0700300 def parse_version(self):
301 """Parses a single version section and returns a Version object."""
302 name = self.current_line.split('{')[0].strip()
303 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700304 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700305 symbols = []
306 global_scope = True
dimitry2be7fa92017-11-21 17:47:33 +0100307 cpp_symbols = False
Dan Albert8bdccb92016-07-29 13:06:22 -0700308 while self.next_line() != '':
309 if '}' in self.current_line:
310 # Line is something like '} BASE; # tags'. Both base and tags
311 # are optional here.
312 base = self.current_line.partition('}')[2]
313 base = base.partition('#')[0].strip()
314 if not base.endswith(';'):
315 raise ParseError(
dimitry2be7fa92017-11-21 17:47:33 +0100316 'Unterminated version/export "C++" block (expected ;).')
317 if cpp_symbols:
318 cpp_symbols = False
319 else:
320 base = base.rstrip(';').rstrip()
321 if base == '':
322 base = None
323 return Version(name, base, tags, symbols)
324 elif 'extern "C++" {' in self.current_line:
325 cpp_symbols = True
326 elif not cpp_symbols and ':' in self.current_line:
Dan Albert8bdccb92016-07-29 13:06:22 -0700327 visibility = self.current_line.split(':')[0].strip()
328 if visibility == 'local':
329 global_scope = False
330 elif visibility == 'global':
331 global_scope = True
332 else:
333 raise ParseError('Unknown visiblity label: ' + visibility)
dimitry2be7fa92017-11-21 17:47:33 +0100334 elif global_scope and not cpp_symbols:
Dan Albert8bdccb92016-07-29 13:06:22 -0700335 symbols.append(self.parse_symbol())
336 else:
Dan Albertf50b6ce2018-09-25 13:39:25 -0700337 # We're in a hidden scope or in 'extern "C++"' block. Ignore
338 # everything.
Dan Albert8bdccb92016-07-29 13:06:22 -0700339 pass
340 raise ParseError('Unexpected EOF in version block.')
341
342 def parse_symbol(self):
343 """Parses a single symbol line and returns a Symbol object."""
344 if ';' not in self.current_line:
345 raise ParseError(
346 'Expected ; to terminate symbol: ' + self.current_line)
347 if '*' in self.current_line:
348 raise ParseError(
349 'Wildcard global symbols are not permitted.')
350 # Line is now in the format "<symbol-name>; # tags"
351 name, _, _ = self.current_line.strip().partition(';')
352 tags = get_tags(self.current_line)
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700353 tags = decode_api_level_tags(tags, self.api_map)
Dan Albert8bdccb92016-07-29 13:06:22 -0700354 return Symbol(name, tags)
355
356 def next_line(self):
357 """Returns the next non-empty non-comment line.
358
359 A return value of '' indicates EOF.
360 """
361 line = self.input_file.readline()
362 while line.strip() == '' or line.strip().startswith('#'):
363 line = self.input_file.readline()
364
365 # We want to skip empty lines, but '' indicates EOF.
366 if line == '':
367 break
368 self.current_line = line
369 return self.current_line
370
371
372class Generator(object):
373 """Output generator that writes stub source files and version scripts."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900374 def __init__(self, src_file, version_script, arch, api, llndk, apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700375 self.src_file = src_file
376 self.version_script = version_script
377 self.arch = arch
378 self.api = api
Jiyong Park92d6bc12019-11-06 12:37:43 +0900379 self.llndk = llndk
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900380 self.apex = apex
Dan Albert8bdccb92016-07-29 13:06:22 -0700381
382 def write(self, versions):
383 """Writes all symbol data to the output files."""
384 for version in versions:
385 self.write_version(version)
386
387 def write_version(self, version):
388 """Writes a single version block's data to the output files."""
Jiyong Park92d6bc12019-11-06 12:37:43 +0900389 if should_omit_version(version, self.arch, self.api, self.llndk, self.apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700390 return
391
Dan Albert756f2d02018-10-09 16:36:03 -0700392 section_versioned = symbol_versioned_in_api(version.tags, self.api)
Dan Albert8bdccb92016-07-29 13:06:22 -0700393 version_empty = True
394 pruned_symbols = []
395 for symbol in version.symbols:
Jiyong Park92d6bc12019-11-06 12:37:43 +0900396 if should_omit_symbol(symbol, self.arch, self.api, self.llndk, self.apex):
Dan Albert8bdccb92016-07-29 13:06:22 -0700397 continue
398
399 if symbol_versioned_in_api(symbol.tags, self.api):
400 version_empty = False
401 pruned_symbols.append(symbol)
402
403 if len(pruned_symbols) > 0:
Dan Albertae452cc2017-01-03 14:27:41 -0800404 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700405 self.version_script.write(version.name + ' {\n')
406 self.version_script.write(' global:\n')
407 for symbol in pruned_symbols:
Dan Albertae452cc2017-01-03 14:27:41 -0800408 emit_version = symbol_versioned_in_api(symbol.tags, self.api)
409 if section_versioned and emit_version:
Dan Albert8bdccb92016-07-29 13:06:22 -0700410 self.version_script.write(' ' + symbol.name + ';\n')
411
Dan Albertf55f0782017-07-28 11:00:22 -0700412 weak = ''
413 if 'weak' in symbol.tags:
414 weak = '__attribute__((weak)) '
415
Dan Albert8bdccb92016-07-29 13:06:22 -0700416 if 'var' in symbol.tags:
Dan Albertf55f0782017-07-28 11:00:22 -0700417 self.src_file.write('{}int {} = 0;\n'.format(
418 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700419 else:
Dan Albertf55f0782017-07-28 11:00:22 -0700420 self.src_file.write('{}void {}() {{}}\n'.format(
421 weak, symbol.name))
Dan Albert8bdccb92016-07-29 13:06:22 -0700422
Dan Albertae452cc2017-01-03 14:27:41 -0800423 if not version_empty and section_versioned:
Dan Albert8bdccb92016-07-29 13:06:22 -0700424 base = '' if version.base is None else ' ' + version.base
425 self.version_script.write('}' + base + ';\n')
Dan Albert914449f2016-06-17 16:45:24 -0700426
427
Dan Albert49927d22017-03-28 15:00:46 -0700428def decode_api_level(api, api_map):
429 """Decodes the API level argument into the API level number.
430
431 For the average case, this just decodes the integer value from the string,
432 but for unreleased APIs we need to translate from the API codename (like
433 "O") to the future API level for that codename.
434 """
435 try:
436 return int(api)
437 except ValueError:
438 pass
439
440 if api == "current":
441 return FUTURE_API_LEVEL
442
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700443 return api_map[api]
Dan Albert49927d22017-03-28 15:00:46 -0700444
445
Dan Albert914449f2016-06-17 16:45:24 -0700446def parse_args():
447 """Parses and returns command line arguments."""
448 parser = argparse.ArgumentParser()
449
Dan Albert8bdccb92016-07-29 13:06:22 -0700450 parser.add_argument('-v', '--verbose', action='count', default=0)
451
Dan Albert914449f2016-06-17 16:45:24 -0700452 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700453 '--api', required=True, help='API level being targeted.')
Dan Albert8bdccb92016-07-29 13:06:22 -0700454 parser.add_argument(
455 '--arch', choices=ALL_ARCHITECTURES, required=True,
Dan Albert914449f2016-06-17 16:45:24 -0700456 help='Architecture being targeted.')
Dan Willemsenb01e7f72017-04-03 14:28:36 -0700457 parser.add_argument(
Jiyong Park92d6bc12019-11-06 12:37:43 +0900458 '--llndk', action='store_true', help='Use the LLNDK variant.')
Jiyong Parkbb4e1352018-12-07 15:54:52 +0900459 parser.add_argument(
460 '--apex', action='store_true', help='Use the APEX variant.')
Dan Albert914449f2016-06-17 16:45:24 -0700461
462 parser.add_argument(
Dan Albert49927d22017-03-28 15:00:46 -0700463 '--api-map', type=os.path.realpath, required=True,
464 help='Path to the API level map JSON file.')
465
466 parser.add_argument(
Dan Albert914449f2016-06-17 16:45:24 -0700467 'symbol_file', type=os.path.realpath, help='Path to symbol file.')
468 parser.add_argument(
469 'stub_src', type=os.path.realpath,
470 help='Path to output stub source file.')
471 parser.add_argument(
472 'version_script', type=os.path.realpath,
473 help='Path to output version script.')
474
475 return parser.parse_args()
476
477
478def main():
479 """Program entry point."""
480 args = parse_args()
481
Dan Albert3f6fb2d2017-03-28 16:04:25 -0700482 with open(args.api_map) as map_file:
483 api_map = json.load(map_file)
484 api = decode_api_level(args.api, api_map)
Dan Albert49927d22017-03-28 15:00:46 -0700485
Dan Albert8bdccb92016-07-29 13:06:22 -0700486 verbose_map = (logging.WARNING, logging.INFO, logging.DEBUG)
487 verbosity = args.verbose
488 if verbosity > 2:
489 verbosity = 2
490 logging.basicConfig(level=verbose_map[verbosity])
491
Dan Albert914449f2016-06-17 16:45:24 -0700492 with open(args.symbol_file) as symbol_file:
Dan Albert756f2d02018-10-09 16:36:03 -0700493 try:
494 versions = SymbolFileParser(symbol_file, api_map, args.arch, api,
Jiyong Park92d6bc12019-11-06 12:37:43 +0900495 args.llndk, args.apex).parse()
Dan Albert756f2d02018-10-09 16:36:03 -0700496 except MultiplyDefinedSymbolError as ex:
497 sys.exit('{}: error: {}'.format(args.symbol_file, ex))
Dan Albert8bdccb92016-07-29 13:06:22 -0700498
499 with open(args.stub_src, 'w') as src_file:
500 with open(args.version_script, 'w') as version_file:
Dan Albert49927d22017-03-28 15:00:46 -0700501 generator = Generator(src_file, version_file, args.arch, api,
Jiyong Park92d6bc12019-11-06 12:37:43 +0900502 args.llndk, args.apex)
Dan Albert8bdccb92016-07-29 13:06:22 -0700503 generator.write(versions)
Dan Albert914449f2016-06-17 16:45:24 -0700504
505
506if __name__ == '__main__':
507 main()